Na większości prowadzonych przeze mnie warsztatów poruszam temat zapisu informacji do bazy danych. Wiadomo, niemal w każdym systemie i aplikacji są miejsca, gdzie potrzebujemy przechowywać dane na stałe. W większości przypadków będziesz do tego używać biblioteki ORM(object relational mapper), która wspomoże Cię przy wykonywaniu zapytań. No i tu dochodzimy do sedna problemu. Czy biblioteka musi zaśmiecać Twój model milionem atrybutów? Otóż nie! Zobaczmy jakie możliwości oferuje nam Entity Framework Core aby oddzielić kod infrastruktury od modelu.
Przykładowy Model
Zacznijmy od prostego modelu, który występował we współtworzonej przeze mnie aplikacji. Jest to standardowa encja przechowująca dane osobowe: imię, nazwisko, adres - nic wielce wysublimowanego. Nie potrzebowaliśmy bardziej zaawansowanych mapping-ów, więc wyglądał niemal jak typowy obiekt POCO.
public class Person {
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string AddressLine { get; set; }
public string AddressCity { get; set; }
public string AddressZipCode { get; set; }
}
Naszą jedyną konfiguracją, która przechowywaliśmy wraz z tym kodem, była definicja klucza głównego za pomocą Fluent Api w konfiguracji Context-u Entity Framework-a. W dzisiejszych czasach kodzik mógłby wyglądać następująco:
public override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity().HasKey(x => x.Id);
}
I tutaj pierwszy spoiler: w obecnej wersji EF Core powyższy przykład jest redundantny. EF Core dodaje pewne elementy na podstawie konwencji i jednym nich jest tworzenie klucza głównego dla kolumn o nazwie Id
.
Mała zmiana modelu
Teraz już wiem, że taka forma zapisu to była zła praktyka. W tamtych czasach po prostu czułem, że powinno to wyglądać inaczej. Prefixy w polach adresowych sprawiały mi dyskomfort, postanowiłem wszystko zrefaktoryzować. Wydzielenie osobnej klasy nie stanowiło żadnego problemu.
public class Person {
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public Address Address { get; set; }
}
public class Address {
public string Line { get; set; }
public string City { get; set; }
public string ZipCode { get; set; }
}
Wszystko wyglądało ładniej, pozostała do napisania konfiguracja Entity Framework-a. Należy ją napisać tak, aby w bazie danych zmapować dane do jednej tabeli. Oczywiście mógłbym użyć tutaj atrybutu OwnedType
(czy też w przestarzałego ComplexType
) jednak chciałem całkowicie rozdzielić konfigurację infrastruktury od mojej encji. Jak się okazało na pomoc przychodzi mechanizm EntityTypeConfiguration
,
internal class PersonConfiguration : IEntityTypeConfiguration<Person>
{
public void Configure(EntityTypeBuilder<Person> builder)
{
builder.HasKey(x => x.Id);
builder.OwnsOne(x => x.Address, a => {
a.Property(x => x.Line).HasColumnName("Address_Line");
a.Property(x => x.City).HasColumnName("Address_City");
a.Property(x => x.ZipCode).HasColumnName("Address_ZipCode");
});
}
}
Jest to prosty sposób na oddzielenie konfiguracji infrastruktury od naszego modelu, a nawet wyciągnięcie jej do oddzielnej klasy. Nie musimy pisać wszystkiego w metodzie OnModelCreating w DbContext.
Jak już wspomniałem o DbContext to muszę również napisać w jaki sposób załadować konfigurację, aby EF Core wiedział jak z niej korzystać.
public class PersonsDbContext : DbContext
{
public DbSet<Person> Persons { get; set; }
//...
public override void OnModelCreating(ModelBuilder builder)
{
modelBuilder.ApplyConfigurationsFromAssembly(typeof(PersonsDbContext).Assembly);
}
}
W wielkim skrócie w instancji klasy ModelBuilder mamy do dyspozycji dwie metody: ApplyConfiguration
i ApplyConfigurationFromAssembly
. W powyższym przykładzie używam tej drugiej, ponieważ w przyszłości pozwoli ona na dodawanie nowych konfiguracji bez potrzeby modyfikowania jej w PersonsDbContext
. Trzeba jednak pamiętać, że pierwsza metoda została wprowadzona do Entity Framework Core w wersji 2.0, natomiast druga jest dostępna od wersji 2.2.
Po takiej konfiguracji wszystko powinno pięknie działać :)
Ale po co to się robi?
Teraz miała nastąpić sekcja podsumowująca artykułu. Nastąpiła szybka zmiana planów, nowy potok myśli i zmiany muszą nastąpić. Postanowiłem dodać jedną ważną rzecz - odpowiedź na pytanie "po co jest taki zabieg?".
Zacznijmy od innego pytania - czy zawsze powinno się stosować takie podejście do konfiguracji Entity Framework Core?
Odpowiedź jest bardzo prosta: nie. Jeżeli masz małą aplikację typu CRUD albo mającą działać bardzo krótko, to w mojej opinii nie ma sensu dodawać dodatkowego kodu. Nie mniej, jeżeli Twoja aplikacja zawiera w sobie sporą ilość logiki i / lub chcesz zastosować podejście "Domain Driven" to separacja modelu domenowego jest pożądana, aby nie powiedzieć że wymagana.
W takim przypadku Twój agregat mógłby wyglądać następująco:
public sealed class Person : Aggregate<PersonId> {
public string FirstName { get; private set; }
public string LastName { get; private set; }
public Address Address { get; private set; }
[Obsolete("Only for EF Core", true)]
private Person(){}
public Person(string firstName, string LastName)
{
FirstName = firstName;
LastName = lastName;
Id = PersonId.New();
}
}
W takim przypadku klasa Address
stała by się obiektem wartości (Value object)
public Address : ValueObject<Address>
{
public string Line { get; private set; }
public string City { get; private set; }
public string ZipCode { get; private set; }
}
Oczywiście to tylko pseudo implementacje, które wymagałyby jeszcze dopieszczenie, ale mam nadzieje że rozumiesz ogólną koncepcje.
Podsumowanie
W tym artykule przeszliśmy przez jedną z możliwości, które daje Entity Framework Core. Zahaczyliśmy trochę o tematy związana z DDD. Jeżeli chcielibyście poczytać więcej na temat samego DDD i mojej perspektywy na to podejście to proszę dajcie mi znać :)
Do Następnego!