异步 SAGA 模式中的验证 - CQRS 和 DDD

Validation within a asynchronous SAGA pattern - CQRS & DDD

让我们考虑以下流程:

我们很难找到一个好地方来验证我们传递给 SAGA 的 "event"。例如,我们要确保: - 指定的 InvitationId 存在 - 相应邀请未过期或已处理

我们尝试了几件事:

  1. 发出命令:

    • 发出命令RequestInvitationConfirmation
    • 同步处理此命令,如果命令无效则return错误,否则引发InvitationConfirmationRequested事件。

其余流程相同

缺点: - 要求我们遵循 "request/response" 模式(在 HTTP 请求生命周期内同步)

  1. 提出一个事件:

    • 引发事件InvitationConfirmationRequested
    • 在 SAGA 中,查询 Invitation 服务并执行验证。如果命令无效,我们将发布一个事件 InvitationConfirmationFailed (...)

缺点: - 据我了解,应该使用 SAGA 来编排流程。这里我们引入"validation"的概念。我不确定这是推荐的方法。

验证是一个非常常见的概念。你会如何在分布式全异步系统中处理它?

您有一个包含 InvitationId 的命令 ConfirmInvitation。您将它从 InvaitationAppService 发送到您的 Invitation 域。您的 Invitation 域名应如下所示

...
public void ConfirmInvitation()
{
    if (this.Status == InvitationStatus.Confirmed)
        throw new InvalidInvitationException("Requested invitation has already been confirmed");
    //check more business logic here
    this.Status = InvitationStatus.Confirmed;
    Publish(new InviationConfirmedEvent(...));
}
...

您的 InvitationAppService 应该如下所示:

...
public void ConfirmInvitation(Guid invitationId)
{
    // rehydrate your domain from eventstore
    var invitation = repo.GetById<Invitation>(invitationId);
    if (invitation == null)
        throw new InvalidInvitationException("Invalid Invitation requested");
    invitation.ConfirmInvitation(new ConfirmInvitation(...));
}

您不需要介绍新活动InvitationConfirmationRequested。 DDD 是一种方法,您的 domain/business 验证应该驻留在域内。不要试图在你的领域中适应其他模式或技术。在 saga 中验证您的域(用于协调跨服务的分布式事务)可能会造成复杂性和混乱

本系统设计的要点是:"Who is the client of this API?".

  • 如果此客户端是内部 ServiceApplication 那是一回事(如在分布式应用程序、微服务等中)。
  • 如果API被第三方客户端使用,那就另当别论了。

简答

如果在Services之间内部使用了API,在系统中发送了无效Id的命令是错误的,应该被系统开发人员记录和检查。像这样的情况也应该通过手动方式修复它们(通过一些管理后端)来解决。记录这些东西并通知开发人员。

如果 API 是从第三方应用程序使用的,那么 API 和它使用的系统的其他部分之间如何划分责任就很重要了。让 API 负责验证并且不发送带有无效 ID 的命令。与第一种情况一样,将具有无效 ID 的命令视为错误。在这种情况下,如果您使用异步流,您将需要一种与第三方应用程序通信的方式来通知它。您可以使用 WebHooks.

对于验证的第二部分,检查 these series of blog posts and the original paper

长答案

如果你四处搜索,你会看到很多关于错误和验证的讨论,所以这是我的看法。

由于我们对系统的其他部分进行了分离,因此分离我们遇到的错误类型似乎很自然。您可以查看该主题的 this paper

让我们定义一些错误类型。

  • 域错误
  • 应用程序错误
  • 技术错误(数据库连接丢失等)

因为我们有不同类型的错误,所以应该从我们系统的不同部分执行验证。

这些错误的传达也可以通过不同的机制来完成,具体取决于:

  • 操作请求者和接收者
  • 使用的通讯渠道
  • 通信类型:同步或异步

现在您拥有的验证是:

  • 验证具有指定 IdInvitation 是否存在
  • 验证 Invitation 没有过期
  • 验证 Invitation 尚未处理(接受、拒绝等)

如何处理这取决于我们如何分离应用程序中的职责。让我们使用 DesignByContract 原则并定义清晰的规则,每一层(域、应用程序等)应该从其他层获得什么。

让我们定义一个规则,即不应创建和调度包含与现有 Invitation 不对应的 InvitationId 的命令。

请注意,此处使用的术语可能会因项目中使用的架构类型(分层架构、六角形等)而有很大差异

这会强制 CommandCreator 在发送命令之前验证指定的 Id 是否存在 Invitation

在 API 的情况下,将接受请求的 RouteHandler(应用程序控制器等)必须:

  • 自己执行此验证
  • 委托他人进行验证

让我们进一步定义这是我们 ApplicationLayer 的一部分(或者模块、组件等,不管它如何被调用,所以我将使用 Layer)并使它成为 ApplicationError .从这里我们可以用许多不同的方式来做到这一点。

一种方法是让 DispatchConfirmInvitationCommandApplicationService 询问 DomainLayer 是否存在具有请求的 IdInvitation 并引发错误(例如抛出异常) 如果没有。此错误将由 RouteHandler 处理并返回给请求者。

您可以同时使用同步和异步通信。如果它是异步的,您将需要为此创建一种机制。您可以参考EnterpriseIntegrationPatterns了解更多相关信息。

这里的要点是:它不是域的一部分

从这里开始,我们系统中的每个人都应该认为存在 ConfirmInvitationCommand 中指定的 Id 的邀请。如果没有,则将其视为系统故障,应由开发人员 and/or 管理员进行检查。应该有一种手动方式(管理后端)来取消这些无效命令,因此在开发系统时必须考虑到这一点,但作为系统故障处理。

另外两个验证是 Domain.

的一部分

所以假设你有一个

  • Invitation合计
  • InvitationConfirmationSaga

让我们让这些聚合与消息进行通信。让我们定义这些类型的消息:

  • RequestConfirmInvitation
  • InvitationExpired
  • InvitationAlreadyProcessed

基本流程如下:

  • ConfirmInvitationCommand 开始 InvitationConfirmationSaga

  • InvitationConfirmationSaga 发送 RequestConfirmInvitation 消息到 Invitation

然后:

  • 如果 Invitation 过期,它会发送 InvitationExpired 消息到 InvitationConfirmationSaga

  • 如果 Invitation 被处理,它发送 InvitationAlreadyProcessed 消息到 InvitationConfirmationSaga

  • 如果 Invitation 没有过期,它会被接受并发送 InvitationAccepted 消息到 InvitationConfirmationSaga

然后:

  • InvitationConfirmationSaga 将接收这些消息并相应地引发事件。

这样您就可以将域逻辑保留在 Domain 中,在本例中是 Invitation 聚合。