明志唯新

MediatR 在 .NET 应用中的实践

发表于

MediatR 简介

MediatR.NET 中的开源简单中介者模式实现.它通过一种进程内消息传递机制(无其他外部依赖),进行请求/响应、命令、查询、通知和事件的消息传递,并通过泛型来支持消息的智能调度。开源库地址是 https://github.com/jbogard/MediatR

MediatR 的作者是 Jimmy Bogard,他也是大名鼎鼎的 AutoMapper 的作者。如果你的英文还不错,推荐你到 https://jimmybogard.com 上拜读一下他博客文章,相信对你会有益处的。

中介者模式

既然MediatR是中介者模式的一种实现,那么我们有必要简单的了解一下什么是中介者模式。

定义一个中介对象来封装一系列对象之间的交互,使原有对象之间的耦合松散,且可以独立地改变它们之间的交互。中介者模式又叫调停模式,它是迪米特法则的典型应用。

普通模式下,常常会出现好多对象之间存在复杂的直接交互关系,这种交互关系常常是“网状结构”,它要求每个对象都必须知道它需要交互的对象。在现实生活中,比如房产买卖如果没有中介的情况,卖方要与无数的买方联系,买方也与无数卖方联系,通过极为复杂的网状沟通才能获得自己心仪的买(卖)家。

如果把这种“网状结构”改为“星形结构”的话,将大大降低它们之间的“耦合性”,这时只要找一个“中介者”就可以了。单一的买方或卖方都只跟一个中介服务人员联系,中介会帮你甄别联系另外一方。这样将大大降低对象之间的耦合性。

中介者模式是一种对象行为型模式,其主要优点:

  • 类之间各司其职,符合迪米特法则;
  • 降低了对象之间的耦合性,使得对象易于独立地被复用;
  • 将对象间的一对多关联转变为一对一的关联,提高灵活性,便于于维护和扩展。

MediatR 服务的注册

添加 Nuget 包

在 Visual Studio 中添加下图两个包:

也可以通过命令行工具添加:

dotnet add package MediatR
dotnet add package MediatR.Extensions.Microsoft.DependencyInjection

注册MediatR

.NET 6之前的ASP.NET Core项目中需要在Startup类中添加一下注册:

.NET 6的应用中,则可在Program中添加注册:

MediatR 的基本用法

MediatR中有两种消息传递的方式:

  • Request/Response,用于一个单独的Handler。
  • Notification,用于多个Handler。

Request/Response

Request/Response 有点类似于 HTTP 的 Request/Response,发出一个 Request 会得到一个 Response。

Request 消息在 MediatR 中,有两种类型:

  • IRequest<T> 返回一个T类型的值。
  • IRequest 不返回值。

对于每个 request 类型,都有相应的 handler 接口:

  • IRequestHandler<T, U> 实现该接口并返回 Task<U>
  • RequestHandler<T, U> 继承该类并返回 U
  • IRequestHandler<T> 实现该接口并返回 Task<Unit>
  • AsyncRequestHandler<T> 继承该类并返回 Task
  • RequestHandler<T> 继承该类不返回

这样一个创建订单的命令和对应的处理程序,就如下图所示:

而在 Controller 中使用时,就简单如下:

Notification

Notification 就是通知,调用者发出一次,然后可以有多个处理者参与处理。

noti

Notification 消息的定义很简单,只需要让你的类继承一个空接口 INotification 即可。而处理程序则实现 INotificationHandler<T> 接口的 Handle 方法就行:

有了上述定义后,只需要一行代码即可完成调用:

await _mediator.Publish(new QueryOrder());

然后,我们会得到如下结果:

是不是很简单呢?

注意:

默认情况下 通知的执行过程不是异步的。Publish 方法调用后,MediatR 会将所有该通知的Handler依次执行完好返回。也就是说如果一个通知的handler执行需要1秒钟,共有3个handler,则这个通知的Publish方法会执行3秒钟。 作者在 Github 的 MediatR 库中,给出了各种丰富场景的通知处理调度程序样例代码,开发者可以根据自己的业务情况自行定制修改 MediatR 的默认通知调度模式。

ISender 与 IPublisher

前面的例子中,我们都是直接使用的IMediator接口服务进行调用,MediatR 的作者在发布 9.0.0 版时,有意把原本孤立大一统的 IMediator 接口拆成了两个 ISenderIPublisher,分别仅用于 Reuest/ResponseNotification 场景,即:

  • ISender 接口只有 Send 方法
  • IPublisher 接口只有 Publish 方法

MediatR 的管线

.NET Core 中一个大量存在但是被不少人忽视的概念就是 Pipeline,也就是管线。比如,ASP.NET Core 中的管线模型大概如下图:

这套管线模型可以使得我们在 HTTP Request 的真正处理逻辑之前,经过一层层的管线逻辑对数据做预处理或者鉴权等;也可在处理逻辑返回结果后,在调用者得到响应前,由管线对结果进行二次加工。这就给我们带来一个很好的分工协作模型,可以轻松应对必然变化的客户需求,而不必修改核心业务逻辑代码。毕竟,你知道客户的需求经常还要改回去

MediatR 中具有与此类似的管线机制,可通过泛型接口 IPipelineBehavior<,> 来定义:

使得我们在 Handler 的 Handle 真正执行前或后可以额外做一些事情:记录日志、对消息做校验、对数据做预处理(如:把中文逗号改为英文逗号)、记录性能较差的Handler 等等。

下面是我们对一个处理时长超过 2 秒的进行预警日志记录的情景:

这时候可能有人会问了,我们怎么控制管线的执行顺序呢?嗯,这个问题很好,作者也早就想到了,MeidatR 的管线是通过注册的顺序来决定执行的顺序的。

上图中的性能记录管线(RequestPerformanceBehavior)就会比数据验证管线(RequestValidationBehavior)先执行,毕竟验证数据有时候也是需要花一些时间的。

消息验证管线是一个相对复杂的场景,我会在之后另起一篇单独进行分享和说明。

注意

MediatR 中的管线有两个比较特殊的预定:

  • IRequestPreProcessor<> 请求执行前的预处理
  • IRequestPostProcessor<,> 请求执行后的再处理

他们两个的实现不必单独注册,在默认 MediatR 注册逻辑中会自动注册好,他们在所有管线中执行的位置顺序也就显而易见了。

CQRS or DDD?

软件开发发展到今天,模式和理念不断在架构中刷新:从分布式到微服务,再到云原生 ……。时代对一个程序员,尤其是服务端程序员,提出的要求越来越高。DDD(领域驱动设计)在微服务架构中一再被提及,甚至有人提出这是必须项!

实施一个完美的 DDD 还是有难度的,现实中还有很多奋战在一线的 CRUD 程序员还是不少。那么在 CRUD 和 DDD 之间我们是否还有缓冲区呢?MediatR 的作者曾经也撰文讨论过这个问题,我很认同他的基本观点:设计是为应用服务的,不能为了 DDD 而 DDD。

CQRS 的全称是:"Command and Query Responsibility Segregation",直译过来就是命令与查询责任分离,可以通俗的理解为 读写分离

微软的官方文档中对此做过如下陈述:

CQRS 命令和查询责任分离数据存储的读取和更新操作分离的模式。 在应用程序中实现 CQRS 可以最大程度地提高其性能、可伸缩性和安全性。 通过迁移到 CQRS 而创建的灵活性使系统能够随着时间的推移更好地发展,并防止更新命令在域级别导致合并冲突。

微软也给出了相应的隔离模型解决方案:

CQRS 使用命令来更新数据,使用查询来读取数据,将读取和写入 分离到不同的 模型中。

  • 命令应基于任务,而不是以数据为中心。
  • 命令可以放置在队列中进行异步处理,而不是同步处理。
  • 查询从不修改数据库。 查询返回的 DTO 不封装任何域知识。

CQRS 的好处包括:

  • 独立缩放: CQRS 允许读取和写入工作负载独立缩放,这可能会减少锁争用。
  • 优化的数据架构: 读取端可使用针对查询优化的架构,写入端可使用针对更新优化的架构。
  • 安全性: 更轻松地确保仅正确的域实体对数据执行写入操作。
  • 关注点分离: 分离读取和写入端可使模型更易维护且更灵活。 大多数复杂的业务逻辑被分到写模型。 读模型会变得相对简单。
  • 查询更简单: 通过将具体化视图存储在读取数据库中,应用程序可在查询时避免复杂联接。

有了 MediatR 我们可以在应用中轻松实现 CQRS:

  • IRequest<> 的消息名称以 Command 为结尾的是命令,其对应的 Handler 执行任务
  • IRequest<> 的消息名称以 Query 为结尾的是查询,其对应的 Handler 执行数据

结束语

MediatR 是一个简单的中介者实现,可以极大降低我们的应用复杂度,也能够使得我们一路从 CRUDCQRSDDD 进行逐级演进。毕竟我们是生活在现实中的人,不能罔顾商业现实,纯粹一味追求技术。

商业技术的演进,应该是一路持续的改革而不是来一场革命。疫情总有反复,但是我们得活着,相对轻松的活着!