


ドメイン駆動(DDD)の.NETモジュールを、レイヤーと公開APIを壊さずに作る
ドメイン駆動設計で作るモジュール式の.NETアプリを、AIに書かせたり直させたりする時に使う。今あるやり方に合わせ、レイヤーの依存を一方向に保ち、集約・リポジトリ・DTOを正しいレイヤーに置き、対応するすべてのデータベースで、今の利用者を壊さないかテストする。
このAIルールを使うには
## 今あるやり方に合わせ、レイヤーの依存を一方向に保つ
コードを作成する前に、同等の実装をリポジトリ内で検索してください。依頼された変更で明示的に置き換える場合を除き、確立されたプロジェクト構造、命名、依存関係、テスト、拡張パターンに従います。差分は対象に絞り、付随的なフォーマット変更、リファクタリング、依存関係の更新を避けてください。
一方向の依存関係を持つ明示的なレイヤーを使用します。
- **Domain.Shared:** すべてのレイヤーと外部クライアントで安全に使える定数、enum、ローカライズリソース、イベント契約、共有型。
- **Domain:** エンティティ、集約ルート、リポジトリ契約、ドメインサービス、ビジネスルール。
- **Application.Contracts:** アプリケーションサービスのインターフェースとデータ転送オブジェクト。
- **Application:** アプリケーションサービスの実装とユースケースのオーケストレーション。
- **Persistence integrations:** ドメインに依存する、プロバイダー固有のコンテキスト、マッピング、リポジトリ実装。
- **HTTP API:** 実装ではなくアプリケーション契約に依存するトランスポートアダプター。
- **Remote client:** アプリケーション契約に依存する、自動生成または手書きのプロキシ。
- **Web UI:** HTTP APIを利用するプレゼンテーションコード。
Web、トランスポート、認証コンテキスト、ORMに関する関心事をドメインへ持ち込まないでください。アプリケーションロジックをWebレイヤーから独立させます。実装を短くできる場合でも、禁止された参照はアーキテクチャ上の欠陥として扱ってください。
公開APIとパッケージ境界を維持してください。明示的に依頼され、正当化され、移行が用意され、互換性テストでカバーされていない限り、破壊的変更を導入してはいけません。
## 集約を最初から正しい状態で作り、エンティティをまたぐ決まりはドメインサービスに置く
エンティティはドメインレイヤーで作成し、構築時点で有効な状態にしてください。主要コンストラクターで不変条件を強制し、所有するすべてのコレクションを初期化します。ORMが必要とする場合は、通常の生成規則を弱めずにprotectedの引数なしコンストラクターを用意してください。識別子をエンティティ内部で生成せず、呼び出し元から受け取ります。
非公開setterと意図が明確なドメインメソッドで整合性を守ってください。ランタイム、プロキシシステム、またはサポート対象の拡張モデルで必要な場合は、メンバーをoverride可能にします。集約ルートには複合キーではなく安定した識別子を一つ使用し、小さく保ち、他の集約ルートはナビゲーションプロパティではなく識別子で参照してください。
モデルに属するエンティティ横断ルールは、ドメインサービスへ置いてください。複数実装、置換、またはテスト分離で本当に必要な場合に限り、インターフェースを作成します。`Manager`などドメイン指向の明確な接尾辞を使い、DTOではなくドメインオブジェクトとプリミティブを受け取り、ルール違反時は名前空間付きの一意なエラーコードを持つローカライズ済みビジネス例外をthrowしてください。
ドメインサービスはドメイン固有の検索、正規化、キャッシュ、解決を行えますが、単純な永続化クエリはリポジトリに属します。ドメイン内で現在の認証済みユーザーを読み取らず、必要な実行者またはtenantの値をアプリケーションレイヤーから渡してください。
## リポジトリを集約ルートごとに1つ、データベースに依存しない形で作る
リポジトリ契約はドメインレイヤーで定義し、集約ルートごとに専用契約を一つ用意してください。集約が所有する内部エンティティのリポジトリは作成しないでください。
公開リポジトリ契約をプロバイダー非依存に保ってください。基本または読み取り専用のリポジトリ機能を優先し、`IQueryable`のようなクエリプロバイダー抽象をモジュール利用者へ露出させないでください。プロバイダー実装では、内部でより高機能なクエリAPIを使用できます。
すべてのリポジトリ操作を非同期にし、任意指定のキャンセルトークンを受け取れるようにしてください。一つの集約を返すメソッドでは、通常、呼び出し元が完全な集約を必要とする場合、詳細読み込みを既定で有効にします。リストメソッドでは、無制限のjoinとデータ転送を避けるため既定で無効にしてください。
場当たり的な複合projectionクラスを返す代わりに、意味のある集約返却メソッドを使用してください。projection固有のメソッドは、測定済みの性能要件で正当化される場合に限って追加します。eager loadingのロジックはプロバイダー固有の`IncludeDetails`ヘルパーに置き、ストアが集約を組み立てる方法をアプリケーションサービスから隠してください。
フレームワークに正規化ヘルパーがある場合は、それを通じてキャンセルトークンを渡してください。内部でqueryableを取得する際も、プロバイダーレベルのデータフィルター、tenancy、soft-deleteの規則が有効な状態を維持します。
## アプリケーションサービスのインターフェースを、操作ごとに分けたDTOで作る
アプリケーションサービスごとに一つのインターフェースを契約パッケージへ配置してください。境界ではDTOとプリミティブを使用し、ドメインエンティティを受け取ったり返したりしてはいけません。メソッドは非同期にし、一貫した`Async`接尾辞を使用します。サービスの文脈から明らかな場合は、エンティティ名を繰り返さないでください。
予測可能なCRUD形式を使用してください。識別子の引数は更新DTOと分け、createとupdate操作は有用な場合に詳細DTOを返し、delete操作はペイロードを返さないようにします。リストクエリには、任意指定のフィルター、並べ替え、ページングのフィールドを持たせ、安全な最大ページサイズを設定してください。
操作ごとに個別の入力DTOを作成してください。現在フィールドが一致しているという理由だけで入力DTOを再利用したり、入力DTO間で継承を使ったりしないでください。DTOにビジネスロジックを含めず、境界で検証し、上限には共有定数を再利用します。重複する多数の形式より、基本出力DTOとその詳細版を優先してください。
アプリケーション実装はユースケースを調整します。専用リポジトリを使用して必要な集約を読み込み、ドメインメソッドを呼び出し、change trackingを前提とせず更新を明示的に永続化してください。データクエリをアプリケーションサービスへ置かないでください。拡張可能なプロパティは、サポートされたmapper規約を通じてマッピングします。
Web型、アップロード抽象、streamをアプリケーション契約に渡さないでください。トランスポートアダプターでファイルを安定したアプリケーションレベルの表現へ変換します。同じモジュール内の別のアプリケーションサービスを呼び出すことを避け、再利用可能なルールをドメインまたは対象を絞った共有ヘルパーへ移してください。モジュール間の呼び出しは、相手モジュールの公開契約を経由しなければなりません。
公開操作と意図された拡張ヘルパーをoverride可能にしてください。カスタマイズがフレームワーク契約の一部である場合は、privateヘルパーよりprotectedの拡張接合部を優先します。
## データベースの種類ごとにコードを分け、モジュールを単独で使えるようにする
各モジュールに、サポートする永続化プロバイダーごとの個別コンテキストインターフェースと実装を用意してください。setまたはコレクションは、集約ルートに限って公開します。モジュールの接続名を明示的に付け、テーブルまたはコレクションの接頭辞を設定可能にし、フレームワーク所有の接頭辞を予約してください。
lazy loadingに依存しないでください。コンテキストのライフサイクルメソッドをエンティティの詳細で埋めるのではなく、名前付きmodel builder拡張でマッピングを構成します。モジュール固有の規則より先に、すべてのエンティティへフレームワークの規約マッピングを適用してください。
プロバイダー固有のリポジトリ実装は、そのプロバイダーの標準基底を継承し、コンテキストインターフェースを境界として使用し、キャンセルトークンを正しく渡し、プロバイダーに適した方法で集約の詳細読み込みを実装する必要があります。ドキュメントストアは所有コレクションを自然に読み込める一方、リレーショナルプロバイダーは明示的なincludeを必要とします。どちらの違いもドメインまたはアプリケーション契約へ漏らしてはいけません。
各パッケージは、依存関係を明示したモジュール構成クラスを一つ公開してください。構成時にサービスとoptionを登録し、稼働中のアプリケーションを本当に必要とする作業に限ってアプリケーション初期化hookを使用します。モジュールは、別パッケージとの隠れた結合なしに単独で利用できなければなりません。
モジュールが複数のプロバイダーをサポートする場合は、各統合に対して同じリポジトリ契約と動作契約を実行してください。プロバイダー非依存性は、命名規約ではなくテスト済みの性質です。
## 拡張できる所を用意し、古いAPIは壊さずに段階を踏んで廃止する
フレームワークがサポートする拡張モデルに合わせて、公開メンバーとprotectedメンバーを設計してください。override可能なメソッド、置換可能なサービス登録、真の置換点におけるインターフェース、構成用optionオブジェクトを使用します。拡張点と、カスタム実装が守るべき不変条件を文書化してください。
非推奨化サイクルなしに公開メンバーを取り除いたり改名したりしないでください。古いメンバーをobsoleteとしてマークし、明確な移行指針を示します。既定値付きの任意パラメーターを追加し、新しい基底クラスメンバーには可能な限り既定実装を提供してください。既存インターフェースを壊すより、新しいインターフェースの追加を優先します。
ユーザー向け文字列は共有ドメインパッケージが所有するローカライズリソースに置いてください。ハードコードされたテキストではなく、名前空間付きキーとローカライズ済みビジネスエラーを使用します。設定、機能、permissionの名前は所有レイヤーで定数として定義し、安定したモジュール名前空間の下に整理してください。
一つのprocess内の通信にはin-processイベントバスを使用し、モジュールまたはサービスをまたぐ通信にはdistributedイベントバスを使用してください。分散イベント契約は共有ドメインパッケージに置き、単純でserializableに保ち、ドメインエンティティではなくプリミティブまたは入れ子のイベント契約を使用します。統合イベントはアプリケーションレイヤーで処理してください。
互換性、ローカライズ、拡張動作を製品の公開領域として扱ってください。それらの変更は、対象を絞ったテストと移行ノートでカバーします。
## 対応するすべてのデータベースでテストし、最後の差分を確かめる
重要なアーキテクチャ変更または公開契約の変更を行う前に、プロジェクトで承認された設計またはissueのプロセスを通じて合意してください。pull requestの範囲を限定してレビュー可能に保ち、影響する動作を証明するために必要なテストを含めます。
分離されたドメインルールには単体テストを、リポジトリ、マッピング、permission、検証、モジュール構成には統合テストを使用してください。契約がプロバイダー非依存性をうたう場合は、サポート対象のすべての永続化プロバイダーを実行します。テスト名には、操作、シナリオ、期待する結果を含めてください。
最終差分を次のチェックリストと照合してください。
- Webや永続化の関心事が内側へ漏れず、依存関係の方向が維持されています。
- エンティティが不変条件を強制し、所有コレクションが初期化され、集約参照が識別子を使用しています。
- リポジトリ契約が非同期、キャンセル可能、集約中心、プロバイダー非依存のままです。
- アプリケーション契約が最小限の検証済みDTOと安定したメソッド形式を使用しています。
- アプリケーションサービスがクエリをリポジトリへ、変更をドメインメソッドへ委譲しています。
- リレーショナル統合とドキュメント統合が、それぞれ固有のマッピングと詳細読み込み規約に従っています。
- 公開API、ローカライズキー、設定、機能、permission、イベントの互換性が保たれています。
- テストがpermission、検証、失敗ケース、プロバイダー差異、境界条件をカバーしています。
- 差分に無関係なリファクタリング、生成ファイルの編集、不要なAPI領域が含まれていません。
実行した対象チェックと、未検証のまま残るプロバイダーまたは互換性の経路を報告してください。コンパイルだけを根拠にアーキテクチャ準拠を主張してはいけません。
