CQRS: 命令返回值

对于命令是否应该具有返回值,似乎存在着无穷无尽的困惑。我想知道这种混淆是否仅仅是因为与会者没有说明他们的背景或情况。

混乱

这里有一些混乱的例子。

  • Udi Dahan 说命令“不会向客户端返回错误”,但是 在同一篇文章里给出了一个图表,其中命令确实向客户端返回错误。

  • Microsoft Press Store 的一篇文章称“命令... ... 不会返回响应”,但接下来给出了一个模棱两可的警告:

随着 CQRS 周围战场经验的增长,一些实践得到巩固并趋于成为最佳实践。在一定程度上与我们刚才所说的相反... ... 现在普遍认为,命令处理程序和应用程序都需要知道事务操作是如何进行的。必须知道结果。

  • Jimmy Bogard 说“ 命令总是有结果”,但随后做了额外的努力来展示命令如何返回 void。

那么,命令处理程序是否返回值?

答案?

从吉米 · 博加德(Jimmy Bogard)的“ CQRS 神话”中得到的启示,我认为这个问题的答案取决于你所说的程序性/上下文“象限”:

+-------------+-------------------------+-----------------+
|             | Real-time, Synchronous  |  Queued, Async  |
+-------------+-------------------------+-----------------+
| Acceptance  | Exception/return-value* | <see below>     |
| Fulfillment | return-value            | n/a             |
+-------------+-------------------------+-----------------+

验收(例如验证)

命令“验收”主要指的是验证。假定验证结果必须同步地提供给调用方,无论命令“履行”是同步的还是排队的。

但是,似乎许多实践者并不从命令处理程序内部启动验证。在我看来,这要么是因为(1)他们已经找到了一种奇妙的方式来处理应用层的验证(例如,ASP.NET MVC 控制器通过数据注释检查有效状态) ,要么是(2)一种体系结构已经到位,假设命令被提交到(进程外)总线或队列。这些后面的异步形式通常不提供同步验证语义或接口。

简而言之,许多设计人员可能希望命令处理程序将验证结果作为(同步)返回值提供,但是他们必须接受所使用的异步工具的限制。

满足感

关于命令的“执行”,发出该命令的客户机可能需要知道新创建的记录或可能的故障信息(如“帐户透支”)的 scope _ Identity

在实时设置中,返回值似乎是最有意义的; 不应该使用异常来传达与业务相关的失败结果。但是,在“排队”上下文中... ... 返回值自然是没有意义的。

这也许可以概括所有的困惑:

很多(大多数?)CQRS 实践者假设他们现在或将来会合并一个异步框架或平台(总线或队列) ,从而声明命令处理程序没有返回值。但是,有些实践者并不打算使用这种事件驱动的构造,因此他们会认可(同步地)返回值的命令处理程序。

因此,例如,我认为当 Jimmy Bogard 提供了这个示例命令接口:

public interface ICommand<out TResult> { }


public interface ICommandHandler<in TCommand, out TResult>
where TCommand : ICommand<TResult>
{
TResult Handle(TCommand command);
}

毕竟,他的 Mediator 产品是一种内存工具。有鉴于此,我认为 Jimmy 小心翼翼地花时间从命令中产生一个空白的回复的原因不是因为“命令处理程序不应该有返回值”,而是因为他只是希望 Mediator 类具有一致的接口:

public interface IMediator
{
TResponse Request<TResponse>(IQuery<TResponse> query);
TResult Send<TResult>(ICommand<TResult> query);  //This is the signature in question.
}

即使并非所有命令都有有意义的值可以返回。

重复和总结

我是否正确地捕捉到了为什么在这个主题上会有困惑? 我是否遗漏了什么?

更新(6/2020)

在给出的答案的帮助下,我想我已经解开了困惑。简单地说,如果 CQRS 命令能够返回指示 完成状态的成功/失败,那么返回值是有意义的。这包括返回新的 DB 行标识,或任何不读取或返回域模型(业务)内容的结果。

我认为“ CQRS 命令”混淆的地方在于“异步”的定义和作用。“基于任务的”异步 IO 和异步体系结构(例如基于队列的中间件)之间有很大的区别。在前者中,异步“任务”可以并且将为异步命令提供完成结果。但是,发送到 RabbitMQ 的命令不会同样地收到请求/响应完成通知。正是后一种异步体系结构的上下文导致一些人说“没有异步命令这种东西”或“命令不返回值”

30338 次浏览

Following the advice in Tackling Complexity in CQRS by Vladik Khononov suggests command handling can return information relating to its outcome.

Without violating any [CQRS] principles, a command can safely return the following data:

  • Execution result: success or failure;
  • Error messages or validation errors, in case of a failure;
  • The aggregate’s new version number, in case of success;

This information will dramatically improve the user experience of your system, because:

  • You don’t have to poll an external source for command execution result, you have it right away. It becomes trivial to validate commands, and to return error messages.
  • If you want to refresh the displayed data, you can use the aggregate’s new version to determine whether the view model reflects the executed command or not. No more displaying stale data.

Daniel Whittaker advocates returning a "common result" object from a command handler containing this information.

Well, do command handlers return values or not?

They should not return Business Data, only meta data (regarding the success or failure of executing the command). CQRS is CQS taken to a higher level. Even if you would break the purist's rules and return something, what would you return? In CQRS the command handler is a method of an application service that loads the aggregate then calls a method on the aggregate then it persists the aggregate. The intend of the command handler is to modify the aggregate. You wouldn't know what to return that would be independent of the caller. Every command handler caller/client would want to know something else about the new state.

If the command execution is blocking (aka synchronous) then all you would need to know if whether the command executed successfully or not. Then, in a higher layer, you would query the exact thing that you need to know about the new application's state using a query-model that is best fitted to your needs.

Think otherwise, if you return something from a command handler you give it two responsibilities: 1. modify the aggregate state and 2. query some read-model.

Regarding command validation, there are at least two types of command validation:

  1. command sanity check, that verifies that a command has the correct data (i.e. an email address is valid); this is done before the command reaches the aggregate, in the command handler (the application service) or in the command constructor;
  2. domain invariants check, that is performed inside the aggregate, after the command reaches the aggregate (after a method is called on the aggregate) and it checks that the aggregate can mutate to the new state.

However, if we go some level up, in the Presentation layer (i.e. a REST endpoint), the client of the Application layer, we could return anything and we wont' break the rules because the endpoints are designed after the use cases, you know exactly what you want to return after a command is executed, in every use case.

CQRS and CQS are like microservices and class decomposition: the main idea is the same ("tend to small cohesive modules"), but they lie on different semantic levels.

The point of CQRS is to make write/read models separation; such low-level details like return value from specific method is completely irrelevant.

Take notice on the following Fowler's quote:

The change that CQRS introduces is to split that conceptual model into separate models for update and display, which it refers to as Command and Query respectively following the vocabulary of CommandQuerySeparation.

It is about models, not methods.

Command handler may return anything excepting read models: status (success/failure), generated events (it's primary goal of command handlers, btw: to generate events for the given command), errors. Command handlers very often throw unchecked exception, it is example of output signals from command handlers.

Moreover, author of the term, Greg Young, says that commands are always sync (otherwise, it becomes event): https://groups.google.com/forum/#!topic/dddcqrs/xhJHVxDx2pM

Greg Young

actually I said that an asynchronous command doesn't exist :) its actually another event.

Reply for @Constantin Galbenu, I faced limit.

@Misanthrope And what exactly do you do with those events?

@Constantin Galbenu, in most cases I don't need them as result of the command, of course. In some cases -- I need to notify client in response of this API request.

It's extremely useful when:

  1. You need to notify about error via events instead of exceptions. It happens usually when your model need to be saved (for example, it counts number of attempts with wrong code/password) even if error happened. Also, some guys doesn't use exceptions for business errors at all -- only events (http://andrzejonsoftware.blogspot.com/2014/06/custom-exceptions-or-domain-events.html) There is no particular reason to think that throwing business exceptions from command handler is OK, but returning domain events is not.
  2. When event happens only with some circumstances inside of your aggregate root.

And I can provide example for the second case. Imagine we make Tinder-like service, we have LikeStranger command. This command MAY result in StrangersWereMatched if we like person who already liked us before. We need to notify mobile client in response whether match was happened or not. If you just want to check matchQueryService after the command, you may find match there, but there is no gurantee that match was happened right now, because SOMETIMES Tinder shows already matched strangers (probably, in unpopulated areas, maybe inconsistency, probably you just have 2nd device, etc.).

Checking response if StrangersWereMatched really happened right now is so straightforward:

$events = $this->commandBus->handle(new LikeStranger(...));


if ($events->contains(StrangersWereMatched::class)) {
return LikeApiResponse::matched();
} else {
return LikeApiResponse::unknown();
}

Yes, you can introduce command id, for example, and make Match read model to keep it:

// ...


$commandId = CommandId::generate();


$events = $this->commandBus->handle(
$commandId,
new LikeStranger($strangerWhoLikesId, $strangerId)
);


$match = $this->matchQueryService->find($strangerWhoLikesId, $strangerId);


if ($match->isResultOfCommand($commandId)) {
return LikeApiResponse::matched();
} else {
return LikeApiResponse::unknown();
}

... but think about it: why do you think that first example with straightforward logic is worse? It doesn't violate CQRS anyway, I just made the implicit explicit. It is stateless immutable approach. Less chances to hit a bug (e.g. if matchQueryService is cached/delayed [not instantly consistent], you have a problem).

Yes, when the fact of matching is not enough and you need to get data for response, you have to use query service. But nothing prevents you to receive events from command handler.