Implementowanie obiektów wartości
Napiwek
Ta zawartość jest fragmentem książki eBook, architektury mikrousług platformy .NET dla konteneryzowanych aplikacji platformy .NET dostępnych na platformie .NET Docs lub jako bezpłatnego pliku PDF, który można odczytać w trybie offline.
Jak opisano we wcześniejszych sekcjach dotyczących jednostek i agregacji, tożsamość jest podstawowa dla jednostek. Istnieje jednak wiele obiektów i elementów danych w systemie, które nie wymagają śledzenia tożsamości i tożsamości, takich jak obiekty wartości.
Obiekt wartości może odwoływać się do innych jednostek. Na przykład w aplikacji, która generuje trasę, która opisuje sposób pobierania z jednego punktu do drugiego, ta trasa będzie obiektem wartości. Byłaby to migawka punktów na określonej trasie, ale sugerowana trasa nie będzie miała tożsamości, mimo że wewnętrznie może odnosić się do jednostek, takich jak City, Road itp.
Rysunek 7–13 przedstawia obiekt wartości Address w agregacji Order.
Rysunek 7–13. Obiekt wartości adresu w agregacji Order
Jak pokazano na rysunku 7–13, jednostka zwykle składa się z wielu atrybutów. Na przykład Order
jednostka może być modelowana jako jednostka z tożsamością i składać się wewnętrznie z zestawu atrybutów, takich jak OrderId, OrderDate, OrderItems itp. Ale adres, który jest po prostu złożoną wartością składającą się z kraju/regionu, ulicy, miasta itp., i nie ma tożsamości w tej domenie, musi być modelowany i traktowany jako obiekt wartości.
Ważne cechy obiektów wartości
Istnieją dwie główne cechy obiektów wartości:
Nie mają tożsamości.
Są one niezmienne.
Pierwsza cecha została już omówiona. Niezmienność jest ważnym wymaganiem. Wartości obiektu wartości muszą być niezmienne po utworzeniu obiektu. W związku z tym podczas konstruowania obiektu należy podać wymagane wartości, ale nie można zezwolić na ich zmianę w okresie istnienia obiektu.
Obiekty wartości umożliwiają wykonywanie pewnych sztuczek na wydajność dzięki niezmiennej naturze. Dotyczy to szczególnie systemów, w których mogą istnieć tysiące wystąpień obiektów wartości, z których wiele ma te same wartości. Ich niezmienny charakter pozwala na ich ponowne użycie; mogą być obiektami zamiennymi, ponieważ ich wartości są takie same i nie mają tożsamości. Ten typ optymalizacji może czasami mieć różnicę między oprogramowaniem, które działa wolno, a oprogramowaniem o dobrej wydajności. Oczywiście wszystkie te przypadki zależą od środowiska aplikacji i kontekstu wdrożenia.
Implementacja obiektu wartości w języku C#
Jeśli chodzi o implementację, można mieć klasę bazową obiektu wartości, która ma podstawowe metody narzędziowe, takie jak równość, na podstawie porównania wszystkich atrybutów (ponieważ obiekt wartości nie może być oparty na tożsamości) i innych podstawowych cech. W poniższym przykładzie przedstawiono klasę bazową obiektu wartości używaną w mikrousłudze porządkowania z modułów eShopOnContainers.
public abstract class ValueObject
{
protected static bool EqualOperator(ValueObject left, ValueObject right)
{
if (ReferenceEquals(left, null) ^ ReferenceEquals(right, null))
{
return false;
}
return ReferenceEquals(left, right) || left.Equals(right);
}
protected static bool NotEqualOperator(ValueObject left, ValueObject right)
{
return !(EqualOperator(left, right));
}
protected abstract IEnumerable<object> GetEqualityComponents();
public override bool Equals(object obj)
{
if (obj == null || obj.GetType() != GetType())
{
return false;
}
var other = (ValueObject)obj;
return this.GetEqualityComponents().SequenceEqual(other.GetEqualityComponents());
}
public override int GetHashCode()
{
return GetEqualityComponents()
.Select(x => x != null ? x.GetHashCode() : 0)
.Aggregate((x, y) => x ^ y);
}
// Other utility methods
}
Jest ValueObject
to abstract class
typ, ale w tym przykładzie nie przeciąża ==
operatorów i !=
. Możesz to zrobić, co deleguje porównania do Equals
przesłonięcia. Rozważmy na przykład następujące przeciążenia operatora dla ValueObject
typu:
public static bool operator ==(ValueObject one, ValueObject two)
{
return EqualOperator(one, two);
}
public static bool operator !=(ValueObject one, ValueObject two)
{
return NotEqualOperator(one, two);
}
Tej klasy można użyć podczas implementowania rzeczywistego Address
obiektu wartości, tak jak w przypadku obiektu wartości pokazanego w poniższym przykładzie:
public class Address : ValueObject
{
public String Street { get; private set; }
public String City { get; private set; }
public String State { get; private set; }
public String Country { get; private set; }
public String ZipCode { get; private set; }
public Address() { }
public Address(string street, string city, string state, string country, string zipcode)
{
Street = street;
City = city;
State = state;
Country = country;
ZipCode = zipcode;
}
protected override IEnumerable<object> GetEqualityComponents()
{
// Using a yield return statement to return each element one at a time
yield return Street;
yield return City;
yield return State;
yield return Country;
yield return ZipCode;
}
}
Ta implementacja Address
obiektu wartości nie ma tożsamości i dlatego nie zdefiniowano dla niego pola IDENTYFIKATORa w Address
definicji klasy lub ValueObject
definicji klasy.
Brak pola identyfikatora w klasie do użycia przez program Entity Framework (EF) nie był możliwy do czasu, gdy program EF Core 2.0 znacznie ułatwia implementowanie obiektów o lepszej wartości bez identyfikatora. To właśnie wyjaśnienie następnej sekcji.
Można argumentować, że obiekty wartości, które są niezmienne, powinny być tylko do odczytu (czyli mają właściwości tylko do pobrania) i to rzeczywiście prawda. Jednak obiekty wartości są zwykle serializowane i deserializowane w celu przechodzenia przez kolejki komunikatów i są zatrzymywane tylko do odczytu, aby deserializator przypisał wartości, więc wystarczy pozostawić je jako private set
, co jest wystarczające do odczytu, aby być praktyczne.
Semantyka porównania obiektów wartości
Dwa wystąpienia Address
typu można porównać przy użyciu wszystkich następujących metod:
var one = new Address("1 Microsoft Way", "Redmond", "WA", "US", "98052");
var two = new Address("1 Microsoft Way", "Redmond", "WA", "US", "98052");
Console.WriteLine(EqualityComparer<Address>.Default.Equals(one, two)); // True
Console.WriteLine(object.Equals(one, two)); // True
Console.WriteLine(one.Equals(two)); // True
Console.WriteLine(one == two); // True
Gdy wszystkie wartości są takie same, porównania są poprawnie oceniane jako true
. Jeśli nie wybrano przeciążenia ==
operatorów i !=
, ostatnie porównanie wartości zostanie obliczone one == two
jako false
. Aby uzyskać więcej informacji, zobacz Przeciążenie operatorów równości valueObject.
Jak utrwalać obiekty wartości w bazie danych za pomocą programu EF Core 2.0 lub nowszego
Pokazano, jak zdefiniować obiekt wartości w modelu domeny. Ale jak można rzeczywiście utrwalić go w bazie danych przy użyciu programu Entity Framework Core, ponieważ zwykle jest ona przeznaczona dla jednostek z tożsamością?
Podstawowe i starsze podejścia korzystające z programu EF Core 1.1
W tle ograniczenie w przypadku korzystania z programów EF Core 1.0 i 1.1 było to, że nie można używać typów złożonych zgodnie z definicją w programie EF 6.x w tradycyjnym programie .NET Framework. W związku z tym w przypadku korzystania z programu EF Core 1.0 lub 1.1 należy przechowywać obiekt wartości jako jednostkę EF z polem identyfikatora. Następnie wyglądało to bardziej jak obiekt wartości bez tożsamości, można ukryć jego identyfikator, aby wyjaśnić, że tożsamość obiektu wartości nie jest ważna w modelu domeny. Możesz ukryć ten identyfikator przy użyciu identyfikatora jako właściwości w tle. Ponieważ ta konfiguracja ukrywania identyfikatora w modelu jest skonfigurowana na poziomie infrastruktury EF, byłaby ona rodzajem przezroczystości dla modelu domeny.
W początkowej wersji aplikacji eShopOnContainers (.NET Core 1.1) ukryty identyfikator wymagany przez infrastrukturę platformy EF Core został zaimplementowany w następujący sposób na poziomie DbContext przy użyciu interfejsu API Fluent w projekcie infrastruktury. W związku z tym identyfikator był ukryty z punktu widzenia modelu domeny, ale nadal obecny w infrastrukturze.
// Old approach with EF Core 1.1
// Fluent API within the OrderingContext:DbContext in the Infrastructure project
void ConfigureAddress(EntityTypeBuilder<Address> addressConfiguration)
{
addressConfiguration.ToTable("address", DEFAULT_SCHEMA);
addressConfiguration.Property<int>("Id") // Id is a shadow property
.IsRequired();
addressConfiguration.HasKey("Id"); // Id is a shadow property
}
Jednak trwałość tego obiektu wartości w bazie danych została wykonana jak zwykła jednostka w innej tabeli.
W przypadku programu EF Core 2.0 lub nowszego istnieją nowe i lepsze sposoby utrwalania obiektów wartości.
Utrwalanie obiektów wartości jako należących do niego typów jednostek w programie EF Core 2.0 lub nowszym
Nawet w przypadku niektórych luk między wzorcem obiektu wartości kanonicznej w DDD a typem jednostki należącej do użytkownika w programie EF Core jest obecnie najlepszym sposobem utrwalania obiektów wartości za pomocą programu EF Core 2.0 lub nowszego. Ograniczenia można zobaczyć na końcu tej sekcji.
Funkcja typu jednostki własności została dodana do platformy EF Core od wersji 2.0.
Typ jednostki należącej umożliwia mapowanie typów, które nie mają własnej tożsamości jawnie zdefiniowanej w modelu domeny i są używane jako właściwości, takie jak obiekt wartości, w ramach dowolnej jednostki. Typ jednostki będącej własnością współudzieli ten sam typ CLR z innym typem jednostki (czyli jest to zwykła klasa). Jednostka zawierająca definiowaną nawigację jest jednostką właściciela. Podczas wykonywania zapytań względem właściciela domyślnie są uwzględniane typy należące do użytkownika.
Po prostu patrząc na model domeny, typ własności wygląda tak, jakby nie miał żadnej tożsamości. Jednak w ramach okładek typy własności mają tożsamość, ale właściwość nawigacji właściciela jest częścią tej tożsamości.
Tożsamość wystąpień typów należących do użytkownika nie jest całkowicie własna. Składa się z trzech składników:
Tożsamość właściciela
Właściwość nawigacji wskazująca je
W przypadku kolekcji typów będących własnością niezależny składnik (obsługiwany w programie EF Core 2.2 lub nowszym).
Na przykład w modelu domeny Ordering w elemencie eShopOnContainers w ramach jednostki Order obiekt wartości Address jest implementowany jako należący typ jednostki w jednostce właściciela, która jest jednostką Order. Address
jest typem bez właściwości tożsamości zdefiniowanej w modelu domeny. Jest on używany jako właściwość typu Zamówienie, aby określić adres wysyłki dla określonego zamówienia.
Zgodnie z konwencją klucz podstawowy w tle jest tworzony dla typu należącego do użytkownika i zostanie zamapowany na tę samą tabelę co właściciel przy użyciu podziału tabeli. Dzięki temu można używać typów należących podobnie do tego, jak złożone typy są używane w programie EF6 w tradycyjnym programie .NET Framework.
Należy pamiętać, że typy własności nigdy nie są odnajdywane zgodnie z konwencją w programie EF Core, dlatego należy je jawnie zadeklarować.
W pliku OrderingContext.cs w pliku OnModelCreating()
eShopOnContainers stosowane jest wiele konfiguracji infrastruktury. Jedna z nich jest powiązana z jednostką Order.
// Part of the OrderingContext.cs class at the Ordering.Infrastructure project
//
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfiguration(new ClientRequestEntityTypeConfiguration());
modelBuilder.ApplyConfiguration(new PaymentMethodEntityTypeConfiguration());
modelBuilder.ApplyConfiguration(new OrderEntityTypeConfiguration());
modelBuilder.ApplyConfiguration(new OrderItemEntityTypeConfiguration());
//...Additional type configurations
}
W poniższym kodzie infrastruktura trwałości jest definiowana dla jednostki Order:
// Part of the OrderEntityTypeConfiguration.cs class
//
public void Configure(EntityTypeBuilder<Order> orderConfiguration)
{
orderConfiguration.ToTable("orders", OrderingContext.DEFAULT_SCHEMA);
orderConfiguration.HasKey(o => o.Id);
orderConfiguration.Ignore(b => b.DomainEvents);
orderConfiguration.Property(o => o.Id)
.ForSqlServerUseSequenceHiLo("orderseq", OrderingContext.DEFAULT_SCHEMA);
//Address value object persisted as owned entity in EF Core 2.0
orderConfiguration.OwnsOne(o => o.Address);
orderConfiguration.Property<DateTime>("OrderDate").IsRequired();
//...Additional validations, constraints and code...
//...
}
W poprzednim kodzie metoda określa, orderConfiguration.OwnsOne(o => o.Address)
że Address
właściwość jest własnością jednostki Order
typu.
Domyślnie konwencje platformy EF Core nazywają kolumny bazy danych właściwościami należącego typu jednostki jako EntityProperty_OwnedEntityProperty
. W związku z tym wewnętrzne właściwości Address
funkcji będą wyświetlane w Orders
tabeli o nazwach Address_Street
, Address_City
(itd. dla State
, Country
i ZipCode
).
Możesz dołączyć płynną metodę Property().HasColumnName()
, aby zmienić nazwy tych kolumn. W przypadku, gdy Address
jest właściwością publiczną, mapowania będą podobne do następujących:
orderConfiguration.OwnsOne(p => p.Address)
.Property(p=>p.Street).HasColumnName("ShippingStreet");
orderConfiguration.OwnsOne(p => p.Address)
.Property(p=>p.City).HasColumnName("ShippingCity");
Istnieje możliwość utworzenia łańcucha OwnsOne
metody w płynnym mapowaniu. W poniższym hipotetycznym przykładzie OrderDetails
jest właścicielem BillingAddress
i ShippingAddress
, które są oba Address
typy. Następnie OrderDetails
jest własnością Order
typu.
orderConfiguration.OwnsOne(p => p.OrderDetails, cb =>
{
cb.OwnsOne(c => c.BillingAddress);
cb.OwnsOne(c => c.ShippingAddress);
});
//...
//...
public class Order
{
public int Id { get; set; }
public OrderDetails OrderDetails { get; set; }
}
public class OrderDetails
{
public Address BillingAddress { get; set; }
public Address ShippingAddress { get; set; }
}
public class Address
{
public string Street { get; set; }
public string City { get; set; }
}
Dodatkowe szczegóły dotyczące typów jednostek należących do użytkownika
Typy własności są definiowane podczas konfigurowania właściwości nawigacji do określonego typu przy użyciu interfejsu API OwnsOne fluent.
Definicja typu należącego do naszego modelu metadanych jest złożona z: typu właściciela, właściwości nawigacji i typu CLR typu należącego do użytkownika.
Tożsamość (klucz) należącego wystąpienia typu w naszym stosie jest złożona z tożsamości typu właściciela i definicji typu należącego do użytkownika.
Możliwości jednostek będących własnością
Typy należące do nich mogą odwoływać się do innych jednostek, należących do innych (zagnieżdżonych typów należących) lub nienależących do nich (zwykłych właściwości nawigacji odwołania do innych jednostek).
Można mapować ten sam typ CLR co różne typy należące do tej samej jednostki właściciela za pomocą oddzielnych właściwości nawigacji.
Dzielenie tabeli jest konfigurowane zgodnie z konwencją, ale można zrezygnować z mapowania typu należącego do innej tabeli przy użyciu tabeli ToTable.
Chętne ładowanie jest wykonywane automatycznie na należących do użytkownika typach, czyli nie ma potrzeby wywoływania
.Include()
zapytania.Można skonfigurować za pomocą atrybutu
[Owned]
, przy użyciu programu EF Core 2.1 lub nowszego.Może obsługiwać kolekcje typów należących do użytkownika (przy użyciu wersji 2.2 lub nowszej).
Ograniczenia jednostek będących własnością
Nie można utworzyć
DbSet<T>
typu należącego (zgodnie z projektem).Nie można wywołać
ModelBuilder.Entity<T>()
typów należących do użytkownika (obecnie według projektu).Brak obsługi opcjonalnych (czyli dopuszczających wartość null) typów należących do właściciela w tej samej tabeli (czyli przy użyciu dzielenia tabel). Jest to spowodowane tym, że mapowanie jest wykonywane dla każdej właściwości, nie ma oddzielnego sentinela dla wartości złożonej o wartości null jako całości.
Brak obsługi mapowania dziedziczenia dla typów należących do właścicieli, ale powinno być możliwe mapowanie dwóch typów liści w tych samych hierarchiach dziedziczenia co różne typy własności. Program EF Core nie będzie wnioskował o tym, że są one częścią tej samej hierarchii.
Główne różnice w typach złożonych ef6
- Dzielenie tabeli jest opcjonalne, czyli opcjonalnie można je mapować na oddzielną tabelę i nadal być własnością typów.
Dodatkowe zasoby
Martin Fowler. Wzorzec valueObject
https://martinfowler.com/bliki/ValueObject.htmlEric Evans. Projektowanie oparte na domenie: walka ze złożonością w samym sercu oprogramowania. (Książka; zawiera dyskusję na temat obiektów wartości)
https://www.amazon.com/Domain-Driven-Design-Tackling-Complexity-Software/dp/0321125215/Vaughn Vernon. Implementowanie projektowania opartego na domenie. (Książka; zawiera dyskusję na temat obiektów wartości)
https://www.amazon.com/Implementing-Domain-Driven-Design-Vaughn-Vernon/dp/0321834577/Posiadane typy jednostek
https://learn.microsoft.com/ef/core/modeling/owned-entitiesWłaściwości w tle
https://learn.microsoft.com/ef/core/modeling/shadow-propertiesTypy złożone i/lub obiekty wartości. Dyskusja w repozytorium GitHub platformy EF Core (karta Problemy)
https://github.com/dotnet/efcore/issues/246ValueObject.cs. Klasa obiektów wartości bazowej w eShopOnContainers.
https://github.com/dotnet-architecture/eShopOnContainers/blob/dev/src/Services/Ordering/Ordering.Domain/SeedWork/ValueObject.csValueObject.cs. Klasa obiektów wartości bazowej w CSharpFunctionalExtensions.
https://github.com/vkhorikov/CSharpFunctionalExtensions/blob/master/CSharpFunctionalExtensions/ValueObject/ValueObject.csKlasa adresu. Przykładowa klasa obiektu wartości w eShopOnContainers.
https://github.com/dotnet-architecture/eShopOnContainers/blob/dev/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/Address.cs