Entity Framework Core – Fluent API Mapper

Entity Framework Core – Fluent API Mapper

Kiedy potrzebujemy użyć jakiegoś ORM’a (Object – Relational Mapping) w naszym projekcie, z reguły pierwsza myśl jaka przychodzi nam do głowy to Entity Framework. Dzięki temu narzędziu w bardzo prosty i szybki sposób jesteśmy w stanie skonfigurować połączenie z naszą bazą danych i odwzorować nasze tabele w projekcie. W najnowszej wersji dla .NET Core’a otrzymaliśmy również wsparcie dla innych baz. Poza Microsoft SQL Server połączymy się również z SQLite, PostgreSQL, czy MySQL. W najczęściej stosowanym podejściu do odwzorowania naszej bazy w projekcie, którym jest Code First (i na tę chwilę jedynym dostępnym dla Entity Framework Core), pojawia się problem odpowiedniej konfiguracji encji. Musimy zdefiniować odpowiednie klucze, relacje i właściwości kolumn. Możemy do tego wykorzystać odpowiednie atrybuty (Data Annotations) lub nadpisać metodę OnModelCreating na naszym kontekście i użyć Fluent API. Poniżej przedstawię Wam jak w schludny sposób definiować encje w Entity Framework Core.

Atrybyty, czy Fluent API?

To, którego podejścia użyjemy w projekcie zależy tylko i wyłącznie od zespołu, jego pomysłów i potrzeb projektu. Osobiście jestem zwolennikiem używania Fluent API, natomiast zdecydowanie nie jestem zwolennikiem stosowania obu podejść. Załóżmy, że mamy taką encję:

Teraz spójrzmy na przykładową metodę OnModelCreating w naszym kontekście:

W tym momencie mamy konfigurację w dwóch różnych miejscach. Przy jednej encji jest to widoczne, ale co w momencie, gdy mamy ich na przykład dwieście? Nasza metoda przybiera rozmiary na setki linijek kodu. Przyjdzie moment w którym musimy określić jakąś relację między encjami i nie wiemy gdzie wprowadzić zmiany. Niejedna osoba wejdzie do pliku encji, zauważy brak relacji i doda odpowiednie atrybuty. Niestety nie zauważy, że w metodzie OnModelCreating w linii 1236 taka relacja już jest – mamy konflikt. Skoro nie lubię atrybutów, to jak w takim razie uniknąć rozrastającego się kontekstu Entity Framework’a przez kolejne odwołania do Fluent API? Skoro ma być schludnie, to spróbujmy to poukładać.

Interfejsy dla encji

Na początku wyciągam część wspólną każdej encji, czyli klucz główny. Zacznijmy od zdefiniowania takiego generycznego interfejsu:

Klucz główny może przyjmować różne typy: long, int, lub Guid. Warto więc już na początku zadbać o to, aby można było go swobodnie wybrać w zależności od potrzeby. Dodałem tutaj również przydatną deklarację metody IsNew. Następnie interesuje mnie historyczność encji, czyli daty utworzenia i ostatniej modyfikacji. Oddzieliłem je ponieważ zdarzają się tabele, które nie potrzebują takich informacji. Dodatkowo zadbamy też o konkurencję na poziomie encji. Stwórzmy więc kolejny interfejs:

Teraz zdefiniujmy naszą encję i zaimplementujmy nasze interfejsy:

Mapper – implementacja

Za konfigurację encji odpowiada typ EntityTypeBuilder dostępny razem z Entity Framework’iem. Mając tę wiedzę możemy utworzyć klasę abstrakcyjną z mapowaniem klucza głównego:

EntityTypeBuilder musi wiedzieć, że nasz generyczny typ to typ referencyjny, potrzebujemy też informacji o kluczu głównym. Jako parametr konstruktora przyjmujemy nazwę schematu dla naszej tabeli. Metoda Map odpowiada tutaj za ustawienie klucza głównego, nazwy tabeli i schematu. Rozwińmy to o kolejny mapper:

Celowo dziedziczę po klasie IdentifiableEntityMapper. Zabieg ten ma mi ułatwić dodawanie odpowiednich konfiguracji w kontekście Entity Framework’a. Oczywiście możemy zmienić sposób implementacji według naszych potrzeb (np. dla klucza kompozytowego można dodać kolejny mapperm czy usunąć dziedziczenie i zaimplementować dodawanie mapperów na różne sposoby). W metodzie Map ustawiliśmy, że pole RowVersion odpowiada za wersję i konkurencję. Pola dla daty utworzenia i modyfikacji rekordu są domyślnie ustawione jako wymagane (nie są typu nullable), dlatego ustawiamy tylko typ ich pola jako datetime2.

Model Builder Extension Methods

W starszej wersji Entity Framework’a mieliśmy gotowe metody umożliwiające dodanie odpowiedniej konfiguracji. W wersji dla .NET Core’a nie jest to już takie proste, dlatego musimy napisać do tego własny mechanizm. Jeśli przyjrzymy się metodzie Entity w klasie ModelBuilder zauważymy, że jako parametr przyjmuje ona obiekt typu Action<EntityTypeBuilder<TEntity>>. Możemy więc napisać taką klasę:

Entity mapper

W tym momencie możemy już zaimplementować mapper dla naszej encji, a następnie wywołać metodę AddConfiguration w naszym kontekście w metodzie OnModelCreating:

Dla zachowania porządku zalecam dodawanie kolejnych mapperów w porządku alfabetycznym.

Podsumowanie

W każdym projekcie z Entity Framework’iem musimy w jakiś sposób definiować nasze encje. Na pewno warto zastanowić się jak chcemy to robić, ponieważ bez odpowiedniego porządku łatwo o błędy. Powyższe rozwiązania pozwala nam trzymać konfiguracje encji w jednym miejscu, a dzięki Fluent API nie musimy się martwić o atrybuty. Unikamy w ten sposób rozrastającej się metody OnModelCreating, a w razie potrzeby edytowania któregoś mappera wiemy gdzie należy wprowadzić odpowiednie zmiany. To tylko jeden z wielu sposobów na implementację takiego mechanizmu. Zamiast interfejsów można tworzyć klasy abstrakcyjne, można też implementować dodatkowe metody rozszerzające dla klasy Model Builder – wszystko zależy od naszych potrzeb. Jeśli macie jakieś równie ciekawe pomysły na trzymanie porządku w swoich projektach z Entity Framework’iem, zapraszam do dyskusji 🙂

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *

2 komentarzy dla “Entity Framework Core – Fluent API Mapper”

  1. Atrybuty mają jeszcze jedną ważną zaletę – informacje o modelu trzymane są w jednym miejscu i w przypadku dużych struktur, pisanych przez obce osoby łatwiej jest zobaczyć zależności, typy, ograniczenia. We fluent api powstanie dużo kodu, który po osiągnięciu dużych rozmiarów może być trudny w utrzymywaniu.