0

例如:我想删除一个项目,如果控制器操作不存在它,则返回 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();
    }
}
4

2 回答 2

2

我违反了任何规则吗?

不是真的特定于 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

对于像删除这样的事情,我们怀疑对给定 postId 的兴趣只发生在一个地方,那么并发冲突将很少见,也许我们不需要担心它。

一般来说......这里可能会出现真正的问题。


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

你应该努力告诉对象你想让他们做什么;不要问他们关于他们的状态的问题,做出决定,然后告诉他们该怎么做。

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

var deleteResult = await _mediator.Send(new DeletePostCommand(postId)); // command

if (deleteResult == null)
{
    return NotFound();
}

return NoContent(); 

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

于 2022-02-19T12:47:01.507 回答
1

是否违反了 CQRS 原则?

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

CQRS 作为内部 API 架构

如果 CQRS您的 API 中实现,那么您并没有违反任何 CQRS 原则。您的 API 控制器是 CQRS 客户端,它与查询子系统或命令子系统交互。但是,如果您不希望出现状态完整性问题,您的命令子系统必须自己验证帖子存在性,而不是依赖于客户端之前检查过帖子存在性。此外,此验证不应依赖于您的查询子系统(见下文)。

CQRS 作为外部 API 架构

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

无论您是在控制器级别还是控制器操作级别分离查询和命令都是实现细节,但客户端应该与您的命令子系统或查询子系统进行原子交互。如果您的客户端发送单个 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();
于 2022-02-19T14:37:17.083 回答