ヒント
このコンテンツは、.NET Docs で入手できる、またはオフラインで読み取ることができる無料のダウンロード可能な PDF として入手できる、コンテナー化された .NET アプリケーションの電子ブックである .NET マイクロサービス アーキテクチャからの抜粋です。
エンティティと集計に関する前のセクションで説明したように、ID はエンティティの基本です。 ただし、システムには、ID と ID の追跡を必要としないオブジェクトやデータ項目 (値オブジェクトなど) が多数存在します。
値オブジェクトは、他のエンティティを参照できます。 たとえば、あるポイントから別のポイントへの取得方法を記述するルートを生成するアプリケーションでは、そのルートは値オブジェクトになります。 これは特定のルート上のポイントのスナップショットになりますが、内部的には City、Road などのエンティティを参照している場合でも、この推奨されるルートには ID は含まれません。
図 7-13 は、Order 集計内の Address 値オブジェクトを示しています。
図 7-13 Order 集計内の Address 値オブジェクト
図 7-13 に示すように、エンティティは通常、複数の属性で構成されます。 たとえば、 Order エンティティは、ID を持つエンティティとしてモデル化でき、OrderId、OrderDate、OrderItems などの一連の属性で内部的に構成できます。ただし、このアドレスは、単に国/地域、道路、市区町村などで構成される複雑な値であり、このドメインに ID を持たない場合は、値オブジェクトとしてモデル化して扱う必要があります。
値オブジェクトの重要な特性
値オブジェクトには、主に次の 2 つの特性があります。
ID がありません。
これらは不変です。
最初の特性は既に説明されています。 不変性は重要な要件です。 値オブジェクトの値は、オブジェクトが作成されたら変更できない必要があります。 そのため、オブジェクトが構築されるときに、必要な値を指定する必要がありますが、オブジェクトの有効期間中に変更を許可することはできません。
値オブジェクトを使用すると、不変の性質により、パフォーマンスに関する特定のテクニックを実行できます。 これは特に、何千もの値オブジェクト インスタンスがあり、その多くが同じ値を持つシステムに当てはまります。 不変の性質により、再利用することができます。値は同じであり、ID がないため、交換可能なオブジェクトにすることができます。 この種類の最適化により、実行速度が遅いソフトウェアとパフォーマンスの良いソフトウェアが異なる場合があります。 もちろん、これらすべてのケースは、アプリケーション環境とデプロイ コンテキストによって異なります。
C での値オブジェクトの実装#
実装に関しては、すべての属性の比較に基づく等値などの基本的なユーティリティ メソッドを持つ値オブジェクト基底クラスを持つことができます (値オブジェクトは ID に基づいていてはなりません)。 次の例は、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
}
ValueObjectはabstract class型ですが、この例では、==演算子と!=演算子をオーバーロードしません。 これを選択して、比較を Equals オーバーライドに委任することもできます。 たとえば、 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);
}
このクラスは、次の例に示す Address 値オブジェクトと同様に、実際の値オブジェクトを実装するときに使用できます。
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;
}
}
Addressのこの値オブジェクトの実装には ID がないため、Address クラス定義または ValueObject クラス定義で ID フィールドが定義されていません。
Entity Framework (EF) で使用するクラスに ID フィールドがない場合は、EF Core 2.0 まで使用できませんでした。これは、ID のないより優れた値オブジェクトを実装するのに大きく役立ちます。 それはまさに次のセクションの説明です。
値オブジェクトは不変であり、読み取り専用 (つまり、取得専用プロパティを持つ) である必要があり、それは実際には当てはまると主張できます。 ただし、値オブジェクトは通常、メッセージ キューを通過するためにシリアル化および逆シリアル化されます。読み取り専用にすると、逆シリアライザーは値を割り当てなくなります。そのため、値は private setのままにするだけで、読み取り専用で実用的です。
値オブジェクトの比較セマンティクス
次のすべてのメソッドを使用して、 Address 型の 2 つのインスタンスを比較できます。
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
すべての値が同じ場合、比較は trueとして正しく評価されます。
==演算子と!=演算子をオーバーロードすることを選択しなかった場合、one == twoの最後の比較はfalseとして評価されます。 詳細については、「 ValueObject 等値演算子のオーバーロード」を参照してください。
EF Core 2.0 以降を使用してデータベース内の値オブジェクトを保持する方法
ドメイン モデルで値オブジェクトを定義する方法を確認しました。 しかし、通常は ID を持つエンティティを対象とするため、Entity Framework Core を使用してデータベースに実際に永続化するにはどうすればよいですか?
EF Core 1.1 を使用した背景と以前のアプローチ
背景として、EF Core 1.0 と 1.1 を使用する場合の制限事項は、従来の .NET Framework で EF 6.x で定義されている 複合型 を使用できないというものでした。 そのため、EF Core 1.0 または 1.1 を使用する場合は、ID フィールドを持つ EF エンティティとして値オブジェクトを格納する必要があります。 次に、ID のない値オブジェクトのように見えたので、その ID を非表示にして、値オブジェクトの ID がドメイン モデルで重要ではないことを明確にすることができます。 この ID を シャドウ プロパティとして使用すると、その ID を非表示にできます。 モデル内の ID を非表示にするためのその構成は EF インフラストラクチャ レベルで設定されるため、ドメイン モデルでは透過的になります。
eShopOnContainers (.NET Core 1.1) の初期バージョンでは、EF Core インフラストラクチャに必要な非表示 ID は、インフラストラクチャ プロジェクトで Fluent API を使用して、DbContext レベルで次のように実装されていました。 そのため、ID はドメイン モデルの観点からは隠されていましたが、インフラストラクチャには引き続き存在します。
// 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
}
ただし、データベースへのその値オブジェクトの永続化は、別のテーブルの通常のエンティティのように実行されました。
EF Core 2.0 以降では、値オブジェクトを保持するための新しいより優れた方法があります。
EF Core 2.0 以降で値オブジェクトを所有エンティティ型として保持する
DDD の正規値オブジェクト パターンと EF Core の所有エンティティ型の間にギャップがある場合でも、現在は EF Core 2.0 以降で値オブジェクトを保持する最適な方法です。 このセクションの最後に制限事項があります。
所有エンティティ型機能は、バージョン 2.0 以降に EF Core に追加されました。
所有エンティティ型を使用すると、ドメイン モデルで明示的に定義された独自の ID を持たない型をマップし、任意のエンティティ内で値オブジェクトなどのプロパティとして使用できます。 所有エンティティ型は、同じ CLR 型を別のエンティティ型 (つまり、通常のクラス) と共有します。 定義ナビゲーションを含むエンティティは所有者エンティティです。 所有者に対してクエリを実行すると、所有されている型が既定で含まれます。
ドメイン モデルを見るだけで、所有型は ID を持っていないように見えます。 実際には所有型に ID はありますが、所有者ナビゲーション プロパティはこの ID の一部です。
所有型のインスタンスの ID は、完全に独自のものではありません。 次の 3 つのコンポーネントで構成されます。
所有者の ID
それらを指すナビゲーション プロパティ
所有型のコレクションの場合は、独立したコンポーネント (EF Core 2.2 以降でサポートされています)。
たとえば、eShopOnContainers の Ordering ドメイン モデルでは、Order エンティティの一部として、Address 値オブジェクトは所有者エンティティ (Order エンティティ) 内の所有エンティティ型として実装されます。
Address は、ドメイン モデルで ID プロパティが定義されていない型です。 特定の注文の配送先住所を指定するために、注文の種類のプロパティとして使用されます。
慣例により、所有型のシャドウ主キーが作成され、テーブル分割という手法を利用して所有者のテーブルと同じテーブルにマッピングされます。 これにより、従来の .NET Framework の EF6 で複合型を使用する方法と同様に、所有型を使用できます。
所有型は EF Core の規則によって検出されないため、明示的に宣言する必要があることに注意してください。
eShopOnContainers では、OrderingContext.cs ファイル内の OnModelCreating() メソッド内で、複数のインフラストラクチャ構成が適用されます。 そのうちの 1 つは 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
}
次のコードでは、永続化インフラストラクチャが 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...
//...
}
前のコードでは、 orderConfiguration.OwnsOne(o => o.Address) メソッドは、 Address プロパティが Order 型の所有エンティティであることを指定しています。
既定では、EF Core 規則では、所有エンティティ型のプロパティのデータベース列に EntityProperty_OwnedEntityProperty名前を付けます。 そのため、Addressの内部プロパティは、名前がOrders、Address_Street (Address_City、State、Countryなど) のZipCode テーブルに表示されます。
Property().HasColumnName() fluent メソッドを追加して、それらの列の名前を変更できます。
Addressがパブリック プロパティである場合、マッピングは次のようになります。
orderConfiguration.OwnsOne(p => p.Address)
.Property(p=>p.Street).HasColumnName("ShippingStreet");
orderConfiguration.OwnsOne(p => p.Address)
.Property(p=>p.City).HasColumnName("ShippingCity");
流暢なマッピングでOwnsOneメソッドを連結することができます。 次の仮定の例では、OrderDetailsは両方ともBillingAddress型であるShippingAddressとAddressを所有しています。 その後、 OrderDetails は 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; }
}
所有エンティティの種類に関する追加の詳細
所有型は、OwnsOne fluent API を使用して特定の型へのナビゲーション プロパティを構成するときに定義されます。
メタデータ モデルの所有型の定義は、所有者の型、ナビゲーション プロパティ、所有型の CLR 型の複合です。
スタック内の所有型インスタンスの ID (キー) は、所有者型の ID と所有型の定義の複合です。
所有エンティティの機能
所有型は、他の所有 (入れ子にされた所有型) エンティティまたは非所有 (他のエンティティに対する通常の参照ナビゲーション プロパティ) エンティティを参照できます。
別のナビゲーション プロパティを使用して、同じ CLR 型を同じ所有者エンティティ内の異なる所有型とマップできます。
テーブル分割は規則によって設定されますが、ToTable を使用して所有型を別のテーブルにマッピングすることでオプトアウトできます。
一括読み込みは、所有されている型に対して自動的に実行されます。つまり、クエリで
.Include()を呼び出す必要はありません。EF Core 2.1 以降を使用して、属性
[Owned]を使用して構成できます。所有型のコレクションを処理できます (バージョン 2.2 以降を使用)。
所有エンティティの制限事項
所有型の
DbSet<T>を作成することはできません (仕様)。所有型で
ModelBuilder.Entity<T>()を呼び出すことはできません (現在は仕様です)。同じテーブル内の所有者とマップされている (つまり、テーブル分割を使用している) 省略可能な (つまり、null を許容する) 所有型はサポートされていません。 これは、マッピングはプロパティごとに行われるためです。null 複合値全体に対して個別のセンチネルはありません。
所有型の継承マッピングはサポートされていませんが、同じ継承階層の 2 つのリーフ型を異なる所有型としてマップできる必要があります。 EF Core は、それらが同じ階層に含まれているという事実を理由にしません。
EF6 の複合型との主な違い
- テーブル分割は任意です。つまり、必要に応じて別のテーブルにマッピングされても依然として所有される型として扱うことができます。
その他のリソース
Martin Fowler。 ValueObject パターン
https://martinfowler.com/bliki/ValueObject.htmlEric Evans。 Domain-Driven 設計: ソフトウェアの中心にある複雑さに取り組む。 (書籍。値オブジェクトの説明が含まれます)
https://www.amazon.com/Domain-Driven-Design-Tackling-Complexity-Software/dp/0321125215/Vaughn Vernon。 Domain-Driven デザインの実装。 (書籍。値オブジェクトの説明が含まれます)
https://www.amazon.com/Implementing-Domain-Driven-Design-Vaughn-Vernon/dp/0321834577/所有されたエンティティタイプ
https://learn.microsoft.com/ef/core/modeling/owned-entitiesシャドウプロパティ
https://learn.microsoft.com/ef/core/modeling/shadow-properties複合型または値オブジェクト。 EF Core GitHub リポジトリでのディスカッション ([問題] タブ)
https://github.com/dotnet/efcore/issues/246ValueObject.cs。 eShopOnContainers の基本値オブジェクト クラス。
https://github.com/dotnet-architecture/eShopOnContainers/blob/dev/src/Services/Ordering/Ordering.Domain/SeedWork/ValueObject.csValueObject.cs。 CSharpFunctionalExtensions の基本値オブジェクト クラス。
https://github.com/vkhorikov/CSharpFunctionalExtensions/blob/master/CSharpFunctionalExtensions/ValueObject/ValueObject.csAddress クラス。 eShopOnContainers のサンプル値オブジェクト クラス。
https://github.com/dotnet-architecture/eShopOnContainers/blob/dev/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/Address.cs
.NET