Freigeben über


Implementieren von Wertobjekten

Tipp

Dieser Inhalt ist ein Auszug aus dem eBook .NET Microservices Architecture for Containerized .NET Applications, verfügbar auf .NET Docs oder als kostenlose herunterladbare PDF, die offline gelesen werden kann.

.NET Microservices-Architektur für containerisierte .NET-Anwendungen eBook-Cover-Thumbnail.

Wie in früheren Abschnitten zu Entitäten und Aggregaten erläutert, ist die Identität für Entitäten grundlegend. Es gibt jedoch viele Objekte und Datenelemente in einem System, die keine Identitäts- und Identitätsnachverfolgung erfordern, z. B. Wertobjekte.

Ein Wertobjekt kann auf andere Entitäten verweisen. Beispiel: In einer Anwendung, die eine Route generiert, die beschreibt, wie sie von einem Punkt zu einem anderen abgerufen werden kann, wäre diese Route ein Wertobjekt. Es wäre eine Momentaufnahme von Punkten auf einer bestimmten Route, aber diese vorgeschlagene Route hätte keine Identität, obwohl sie intern auf Entitäten wie Stadt, Straße usw. verweisen könnte.

In Abbildung 7-13 wird das Wertobjekt „Address“ im Aggregat „Order“ angezeigt.

Diagramm, das das Address-Wertobjekt innerhalb des Order Aggregates zeigt.

Abbildung 7-13. Wertobjekt „Address“ im Aggregat „Order“

Wie in Abbildung 7-13 dargestellt, besteht eine Entität in der Regel aus mehreren Attributen. Beispielsweise kann die Order Entität als Entität mit einer Identität modelliert und intern aus einer Gruppe von Attributen wie OrderId, OrderDate, OrderItems usw. zusammengesetzt werden. Aber die Adresse, die einfach ein komplexer Wert ist, der aus Land/Region, Straße, Stadt usw. besteht und keine Identität in dieser Domäne hat, muss modelliert und als Wertobjekt behandelt werden.

Wichtige Merkmale von Wertobjekten

Es gibt zwei Hauptmerkmale für Wertobjekte:

  • Sie haben keine Identität.

  • Sie sind unveränderlich.

Das erste Merkmal wurde bereits diskutiert. Unveränderlichkeit ist eine wichtige Voraussetzung. Die Werte eines Wertobjekts müssen unveränderlich sein, nachdem das Objekt erstellt wurde. Wenn das Objekt erstellt wird, müssen Sie daher die erforderlichen Werte angeben, sie dürfen jedoch nicht während der Lebensdauer des Objekts geändert werden.

Wertobjekte ermöglichen es Ihnen, bestimmte Tricks für die Leistung durchzuführen, dank ihrer unveränderlichen Natur. Dies gilt insbesondere für Systeme, in denen Tausende von Wertobjektinstanzen vorhanden sein können, von denen viele dieselben Werte aufweisen. Ihre unveränderliche Natur ermöglicht es ihnen, wiederverwendet zu werden; sie können austauschbare Objekte sein, da ihre Werte identisch sind und keine Identität haben. Diese Art von Optimierung kann manchmal einen Unterschied zwischen Software machen, die langsam und software mit guter Leistung ausgeführt wird. Natürlich hängen all diese Fälle von der Anwendungsumgebung und dem Bereitstellungskontext ab.

Implementierung des Wertobjekts in C#

Im Hinblick auf die Implementierung können Sie eine Wertobjektbasisklasse mit grundlegenden Hilfsmethoden wie Gleichheit basierend auf dem Vergleich zwischen allen Attributen (da ein Wertobjekt nicht auf Identität basieren darf) und andere grundlegende Merkmale aufweisen. Das folgende Beispiel zeigt eine Wertobjekt-Basisklasse, die im Bestell-Microservice von eShopOnContainers verwendet wird.

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
}

Der ValueObject ist ein abstract class-Typ, aber in diesem Beispiel werden weder die ==- noch die !=-Operatoren überladen. Dies ist jedoch möglich, indem Sie Vergleiche an die Equals-Überschreibung delegieren. Betrachten Sie beispielsweise die folgenden Operatorüberladungen für den ValueObject-Typ:

public static bool operator ==(ValueObject one, ValueObject two)
{
    return EqualOperator(one, two);
}

public static bool operator !=(ValueObject one, ValueObject two)
{
    return NotEqualOperator(one, two);
}

Sie können diese Klasse verwenden, wenn Sie Ihr tatsächliches Wertobjekt implementieren, wie Address das im folgenden Beispiel gezeigte Wertobjekt:

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;
    }
}

Diese Objektimplementierung des Wertobjekts Address hat keine Identität, und daher wird kein ID-Feld dafür definiert, entweder in der Address Klassendefinition oder in der ValueObject Klassendefinition.

Das Fehlen eines ID-Feldes in einer für das Entity Framework (EF) verwendeten Klasse war bis EF Core 2.0 nicht möglich, was erheblich dazu beiträgt, bessere Wertobjekte ohne ID zu implementieren. Das ist genau die Erläuterung des nächsten Abschnitts.

Es ließe sich die Ansicht vertreten, dass Wertobjekte – da sie unveränderlich sind – schreibgeschützte Eigenschaften aufweisen sollten, und dies stimmt tatsächlich. Allerdings werden Wertobjekte in der Regel zum Durchlaufen von Warteschlangen serialisiert und deserialisiert. Wenn sie schreibgeschützt sind, kann das Deserialisierungsprogramm keine Werte zuweisen. Daher behalten Sie die Eigenschaft private set bei, wodurch sie zu einem Maß schreibgeschützt sind, dass sie dennoch verwendet werden können.

Wertobjektvergleichsemantik

Zwei Instanzen des Address Typs können mit allen folgenden Methoden verglichen werden:

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

Wenn alle Werte gleich sind, werden die Vergleiche richtig ausgewertet als true. Wenn Sie nicht gewählt haben, die ==- und !=-Operatoren zu überladen, dann würde der letzte Vergleich von one == two als false ausgewertet werden. Weitere Informationen finden Sie unter Overload ValueObject-Gleichheitsoperatoren.

Speichern von Wertobjekten in der Datenbank mit EF Core 2.0 und höher

Sie haben gerade gesehen, wie Sie ein Wertobjekt in Ihrem Domänenmodell definieren. Aber wie können Sie sie tatsächlich mithilfe von Entity Framework Core in der Datenbank speichern, da sie in der Regel auf Entitäten mit Identität ausgerichtet ist?

Hintergrund und ältere Ansätze mit EF Core 1.1

Zur Einleitung war eine Einschränkung bei Verwendung von EF Core 1.0 und 1.1, dass Sie keine komplexen Typen, so wie sie in EF 6.x im herkömmlichen .NET Framework definiert wurden, verwenden konnten. Wenn Sie daher EF Core 1.0 oder 1.1 verwenden, mussten Sie Ihr Wertobjekt als EF-Entität mit einem ID-Feld speichern. Dann sah es eher wie ein Wertobjekt ohne Identität aus, sie könnten die ID ausblenden, sodass Sie deutlich machen, dass die Identität eines Wertobjekts im Domänenmodell nicht wichtig ist. Sie können diese ID ausblenden, indem Sie die ID als Schatteneigenschaft verwenden. Da diese Konfiguration zum Ausblenden der ID im Modell auf der EF-Infrastrukturebene eingerichtet ist, wäre sie für Ihr Domänenmodell transparent.

In der ersten Version von eShopOnContainers (.NET Core 1.1) wurde die von der EF Core-Infrastruktur verlangte versteckte ID wie folgt auf DbContext-Ebene implementiert. Dafür wurde die Fluent-API im Infrastrukturprojekt verwendet. Daher wurde die ID aus Sicht des Domain-Modells verborgen, aber in der Infrastruktur noch vorhanden.

// 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
}

Die Persistenz dieses Wertobjekts in der Datenbank wurde jedoch wie eine normale Entität in einer anderen Tabelle ausgeführt.

Mit EF Core 2.0 und höher gibt es neue und bessere Methoden zum Speichern von Wertobjekten.

Permanentes Speichern von Wertobjekten als nicht eigenständige Entitätstypen in EF Core 2.0 und höher

Selbst bei einigen Lücken zwischen dem kanonischen Objektmuster in DDD und dem eigenen Entitätstyp in EF Core ist es derzeit die beste Möglichkeit, Wertobjekte mit EF Core 2.0 und höher zu speichern. Sie können Einschränkungen am Ende dieses Abschnitts sehen.

Das eigene Entitätstypenfeature wurde schon mit Version 2.0 von EF Core hinzugefügt.

Mit einem eigenen Entitätstyp können Sie Typen zuordnen, die ihre eigene Identität nicht explizit im Domänenmodell definiert haben und als Eigenschaften verwendet werden, z. B. ein Wertobjekt, innerhalb einer Ihrer Entitäten. Ein nicht eigenständiger Entitätstyp teilt sich denselben CLR-Typ mit einem anderen Entitätstyp (d. h., es handelt sich lediglich um eine reguläre Klasse). Die Entität, die die definierende Navigation enthält, ist die Besitzerentität. Beim Abfragen des Besitzers werden standardmäßig eigene Typen eingeschlossen.

Bei alleiniger Betrachtung des Domänenmodell sieht es so aus, als ob ein nicht eigenständiger Typ keine Identität aufweist. Allerdings verfügen die eigenen Typen im Hintergrund über die Identität, aber die Besitzernavigationseigenschaft ist Teil dieser Identität.

Die Identität von Instanzen von eigenen Typen ist nicht ausschließlich auf diese beschränkt. Sie besteht aus drei Komponenten:

  • Die Identität des Besitzers

  • Der Navigationseigenschaft, die auf diese zeigt

  • Im Fall von Sammlungen nicht eigenständiger Entitätstypen eine unabhängige Komponente (unterstützt ab EF Core 2.2).

Beispielsweise wird im Domänenmodell für die Bestellung in eShopOnContainers das Wertobjekt „Address“ als Teil der Entität „Order“ als eigener Entitätstyp in die besitzende Entität (also der Entität „Order“) implementiert. Address ist ein Typ ohne Identitätseigenschaft, die im Domänenmodell definiert ist. Dieser Typ wird als Eigenschaft des Typs „Order“ verwendet, um die Lieferadresse für eine bestimmte Bestellung anzugeben.

Gemäß den Konventionen wird ein Schattenprimärschlüssel für den eigenen Typ erstellt und mithilfe der Tabellenaufteilung derselben Tabelle wie der Besitzer zugeordnet. Auf diese Weise können besitzereigene Typen ähnlich wie komplexe Typen in EF6 im herkömmlichen .NET Framework verwendet werden.

Sie sollten wissen, dass eigene Typen standardmäßig nie von EF Core ermittelt werden. D.h., Sie müssen sie explizit deklarieren.

In eShopOnContainers werden in der datei OrderingContext.cs innerhalb der OnModelCreating() Methode mehrere Infrastrukturkonfigurationen angewendet. Einer von ihnen bezieht sich auf die Order-Entität.

// 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
}

Im folgenden Code wird die Persistenzinfrastruktur für die Order-Entität definiert:

// 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...
    //...
}

Im vorherigen Code gibt die orderConfiguration.OwnsOne(o => o.Address) Methode an, dass die Address Eigenschaft eine eigene Entität des Order Typs ist.

Standardmäßig benennen EF Core-Konventionen die Datenbankspalten für die Eigenschaften des eigenen Entitätstyps als EntityProperty_OwnedEntityProperty. Daher erscheinen die internen Eigenschaften von Address in der Orders Tabelle mit den Namen Address_Street, Address_City (und so weiter für State, Country und ZipCode).

Sie können die Property().HasColumnName() Fluent-Methode anfügen, um diese Spalten umzubenennen. Wenn Address eine öffentliche Eigenschaft ist, sehen die Zuordnungen in etwa wie folgt aus:

orderConfiguration.OwnsOne(p => p.Address)
                            .Property(p=>p.Street).HasColumnName("ShippingStreet");

orderConfiguration.OwnsOne(p => p.Address)
                            .Property(p=>p.City).HasColumnName("ShippingCity");

Sie können die OwnsOne-Methode auch in eine Fluentzuordnung ketten. Im folgenden hypothetischen Beispiel besitzt OrderDetails sowohl BillingAddress als auch ShippingAddress, welche beide Address-Arten sind. Dann besitzt der OrderDetails-Typ Order.

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; }
}

Zusätzliche Details zu eigenen Entitätstypen

  • Eigene Typen werden definiert, wenn Sie eine Navigationseigenschaft für einen Typ mit der OwnsOne-Fluent-API konfigurieren.

  • Die Definition eines eigenen Typs in unserem Metadatenmodell besteht aus: dem Besitzertyp, der Navigationseigenschaft und dem CLR-Typ des besitzereigenen Typs.

  • Die Identität (der Schlüssel) einer eigenen Typinstanz in diesem Beispiel ist eine Zusammensetzung aus der Identität des Besitzertyps und der Definition des eigenen Typs.

Funktionen nicht eigenständiger Entitätstypen

  • Eigene Typen können auf andere Entitäten verweisen, die entweder eigen (geschachtelte eigene Typen) oder nicht eigen (reguläre Navigationseigenschaften zum Verweis auf andere Entitäten) sind.

  • Sie können über separate Navigationseigenschaften denselben CLR-Typ als andere eigene Typen in derselben Besitzerentität zuordnen.

  • Die Tabellenaufteilung ist konventionsgesteuert eingerichtet, Sie können sich jedoch abmelden, indem Sie den eigenen Typ einer anderen Tabelle mithilfe von ToTable zuordnen.

  • Für nicht eigenständige Entitätstypen erfolgt automatisch Eager Loading (vorzeitiges Laden). Es besteht also keine Notwendigkeit, .Include() in der Abfrage aufzurufen.

  • Kann mit Attribut [Owned]konfiguriert werden, mit EF Core 2.1 und höher.

  • Kann Sammlungen nicht eigenständiger Entitätstypen verarbeiten (ab Version 2.2).

Einschränkungen nicht eigenständiger Entitätstypen

  • Sie können (gezielt) kein DbSet<T> eines nicht eigenständigen Entitätstyps erstellen.

  • Sie können für nicht eigenständige Entitätstypen (derzeit gezielt) ModelBuilder.Entity<T>() nicht aufrufen.

  • Optionale (d. h. Nullwerte zulassende) nicht eigenständige Entitätstypen, die (über Tabellenaufteilung) dem Besitzer in derselben Tabelle zugeordnet sind, werden nicht unterstützt. Dies liegt daran, dass die Zuordnung für jede Eigenschaft erfolgt, es gibt keinen separaten Sentinel für den komplexen NULL-Wert als Ganzes.

  • Die Vererbungszuordnung für eigene Typen wird nicht unterstützt, aber Sie sollten zwei Blatttypen derselben Schnittstellenvererbungshierarchie als unterschiedliche eigene Typen zuordnen können. EF Core wird nicht darüber nachdenken, dass sie Teil derselben Hierarchie sind.

Hauptunterschiede bei komplexen EF6-Typen

  • Die Tabellenaufteilung ist optional, d. h., diese Typen können einer anderen Tabelle zugeordnet werden und bleiben trotzdem nicht eigenständige Typen.

Weitere Ressourcen