Poznámka
Přístup k této stránce vyžaduje autorizaci. Můžete se zkusit přihlásit nebo změnit adresáře.
Přístup k této stránce vyžaduje autorizaci. Můžete zkusit změnit adresáře.
Návod
Tento obsah je výňatek z eBooku, architektury mikroslužeb .NET pro kontejnerizované aplikace .NET, které jsou k dispozici na .NET Docs nebo jako zdarma ke stažení PDF, které lze číst offline.
Jak je popsáno v předchozích částech o entitách a agregaci, identita je pro entity zásadní. V systému však existuje mnoho objektů a datových položek, které nevyžadují sledování identity a identity, například objekty hodnot.
Objekt hodnoty může odkazovat na jiné entity. Například v aplikaci, která generuje trasu, která popisuje, jak získat z jednoho bodu do druhého, by tato trasa byla objektem hodnoty. Jedná se o snímek bodů na konkrétní trase, ale tato navrhovaná trasa by neměla identitu, i když interně může odkazovat na entity, jako je Město, Silnice atd.
Obrázek 7–13 znázorňuje objekt hodnoty adresy v rámci agregátu Order.
Diagram znázorňující objekt hodnoty adresy uvnitř agregátu objednávky.
Obrázek 7–13 Objekt hodnoty adresy v agregátu objednávky
Jak je znázorněno na obrázku 7–13, entita se obvykle skládá z více atributů. Entitu Order
lze například modelovat jako entitu s identitou a interně se skládat ze sady atributů, jako jsou OrderId, OrderDate, OrderItems atd. Adresa, která je jednoduše složitá hodnota složená ze země/oblasti, ulice, města atd., a nemá v této doméně žádnou identitu, se musí modelovat a považovat za objekt hodnoty.
Důležité charakteristiky objektů hodnot
Objekty hodnot mají dvě hlavní charakteristiky:
Nemají žádnou identitu.
Jsou neměnné.
První charakteristika byla již popsána. Neměnnost je důležitým požadavkem. Po vytvoření objektu musí být hodnoty objektu hodnoty neměnné. Proto při vytváření objektu je nutné zadat požadované hodnoty, ale nesmíte jim povolit změnu během životnosti objektu.
Objekty hodnot umožňují provádět určité triky pro výkon, díky jejich neměnné povaze. To platí zejména v systémech, kde mohou existovat tisíce instancí objektů hodnot, z nichž mnohé mají stejné hodnoty. Jejich neměnná povaha umožňuje jejich opakované použití; mohou být zaměnitelné objekty, protože jejich hodnoty jsou stejné a nemají žádnou identitu. Tento typ optimalizace může někdy mít rozdíl mezi softwarem, který běží pomalu a software s dobrým výkonem. Všechny tyto případy samozřejmě závisejí na prostředí aplikace a kontextu nasazení.
Implementace objektu hodnoty v jazyce C#
Z hlediska implementace můžete mít základní třídu objektu hodnoty, která má základní metody utility, jako je rovnost na základě porovnání mezi všemi atributy (protože objekt hodnoty nesmí být založen na identitě) a další základní charakteristiky. Následující příklad ukazuje základní třídu objektu hodnoty použitou v objednávání mikroslužby z 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
}
Jedná se o ValueObject
typu abstract class
, ale v tomto příkladu nepřetěžuje operátory ==
a !=
. Můžete se rozhodnout to udělat tak, že delegujete porovnávání na přepsání Equals
. Představte si například následující přetížení operátoru typu ValueObject
:
public static bool operator ==(ValueObject one, ValueObject two)
{
return EqualOperator(one, two);
}
public static bool operator !=(ValueObject one, ValueObject two)
{
return NotEqualOperator(one, two);
}
Tuto třídu můžete použít při implementaci skutečného objektu hodnoty, stejně jako u objektu Address
hodnoty zobrazeného v následujícím příkladu:
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;
}
}
Tato implementace objektu Address
hodnoty nemá žádnou identitu, a proto pro ni není definováno žádné pole ID, a to buď v Address
definici třídy, nebo ValueObject
v definici třídy.
Ve třídě nebylo možné nemít pole ID pro použití v Entity Frameworku (EF) až do verze EF Core 2.0, což výrazně pomáhá implementovat lepší hodnotové objekty bez ID. To je přesně vysvětlení další části.
Bylo by možné argumentovat, že objekty hodnot, které jsou neměnné, by měly být jen pro čtení (tj. mají pouze get-only vlastnosti) a to je skutečně pravda. Objekty hodnot jsou však obvykle serializovány a deserializovány, aby mohly procházet frontami zpráv. Když jsou určeny pouze pro čtení, deserializátor nemůže přiřadit hodnoty, takže je jednoduše ponecháte jako private set
, což je praktické díky dostatečné úrovni pro čtení.
Sémantika porovnání objektů hodnot
Dvě instance Address
typu lze porovnat pomocí všech následujících 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
Pokud jsou všechny hodnoty stejné, porovnání se správně vyhodnotí jako true
. Pokud jste se rozhodli nepřetížit operátory ==
a !=
, pak by se poslední porovnání one == two
vyhodnotilo jako false
. Další informace naleznete v tématu Přetížení operátorů rovnosti pro ValueObject.
Jak zachovat objekty hodnot v databázi pomocí EF Core 2.0 a novější
Právě jste viděli, jak definovat objekt hodnoty ve vašem doménovém modelu. Jak ji ale můžete skutečně zachovat v databázi pomocí Entity Framework Core, protože obvykle cílí na entity s identitou?
Historie a starší přístupy s využitím EF Core 1.1
Omezení při použití EF Core 1.0 a 1.1 na pozadí bylo, že v tradičním rozhraní .NET Framework nebylo možné používat složité typy definované v EF 6.x. Proto pokud používáte EF Core 1.0 nebo 1.1, potřebujete uložit objekt hodnoty jako entitu EF s polem ID. Pak to vypadalo spíše jako objekt hodnoty bez identity, můžete skrýt jeho ID, abyste jasně dokázali, že identita objektu hodnoty není v doménovém modelu důležitá. ID můžete skrýt jako stínovou vlastnost. Vzhledem k tomu, že tato konfigurace pro skrytí ID v modelu je nastavena na úrovni infrastruktury EF, bude pro váš doménový model víceméně neviditelná.
V počáteční verzi eShopOnContainers (.NET Core 1.1) bylo skryté ID potřebné infrastrukturou EF Core implementováno následujícím způsobem na úrovni DbContext pomocí rozhraní Fluent API v projektu infrastruktury. PROTO id bylo skryto z pohledu doménového modelu, ale stále se nachází v infrastruktuře.
// 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
}
Ukládání tohoto objektu hodnoty do databáze bylo provedeno stejně jako běžná entita, ale do jiné tabulky.
S EF Core 2.0 a novějšími existují nové a lepší způsoby, jak zachovat objekty hodnot.
Zachování objektů hodnot jako vlastněných typů entit v EF Core 2.0 a novějších verzích
I přes určité nesrovnalosti mezi vzorem hodnotového objektu v DDD (Domain-Driven Design) a typem vlastněné entity v EF Core je nyní nejlepší metodou, jak uchovávat hodnotové objekty pomocí EF Core 2.0 a novějšího. Omezení se zobrazí na konci této části.
Funkce typu vlastněné entity byla přidána do EF Core od verze 2.0.
Typ vlastněné entity umožňuje mapovat typy, které nemají vlastní identitu explicitně definovanou v doménovém modelu a používají se jako vlastnosti, jako je například objekt hodnoty, v rámci libovolné entity. Typ vlastněné entity sdílí stejný typ CLR s jiným typem entity (to znamená jen běžnou třídou). Entita obsahující definici navigace je entita vlastníka. Při dotazování vlastníka jsou vlastněné typy automaticky zahrnuty ve výchozím nastavení.
Když se podíváte na doménový model, vlastněný typ vypadá, jako by neměl žádnou identitu. V rámci krytů však vlastní typy mají identitu, ale vlastnost navigace vlastníka je součástí této identity.
Identita instancí typů ve vlastnictví není úplně jejich vlastní. Skládá se ze tří součástí:
Identita vlastníka
Navigační vlastnost odkazující na ně
V případě kolekcí vlastněných typů je zavedena nezávislá komponenta (podporovaná v EF Core 2.2 a novějších verzích).
Například v modelu domény Ordering u eShopOnContainers jako součást entity Order je objekt Hodnota adresy implementován jako vlastněný typ entity v rámci entity vlastníka, což je entita Order.
Address
je typ bez vlastnosti identity definované v doménovém modelu. Slouží jako vlastnost typu Objednávka k určení dodací adresy pro určitou objednávku.
Podle konvence se vytvoří stínový primární klíč pro vlastněný typ a bude mapován na stejnou tabulku jako vlastník pomocí rozdělení tabulky. To umožňuje používat vlastněné typy podobně jako způsob použití složitých typů v EF6 v tradičním rozhraní .NET Framework.
Je důležité si uvědomit, že vlastněné typy nejsou nikdy zjištěny konvencí v EF Core, takže je musíte deklarovat explicitně.
V eShopOnContainers se v souboru OrderingContext.cs v rámci OnModelCreating()
metody použije více konfigurací infrastruktury. Jeden z nich souvisí s entitou Order(Objednávka).
// 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
}
V následujícím kódu je pro entitu Order definována infrastruktura trvalosti:
// 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...
//...
}
V předchozím kódu metoda orderConfiguration.OwnsOne(o => o.Address)
určuje, že vlastnost Address
je entita vlastněná typu Order
.
Ve výchozím nastavení EF Core konvence pojmenovávají sloupce databáze pro vlastnosti vlastněného typu entity jako EntityProperty_OwnedEntityProperty
. Vnitřní vlastnosti Address
se proto zobrazí v Orders
tabulce s názvy Address_Street
( Address_City
a tak dále pro State
, Country
a ZipCode
).
K přejmenování těchto sloupců můžete připojit metodu Property().HasColumnName()
fluent. V případě, že Address
je veřejnou vlastností, by mapování vypadalo takto:
orderConfiguration.OwnsOne(p => p.Address)
.Property(p=>p.Street).HasColumnName("ShippingStreet");
orderConfiguration.OwnsOne(p => p.Address)
.Property(p=>p.City).HasColumnName("ShippingCity");
Je možné zřetězit metodu OwnsOne
v rámci plynulého mapování. V následujícím hypotetickém příkladu OrderDetails
vlastní BillingAddress
a ShippingAddress
, které jsou oba Address
typy. Potom OrderDetails
je vlastníkem 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; }
}
Další podrobnosti o typech vlastněných entit
Vlastní typy jsou definovány při konfiguraci navigační vlastnosti na konkrétní typ pomocí rozhraní API Fluent OwnsOne.
Definice vlastněného typu v našem modelu metadat je složená z typu vlastníka, navigační vlastnosti a typu CLR vlastněného typu.
Identita (klíč) instance vlastněného typu v našem zásobníku je složená z identity typu vlastníka a definice vlastněného typu.
Možnosti vlastněných entit
Vlastněné typy můžou odkazovat na jiné entity, vlastněné (vnořené typy) nebo nevlastní (běžné odkazové navigační vlastnosti na jiné entity).
Stejný typ CLR můžete mapovat jako různé vlastněné typy v rámci téže vlastnické entity prostřednictvím oddělených navigačních vlastností.
Rozdělení tabulek je nastavené podle konvence, ale můžete se odhlásit mapováním vlastněného typu na jinou tabulku pomocí ToTable.
Nenasytné načítání se provádí automaticky u vlastněných typů, to znamená, že není potřeba volat dotaz pomocí
.Include()
.Lze nakonfigurovat s atributem
[Owned]
, pomocí EF Core 2.1 a novější.Může zpracovávat kolekce vlastněných typů (verze 2.2 a novější).
Omezení vlastněných entit
Nemůžete vytvořit
DbSet<T>
vlastní typ (podle návrhu).Nemůžete volat na typy, které vlastníte (aktuálně podle návrhu).
Nepodporuje nepovinné (tj. nullable) vlastněné typy, které jsou mapované s vlastníkem ve stejné tabulce (tj. pomocí dělení tabulky). Důvodem je to, že mapování se provádí pro každou vlastnost, a neexistuje žádný samostatný strážce pro celkovou komplexní hodnotu null.
Žádná podpora mapování dědičnosti pro vlastněné typy, ale měli byste být schopni mapovat dva typy listových hierarchií stejné dědičnosti jako různé vlastněné typy. EF Core nezdůvodní skutečnost, že jsou součástí stejné hierarchie.
Hlavní rozdíly oproti složitým typům EF6
- Rozdělení tabulky je volitelné, to znamená, že je možné je přiřadit na samostatnou tabulku a přesto zůstávají vlastněnými typy.
Dodatečné zdroje
Martin Fowler. Vzorec ValueObject
https://martinfowler.com/bliki/ValueObject.htmlEric Evans. Domain-Driven návrh: řešení složitosti v srdci softwaru. (Kniha; obsahuje diskuzi o objektech hodnot)
https://www.amazon.com/Domain-Driven-Design-Tackling-Complexity-Software/dp/0321125215/Vaughn Vernon. Implementace návrhu Domain-Driven (Kniha; obsahuje diskuzi o objektech hodnot)
https://www.amazon.com/Implementing-Domain-Driven-Design-Vaughn-Vernon/dp/0321834577/Vlastněné typy entit
https://learn.microsoft.com/ef/core/modeling/owned-entitiesStínové vlastnosti
https://learn.microsoft.com/ef/core/modeling/shadow-propertiesKomplexní typy a/nebo objekty hodnot. Diskuze v úložišti GITHubu EF Core (karta Problémy)
https://github.com/dotnet/efcore/issues/246ValueObject.cs. Třída objektu základní hodnoty v eShopOnContainers.
https://github.com/dotnet-architecture/eShopOnContainers/blob/dev/src/Services/Ordering/Ordering.Domain/SeedWork/ValueObject.csValueObject.cs. Třída objektu základní hodnoty v CSharpFunctionalExtensions.
https://github.com/vkhorikov/CSharpFunctionalExtensions/blob/master/CSharpFunctionalExtensions/ValueObject/ValueObject.csTřída adresy. Ukázková třída objektu hodnoty v eShopOnContainers.
https://github.com/dotnet-architecture/eShopOnContainers/blob/dev/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/Address.cs