在域模型层中设计验证

小窍门

此内容摘自电子书《适用于容器化 .NET 应用程序的 .NET 微服务体系结构》,可在 .NET Docs 上找到,或下载免费的 PDF 以便脱机阅读。

适用于容器化 .NET 应用程序的 .NET 微服务体系结构电子书封面缩略图。

在 DDD 中,可以将验证规则视为固定规则。 聚合的主要责任是在状态变化过程中,为该聚合中所有实体强制执行不变式。

域实体应始终是有效的实体。 对象拥有一定数量的始终为真的不变量。 例如,订单项对象始终必须具有必须是正整数的数量,以及项目名称和价格。 因此,不变量的维护是域实体(尤其是聚合根实体)的责任,一个实体对象不应在无效时存在。 不变规则以协定的形式表示,当它们被违反时,会引发异常或通知。

其背后的原因是,许多 bug 都因为对象处于他们不应该处于的状态而发生。

我们假设,现在有一个 SendUserCreationEmailService,它接受一个 UserProfile ...如何在该服务中确保名称不为空? 我们是否再次检查它? 或者更有可能...你只是懒得去检查,只是心存侥幸——希望有人在发送给你之前已经费心验证过它。 当然,在使用 TDD 时,我们应该优先编写的第一个测试是:如果我传递一个名称为空的客户对象,它应该引发一个错误。 但是,一旦我们开始编写这些类型的测试一遍又一遍,我们意识到...“如果我们从未允许名称变为 null,该怎么办?我们不会进行所有这些测试!

在域模型层中实现验证

验证通常在域实体构造函数或可以更新实体的方法中实现。 有多种方法可以实施验证,例如验证数据,并在验证失败时引发异常。 还有更高级的设计模式,例如使用 Specification 模式进行验证,以及使用 Notification 模式返回错误的集合,而不是在每次验证发生时单独抛出异常。

验证条件并抛出异常

下面的代码示例演示了通过引发异常在域实体中验证的最简单方法。 在本部分末尾的参考表中,你可以根据前面讨论的模式查看指向更高级的实现的链接。

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

一个更好的示例应该说明确保内部状态没有改变,或者所有方法的变动都得以执行的必要性。 例如,以下实现将使对象保持无效状态:

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

如果州的状态值无效,第一个地址行和城市已经被更改。 这可能会使地址无效。

在实体的构造函数中可以使用类似的方法,引发异常以确保实体在创建后有效。

基于数据注释在模型中使用验证属性

数据注解(如 Required 或 MaxLength 属性)可用于配置 EF Core 数据库字段属性,如 表映射 部分中的详细说明,但它们 不再适用于 EF Core 中的实体验证IValidatableObject.Validate 方法也不适用),这种作用自 .NET Framework 的 EF 4.x 开始就已不复存在。

在像往常一样进行控制器动作调用之前,数据注释和 IValidatableObject 接口仍然可以用于模型验证,但该模型应该是 ViewModel 或 DTO,这是 MVC 或 API 的关注点,而不是域模型的关注点。

明确概念性差异后,如果你的操作接收到实体类对象参数(不推荐这样做),你仍然可以在实体类中使用数据注释和IValidatableObject进行验证。 在这种情况下,模型绑定时会进行验证,紧接着调用动作之前,你可以检查控制器的 ModelState.IsValid 属性以检查结果。不过,这一过程发生在控制器中,而不是在将实体对象持久化到 DbContext 之前进行的,这与 EF 4.x 以来的做法相同。

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

可以在 IValidatableObject中看到用于验证实体的示例实现。 这一示例没有进行基于属性的验证,但它们可以很容易地在同一个重载中通过反射来实现。

但是,从 DDD 的角度来看,应该通过在实体的行为方法中使用异常,或者通过实现规范和通知模式来强制执行验证规则,以保持域模型的简洁。

在 ViewModel 类(而不是接受输入的域实体)中,在应用程序层使用数据注释可以允许在 UI 层内进行模型验证,这很有意义。 然而,这不应该以排除域模型中的验证为代价来执行。

通过实现规范模式和通知模式来验证实体

最后,在域模型中实现验证的更详细的方法是将规范模式与通知模式结合使用,如稍后列出的一些附加资源中所述。

值得一提的是,还可以只使用这些模式之一,例如,使用控制语句手动验证,但使用通知设计模式来累积并返回验证错误列表。

在域中使用延迟验证

有多种方法可以处理域中的延迟验证。 在《 实施 Domain-Driven 设计》一书中,Vaughn Vernon 在关于验证的部分讨论了这些内容。

双重验证

另请考虑双重验证。 在命令数据传输对象(DTO)上使用字段级验证,在实体内部进行域级验证。 可以通过返回结果对象而不是异常来执行此作,以便更轻松地处理验证错误。

例如,将字段验证与数据注释一起使用时,不会复制验证定义。 不过,在 DTO(例如命令和 ViewModel)的情况下,执行可以是服务器端和客户端。

其他资源