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!