在域模型层中设计验证

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.

在 DDD 中,验证规则可以被看作是不变量(invariants)。聚合(Aggregate)的主要职责,就是强制维护该聚合内所有实体在状态变更过程中的不变量。

Domain entities should always be valid entities. There are a certain number of invariants for an object that should always be true. 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.

这样设计的根本原因在于,许多程序错误(Bug)的发生,都是因为对象处于一个它们本不应该处于的状态。

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. 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. But once we start writing these kinds of tests over and over again we realize ... "what if we never allowed name to become null? we wouldn't have all of these tests!".

试想一下,我们现在有一个 SendUserCreationEmailService 服务,它接收一个 UserProfile 对象……在这个服务里,我们要怎么去保证 Name 不为空呢?我们需要再检查一遍吗?或者更有可能的是……你根本懒得去检查,只是“祈祷一切顺利”——祈祷在你之前已经有人检查过它了。当然,如果使用测试驱动开发(TDD),我们最早应该编写的测试之一,就是验证“如果我发送一个名字为空的客户对象,它应该抛出错误”。但是,当我们开始一遍又一遍地编写这类测试时,我们就会意识到……“如果我们从一开始就绝不允许名字为空,那不就不用写所有这些测试了吗!”

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.

验证通常是在领域实体的构造函数中,或者在那些可以更新实体的方法中实现的。实现验证有多种方式,比如校验数据并在验证失败时抛出异常。此外,还有一些更高级的模式,例如使用规约模式(Specification pattern)进行验证,以及使用通知模式(Notification pattern)来返回一个包含所有错误的集合,而不是在每次验证发生时都立即抛出异常。

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 …);
}

If the value of the state is invalid, the first address line and the city have already been changed. That might make the address invalid.

如果 state(州/省)的值无效,前面的地址第一行和城市可能已经被修改了。这可能会导致整个地址变得无效。

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    使用基于数据注解(Data Annotations)的模型验证属性

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.

像 Required 或 MaxLength 这样的数据注解,可以用来配置 EF Core 的数据库字段属性(这在“表映射”一节中有详细解释),但它们不再适用于 EF Core 中的实体验证了(IValidatableObject.Validate 方法也不再适用),这与 .NET Framework 版 EF 4.x 以来的情况不同。

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 接口依然可以用于模型绑定期间、控制器动作调用之前的模型验证,但那个模型通常应该是一个视图模型(ViewModel)或数据传输对象(DTO),这属于 MVC 或 API 层面的关注点,而不是领域模型的关注点。

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. 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.

在明确了这个概念上的区别后,如果你的动作(Action)接收的是一个实体类对象参数(虽然不推荐这样做),你依然可以在实体类中使用数据注解和 IValidatableObject 进行验证。在这种情况下,验证会在模型绑定时发生,就在调用动作之前,你可以检查控制器的 ModelState.IsValid 属性来获取结果。但再次强调,这发生在控制器层,而不是在 DbContext 中持久化实体对象之前,这与 EF 4.x 以来的做法不同。

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.

你依然可以通过重写 DbContext 的 SaveChanges 方法,在实体类中使用数据注解和 IValidatableObject.Validate 方法来实现自定义验证。

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.

你可以在 GitHub 上的这条评论中看到一个验证 IValidatableObject 实体的示例实现。那个示例并没有做基于特性的验证,但在同一个重写方法中,使用反射应该很容易实现。

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.

不过,从 DDD 的角度来看,领域模型最好保持精简,要么在实体的行为方法中使用异常,要么通过实现规约模式和通知模式来强制执行验证规则。

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.

在应用层使用视图模型类(而不是领域实体)来接收输入时,使用数据注解是有意义的,这样可以方便在 UI 层进行模型验证。但是,这不应该以牺牲领域模型内部的验证为代价。

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.

最后,在领域模型中实现验证还有一种更精细的方法,那就是结合使用规约模式和通知模式,正如后面列出的一些额外资源中所解释的那样。

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. In his book Implementing Domain-Driven Design, Vaughn Vernon discusses these in the section on validation.

在领域中处理延迟验证有多种方法。Vaughn Vernon 在他的《实现领域驱动设计》(Implementing Domain-Driven Design)一书中,在验证相关的章节里讨论了这些方法。

Two-step validation    两步验证

Also consider two-step validation. 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.

还可以考虑两步验证。在命令数据传输对象(DTO)上进行字段级验证,而在你的实体内部进行领域级验证。你可以通过返回一个结果对象(而不是抛出异常)来实现这一点,这样会更容易处理验证错误。

Using field validation with data annotations, for example, you do not duplicate the validation definition. The execution, though, can be both server-side and client-side in the case of DTOs (commands and ViewModels, for instance).

使用带有数据注解的字段验证时,你不会重复定义验证规则。不过,执行验证既可以发生在服务器端,也可以在 DTO 的情况下(例如命令和视图模型)发生在客户端。

posted @ 2026-05-17 19:06  菜鸟吊思  阅读(10)  评论(0)    收藏  举报