CQRS 中是否有任何规则不允许在同一控制器操作中使用查询和命令?



例如:我想删除一个项,如果控制器操作中不存在,则返回404。我违反了规则吗?命令仍然与查询分离。

[ApiController]
public class PostsController : ControllerBase
{
[HttpDelete("/posts/{postId}")]
public async Task<IActionResult> DeletePost(Guid postId)
{
var postDTO = await _mediator.Send(new GetPostByIdQuery(postId)); // query
if (postDTO == null)
{
return NotFound();
}
await _mediator.Send(new DeletePostCommand(postId)); // command
return NoContent();
}
}

我是否违反了任何规则?

不是CQRS特有的,但可能?

如果您处于控制器的上下文中,那么您所处的世界中,我们可以合理地预期会同时处理许多不同的请求。

因此,我们必须意识到,当我们的流程运行时,解锁的数据可能会从我们下面发生变化。

var postDTO = await _mediator.Send(new GetPostByIdQuery(postId)); // query
// now some other thread comes along, and does work that
// changes the result we would get from GetPostByIdQuery
// when we resume, our if test takes the wrong branch....
if (postDTO == null)
{
return NotFound();
}
await _mediator.Send(new DeletePostCommand(postId)); // command

对于像Delete这样的东西,我们怀疑对给定postId的兴趣只发生在一个地方,那么并发冲突将是罕见的,也许我们不需要担心

总的来说。。。这里有可能出现真正的问题。


这里的部分问题:你的设计违反了Tell,不要问

您应该努力告诉对象您希望它们做什么;不要问他们关于自己状态的问题,做出决定,然后告诉他们该做什么。

更好的设计可能看起来像:

var deleteResult = await _mediator.Send(new DeletePostCommand(postId)); // command
if (deleteResult == null)
{
return NotFound();
}
return NoContent(); 

这允许您确保读取和写入发生在同一事务中,这意味着您获得了正确的锁定。

是否违反了CQRS原则

CQRS是命令查询职责分离。这意味着您的系统应该将命令和查询隔离在两个不同的子系统中,客户端应该与其中一个或另一个交互,但不能同时与两者交互。现在,这里有两种情况:

作为内部API架构的CQRS

如果在您的API内部实现了CQRS,那么您没有违反任何适当的CQRS原则。API控制器是CQRS客户端,它与查询子系统或命令子系统交互。但是,如果您不希望出现状态完整性问题,则命令子系统必须自己验证后期存在,而不是依赖于客户端先前检查过后期存在。此外,此验证应该而不是取决于您的查询子系统(请参阅下文)。

作为外部API架构的CQRS

是的,你在这里违反了规则。

是在控制器级别还是在控制器操作级别隔离查询和命令是一个实现细节,但客户端应该与命令子系统或查询子系统进行原子交互。如果您的客户端发送了一个HTTP请求,导致在两个子系统中执行代码,那么命令和查询不会被隔离,并且您的系统违反了CQRS原则。

这是好是坏,相关与否,取决于你,没有评判。

为什么使用查询子系统不好

用户希望系统做什么?它想知道后存在吗?不。它想删除一个帖子这意味着您的用户想要更改系统的状态。CQRS客户端可以在发送命令之前完美地查询系统,以限制对命令子系统的压力,这是确保性能的好方法。但是,系统不能依赖查询子系统来确保状态完整性。为什么?

CQRS背后的一个想法是,如果在每次写操作中验证应用程序状态,则不需要在读操作中验证该应用程序状态。这允许设计具有三个子系统的应用程序:

  • 一个命令子系统,设计为完整性第一,写入性能第二
  • 查询子系统,专为读取性能而设计
  • 最终的一致性子系统,将应用程序状态更改传播到查询模型

在CQRS应用程序中,查询子系统不需要知道应用程序状态、业务规则或任何与完整性相关的信息。它有一个保存查询状态的持久性存储,并且该系统中的所有内容都针对读取性能进行了优化。由于您不需要该子系统中的完整性,因此您可以接受查询模型中的冲突,只要最终可以强制执行与应用程序状态的一致性即可。这也意味着查询模型在命令子系统之后更新

由于两个子系统上的应用程序状态在不同的时间发生变化,您需要决定哪种变化被认为是应用程序状态的真实来源。由于应用程序状态的完整性是由命令子系统强制执行的,而查询子系统不知道完整性,所以不可能是后者。这就是为什么命令子系统模型被认为是关于CQRS应用中的应用状态的真相的来源。这意味着,您不能信任查询子系统来决定应用程序状态

如何正确操作

您的命令子系统是CQRS系统的一部分,负责应用程序状态完整性和业务规则执行。这意味着该子系统负责维护应用程序状态的知识,以便能够验证命令,并应保持该模型以实现连续性。有两种对命令持久性模式进行建模的经典方法:

您可以使用传统的持久性设计,使用ORM等技术将对象建模的应用程序状态存储到数据库/磁盘。命令子系统通常使用RDBMS来实现持久性,因为这是实现持久性的最安全的方法,也是完整性方面的方法。这种存储可以像任何经典的分层应用程序一样轻松地读取或写入。读取不被视为CQRS术语意义上的查询,只是域模型再水合。你可以这样实现:

using(var tx = new context.Database.BeginTransaction());
var entity = context.Posts.Find(postId);
if(entity == null) { return NotFound(); }
context.Posts.Remove(entity);
context.SaveChanges();
NotifyEventualConsistency();
tx.Commit();
return NoContent();

另一种选择是使用事件源,它包括存储事件而不是状态。但是,命令子系统仍然负责重新水合应用程序状态并确保业务完整性。有了活动来源,您可以拥有一个";经典的";对象域模型,它通常涉及大量的";翻译";在持久层和域模型之间。或者,您也可以基于事件不变量重写业务规则。在这种情况下,您可以想象这样实现您的业务规则:

using(var tx = new context.Database.BeginTransaction());
if(PostStatus.Created != context.PostStatusChanged.AsNoTracking()
.OrderByDesc(event => event.Date)
.Where(event => event.PostId == postId)
.Select(event => event.NewState)
.FirstOrDefault())
{
return NotFound();
}
context.PostStatusChanged.Add(new PostStatusChanged {
PostId = postId,
NewState = PostStatus.Deleted
});
context.SaveChanges();
NotifyEventualConsistency();
tx.Commit();
return NoContent();

最新更新