Wprowadzenie
Kilka miesięcy temu w artykule o uruchamianiu migracji Entity Framework w Azure DevOps pokazałem jak utworzyć aplikacje konsolową i za jej pomocą migrować schemat bazy danych podczas wdrażania nowej wersji aplikacji hostowanej w Azure App Service. Tamten wpis dotyczył starego Entity Framework. Dostawałem od Was pytania o aktualizację wpisu dla Entity Framework Core, co czynię w poniższym wpisie.
Migrator
W tym wpisie skupie się bardziej na różnicach w stosunku do poprzedniego przykładu. Dlatego w pierwszej kolejności zachęcam do przeczytania tamtego artykułu – Uruchamianie migracji bazy danych w Azure DevOps.
Pominę również kwestię tworzenia snapshotu bazy danych, którą opisałem w poprzednim wpisie. Ta kwestia tutaj ewentualnie zostaje bez zmiany i tak naprawdę jest tylko potrzebna w momencie, gdy chcesz wykonywać automatyczne testy Postmana, które kiedyś opisywałem.
Podobnie jak w tamtym przykładzie, wykorzystam bibliotekę CommandLinePaser do przekazania connection stringa do migratora. Użyłem tylko jeden parametr w klasie Options:
public class Options | |
{ | |
[Option('c', "connectionString", Required = true, HelpText = "The connection string to database that needs to be updated.")] | |
public string ConnectionString { get; set; } | |
} |
Natomiast kod samego migratora zmienił się praktycznie w całości. Po sparsowaniu przekazanych parametrów w pierwszej kolejności konfigurujemy instancję klasy ServiceProvider, która posłuży nam do utworzenia obiektu DbContext z Entity Framework Core.
private static ServiceProvider GetServiceCollection(Options options) | |
{ | |
var serviceCollection = new ServiceCollection(); | |
var config = new ConfigurationBuilder() | |
.AddInMemoryCollection(new Dictionary<string, string>() | |
{ | |
{"ConnectionStrings:DefaultConnection", options.ConnectionString} | |
}) | |
.Build(); | |
serviceCollection.AddSingleton<IConfiguration>(config); | |
var startup = new Startup(config); | |
startup.ConfigureServices(serviceCollection); | |
return serviceCollection.BuildServiceProvider(); | |
} |
Pierwszym obiektem jaki zarejestrujemy jest obiekt konfiguracji aplikacji. Jest nam to potrzebne, abyśmy mogli przekazać connection stringa do bazy danych. W tym przypadku skorzystamy tylko z źródła w pamięci aplikacji, aby dodać tylko connection stringa do bazy. W przypadku innych parametrów, które nie muszą być przekazywane do migratora, możemy użyć plików json, jak to jest robione w normalnej aplikacji.
Pozostałą konfigurację ServiceProvidera robimy za pomocą metody ConfigureService z klasy Startup głównego projektu. Metoda ta zarejestruje obiekt DbContext, który za chwilkę wykorzystamy.
Samo wykonanie migracji jest dość proste. Tworzymy instancje klasy DbContext z wykorzystaniem service providera. Następnie na właściwości Database wywołujemy metodę Migrate:
private static void Migrate(Options options) | |
{ | |
var serviceProvider = GetServiceCollection(options); | |
var context = serviceProvider.GetRequiredService<DataContext>(); | |
Console.WriteLine("Migration is in progress..."); | |
Console.WriteLine("All:"); | |
Console.WriteLine(FormatMigrations(context.Database.GetMigrations())); | |
Console.WriteLine("Applied:"); | |
Console.WriteLine(FormatMigrations(context.Database.GetAppliedMigrations())); | |
Console.WriteLine("Pending:"); | |
Console.WriteLine(FormatMigrations(context.Database.GetPendingMigrations())); | |
Console.WriteLine("Migrating..."); | |
context.Database.Migrate(); | |
Console.WriteLine("Database has been migrated"); | |
} |
Przed wywołaniem metody Migrate na konsoli wyświetlam informacje o wszystkich migracjach z aplikacji, migracjach, które zostały wykonane oraz tych które wykona migrator.
Cały kod klasy Program z migratora:
class Program | |
{ | |
static void Main(string[] args) | |
{ | |
var result = Parser.Default.ParseArguments<Options>(args); | |
result | |
.WithParsed(Migrate); | |
} | |
private static void Migrate(Options options) | |
{ | |
var serviceProvider = GetServiceCollection(options); | |
var context = serviceProvider.GetRequiredService<DataContext>(); | |
Console.WriteLine("Migration is in progress..."); | |
Console.WriteLine("All:"); | |
Console.WriteLine(FormatMigrations(context.Database.GetMigrations())); | |
Console.WriteLine("Applied:"); | |
Console.WriteLine(FormatMigrations(context.Database.GetAppliedMigrations())); | |
Console.WriteLine("Pending:"); | |
Console.WriteLine(FormatMigrations(context.Database.GetPendingMigrations())); | |
Console.WriteLine("Migrating..."); | |
context.Database.Migrate(); | |
Console.WriteLine("Database has been migrated"); | |
} | |
private static string FormatMigrations(IEnumerable<string> migrations) | |
{ | |
if (migrations.Any() == false) | |
{ | |
return "\tNone"; | |
} | |
return string.Join(Environment.NewLine, migrations.Select(m => $"\t{m}")); | |
} | |
private static ServiceProvider GetServiceCollection(Options options) | |
{ | |
var serviceCollection = new ServiceCollection(); | |
var config = new ConfigurationBuilder() | |
.AddInMemoryCollection(new Dictionary<string, string>() | |
{ | |
{"ConnectionStrings:DefaultConnection", options.ConnectionString} | |
}) | |
.Build(); | |
serviceCollection.AddSingleton<IConfiguration>(config); | |
var startup = new Startup(config); | |
startup.ConfigureServices(serviceCollection); | |
return serviceCollection.BuildServiceProvider(); | |
} | |
} |
Uruchomienie migratora w Visual Studio
Podobnie jak we wcześniejszych przykładach możemy w ustawieniach projektu w Visual Studio ustawić domyślny connection string, który użyje migrator podczas uruchamiania aplikacji w VS. Wystarczy, że przejdziemy do ustawień projektu oraz do zakładki Debug i tam ustawimy wartość parametru -c w „Application arguments”:

Po uruchomieniu migratora w efekcie uzyskamy coś takiego:

Tutaj już wcześniej wszystkie migracje zostały wykonane na lokalnej bazie danych.
Uruchomienie migracji w Azure DevOps
Uruchomienie migracji w Azure DevOps będzie bardzo podobne do poprzedniego wpisu. Główną różnicą będzie sposób budowania migratora. Task, który to robi to:
Wykonujemy tutaj komendę dotnet publish na projekcie migratora. Bardzo istotne jest tutaj przekazanie parametrów „–self-contained true -r win10-x64”, które spowodują, że efekcie otrzymamy plik exe migratora, która później łatwiej będzie uruchomić podczas wdrażania aplikacji.
Cała zawartość pliku azure-pipelines.yml, którą użyłem w przykładzie możesz znaleźć na githubie – https://github.com/danielplawgo/EFCoreMigrations/blob/master/azure-pipelines.yml
Sama konfiguracja release w Azure DevOps jest taka sama jak w poprzednim wpisie. Czyli mamy dwa kroki. Pierwszy uruchamia migratora, do którego przekazujemy connection stringa z zmiennej. A drugi to wrzucenie nowej wersji do Azure App Service:

Problem z buildem migratora
Podczasu budowy migratora w Azure DevOps możecie spotkać się z błędem:
Bazując na opisie błędu na githubie https://github.com/dotnet/sdk/issues/10566 można to obejść ustawiając w referencji do projektu w webowego właściwość GlobalPropertiesToRemove ustawioną na SelfContained . Tak jak to widać poniżej:
<ItemGroup> | |
<ProjectReference Include="..\EFCoreMigrations.Web\EFCoreMigrations.Web.csproj" GlobalPropertiesToRemove="SelfContained" /> | |
</ItemGroup> |
To powinno rozwiązać problem z budowaniem migratora w Azure DevOps.
Przykład
Tradycyjnie przykład znajduje się na githubie – https://github.com/danielplawgo/EFCoreMigrations. Po jego pobraniu należy ustawić poprawnego connection stringa w ustawieniach projektu, o których wspomniałem w treści wpisu.
Podsumowanie
Jak widać w przypadku Entity Framework Core możemy również łatwo przygotować prostą aplikację konsolową, która wykona migrację bazy danych podczas wdrażania nowej wersji. Z czasem możną tą aplikację rozbudować o jakieś dodatkowe funkcjonalności. Tak jak tworzenie snapshotów bazy danych lub wypełnianie bazy testowymi danymi.