何时验证CQRS中的命令? - 企业工艺


在具有单个数据库的典型CQRS应用程序中,写入方面如下所示:

  • 客户端向API(服务器)发送请求。该请求包含由DTO(数据合同)表示的数据。
  • 服务器将请求路由到控制器,然后控制器将传入的DTO转换为命令并将其分派给命令处理程序。
  • 命令处理程序是应用程序逻辑所在的位置。它从数据库中检索必要的数据,将决策委托给域类,并将这些决策的结果保存回数据库。
  • 最后,客户端收到确认:成功或失败的指示。

那么,在这张图片中验证的位置是什么?什么时候应该在采取行动之前验证命令?
在初步验证时,例如检查非可空性等,您可以将前置条件检查放入命令本身:

public sealed class EnrollCommand : ICommand
{
    public long StudentId { get; }
    public string Course { get; }
    public string Grade { get; }

    public EnrollCommand(long studentId, string course, string grade)
    {
        if (course == null || grade == null) // Precondition checks
            throw ArgumentException();
        
        Id = id;
        Course = course;
        Grade = grade;
    }
}

这有助于避免向命令处理程序发送无效命令并遵循fail fast原则。您甚至可以引入一个返回Result实例的静态工厂方法,类似于Value Objects:

public sealed class EnrollCommand : ICommand
{
    public long StudentId { get; }
    public string Course { get; }
    public string Grade { get; }

    /* ... */
    
    public static Result<EnrollCommand> Create(long studentId, string course, string grade)
    {
       
/* Validate, return either success or failure */
    }
}

这是个好主意吗?
要回答这个问题,我们需要重新审视命令是什么。命令是一条消息,告诉您的应用程序执行某些操作。应用程序可以接受或拒绝此消息,具体取决于应用程序的当前状态。无法保证应用程序将继续执行命令。
这是命令和事件之间的区别因素之一。与命令不同,领域事件表示已经发生的事实,应用程序无法对其执行任何操作。它既可以考虑也可以忽略,但不能改变或拒绝它。
因为命令代表客户端询问您的应用程序的内容,所以可能是任何内容,包括无效的内容。因此,命令不应附加任何不变性要求。反过来,应用程序可以自由拒绝无效命令。这意味着命令应该是一个没有验证的简单属性包:

public sealed class EnrollCommand : ICommand
{
    public long StudentId { get; }
    public string Course { get; }
    public string Grade { get; }

    public EnrollCommand(long studentId, string course, string grade)
    {
        // No invariant checks
        Id = id;
        Course = course;
        Grade = grade;
    }
}

那么在哪里进行验证呢?有几个选项:在调度命令之前(在控制器中)或之后(在命令处理程序中)。
理想情况下,所有验证都应在命令发送后完成。您希望保持控制器尽可能薄 - 它应该只负责内置的ASP.NET功能(例如路由)并将DTO转换为命令。其他所有内容都应该转到域模型或命令处理程序。这将有助于您在ASP.NET相关问题和与应用程序相关的问题之间保持良好的分离。
即使是微不足道的验证,例如检查空值,字段长度等,也应该在理想情况下进入域模型。这是因为关于什么构成有效电子邮件,课程名称或学生地址的信息是领域知识的一部分,应该保存在核心域层(最好是在值对象中)。
值对象验证方法的缺点是它非常令人生畏,特别是在没有太多复杂性的项目中。在这样的简单项目中,为域模型中的每个概念创建值对象可能过分,例如Email,CourseName等。
更简单的方法是依赖内置的验证机制,例如ASP.NET属性,或者像FluentValidator这样的工具。在这种情况下,您将在命令到达命令处理程序之前验证命令。从纯粹的角度来看,这并不理想,但如果你认为简单的好处是值得的,那么整体上就是好的。

注意两件事:

  • 即使您采用更简单的方法(采取属性验证而不是值对象),仍应在命令处理程序中完成复杂的验证(例如检查学生的电子邮件唯一性)。
  • 如果你有一个HTML客户端,你仍然需要在UI上复制验证,至少是简单的验证,以获得更好的用户体验。没有人想等待与服务器往返一次只是为了看到他们输入的电子邮件无效。

总结

  • 命令应该没有附加不变量。服务器可以自由拒绝无效命令。
  • 理想情况下,所有验证都应驻留在命令处理程序或域模型中。这种方法的缺点是其复杂性。
  • 在更简单的项目中,使用属性来简化验证。这种方法并不纯粹,但更简单。