ヒント
このコンテンツは、.NET Docs で入手できる、またはオフラインで読み取ることができる無料のダウンロード可能な PDF として入手できる、コンテナー化された .NET アプリケーションの電子ブックである .NET マイクロサービス アーキテクチャからの抜粋です。
前のセクションでは、ドメイン モデルを設計するための基本的な設計原則とパターンについて説明しました。 次に、.NET (プレーンな C# コード) と EF Core を使用してドメイン モデルを実装する方法について説明します。 ドメイン モデルは、コードだけで構成されます。 EF Core モデルの要件はありますが、EF への実際の依存関係はありません。 ドメイン モデルに EF Core やその他の ORM へのハード依存関係や参照を含めることはできません。
カスタム .NET 標準ライブラリのドメイン モデル構造
eShopOnContainers 参照アプリケーションに使用されるフォルダー組織は、アプリケーションの DDD モデルを示しています。 別のフォルダー組織が、アプリケーションに対して行われた設計の選択をより明確に伝える場合があります。 図 7-10 でわかるように、注文ドメイン モデルには、注文集計と購入者集計という 2 つの集計があります。 各集計はドメイン エンティティと値オブジェクトのグループですが、1 つのドメイン エンティティ (集約ルートまたはルート エンティティ) で構成される集計を作成することもできます。
Ordering.Domain プロジェクトのソリューション エクスプローラー ビュー。BuyerAggregate フォルダーと OrderAggregate フォルダーを含む AggregatesModel フォルダーが表示され、それぞれにエンティティ クラス、値オブジェクト ファイルなどが含まれています。
図 7-10 eShopOnContainers での注文マイクロサービスのドメイン モデル構造
さらに、ドメイン モデル レイヤーには、ドメイン モデルのインフラストラクチャ要件であるリポジトリ コントラクト (インターフェイス) が含まれています。 言い換えると、これらのインターフェイスは、インフラストラクチャ レイヤーが実装する必要があるリポジトリとメソッドを表します。 リポジトリの実装は、ドメイン モデル レイヤーの外部のインフラストラクチャ レイヤー ライブラリに配置することが重要です。そのため、ドメイン モデル レイヤーは、ENTITY Framework などのインフラストラクチャ テクノロジの API やクラスによって "汚染" されることはありません。
また、ドメイン エンティティと値オブジェクトのベースとして使用できるカスタム 基本クラスを含む SeedWork フォルダーを表示することもできます。そのため、各ドメインのオブジェクト クラスに冗長なコードはありません。
カスタム .NET Standard ライブラリでの構造の集約
集計とは、トランザクションの整合性に合わせてグループ化されたドメイン オブジェクトのクラスターを指します。 これらのオブジェクトには、エンティティのインスタンス (そのうちの 1 つは集約ルートまたはルート エンティティ) に加えて、追加の値オブジェクトを指定できます。
トランザクション整合性とは、ビジネス アクションの終了時に集計が一貫性があり、最新であることが保証されることを意味します。 たとえば、図 7-11 に示すように、eShopOnContainers 注文マイクロサービス ドメイン モデルからの注文集計が構成されます。
OrderAggregate フォルダーの詳細ビュー: Address.csは値オブジェクト、IOrderRepository はリポジトリ インターフェイス、Order.csは集約ルート、OrderItem.csは子エンティティ、OrderStatus.csは列挙クラスです。
図 7-11 Visual Studio ソリューションにおける注文集約
集計フォルダー内のいずれかのファイルを開くと、 SeedWork フォルダーに実装されているように、エンティティや値オブジェクトなどのカスタム 基底クラスまたはインターフェイスとしてマークされている方法を確認できます。
ドメイン エンティティを POCO クラスとして実装する
.NET でドメイン モデルを実装するには、ドメイン エンティティを実装する POCO クラスを作成します。 次の例では、Order クラスはエンティティとして定義され、集計ルートとしても定義されています。 Order クラスは Entity 基底クラスから派生しているため、エンティティに関連する共通コードを再利用できます。 これらの基底クラスとインターフェイスはドメイン モデル プロジェクトで定義されているため、EF のような ORM のインフラストラクチャ コードではなく、自分のコードであることに注意してください。
// COMPATIBLE WITH ENTITY FRAMEWORK CORE 5.0
// Entity is a custom base class with the ID
public class Order : Entity, IAggregateRoot
{
private DateTime _orderDate;
public Address Address { get; private set; }
private int? _buyerId;
public OrderStatus OrderStatus { get; private set; }
private int _orderStatusId;
private string _description;
private int? _paymentMethodId;
private readonly List<OrderItem> _orderItems;
public IReadOnlyCollection<OrderItem> OrderItems => _orderItems;
public Order(string userId, Address address, int cardTypeId, string cardNumber, string cardSecurityNumber,
string cardHolderName, DateTime cardExpiration, int? buyerId = null, int? paymentMethodId = null)
{
_orderItems = new List<OrderItem>();
_buyerId = buyerId;
_paymentMethodId = paymentMethodId;
_orderStatusId = OrderStatus.Submitted.Id;
_orderDate = DateTime.UtcNow;
Address = address;
// ...Additional code ...
}
public void AddOrderItem(int productId, string productName,
decimal unitPrice, decimal discount,
string pictureUrl, int units = 1)
{
//...
// Domain rules/logic for adding the OrderItem to the order
// ...
var orderItem = new OrderItem(productId, productName, unitPrice, discount, pictureUrl, units);
_orderItems.Add(orderItem);
}
// ...
// Additional methods with domain rules/logic related to the Order aggregate
// ...
}
これは POCO クラスとして実装されたドメイン エンティティであることに注意してください。 Entity Framework Core やその他のインフラストラクチャ フレームワークへの直接的な依存関係はありません。 この実装は DDD の場合と同じであり、ドメイン モデルを実装する C# コードだけです。
さらに、クラスは IAggregateRoot という名前のインターフェイスで装飾されます。 このインターフェイスは、 マーカー インターフェイスとも呼ばれる空のインターフェイスであり、このエンティティ クラスも集約ルートであることを示すために使用されます。
マーカーインターフェイスはアンチパターンと見なされることがあります。ただし、特にそのインターフェイスが進化している可能性がある場合は、クラスをマークするクリーンな方法でもあります。 マーカーの他の選択肢として属性を指定することもできますが、集計属性マーカーをクラスの上に配置する代わりに、IAggregate インターフェイスの横にある基底クラス (Entity) を表示する方が簡単です。 いずれの場合も、これは好みの問題です。
集約ルートを持つことは、集計のエンティティの整合性とビジネス ルールに関連するほとんどのコードを、Order 集計ルート クラス (たとえば、集計に OrderItem オブジェクトを追加する場合は AddOrderItem) のメソッドとして実装する必要があることを意味します。 OrderItems オブジェクトを個別または直接作成または更新しないでください。AggregateRoot クラスは、子エンティティに対する更新操作の制御と一貫性を維持する必要があります。
ドメイン エンティティにデータをカプセル化する
エンティティ モデルの一般的な問題は、コレクション ナビゲーション プロパティをパブリックにアクセス可能なリスト型として公開することです。 これにより、コラボレーター開発者はこれらのコレクション型のコンテンツを操作できます。これにより、コレクションに関連する重要なビジネス ルールがバイパスされ、オブジェクトが無効な状態のままになる可能性があります。 これに対する解決策は、関連するコレクションへの読み取り専用アクセスを公開し、クライアントがそれらを操作する方法を定義するメソッドを明示的に提供することです。
前のコードでは、多くの属性は読み取り専用またはプライベートであり、クラス メソッドによってのみ更新可能であるため、すべての更新では、クラス メソッド内で指定されたビジネス ドメインの不変性とロジックが考慮されることに注意してください。
たとえば、次の DDD パターンでは、コマンド ハンドラー メソッドまたはアプリケーション レイヤー クラスから次の操作を行わないでください (実際には、これを実行することはできません)。
// WRONG ACCORDING TO DDD PATTERNS – CODE AT THE APPLICATION LAYER OR
// COMMAND HANDLERS
// Code in command handler methods or Web API controllers
//... (WRONG) Some code with business logic out of the domain classes ...
OrderItem myNewOrderItem = new OrderItem(orderId, productId, productName,
pictureUrl, unitPrice, discount, units);
//... (WRONG) Accessing the OrderItems collection directly from the application layer // or command handlers
myOrder.OrderItems.Add(myNewOrderItem);
//...
この場合、Add メソッドは純粋にデータを追加する操作であり、OrderItems コレクションに直接アクセスできます。 そのため、子エンティティを使用したその操作に関連するドメイン ロジック、ルール、または検証のほとんどは、アプリケーション レイヤー (コマンド ハンドラーと Web API コントローラー) に分散されます。
集約ルートを回避する場合、集約ルートでは、その不変性、有効性、またはその一貫性を保証できません。 最終的には、スパゲッティ コードまたはトランザクション スクリプト コードが作成されます。
DDD パターンに従うには、エンティティプロパティにパブリックセッターを含めてはなりません。 エンティティの変更は、エンティティで実行されている変更に関する明示的なユビキタス言語を持つ明示的なメソッドによって行われる必要があります。
さらに、エンティティ内のコレクション (注文項目など) は読み取り専用プロパティにする必要があります (AsReadOnly メソッドは後で説明します)。 更新できるのは、集約ルート クラス メソッドまたは子エンティティ メソッド内からのみである必要があります。
Order 集計ルートのコードでわかるように、エンティティのデータまたはその子エンティティに対する操作をエンティティ クラスのメソッドを介して実行する必要があるため、すべてのセッターはプライベートであるか、少なくとも外部で読み取り専用である必要があります。 これにより、トランザクション スクリプト コードを実装する代わりに、制御されたオブジェクト指向の方法で一貫性が維持されます。
次のコード スニペットは、OrderItem オブジェクトを Order 集計に追加するタスクをコーディングする適切な方法を示しています。
// RIGHT ACCORDING TO DDD--CODE AT THE APPLICATION LAYER OR COMMAND HANDLERS
// The code in command handlers or WebAPI controllers, related only to application stuff
// There is NO code here related to OrderItem object's business logic
myOrder.AddOrderItem(productId, productName, pictureUrl, unitPrice, discount, units);
// The code related to OrderItem params validations or domain rules should
// be WITHIN the AddOrderItem method.
//...
このスニペットでは、OrderItem オブジェクトの作成に関連する検証またはロジックのほとんどは、AddOrderItem メソッドの Order 集計ルート (特に集計内の他の要素に関連する検証とロジック) の制御下にあります。 たとえば、AddOrderItem への複数の呼び出しの結果と同じ製品項目を取得できます。 このメソッドでは、製品項目を調べ、同じ製品項目を複数の単位で 1 つの OrderItem オブジェクトに統合することができます。 さらに、割引額が異なるが、製品 ID が同じである場合は、より高い割引を適用する可能性があります。 この原則は、OrderItem オブジェクトの他のドメイン ロジックに適用されます。
さらに、新しい OrderItem(params) 操作も、Order 集計ルートの AddOrderItem メソッドによって制御および実行されます。 そのため、その操作に関連するロジックまたは検証の大部分 (特に、他の子エンティティ間の整合性に影響するもの) は、集約ルート内の 1 つの場所に配置されます。 アグリゲートルートパターンの究極的な目的はこれです。
Entity Framework Core 1.1 以降を使用する場合、DDD エンティティはプロパティに加えて フィールドにマッピング できるため、より適切に表現できます。 これは、子エンティティまたは値オブジェクトのコレクションを保護する場合に便利です。 この機能強化により、プロパティの代わりに単純なプライベート フィールドを使用でき、パブリック メソッドでフィールド コレクションへの更新を実装し、AsReadOnly メソッドを使用して読み取り専用アクセスを提供できます。
DDD では、データの不変性と整合性を制御するために、エンティティ (またはコンストラクター) 内のメソッドを介してのみエンティティを更新する必要があるため、プロパティは get アクセサーでのみ定義されます。 プロパティはプライベートフィールドによって支えられています。 プライベート メンバーには、クラス内からのみアクセスできます。 ただし、1 つの例外があります。EF Core では、これらのフィールドも設定する必要があります (適切な値を持つオブジェクトを返すことができるようにします)。
get アクセサーのみを持つプロパティをデータベース テーブル内のフィールドにマップする
データベース テーブル列へのプロパティのマッピングは、ドメインの役割ではなく、インフラストラクチャと永続化レイヤーの一部です。 ここでは、エンティティをモデル化する方法に関連する EF Core 1.1 以降の新機能を理解できるように説明します。 このトピックの詳細については、「インフラストラクチャと永続化」セクションで説明します。
EF Core 1.0 以降を使用する場合は、DbContext 内で、getter でのみ定義されているプロパティをデータベース テーブルの実際のフィールドにマップする必要があります。 これは、PropertyBuilder クラスの HasField メソッドを使用して行われます。
プロパティのないフィールドをマップする
EF Core 1.1 以降の機能を使用して列をフィールドにマップする場合は、プロパティを使用しないようにすることもできます。 代わりに、テーブルの列をフィールドにマップできます。 この一般的なユース ケースは、エンティティの外部からアクセスする必要のない内部状態のプライベート フィールドです。
たとえば、前の OrderAggregate コード例では、セッターまたはゲッターに関連するプロパティを持たない、 _paymentMethodId フィールドなど、いくつかのプライベート フィールドがあります。 このフィールドは、注文のビジネス ロジック内で計算し、注文のメソッドから使用することもできますが、データベースにも永続化する必要があります。 したがって、EF Core (v1.1 以降) では、関連プロパティのないフィールドをデータベース内の列にマップする方法があります。 これについては、このガイドの 「インフラストラクチャ レイヤー 」セクションでも説明します。
その他のリソース
Vaughn Vernon。 DDD と Entity Framework を使用した集計のモデリング。 これは Entity Framework Core ではないこと に注意してください。
https://kalele.io/blog-posts/modeling-aggregates-with-ddd-and-entity-framework/Julie Lerman。 データ ポイント - Domain-Driven 設計のコーディング: Data-Focused 開発のためのヒント
https://learn.microsoft.com/archive/msdn-magazine/2013/august/data-points-coding-for-domain-driven-design-tips-for-data-focused-devsUdi Dahan。 完全にカプセル化されたドメイン モデルを作成する方法
https://udidahan.com/2008/02/29/how-to-create-fully-encapsulated-domain-models/Steve Smith。 DTO と POCO の違いは何ですか? \ https://ardalis.com/dto-or-poco/
.NET