ドメイン モデル レイヤーでの検証を設計するDesign validations in the domain model layer

DDD では、検証ルールは不変条件として考えることができます。In DDD, validation rules can be thought as invariants. 集計の主な役割は、その集計内のすべてのエンティティの状態の変更にわたってインバリアントを強制することです。The main responsibility of an aggregate is to enforce invariants across state changes for all the entities within that aggregate.

ドメイン エンティティは、常に有効なエンティティである必要があります。Domain entities should always be valid entities. 常に true にする必要のあるオブジェクトには、特定のインバリアント数があります。There are a certain number of invariants for an object that should always be true. たとえば、order item オブジェクトは、正の整数である必要がある数量と、アーティクル名および価格を常に持っている必要があります。For example, an order item object always has to have a quantity that must be a positive integer, plus an article name and price. そのため、インバリアントの強制は、(特に集計ルートの) ドメイン エンティティの役目となり、エンティティ オブジェクトは有効になっていない限り存在できません。Therefore, invariants enforcement is the responsibility of the domain entities (especially of the aggregate root) and an entity object should not be able to exist without being valid. インバリアント ルールは、コントラクトとして単純に表され、違反があった場合には例外または通知が発生します。Invariant rules are simply expressed as contracts, and exceptions or notifications are raised when they are violated.

この背後にある理由は、オブジェクトがなってはならない状態にあるため、多くのバグが発生するからです。The reasoning behind this is that many bugs occur because objects are in a state they should never have been in. オンライン ディスカッションでの Greg Young による適切な説明を次に示します。The following is a good explanation from Greg Young in an online discussion:

UserProfile を受け取る SendUserCreationEmailService があるとします。Name が null でないそのサービスでどのように合理化できるでしょうか。Let's propose we now have a SendUserCreationEmailService that takes a UserProfile ... how can we rationalize in that service that Name is not null? 再度チェックしますか。Do we check it again? 多くの場合、わざわざチェックなどせずに、他の誰かが検証してから自分に送信してくれる "最善の結果を期待" しているのではないでしょうか。Or more likely ... you just don't bother to check and "hope for the best"—you hope that someone bothered to validate it before sending it to you. もちろん、記述すべき最初のテストのうちの 1 つである TDD を使用すると、null 名を持つ顧客を送信した場合に、エラーが発生します。Of course, using TDD one of the first tests we should be writing is that if I send a customer with a null name that it should raise an error. しかし、この種のテストを繰り返し記述しているうちに、"名前が null になることを許可しなかったら、これらのテストのすべては必要ないのではないか" ということに気が付きました。But once we start writing these kinds of tests over and over again we realize ... "wait if we never allowed name to become null we wouldn't have all of these tests"

ドメイン モデル レイヤーでの検証を実装するImplement validations in the domain model layer

検証は通常、ドメイン エンティティ コンストラクター内、またはエンティティを更新できるメソッドに実装されます。Validations are usually implemented in domain entity constructors or in methods that can update the entity. 検証を実装するには、データを検証し、検証が失敗した場合に例外を発生させるといった、複数の方法があります。There are multiple ways to implement validations, such as verifying data and raising exceptions if the validation fails. 検証に使用する仕様パターンや、例外が発生すると、検証ごとに例外を返すのではなく、エラーのコレクションを返す通知パターンのような、より高度なパターンもあります。There are also more advanced patterns such as using the Specification pattern for validations, and the Notification pattern to return a collection of errors instead of returning an exception for each validation as it occurs.

条件を検証して例外をスローするValidate conditions and throw exceptions

次のコード例は、例外を発生させることによってドメイン エンティティで検証するための最も簡単な方法を示しています。The following code example shows the simplest approach to validation in a domain entity by raising an exception. このセクションの最後にある参照テーブルには、前述したパターンに基づくより高度な実装へのリンクがあります。In the references table at the end of this section you can see links to more advanced implementations based on the patterns we have discussed previously.

public void SetAddress(Address address)
{
    _shippingAddress = address?? throw new ArgumentNullException(nameof(address));
}

さらによい例は、内部状態が変わっていないか、メソッドのすべての変更が発生したことを確認する必要を示すものでしょう。A better example would demonstrate the need to ensure that either the internal state did not change, or that all the mutations for a method occurred. たとえば、次の実装では、オブジェクトが無効な状態のままになります。For example, the following implementation would leave the object in an invalid state:

public void SetAddress(string line1, string line2,
    string city, string state, int zip)
{
    _shippingAddress.line1 = line1 ?? throw new ...
    _shippingAddress.line2 = line2;
    _shippingAddress.city = city ?? throw new ...
    _shippingAddress.state = (IsValid(state) ? state : throw new …);
}

状態の値が無効な場合は、address の最初の行と city が既に変更されています。If the value of the state is invalid, the first address line and the city have already been changed. address が無効になる可能性があります。That might make the address invalid.

同様の方法は、エンティティのコンストラクターで、エンティティの作成後、例外を発生させてそのエンティティが有効であることを確認するために使用できます。A similar approach can be used in the entity's constructor, raising an exception to make sure that the entity is valid once it is created.

データ注釈に基づいてモデルで検証属性を使用するUse validation attributes in the model based on data annotations

Required 属性や MaxLength 属性のようなデータ注釈を使用すると、「テーブル マッピング」セクションで詳述したように、EF Core データベースのフィールド プロパティを構成することができます。しかし、.NET Framework の EF 4.x 以降に行われているので、それらは EF Core でのエンティティ検証では機能しなくなりました (IValidatableObject.Validate メソッドも機能しません)。Data annotations, like the Required or MaxLength attributes, can be used to configure EF Core database field properties, as explained in detail in the Table mapping section, but they no longer work for entity validation in EF Core (neither does the IValidatableObject.Validate method), as they have done since EF 4.x in .NET Framework.

コントローラーの通常どおりのアクション呼び出しの前に行われるモデル バインディングの際にモデル検証で、データ注釈と IValidatableObject インターフェイスを引き続き使用することができます。しかし、そのモデルは ViewModel または DTO であることを前提としており、それはドメイン モデルに関する問題ではなく、MVC または API に関する問題です。Data annotations and the IValidatableObject interface can still be used for model validation during model binding, prior to the controller's actions invocation as usual, but that model is meant to be a ViewModel or DTO and that's an MVC or API concern not a domain model concern.

概念の違いが明らかにされていれば、ご利用のアクションで非推奨のエンティティ クラス オブジェクトが受信される場合に、検証用のエンティティ クラス内でデータ注釈と IValidatableObject を引き続き使用することができます。Having made the conceptual difference clear, you can still use data annotations and IValidatableObject in the entity class for validation, if your actions receive an entity class object parameter, which is not recommended. その場合、アクションが呼び出されるすぐ前に行われるモデル バインディングの際に検証は実行されるので、コントローラーの ModelState.IsValid プロパティを調べてその結果を確認することができます。ただし、繰り返しになりますが、それはコントローラー内で実行され、実行されるのは DbContext 内にエンティティ オブジェクトが保持される前ではありません。それは EF 4.x 以降に行われているからです。In that case, validation will occur upon model binding, just before invoking the action and you can check the controller's ModelState.IsValid property to check the result, but then again, it happens in the controller, not before persisting the entity object in the DbContext, as it had done since EF 4.x.

DbContext の SaveChanges メソッドをオーバーライドすれば、データ注釈と IValidatableObject.Validate メソッドを使用してエンティティ クラスにカスタム検証を引き続き実装することができます。You can still implement custom validation in the entity class using data annotations and the IValidatableObject.Validate method, by overriding the DbContext's SaveChanges method.

IValidatableObject エンティティを検証するための実装のサンプルについては、GitHub 上のこちらのコメントを参照してください。You can see a sample implementation for validating IValidatableObject entities in this comment on GitHub. そのサンプルでは属性ベースの検証は行われていません。その検証については、同じオーバーライド内でリフレクションを使用することで容易に実装できるはずです。That sample doesn't do attribute-based validations, but they should be easy to implement using reflection in the same override.

ただし、DDD の観点から、ドメイン モデルはエンティティの動作メソッド内の例外を使用して、または検証規則を強制する仕様パターンと通知パターンを実装することで、リーンに保つことをお勧めします。However, from a DDD point of view, the domain model is best kept lean with the use of exceptions in your entity's behavior methods, or by implementing the Specification and Notification patterns to enforce validation rules.

UI 層内でモデルの検証を許可するために、入力を受け取る ViewModel クラス内 (ドメイン エンティティではなく) のアプリケーション層でデータ注釈を使用するのは合理的です。It can make sense to use data annotations at the application layer in ViewModel classes (instead of domain entities) that will accept input, to allow for model validation within the UI layer. ただし、ドメイン モデル内での検証の実行時にはこれを行わないでください。However, this should not be done at the exclusion of validation within the domain model.

仕様パターンと通知パターンを実装することでエンティティを検証するValidate entities by implementing the Specification pattern and the Notification pattern

最後に、ドメイン モデルで検証を実装するより複雑な方法は、後述するその他の技術情報の一部で説明されているように、仕様パターンと通知パターンを組み合わせて実装することです。Finally, a more elaborate approach to implementing validations in the domain model is by implementing the Specification pattern in conjunction with the Notification pattern, as explained in some of the additional resources listed later.

これらのパターンの 1 つだけを使用することもできます。たとえば、コントロール ステートメントを使用して手動で検証していますが、検証エラーの一覧をスタックして返すには通知パターンを使用します。It is worth mentioning that you can also use just one of those patterns—for example, validating manually with control statements, but using the Notification pattern to stack and return a list of validation errors.

ドメインで遅延検証を使用するUse deferred validation in the domain

ドメインで遅延検証に対処するには、さまざまな方法があります。There are various approaches to deal with deferred validations in the domain. Vaughn Vernon 氏は自身の著書『Implementing Domain-Driven Design』で、検証に関するセクションでこれらについて説明しています。In his book Implementing Domain-Driven Design, Vaughn Vernon discusses these in the section on validation.

2 段階検証Two-step validation

2 段階検証も検討してください。Also consider two-step validation. データ転送オブジェクト (DTO) コマンドにはフィールド レベルの検証を使用し、エンティティ内にはドメイン レベルの検証を使用します。Use field-level validation on your command Data Transfer Objects (DTOs) and domain-level validation inside your entities. 検証エラーを処理しやすくするために、例外の代わりに結果オブジェクトを返すことで、これを行うことができます。You can do this by returning a result object instead of exceptions in order to make it easier to deal with the validation errors.

たとえば、データ注釈を使用したフィールドの検証を使用している場合、検証定義を複製しないでください。Using field validation with data annotations, for example, you do not duplicate the validation definition. ただし、DTO の場合、実行はサーバー側とクライアント側の両方で可能です (たとえば、コマンドと ViewModels)。The execution, though, can be both server-side and client-side in the case of DTOs (commands and ViewModels, for instance).

その他の技術情報Additional resources