Implementieren von Wertobjekten
Tipp
Diese Inhalte sind ein Auszug aus dem eBook „.NET Microservices Architecture for Containerized .NET Applications“, verfügbar unter .NET Docs oder als kostenlos herunterladbare PDF-Datei, die offline gelesen werden kann.
Wie bereits in den vorherigen Abschnitten zu den Themen „Entitäten“ und „Aggregate“ ist die Identität ein grundlegender Bestandteil von Entitäten. Allerdings sind viele Objekte und Datenelemente in einem System enthalten, für die keine Identität oder Identitätsnachverfolgung erforderlich ist, z.B. Wertobjekte.
Ein Wertobjekt kann auf mehrere Entitäten verweisen. Ein Wertobjekt ist z.B. eine Route, die in einer Anwendung generiert wird und beschreibt, wie man von einem Punkt zu einem anderen gelangt. Dabei handelt es sich dann um eine Momentaufnahme von verschiedenen Punkten auf einer bestimmten Route. Die vorgeschlagene Route verfügt aber über keine Identität, obwohl sie intern möglicherweise auf Entitäten wie „City“ oder „Road“ verweist.
In Abbildung 7-13 wird das Wertobjekt „Address“ im Aggregat „Order“ angezeigt.
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 einem Satz von Attributen wie OrderId, OrderDate, OrderItems usw. zusammengesetzt werden. Die Adresse, bei der es sich lediglich um einen komplexen Wert handelt, der aus Land/Region, Straße, Stadt usw. besteht und in dieser Domäne keine Identität besitzt, muss als Wertobjekt modelliert und behandelt werden.
Wichtige Merkmale von Wertobjekten
Die beiden wichtigsten Merkmale von Wertobjekten lauten wie folgt:
Sie haben keine Identität.
Sie sind unveränderlich.
Das erste Merkmal wurde bereits erwähnt. Die Unveränderlichkeit ist eine wichtige Anforderung. Die Werte eines Wertobjekts müssen unveränderlich sein, nachdem ein Objekt erstellt wurde. Aus diesem Grund müssen Sie beim Erstellen des Objekts die erforderlichen Werte zur Verfügung stellen. Dabei müssen Sie allerdings festlegen, dass es während seiner gesamten Lebensdauer nicht änderbar ist.
Mithilfe von Wertobjekten können Sie aufgrund ihrer Unveränderlichkeit bestimmte Tricks verwenden, die sich positiv auf die Leistung auswirken. Dies gilt insbesondere für Systeme, in denen tausende von Wertobjektinstanzen enthalten sind, von denen viele denselben Wert haben. Aufgrund ihrer Unveränderlichkeit können sie wiederverwendet werden, und da sie dieselben Werte haben, jedoch nicht über eine Identität verfügen, können sie als austauschbare Objekt fungieren. Diese Art von Optimierung macht häufig den Unterschied zwischen langsamer Software und Software mit hoher Leistung aus. Trotzdem sind all diese Beispiele abhängig von der Anwendungsumgebung und dem Entwicklungskontext.
Implementieren von Wertobjekten in C#
Im Hinblick auf die Implementierung können Sie über eine Wertobjekt-Basisklasse mit grundlegenden Hilfsprogrammmethoden wie Gleichheit, die auf einem Vergleich aller Attribute basiert (da Wertobjekte nicht auf einer Identität basieren dürfen), und anderen grundlegenden Merkmalen verfügen. Im folgenden Beispiel wird eine Wertobjektbasisklasse angezeigt, die im eShopOnContainers-Microservice für Bestellungen 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
}
ValueObject
ist ein abstract class
-Typ, aber in diesem Beispiel werden die Operatoren ==
und !=
nicht ü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 bei der Implementierung Ihres eigentlichen Wertobjekts verwenden, wie im folgenden Beispiel mit dem Address
-Wertobjekt gezeigt:
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 Wertobjektimplementierung von Address
verfügt über keine Identität. Daher ist kein ID-Feld für sie in der Address
-Klassendefinition oder der ValueObject
-Klassendefinition definiert.
In einer Klasse über kein von Entity Framework (EF) verwendbares ID-Feld zu verfügen, war bis EF Core 2.0 nicht möglich. Dies ist für das Implementieren besserer Wertobjekte ohne ID ausgesprochen hilfreich. Außerdem entspricht dies der Erklärung 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.
Wertobjekt-Vergleichssemantik
Zwei Instanzen des Address
-Typs können mit den 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 identisch sind, werden die Vergleiche ordnungsgemäß als true
ausgewertet. Wenn Sie sich nicht dafür entscheiden, die Operatoren ==
und !=
zu überladen, wird der letzte Vergleich von one == two
als false
ausgewertet. Weitere Informationen finden Sie unter Überladen von ValueObject-Gleichheitsoperatoren.
Beibehalten von Wertobjekten in der Datenbank mit EF Core 2.0 und höher
Obenstehend wurde erläutert, wie Sie ein Wertobjekt in Ihrem Domänenmodell definieren. Nun soll erläutert werden, wie Sie es mithilfe von Entity Framework Core dauerhaft in der Datenbank speichern, obwohl dieser Dienst in der Regel auf Entitäten mit Identitäten ausgerichtet ist.
Hintergrund und ältere Ansätze zur Verwendung von EF Core 1.1
Hintergrundinformation: Mit EF Core 1.0 und 1.1 konnten Sie keine komplexen Typen verwenden, die in EF 6.x im herkömmlichen .NET Framework definiert sind. Aus diesem Grund mussten Sie ein Wertobjekt als EF-Entität mit einem ID-Feld speichern, wenn Sie EF Core 1.0 oder 1.1 verwenden. Anschließend konnten Sie die ID ausblenden, um eine Ähnlichkeit zum Wertobjekt ohne Identität herzustellen, und um deutlich zu machen, dass die Identität eines Wertobjekts im Domänenmodell nicht von Bedeutung ist. Die ID konnte ausgeblendet werden, indem sie als Schatteneigenschaft verwendet wurde. Da diese Konfiguration zum Ausblenden der ID im Modell in der EF-Infrastruktur eingerichtet ist, würde Ihr Domänenmodell diese durchschauen.
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 für das Domänenmodell ausgeblendet, sie war aber weiterhin in der Infrastruktur 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 die Datenbank wurde wie eine reguläre Entität in einer anderen Tabelle durchgeführt.
Ab EF Core 2.0 gibt es neue und bessere Möglichkeiten, Wertobjekte permanent zu speichern.
Permanentes Speichern von Wertobjekten als nicht eigenständige Entitätstypen in EF Core 2.0 und höher
Auch wenn das kanonische Wertobjektmuster im domänengesteuerten Design und der nicht eigenständige Entitätstyp in EF Core Nachteile haben, so stellen diese derzeit die beste Möglichkeit dar, Wertobjekte mit EF Core 2.0 und höher permanent zu speichern. Einschränkungen werden am Ende dieses Abschnitts erläutert.
Das eigene Entitätstypenfeature wurde schon mit Version 2.0 von EF Core hinzugefügt.
Über einen eigenen Entitätstyp können Sie Typen zuordnen, für die keine Identität im Domänenmodell explizit definiert ist und die als Eigenschaften verwendet werden, also z.B. als ein Wertobjekt in einer Entität. 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 besitzende Entitä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 Hauptkomponenten:
Der 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, für den keine Identitätseigenschaft 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. Dies ermöglicht die Verwendung von eigenen Typen, ähnlich wie bei den in EF 6 im herkömmlichen .NET Framework verwendeten komplexen Typen.
Sie sollten wissen, dass eigene Typen standardmäßig nie von EF Core ermittelt werden. D.h., Sie müssen sie explizit deklarieren.
In der Datei „OrderingContext.cs“ innerhalb der OnModelCreating()
-Methode des eShopOnContainers-Beispiels werden mehrere Infrastrukturkonfigurationen angewendet. Eine dieser Konfigurationen steht in Beziehung zur Entität „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
}
Im folgenden Code ist die Persistenzinfrastruktur für die Entität „Order“ 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 obigen Code gibt die orderConfiguration.OwnsOne(o => o.Address)
-Methode an, dass es sich bei der Address
-Eigenschaft um eine eigene Entität vom Typ Order
handelt.
Standardmäßig benennen die EF Core-Konventionen die Datenbankspalten für die Eigenschaften des eigenen Entitätstyps mit EntityProperty_OwnedEntityProperty
. Aus diesem Grund werden die internen Eigenschaften von Address
in der Tabelle Orders
mit den Namen Address_Street
und Address_City
(usw. für State
, Country
und ZipCode
) angezeigt.
Sie können die Fluentmethode Property().HasColumnName()
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
BillingAddress
und ShippingAddress
, wobei es sich um Address
-Typen handelt. Dann besitzt der Order
-Typ OrderDetails
.
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 im Metadatenmodell ist eine Zusammensetzung aus den folgenden Bestandteilen: Besitzertyp, Navigationseigenschaft und CLR-Typ des eigenen 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 wird standardmäßig eingerichtet. Sie können diese Funktion aber auch deaktivieren, indem Sie den eigenen Typ mit ToTable einer anderen Tabelle 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 ab EF Core 2.1 mit dem Attribut
[Owned]
konfiguriert werden.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. Der Grund hierfür ist, dass die Zuordnung für jede Eigenschaft erfolgt, es gibt keinen separaten Sentinel für den komplexen NULL-Wert.
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 hat keine Probleme mit der Verarbeitung, weil diese Typen Teil derselben Hierarchie sind.
Wichtige Unterschiede zwischen den komplexen Typen für EF 6
- Die Tabellenaufteilung ist optional, d. h., diese Typen können einer anderen Tabelle zugeordnet werden und bleiben trotzdem nicht eigenständige Typen.
Zusätzliche Ressourcen
Martin Fowler. ValueObject pattern (ValueObject-Muster)
https://martinfowler.com/bliki/ValueObject.htmlEric Evans. Domänengesteuertes Design: Umgang mit Komplexität im Kern einer Software. (Buch, das Erläuterungen zu Wertobjekten enthält)
https://www.amazon.com/Domain-Driven-Design-Tackling-Complexity-Software/dp/0321125215/Vaughn Vernon. Implementing Domain-Driven Design (Implementieren des domänengesteuerten Designs.) (Buch, das Erläuterungen zu Wertobjekten enthält)
https://www.amazon.com/Implementing-Domain-Driven-Design-Vaughn-Vernon/dp/0321834577/Entitätstypen im Besitz
https://learn.microsoft.com/ef/core/modeling/owned-entitiesShadow Properties (Schatteneigenschaften)
https://learn.microsoft.com/ef/core/modeling/shadow-propertiesComplex types and/or value objects (komplexe Typen und/oder Wertobjekte) . Diskussion im EF Core-GitHub-Repository (Registerkarte „Probleme“)
https://github.com/dotnet/efcore/issues/246ValueObject.cs. Basisklasse der Wertobjekte in eShopOnContainers.
https://github.com/dotnet-architecture/eShopOnContainers/blob/dev/src/Services/Ordering/Ordering.Domain/SeedWork/ValueObject.csValueObject.cs. Basisklasse der Wertobjekte in CSharpFunctionalExtensions.
https://github.com/vkhorikov/CSharpFunctionalExtensions/blob/master/CSharpFunctionalExtensions/ValueObject/ValueObject.csKlasse „Address“ Beispielklasse der Wertobjekte in eShopOnContainers.
https://github.com/dotnet-architecture/eShopOnContainers/blob/dev/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/Address.cs