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
就是通知,调用者发出一次,然后可以有多个处理者参与处理。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
接口拆成了两个 ISender
和 IPublisher
,分别仅用于 Reuest/Response
和 Notification
场景,即:
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
是一个简单的中介者实现,可以极大降低我们的应用复杂度,也能够使得我们一路从 CRUD
到 CQRS
到 DDD
进行逐级演进。毕竟我们是生活在现实中的人,不能罔顾商业现实,纯粹一味追求技术。
商业技术的演进,应该是一路持续的改革而不是来一场革命。疫情总有反复,但是我们得活着,相对轻松的活着!