Nie ma nic odkrywczego w stwierdzeniu – mamy jesień. Listopad nie zachęca do spacerów. A cóż brzmi kusząco w zimne, pochmurne dni? Oczywiście kakałko i programowanie!
W momencie kiedy pracujemy w środowiskach rozproszonych czasami potrzebujemy wykonywać pewne zadania jako oddzielny proces. Czasami jest to generowanie raportu, czasami jakieś inne zadanie długo lub krótko trwałe. W takich sytuacjach możemy do tego użyć prostej aplikacji konsolowej, zamkniętej w kontenerze i uruchamianej co jakiś czas za pomocą Kubernetes-a. Nie oceniając jakości metody(są sytuacje, w których nie polecam jej stosowania, ale to temat na inny artykuł), chciałbym wam przedstawić jak podłączyć do prostej aplikacji konsolowej zarządzanie zmiennymi za pomocą pliku appsettings.json
i jak umożliwić migrację EF Core z poziomu CLI.
Dodanie konfiguracji
W pierwszej kolejności musimy dodać sczytywanie konfiguracji z pliku appsettings, a także, jeżeli tego potrzebujemy, ze zmiennych środowiskowych. Musimy dodać kilka paczek nuget-a do naszego projeku:
- Microsoft.Extensions.Configuration
- Microsoft.Extensions.Configuration.EnvironmentVariables
- Microsoft.Extensions.Configuration.FileExtensions
- Microsoft.Extensions.Configuration.Json
Uwielbiam modularność z jaką jest pisany .NET Core, ale czasami mam obawy że liczba potrzebnych paczek będzie podobna do aplikacji pisanych w Node 😂
Przechodząc dalej będziemy musieli załadować konfigurację w dwóch miejscach. Osobiście lubię ją wydzielić do prywatnej metody w klasie Program
private static IConfiguration BuildConfiguration() => new ConfigurationBuilder().SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) .AddEnvironmentVariables() .Build();
Nie ma tutaj nic szczególnego dlatego opisze to krótko i zwięźle. Zaczynamy od utworzenia naszego builder-a konfiguracji. Następnie informujemy go, gdzie znajduje się nasza konfiguracja. Następne linijki służą do załadowania pliku appsettings.json
i zmiennych środowiskowych. Ostatnia linijka to zbudowanie obiektu konfiguracji.
Poniżej możecie zerknąć na plik appsettings, który zawiera tylko connectionString do bazy.
{ "ConnectionStrings": { "SqlServerConnection": "Server=localhost,1433;Database=Test;User Id=SA;Password=yourStrong(!)Password" } }
Dodanie kontenera DI
Możliwe, że w tym przykładzie będzie to overkill
, jednak przyzwyczaiłem się do korzystania z kontenerów DI. Potrzebując konsolowej aplikacji w mniejszym lub większym stopniu kontener DI się przyda, więc wiedza nie pójdzie w las.
Wystarczy załadować jedną paczkę:
- Microsoft.Extensions.DependencyInjection
Całą konfiguracje kontenera możemy zamieścić w głównej metodzie naszej aplikacji konsolowej (Main
):
var configuration = BuildConfiguration(); var connectionString = configuration.GetConnectionString("SqlServerConnection"); var services = new ServiceCollection(); services.AddDbContext( o => o.UseSqlServer(connectionString, x => x.MigrationsAssembly(typeof(PersonContext).Assembly.FullName))); var dbContext = services.BuildServiceProvider().GetRequiredService(); if (dbContext.Database.GetPendingMigrations().Any()) dbContext.Database.Migrate();
Od razu zawarłem automatyczne pobieranie dbcontext-u z kontenera DI i uruchamianie oczekujących migracji.
Sama konfiguracja encji jak i dbContext-u nie ma tutaj dużego znaczenia. Przykładowa konfiguracja wygląda następująco:
public class Person { public int Id { get; set; } public string Name { get; set; } }
public class PersonContext : DbContext { public DbSet Persons { get; set; } public PersonContext(DbContextOptions options) : base(options) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity() .HasKey(x => x.Id); base.OnModelCreating(modelBuilder); } }
Opis Problemu
Wszystko wygląda poprawnie, niemal jak w standardowej aplikacji MVC. Tak więc pojawia się pytanie: co się stanie, jeżeli spróbujemy uruchomić migrację teraz? W konsoli otrzymamy następujący błąd (aby otrzymać szczegółowe logi podczas migracji pamiętajcie o dodaniu flagi -v
):
Finding DbContext classes... Finding IDesignTimeDbContextFactory implementations... Finding application service provider... Finding Microsoft.Extensions.Hosting service provider... No static method 'CreateHostBuilder(string[])' was found on class 'Program'. No application service provider was found. Finding DbContext classes in the project... Found DbContext 'PersonContext'. Microsoft.EntityFrameworkCore.Design.OperationException: Unable to create an object of type 'PersonContext'. For the different patterns supported at design time, see https://go.microsoft.com/fwlink/?linkid=851728
Jak widać EF Core wymaga od nas zdefiniowania kolejnego elementu, który pozwoli my na utworzenie obiektu `PersonContext`.
Implementacja IDesignTimeDbContextFactory
Musimy do projektu dołożyć jeszcze jedną klasą. Nazwijmy ją PersonContextFactory
. Musi ona implementować interfejs IDesignTimeDbContextFactory
. W przypadku małych aplikacji wole dodać ją jako zagnieżdżoną klasę prywatną klasy Program. Pozwala to na dostęp do wcześniej stworzonej metody BuildConfiguration
.
public class Program { //... // ReSharper disable once UnusedType.Local private class PersonContextFactory : IDesignTimeDbContextFactory { public PersonContext CreateDbContext(string[] args) { var configuration = BuildConfiguration(); var connectionString = configuration.GetConnectionString("SqlServerConnection"); var builder = new DbContextOptionsBuilder(); builder.UseSqlServer(connectionString, o => o.MigrationsAssembly(typeof(PersonContext).Assembly.FullName)); return new PersonContext(builder.Options); } } }
Kod powyżej nie zawiera w sobie czarnej magii. Po prostu sczytujemy oraz budujemy konfigurację naszego dbContext-u, a na koniec tworzymy go. Tutaj jeszcze mała uwaga z mojej strony: oprócz standardowych paczek EF Core, takich jak:
- Microsoft.EntityFrameworkCore
- Microsoft.EntityFrameworkCore.SqlServer
Trzeba dodać jeszcze:
- Microsoft.EntityFrameworkCore.Design
Przy małych aplikacjach niemal zawsze o niej zapominam 😂 , chwalmy errory i zdolność ich odczytywania.
Teraz szybki test czy wszystko działa tak jak należy:
Finding DbContext classes in the project... Using DbContext factory 'PersonContextFactory'. Using context 'PersonContext'. Finding design-time services for provider 'Microsoft.EntityFrameworkCore.SqlServer'... Using design-time services from provider 'Microsoft.EntityFrameworkCore.SqlServer'. Finding design-time services referenced by assembly 'EFCoreConsoleMigration'. No referenced design-time services were found. Finding IDesignTimeServices implementations in assembly 'EFCoreConsoleMigration'... No design-time services were found. DetectChanges starting for 'PersonContext'. DetectChanges completed for 'PersonContext'. Writing migration to '/Users/dante/Workspace/EFCoreConsoleMigration/EFCoreConsoleMigration/Migrations/20201107125749_InitialMigration.cs'. Writing model snapshot to '/Users/dante/Workspace/EFCoreConsoleMigration/EFCoreConsoleMigration/Migrations/PersonContextModelSnapshot.cs'. 'PersonContext' disposed. Done. To undo this action, use 'ef migrations remove'
Jak widać po logach EF znalazł naszą klasę PersonContextFactory
i był w stanie jej użyć do zbudowania kontekstu. Następnie, bez problemów, wygenerował migrację.
To by było na tyle, dzięki za przeczytanie i do następnego!
PS. Spodobał Ci się ten artykuł?