异步 SAGA 模式中的验证 - CQRS 和 DDD
Validation within a asynchronous SAGA pattern - CQRS & DDD
让我们考虑以下流程:
- API 客户端调用 [POST]
/api/v1/invitation/:InvitationId/confirm
- 在 SAGA 中确认邀请
- 最终引发
InvitationConfirmed
事件以指示成功
我们很难找到一个好地方来验证我们传递给 SAGA 的 "event"。例如,我们要确保:
- 指定的 InvitationId
存在
- 相应邀请未过期或已处理
我们尝试了几件事:
发出命令:
- 发出命令
RequestInvitationConfirmation
- 同步处理此命令,如果命令无效则return错误,否则引发
InvitationConfirmationRequested
事件。
其余流程相同
缺点:
- 要求我们遵循 "request/response" 模式(在 HTTP 请求生命周期内同步)
提出一个事件:
- 引发事件
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?".
- 如果此客户端是内部
Service
或 Application
那是一回事(如在分布式应用程序、微服务等中)。
- 如果API被第三方客户端使用,那就另当别论了。
简答
如果在Services
之间内部使用了API,在系统中发送了无效Id的命令是错误的,应该被系统开发人员记录和检查。像这样的情况也应该通过手动方式修复它们(通过一些管理后端)来解决。记录这些东西并通知开发人员。
如果 API 是从第三方应用程序使用的,那么 API 和它使用的系统的其他部分之间如何划分责任就很重要了。让 API 负责验证并且不发送带有无效 ID 的命令。与第一种情况一样,将具有无效 ID 的命令视为错误。在这种情况下,如果您使用异步流,您将需要一种与第三方应用程序通信的方式来通知它。您可以使用 WebHooks.
对于验证的第二部分,检查 these series of blog posts and the original paper。
长答案
如果你四处搜索,你会看到很多关于错误和验证的讨论,所以这是我的看法。
由于我们对系统的其他部分进行了分离,因此分离我们遇到的错误类型似乎很自然。您可以查看该主题的 this paper。
让我们定义一些错误类型。
- 域错误
- 应用程序错误
- 技术错误(数据库连接丢失等)
因为我们有不同类型的错误,所以应该从我们系统的不同部分执行验证。
这些错误的传达也可以通过不同的机制来完成,具体取决于:
- 操作请求者和接收者
- 使用的通讯渠道
- 通信类型:同步或异步
现在您拥有的验证是:
- 验证具有指定
Id
的 Invitation
是否存在
- 验证
Invitation
没有过期
- 验证
Invitation
尚未处理(接受、拒绝等)
如何处理这取决于我们如何分离应用程序中的职责。让我们使用 DesignByContract 原则并定义清晰的规则,每一层(域、应用程序等)应该从其他层获得什么。
让我们定义一个规则,即不应创建和调度包含与现有 Invitation
不对应的 InvitationId
的命令。
请注意,此处使用的术语可能会因项目中使用的架构类型(分层架构、六角形等)而有很大差异
这会强制 CommandCreator
在发送命令之前验证指定的 Id
是否存在 Invitation
。
在 API 的情况下,将接受请求的 RouteHandler
(应用程序控制器等)必须:
- 自己执行此验证
- 委托他人进行验证
让我们进一步定义这是我们 ApplicationLayer
的一部分(或者模块、组件等,不管它如何被调用,所以我将使用 Layer)并使它成为 ApplicationError
.从这里我们可以用许多不同的方式来做到这一点。
一种方法是让 DispatchConfirmInvitationCommandApplicationService
询问 DomainLayer
是否存在具有请求的 Id
的 Invitation
并引发错误(例如抛出异常) 如果没有。此错误将由 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
聚合。
让我们考虑以下流程:
- API 客户端调用 [POST]
/api/v1/invitation/:InvitationId/confirm
- 在 SAGA 中确认邀请
- 最终引发
InvitationConfirmed
事件以指示成功
我们很难找到一个好地方来验证我们传递给 SAGA 的 "event"。例如,我们要确保:
- 指定的 InvitationId
存在
- 相应邀请未过期或已处理
我们尝试了几件事:
发出命令:
- 发出命令
RequestInvitationConfirmation
- 同步处理此命令,如果命令无效则return错误,否则引发
InvitationConfirmationRequested
事件。
- 发出命令
其余流程相同
缺点: - 要求我们遵循 "request/response" 模式(在 HTTP 请求生命周期内同步)
提出一个事件:
- 引发事件
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?".
- 如果此客户端是内部
Service
或Application
那是一回事(如在分布式应用程序、微服务等中)。 - 如果API被第三方客户端使用,那就另当别论了。
简答
如果在Services
之间内部使用了API,在系统中发送了无效Id的命令是错误的,应该被系统开发人员记录和检查。像这样的情况也应该通过手动方式修复它们(通过一些管理后端)来解决。记录这些东西并通知开发人员。
如果 API 是从第三方应用程序使用的,那么 API 和它使用的系统的其他部分之间如何划分责任就很重要了。让 API 负责验证并且不发送带有无效 ID 的命令。与第一种情况一样,将具有无效 ID 的命令视为错误。在这种情况下,如果您使用异步流,您将需要一种与第三方应用程序通信的方式来通知它。您可以使用 WebHooks.
对于验证的第二部分,检查 these series of blog posts and the original paper。
长答案
如果你四处搜索,你会看到很多关于错误和验证的讨论,所以这是我的看法。
由于我们对系统的其他部分进行了分离,因此分离我们遇到的错误类型似乎很自然。您可以查看该主题的 this paper。
让我们定义一些错误类型。
- 域错误
- 应用程序错误
- 技术错误(数据库连接丢失等)
因为我们有不同类型的错误,所以应该从我们系统的不同部分执行验证。
这些错误的传达也可以通过不同的机制来完成,具体取决于:
- 操作请求者和接收者
- 使用的通讯渠道
- 通信类型:同步或异步
现在您拥有的验证是:
- 验证具有指定
Id
的Invitation
是否存在 - 验证
Invitation
没有过期 - 验证
Invitation
尚未处理(接受、拒绝等)
如何处理这取决于我们如何分离应用程序中的职责。让我们使用 DesignByContract 原则并定义清晰的规则,每一层(域、应用程序等)应该从其他层获得什么。
让我们定义一个规则,即不应创建和调度包含与现有 Invitation
不对应的 InvitationId
的命令。
请注意,此处使用的术语可能会因项目中使用的架构类型(分层架构、六角形等)而有很大差异
这会强制 CommandCreator
在发送命令之前验证指定的 Id
是否存在 Invitation
。
在 API 的情况下,将接受请求的 RouteHandler
(应用程序控制器等)必须:
- 自己执行此验证
- 委托他人进行验证
让我们进一步定义这是我们 ApplicationLayer
的一部分(或者模块、组件等,不管它如何被调用,所以我将使用 Layer)并使它成为 ApplicationError
.从这里我们可以用许多不同的方式来做到这一点。
一种方法是让 DispatchConfirmInvitationCommandApplicationService
询问 DomainLayer
是否存在具有请求的 Id
的 Invitation
并引发错误(例如抛出异常) 如果没有。此错误将由 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
聚合。