MediatR 库是否在 Web 上的 CQRS 示例中被过度使用?

Is MediatR library overused in CQRS examples on the web?

我很难理解为什么网络上有那么多示例在解释 CQRS 模式、处理命令和查询时都使用 MediatR。

我几乎到处都能看到命令和查询由 MediatR 处理的示例,但除了不需要在依赖注入容器中注册每个命令或查询之外,我看不到它有任何好处。但是随后您需要实现查询对象(继承 IRequest)、查询处理程序和查询响应对象,以便在您的 API 控制器方法中您可以调用 _mediatr.Send(queryObject).

为什么不使用依赖注入将查询对象注入到 API 控制器中,您可以直接在控制器上调用“get”方法?喜欢:

[HttpGet]
[Route("getall")]
public async Task<IncidentQueryResult> GetAll(int page = 0, int pageSize = 25)
{
    var result = await _incidentQueries.GetIncidents(page, pageSize);
    return result;
}

而不是:

[HttpGet]
[Route("getall")]
public async Task<IncidentQueryResult> GetAll(int page = 0, int pageSize = 25)
{
    var query = new IncidentQuery(page, pageSize);
    var result = await _mediatr.Send(query);
    return result;
}

然后,在 GetIncidents 方法内部,直接 sql 调用数据库并将结果映射到 C# 对象。简单明了。

对我来说,MediatR 库的完美且唯一合理的使用是处理领域事件。 在实施 DDD 时,我试图以下面介绍的方式建立一个项目。每个矩形都是解决方案中的一个不同项目。箭头代表参考文献:

让我们想象一个场景:创建一个域对象需要递增存储在另一个域对象(不同的聚合)中的计数器

  1. 向 API 端点发出请求以将一些新域对象添加到数据库 (第 6 层:演示)
  2. 控制器方法使用在其构造函数中注入的命令来创建域对象(第 4 层:命令)
  3. 在命令内部,创建了一个新的域对象以及存储在该对象中的“域对象已创建”事件,准备好在保存到数据库之前进行广播
  4. 然后命令使用基础结构层的存储库将这个新创建的对象添加到数据库。
  5. 然后就在执行数据库保存之前:“域对象创建”事件通过 MediatR (第 2 层:基础设施)
  6. 发送
  7. 事件随后在领域事件处理程序之一的 3 层:Application 中被捕获。
  8. 域事件处理程序(第 3 层:应用程序) 使用来自基础结构层的存储库来获取另一个域聚合,其中包含要递增的计数器,然后递增计数器。
  9. 已处理所有域事件,已执行保存到数据库。

所以我的 MediatR 仅适用于 InfrastructureApplication 层。

人们是否只是为了使用它而将 MediatR 用于命令和查询?对我来说,看起来添加命令和查询处理程序、查询和命令请求和响应类型只会添加更多没有实际价值的代码,只会让它更难理解。

以下是我访问过的一些链接:

我母语的很多网站也有这个。

在整个应用程序中使用过多的处理程序会导致难以阅读您的应用程序做什么以及什么触发什么。我看到的是人们在命令层处理域事件,但域可能不应该直接发送命令?

您是否需要在 CQRS 中使用 MediatR?

MediatR 只是一个用于解决特定需求的库。正如存储库所说:

In-process messaging with no dependencies.

命令查询职责分离(或CQRS)是一种可以通过多种方式实现的设计模式。从根本上说,只要您的读写是独立的,您就是在“遵循”这种模式。

所以不,您可以在不使用 MediatR 或任何其他消息传递库的情况下构建 CQRS“兼容”的整个应用程序。也就是说,您也可以将其构建为一个大文件。 CQRS 只是您可以根据需要部署的众多代码管理工具之一。

您也可以仅将 MediatR 用于进程内消息传递,而不应用 CQRS 模式。


也就是说,您可能会看到教程、博客和其他 .NET CQRS 资源使用 MediatR(或类似的消息传递库)的一个常见原因是,通常任何应用程序都使用 CQRS还需要以下内容:

  1. 一种在某种程度上验证查询仍然是查询而命令仍然是命令的方法
  2. 更大程度的“关注点分离”应用于整个代码。

MediatR 通常通过将命令的执行与其实现(可以存在于单独的项目中)分开来很好地解决这两个问题

例如,在您的情况下,Presentation 必须了解数据库实现及其架构才能执行查询并将它们映射到数据库资源,而不是将其作为一个问题仅基础设施和基础设施。根据我的经验,这可能会导致大量重复代码或项目之间出现大量意外耦合。最好让表示层完全专注于表示并向任何服务发送消息(它不关心哪个或处理在 MediatR 中注册它们)可以根据请求提供查询信息。

基本上在你的图表中 MediatR (and/or NServiceBus, Brighter, MassTransit, Rebus... 如果你需要超出单个进程的规模)将作为一种控制数据流的方式,并将 Query/Command 的客户端与其处理程序分离。


所以终于要回答了:

Are people just using MediatR for Commands and Queries just for sake of using it?

是也不是,他们主要将其用作单独的良好实践,与 CQRS 模式一起很好地控制他们的依赖流。尽管您是对的,对于这些想法的许多初学者来说,他们可以以不需要或不推荐的方式将两者结合在一起。


我建议您看一下他在 Clean Code 上的其他工作,以了解所有这些部分如何协同工作,为从事该项目的未来开发人员创造(他所说的)“成功之路”。他在这里有一个模板回购协议,并在网上多次讨论它:https://github.com/jasontaylordev/CleanArchitecture

你是对的 MediatR 图书馆和 DDD/CQRS 由于某种原因已成为同义词。但这些是相互排斥的。您可以在没有领域驱动设计的情况下使用 MediatR,反之亦然。该方法因其简单性和各种示例项目的可用性而变得流行。

MediatR 流行的原因之一是因为它采用了 Controller 之外的业务和应用程序逻辑,这在历史上很难为其编写测试。

MediatR 模式为您提供了一种分离这些关注点的简单方法。还有其他可用的替代方法,例如 Service Stack. The same concept could be also be used with Vertical Slice Architecture 旨在按功能分隔代码。

同样,在实施领域驱动设计时,您可以自由选择适合您的方法。但是,MediatR 确实为您提供了保持代码可扩展性的灵活性(开闭原则)。将来,如果您需要使用 NServiceBus 或其他机制将此架构替换为真正的消息总线,则无需更改整个代码库。

我发现 MediatR 和 CQRS 之间最大的脱节是命令和请求都通过 IMediator.Send() 方法。如果至少对命令和查询有单独的抽象,我会看到与 CQRS 的连接。

MediatR 在 CQRS 方面充其量是中立的。 MediatR 和 CQRS 之间的隐含联系是不存在的,提供解释它的文章也不存在。

例如,其中一篇文章以这种方式将 MediatR 与 CQRS 相关联:

In CQRS, the responsibilities for Querying and Commands are taken up by their respective Handlers and it becomes quite difficult for us to manually wire up a Command with its Handler and maintain the mappings.

这意味着通过学习如何将命令和查询映射到处理程序,我们正在学习如何实现 CQRS。但是 CQRS 是关于分离查询和命令的。它与应用程序是否或如何将命令映射到处理程序无关。因此,文章倾向于描述 CQRS,然后提供与 CQRS 无关的实现细节。尽管如此,此类文章的流行还是强化了 MediatR 和 CQRS 之间存在内在联系的观念。

这不是 MediatR 的问题。有些文章省略了MediatR,只提到中介者模式,仍然没有把CQRS和中介者模式联系起来。这似乎是核心的误解。我们可能会在应用 CQRS 的应用程序中使用任意数量的设计模式,但在同一篇文章中同时描述设计模式和 CQRS 并不意味着使用该模式会导致 CQRS。

在最坏的情况下,IMediator 接口会阻碍 CQRS,因为它无法区分命令和请求。 MediatR 和任何其他抽象都不能阻止查询处理程序执行命令行为,但如果我们将命令和查询分开——CQRS 的核心——抽象应该支持这种分离。