Sdílet prostřednictvím


Implementace objektů hodnot

Tip

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.

.NET Microservices Architecture for Containerized .NET Applications eBook cover thumbnail.

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 agregaci Order.

Diagram showing the Address value-object inside the Order Aggregate.

Obrázek 7–13 Objekt hodnoty adresy v agregaci Order

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 ValueObject o abstract class typ, ale v tomto příkladu nepřetěžuje operátory == a != operátory. Můžete se rozhodnout, že to uděláte tak, že delegujete porovná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.

Pole ID ve třídě, které by bylo možné používat v Entity Frameworku (EF), nebylo možné, dokud EF Core 2.0, což výrazně pomáhá implementovat objekty s lepší hodnotou 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 tak, aby procházely fronty zpráv, a být jen pro čtení zastaví deserializátor přiřazování hodnot, takže je prostě necháte jako private set, což je dostatečně pro čtení stačí k tomu, aby bylo praktické.

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 přetížit operátory == a != operátory, poslední porovnání one == two by se vyhodnocovalo jako false. Další informace naleznete v tématu Přetížení ValueObject rovnosti operátorů.

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?

Použití EF Core 1.1 na pozadí a starších přístupů

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á. Toto ID můžete skrýt pomocí ID jako stínové vlastnosti. Vzhledem k tomu, že tato konfigurace pro skrytí ID v modelu je nastavená na úrovni infrastruktury EF, bude pro váš doménový model druh transparentní.

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
}

Trvalost tohoto objektu hodnoty do databáze byla však provedena jako běžná entita v jiné tabulce.

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 v případě některých mezer mezi vzorem objektu kanonické hodnoty v DDD a typem vlastněné entity v EF Core je v současné době nejlepším způsobem, jak zachovat objekty hodnot s EF Core 2.0 a novějším. 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 ve výchozím nastavení zahrnuté vlastněné typy.

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í vlastněných typů není úplně 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 nezávislá komponenta (podporovaná v EF Core 2.2 a novější).

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 určuje, orderConfiguration.OwnsOne(o => o.Address) že Address vlastnost je vlastněná entita Order typu.

Ve výchozím nastavení ef Core konvence pojmenováují 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, Countrya ZipCode).

K přejmenování těchto sloupců můžete připojit metodu Property().HasColumnName() fluent. V případě Address veřejné vlastnosti 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");

Metodu je možné zřetězení OwnsOne v plynulém 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 ve stejné entitě vlastníka prostřednictvím samostatný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.

  • Načítání dychtivých dat se provádí automaticky u vlastněných typů, to znamená, že dotaz nemusí volat .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 ModelBuilder.Entity<T>() vlastněné typy (aktuálně záměrně).

  • Nepovinný typ vlastněný (to znamená nullable), které jsou mapované s vlastníkem ve stejné tabulce (to znamená pomocí rozdělení tabulky). Důvodem je to, že mapování se provádí pro každou vlastnost, neexistuje žádná samostatná sentinel pro komplexní hodnotu null jako celek.

  • Žá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 ve složitých typech EF6

  • Rozdělení tabulky je volitelné, to znamená, že je možné je namapovat na samostatnou tabulku a stále být vlastněné typy.

Další materiály