Entity Framework w osobnym projekcie

Entity Framework w osobnym projekcie

Jeśli szukasz szybkiego rozwiązania, kliknij tu. Jeśli chcesz się nieco więcej dowiedzieć, przeczytaj cały post.

Wstęp

Gdy tworzymy nową aplikację w VisualStudio z bazą danych, domyślny kreator tworzy jeden projekt, do którego pcha wszystkie klasy. Do malutkich rzeczy, czy nauki to w zupełności wystarczy. Jednak w świecie rzeczywistym chcielibyśmy mieć osobny projekt do modeli i osobny projekt dla warstwy danych (Data Access Layer).

Niby nie jest to trudne, wystarczy przenieść nasz DbContext do innego projektu i już. A co z migracjami? Migracje nadal będą się tworzyć w projekcie głównym. Nie o to chodzi. Chcemy migracje też w projekcie z danymi.

Dlaczego to nie jest oczywiste?

Musisz zdać sobie sprawę z tego, jak działają migracje w Entity Framework (czy też EfCore), a także jak działa aktualizacja bazy danych.

Gdy uruchamiasz polecenie Add-Migration lub dotnet ef migrations add, narzędzie uruchamia Twoją główną aplikację. Uruchomienie aplikacji następuje w sposób normalny. Czyli przy aplikacji konsolowej, odpalona zostanie metoda Main. Przy aplikacji webowej, pójdzie cała konfiguracja.

Jednym z kroków jest inicjalizacja Entity Framework, np:

services.AddDbContext<ApplicationDbContext>(options =>
	options.UseSqlServer(
		Configuration.GetConnectionString("DefaultConnection")));

W tym momencie tworzymy połączenie z bazą danych i migracje mogą zostać utworzone. Pamiętaj, że do utworzenia migracji konieczne jest połączenie z bazą danych. Narzędzie musi sprawdzić, jak wygląda baza i jak wygląda model – musi mieć możliwość porównania tego.

Teraz jeśli uruchomisz migrację z parametrem -p, wskazując na konkretny projekt, np:

Add-Migration InitialDbCreate -p DataAccessLayer

Entity Framework będzie próbowało uruchomić projekt DataAccessLayer. Jeśli jest to zwykła biblioteka klas (class library), no to co się uruchomi? Nic. Dlatego też migracja nie będzie mogła się odbyć.

Ale można to nieco obejść.

Mamy na to dwa sposoby. Którego użyć? To zależy od aplikacji, jaką tworzysz i od jej wymagań.

Metoda 1 – własna fabryka kontekstu bazy danych

Narzędzie podczas migracji poszuka klasy, która implementuje interfejs IDesignTimeDbContextFactory. Jeśli znajdzie taką, utworzy jej obiekt i za jej pomocą skonfiguruje połączenie z bazą danych.

  1. W swoim projekcie z danymi (tam, gdzie masz DbContext i chcesz mieć migracje) musisz utworzyć klasę implementującą interfejs IDesignTimeDbContextFactory. Ef właśnie tego poszuka (jeśli używasz Sql Servera, dodaj pakiet nuget: Microsoft.EntityFrameworkCore.SqlServer):
public class DbContextFactory : IDesignTimeDbContextFactory<ApplicationDbContext>
{
	public XMoneyDbContext CreateDbContext(string[] args)
	{
		DbContextOptionsBuilder<ApplicationDbContext> optionsBuilder = new DbContextOptionsBuilder<ApplicationDbContext>();

        optionsBuilder.UseSqlServer("tutaj Twój connection string")

        return new ApplicationDbContext(optionsBuilder.Options);
	}
}

Przeanalizujmy go:

  • deklarujesz fabrykę kontekstu bazy danych (Ef poszuka właśnie klasy implementującej ten interfejs), parametrem generycznym jest oczywiście Twój kontekst bazy danych.
  • najpierw tworzysz buildera do opcji kontekstu
  • ustawiasz opcje (np. UseSqlServer) i connection string
  • tworzysz swój kontekst i zwracasz go

I to właściwie tyle. Możesz już teraz uruchomić migrację z przełącznikiem -p:

Add-Migration NazwaMigracji -p NazwaTwojegoProjektu

lub

dotnet ef migrations add NazwaMigracji -p NazwaTwojegoProjektu

Metoda 2 – parametry w komendzie CLI

Znacznie prostszą metodą jest dodanie odpowiednich parametrów do komend migracyjnych. Mamy tutaj dwa fajne parametry:

  • -p – projekt, w którym chcemy mieć migracje
  • -s – główny projekt aplikacji

W tej sytuacji nie potrzebujemy już implementować IDesignTimeDbContextFactory. Po prostu możemy użyć komendy CLI:

dotnet ef migrations add -p 'projekt-z-migracjami.csproj' -s 'main-project.csproj'

Które rozwiązanie wybrać?

Powiem szczerze, że po kilku różnych projektach, zdecydowanie zawsze wybieram drugą metodę. Nie spotkałem się od kilku lat z sytuacją, gdzie pierwsze rozwiązanie faktycznie niosłoby ze sobą jakąś korzyść.

Niemniej jednak, niezależnie od tego, którą metodę wybierzesz, musisz pamiętać o jednym. Uruchamiając migrację, mechanizm CLI NIE WIE na jakim środowisku pracujesz, o ile nie masz ustawionej zmiennej środowiskowej.

Dlaczego to jest problem? Ponieważ jeśli przechowujesz connection stringi w plikach appsettings, zawsze będzie brany pod uwagę plik główny – „produkcyjny” – appsettings.json. Nigdy appsetting.Development.json ani żaden inny.

Żeby móc używać ustawień zależnych od środowiska, na swojej maszynie musisz mieć ustawioną zmienną środowiskową ASPNETCORE_ENVIRONMENT, która mówi na jakim środowisku pracujemy.

I teraz w zależności od potrzeb, możesz to ustawić na sztywno na poziomie maszyny (ja tego jednak nie zalecam), ale możesz też ustawić po prostu w sesji terminala – czyli przed uruchomieniem polecenia dotnet .

Wskazówka na koniec

Ponieważ jest dla mnie bardzo karkołomne pisać całe polecenia obsługujące migracje wraz ze ścieżkami do konkretnych projektów, w swoich rozwiązaniach stosuję bardzo prostą zasadę.

Mam katalog scripts na tym samym poziomie co katalog z kodami źródłowymi. W katalogu scripts trzymam proste skrypty do zarządzania migracjami. Np. create-migration.ps1, który wygląda dokładnie tak:

param ([Parameter(Mandatory)]$name)

Invoke-Expression "dotnet ef migrations add -p '..\src\<ścieżka do projektu DAL>' -s '..\src\<ścieżka do głównego projektu>' $name"

Do tego mam też skrypty usuwające migracje i aktualizujące bazę danych. Jest to bardzo wygodne rozwiązanie, którego się trzymam już jakiś czas i świetnie mi się sprawdza.

Podziel się artykułem na: