MediatR 知多少 - 简书

引言
首先不用查字典了,词典查无此词。猜测是作者笔误将Mediator写成MediatR了。废话少说,转入正题。
先来简单了解下这个开源项目MediatR(作者Jimmy Bogard,也是开源项目AutoMapper的创建者,在此表示膜拜):
Simple mediator implementation in .NET. In-process messaging with no dependencies. Supports request/response, commands, queries, notifications and events, synchronous and async with intelligent dispatching via C# generic variance.
.NET中的简单中介者模式实现,一种进程内消息传递机制(无其他外部依赖)。 支持以同步或异步的形式进行请求/响应,命令,查询,通知和事件的消息传递,并通过C#泛型支持消息的智能调度。
如上所述,其核心是一个中介者模式的.NET实现,其目的是消息发送和消息处理的解耦。它支持以单播和多播形式使用同步或异步的模式来发布消息,创建和侦听事件。
中介者模式
既然是对中介者模式的一种实现,那么我们就有必要简要介绍下中介者这个设计模式,以便后续展开。

中介者模式:用一个中介对象封装一系列的对象交互,中介者使各对象不需要显示地相互作用,从而使耦合松散,而且可以独立地改变它们之间的交互。
看上面的官方定义可能还是有点绕,那么下面这张图应该能帮助你对中介者模式有个直观了解。

使用中介模式,对象之间的交互将封装在中介对象中。对象不再直接相互交互(解耦),而是通过中介进行交互。这减少了对象之间的依赖性,从而减少了耦合。
那其优缺点也在图中很容易看出:
优点:中介者模式的优点就是减少类间的依赖,把原有的一对多的依赖变成了一对一的依赖,同事类只依赖中介者,减少了依赖,当然同时也降低了类间的耦合
缺点:中介者模式的缺点就是中介者会膨胀得很大,而且逻辑复杂,原本N个对象直接的相互依赖关系转换为中介者和同事类的依赖关系,同事类越多,中介者的逻辑就越复杂。
Hello MeidatR
在开始之前,我们先来了解下其基本用法。
单播消息传输
单播消息传输,也就是一对一的消息传递,一个消息对应一个消息处理。其通过IRequest
来抽象单播消息,用IRequestHandler
进行消息处理。
//构建 消息请求
public class Ping : IRequest<string> { }
//构建 消息处理
public class PingHandler : IRequestHandler<Ping, string> {
public Task<string> Handle(Ping request, CancellationToken cancellationToken) {
return Task.FromResult("Pong");
}
}
//发送 请求
var response = await mediator.Send(new Ping());
Debug.WriteLine(response); // "Pong"
多播消息传输
多播消息传输,也就是一对多的消息传递,一个消息对应多个消息处理。其通过INotification
来抽象多播消息,对应的消息处理类型为INotificationHandler
。
//构建 通知消息
public class Ping : INotification { }
//构建 消息处理器1
public class Pong1 : INotificationHandler<Ping> {
public Task Handle(Ping notification, CancellationToken cancellationToken) {
Debug.WriteLine("Pong 1");
return Task.CompletedTask;
}
}
//构建 消息处理器2
public class Pong2 : INotificationHandler<Ping> {
public Task Handle(Ping notification, CancellationToken cancellationToken) {
Debug.WriteLine("Pong 2");
return Task.CompletedTask;
}
}
//发布消息
await mediator.Publish(new Ping());
源码解析
对MediatR有了基本认识后,我们来看看源码,研究下其如何实现的。
从代码图中我们可以看到其核心的对象主要包括:
- IRequest Vs IRequestHandler
- INotification Vs INoticifaitonHandler
- IMediator Vs Mediator
- Unit
- IPipelineBehavior
IRequest Vs IRequestHandler
其中IRequest
和INotification
分别对应单播和多播消息的抽象。
对于单播消息可以决定是否需要返回值选用不同的接口:
- IRequest<T> - 有返回值
- IRequest - 无返回值
这里就不得不提到其中巧妙的设计,通过引入结构类型Unit
来代表无返回的情况。
/// <summary>
/// 代表无需返回值的请求
/// </summary>
public interface IRequest : IRequest<Unit> { }
/// <summary>
/// 代表有返回值的请求
/// </summary>
/// <typeparam name="TResponse">Response type</typeparam>
public interface IRequest<out TResponse> : IBaseRequest { }
/// <summary>
/// Allows for generic type constraints of objects implementing IRequest or IRequest{TResponse}
/// </summary>
public interface IBaseRequest { }
同样对于IRequestHandler
也是通过结构类型Unit
来处理不需要返回值的情况。
public interface IRequestHandler<in TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken);
}
public interface IRequestHandler<in TRequest> : IRequestHandler<TRequest, Unit>
where TRequest : IRequest<Unit>
{
}
从上面我们可以看出定义了一个方法名为Handle
返回值为Task
的包装类型,而因此赋予了其具有以同步和异步的方式进行消息处理的能力。我们再看一下其以异步方式进行消息处理(无返回值)的默认实现AsyncRequestHandler
:
public abstract class AsyncRequestHandler<TRequest> : IRequestHandler<TRequest>
where TRequest : IRequest
{
async Task<Unit> IRequestHandler<TRequest, Unit>.Handle(TRequest request, CancellationToken cancellationToken)
{
await Handle(request, cancellationToken).ConfigureAwait(false);
return Unit.Value;
}
protected abstract Task Handle(TRequest request, CancellationToken cancellationToken);
}
从上面的代码来看,我们很容易看出这是装饰模式的实现方式,是不是很巧妙的解决了无需返回值的场景。
最后我们来看下结构类型Unit
的定义:
public struct Unit : IEquatable<Unit>, IComparable<Unit>, IComparable
{
public static readonly Unit Value = new Unit();
public static readonly Task<Unit> Task = System.Threading.Tasks.Task.FromResult(Value);
// some other code
}
IMediator Vs Mediator
IMediator
主要定义了两个方法Send
和Publish
,分别用于发送消息和发布通知。其默认实现Mediator中定义了两个集合,分别用来保存请求与请求处理的映射关系。
//Mediator.cs
//保存request和requesthandler的映射关系,1对1。
private static readonly ConcurrentDictionary<Type, object> _requestHandlers = new ConcurrentDictionary<Type, object>();
//保存notification与notificationhandler的映射关系,
private static readonly ConcurrentDictionary<Type, NotificationHandlerWrapper> _notificationHandlers = new ConcurrentDictionary<Type, NotificationHandlerWrapper>();
这里面其又引入了两个包装类:RequestHandlerWrapper
和NotificationHandlerWrapper
。这两个包装类的作用就是用来传递ServiceFactory
委托进行依赖解析。
所以说Mediator
借助public delegate object ServiceFactory(Type serviceType);
完成对Ioc容器的一层抽象。这样就可以对接任意你喜欢用的Ioc容器,比如:Autofac、Windsor或ASP.NET Core默认的Ioc容器,只需要在注册IMediator
时指定ServiceFactory
类型的委托即可,比如ASP.NET Core中的做法:
在使用ASP.NET Core提供的原生Ioc容器有些问题:Service registration crashes when registering generic handlers
IPipelineBehavior
MeidatR支持按需配置请求管道进行消息处理。即支持在请求处理前和请求处理后添加额外行为。仅需实现以下两个接口,并注册到Ioc容器即可。
- IRequestPreProcessor<in TRequest> 请求处理前接口
- IRequestPostProcessor<in TRequest, in TResponse> 请求处理后接口
其中IPipelineBehavior
的默认实现:RequestPreProcessorBehavior
和RequestPostProcessorBehavior
分别用来处理所有实现IRequestPreProcessor
和IRequestPostProcessor
接口定义的管道行为。
而处理管道是如何构建的呢?我们来看下RequestHandlerWrapperImpl
的具体实现:
internal class RequestHandlerWrapperImpl<TRequest, TResponse> : RequestHandlerWrapper<TResponse>
where TRequest : IRequest<TResponse>
{
public override Task<TResponse> Handle(IRequest<TResponse> request, CancellationToken cancellationToken,
ServiceFactory serviceFactory)
{
Task<TResponse> Handler() => GetHandler<IRequestHandler<TRequest, TResponse>>(serviceFactory).Handle((TRequest) request, cancellationToken);
return serviceFactory
.GetInstances<IPipelineBehavior<TRequest, TResponse>>()
.Reverse()
.Aggregate((RequestHandlerDelegate<TResponse>) Handler, (next, pipeline) => () => pipeline.Handle((TRequest)request, cancellationToken, next))();
}
}
就这样一个简单的函数,涉及的知识点还真不少,说实话我花了不少时间来理清这个逻辑。
那都涉及到哪些知识点呢?我们一个一个的来理一理。
- C# 7.0的新特性 - 局部函数
- C# 6.0的新特性 - 表达式形式的成员函数
- Linq高阶函数 -
Aggregate
- 匿名委托
- 构造委托函数链
关于第1、2个知识点,请看下面这段代码:
public delegate int SumDelegate();//定义委托
public static void Main()
{
//局部函数(在函数内部定义函数)
//表达式形式的成员函数, 相当于 int Sum() { return 1 + 2;}
int Sum() => 1 + 2;
var sumDelegate = (SumDelegate)Sum;//转换为委托
Console.WriteLine(sumDelegate());//委托调用,输出:3
}
再看第4个知识点,匿名委托:
public delegate int SumDelegate();
SumDelegate delegater1 = delegate(){ return 1+2; }
//也相当于
SumDelegate delegater2 => 1+2;
下面再来介绍一下Aggregate
这个Linq高阶函数。Aggregate
是对一个集合序列进行累加操作,通过指定初始值,累加函数,以及结果处理函数完成计算。
函数定义:
public static TResult Aggregate<TSource,TAccumulate,TResult>
(this IEnumerable<TSource> source,
TAccumulate seed,
Func<TAccumulate,TSource,TAccumulate> func,
Func<TAccumulate,TResult> resultSelector);
根据函数定义我们来写个简单的demo:
var nums = Enumerable.Range(2, 3);//[2,3,4]
// 计算1到5的累加之和,再将结果乘以2
var sum = nums.Aggregate(1, (total, next) => total + next, result => result * 2);// 相当于 (((1+2)+3)+4)*2=20
Console.WriteLine(sum);//20
和函数参数进行一一对应:
- seed : 1
- Func<TAccumulate,TSource,TAccumulate> func : (total, next) => total + next
- Func<TAccumulate,TResult> resultSelector : result => result * 2
基于上面的认识,我们再来回过头梳理一下RequestHandlerWrapperImpl
。
其主要是借助委托:public delegate Task<TResponse> RequestHandlerDelegate<TResponse>();
来构造委托函数链来构建处理管道。
对Aggregate
函数了解后,我们就不难理解处理管道的构建了。请看下图中的代码解读:
那如何保证先执行IRequestPreProcessor
再执行IRequestPostProcessor
呢?
就是在注册到Ioc容器时必须保证顺序,先注册IRequestPreProcessor
再注册IRequestPostProcessor
。(这一点很重要!!!)
看到这里有没有想到ASP.NET Core中请求管道中中间件的构建呢?是不是很像俄罗斯套娃?先由内而外构建管道,再由外而内执行!
至此,MediatR的实现思路算是理清了。
应用场景
如文章开头提到:MediatR是一种进程内消息传递机制。 支持以同步或异步的形式进行请求/响应,命令,查询,通知和事件的消息传递,并通过C#泛型支持消息的智能调度。
那么我们就应该明白,其核心是消息的解耦。因为我们几乎都是在与消息打交道,那因此它的应用场景就很广泛,比如我们可以基于MediatR实现CQRS、EventBus等。
另外,还有一种应用场景:我们知道借助依赖注入的好处是,就是解除依赖,但我们又不得不思考一个问题,随着业务逻辑复杂度的增加,构造函数可能要注入更多的服务,当注入的依赖太多时,其会导致构造函数膨胀。比如:
public DashboardController(
ICustomerRepository customerRepository,
IOrderService orderService,
ICustomerHistoryRepository historyRepository,
IOrderRepository orderRepository,
IProductRespoitory productRespoitory,
IRelatedProductsRepository relatedProductsRepository,
ISupportService supportService,
ILog logger
)
如果借助MediatR
进行改造,也许仅需注入IMediatR
就可以了。
public DashboardController(IMediatR mediatr)
总结
看到这里,也许你应该明白MediatR实质上并不是严格意义上的中介者模式实现,我更倾向于其是基于Ioc容器的一层抽象,根据请求定位相应的请求处理器进行消息处理,也就是服务定位。
那到这里似乎也恍然大悟MediatR这个笔误可能是有意为之了。序员,你怎么看?
参考资料:
CQRS/MediatR implementation patterns
MediatR when and why I should use it? vs 2017 webapi
ABP CQRS 实现案例:基于 MediatR 实现
MediatR 知多少 - 简书的更多相关文章
- iOS---实现简书和知乎的上滑隐藏导航栏下拉显示导航栏效果
因为自己用简书和知乎比较多,所以对其导航栏的效果比较好奇,自己私下里找资料实现了一下.这个效果的关键点在于下方可供滑动的内容的便宜距离inset的改变,以及滑动的scrollview代理的执行,废话不 ...
- Scrapy实战篇(八)之简书用户信息全站抓取
相对于知乎而言,简书的用户信息并没有那么详细,知乎提供了包括学习,工作等在内的一系列用户信息接口,但是简书就没有那么慷慨了.但是即便如此,我们也试图抓取一些基本信息,进行简单地细分析,至少可以看一下, ...
- iOS离屏渲染简书
更详细地址https://zsisme.gitbooks.io/ios-/content/chapter15/offscreen-rendering.html(包含了核心动画) GPU渲染机制: CP ...
- openlayers 3 简书
1. 简书http://www.jianshu.com/p/6785e755fa0d 2. 文档 http://anzhihun.coding.me/ol3-primer/ch03/03-02.htm ...
- Python 2.7_发送简书关注的专题作者最新一篇文章及连接到邮件_20161218
最近看简书文章关注了几个专题作者,写的文章都不错,对爬虫和数据分析都写的挺好,因此想到能不能获取最新的文章推送到Ipad网易邮箱大师.邮件发送代码封装成一个函数,从廖雪峰大神那里学的 http:// ...
- 从刚刚「简书」平台的短暂异常,谈Nginx An error occurred报错~
09.26简书平台的短暂异常 An error occurred. Sorry, the page you are looking for is currently unavailable. Plea ...
- swift调用oc语言文件,第三方库文件或者自己创建的oc文件——简书作者
Swift是怎样调用OC的第三方库的呢?请看下面详情: 情况一: 1.首先打开Xcode,iOS->Application->Single View Application, 选Next. ...
- iOS实现简书的账号识别方式(正则表达式)
通过简书iOS客户端登录,我们会看到请输入手机号或者邮箱登录,但是我们随机输入1234567的时候,便会弹出手机格式不正确,同样也会识别我们的邮箱格式,那么我们在项目中怎么实现这种判断呢? 0E471 ...
- 倒戈了,转投简书 -------->
深情自白 还记得数月前那个月黑风高的晚上,笔主偶遇简书,被那婀娜多姿的Markdown输出深深吸引不能自拔,从此立下毒誓要两边同时发布.然而天有不测风云(这边的太丑),前思后想寝食难安之后作出决定,正 ...
随机推荐
- thinkphp 防止XSS(跨站脚本攻击)
XSS(跨站脚本攻击)可以用于窃取其他用户的Cookie信息,要避免此类问题,可以采用如下解决方案: 直接过滤所有的JavaScript脚本: 转义Html元字符,使用htmlentities.htm ...
- 好用的日期控件jeDate
最近做公司后台系统关于仓库的一些东西,需要根据时间范围来导出一些数据,我们使用的后台框架是基于bs的,bs也有时间控件:bootstrap-datepicker是只能选择日期的, daterangep ...
- NX二次开发-UFUN设置除工作层之外的所有图层的状态UF_LAYER_set_all_but_work
NX11+VS2013 #include <uf.h> #include <uf_ui.h> #include <uf_layer.h> UF_initialize ...
- [JZOJ 5129] 字符串
题意:统计本质不同的串的个数. 思路: 显然后缀自动机,对于每个串建一个\(SAM\)统计即可. #include <bits/stdc++.h> using namespace std; ...
- Codeforce 1175A From Hero to Zero
题目链接:http://codeforces.com/problemset/problem/1175/A AC代码: #include<cstdio> #include<iostre ...
- 4-MySQL拆分表
如上图,将goods表中的cate_name字段拆分一个商品分类表goods_cates,步骤如下: 1,创建商品分类表-goods_cates; create table goods_cates( ...
- NPE问题
“防止 NPE,是程序员的基本修养.”NPE(Null Pointer Exception) 参考: https://www.jianshu.com/p/9915f2e34a13
- Flutter 类似viewDidAppear 的任务处理
前言 在任务之中 ,有些实时任务比较重的需求,需要在类似 iOS viewDidAppear 里面执行数据请求任务,如:上一个页面返回pop 后执行网络请求任务.在flutter中如何实现呢? 目前 ...
- java内存模型和垃圾回收
摘抄并用于自查 JVM内存模型 1. Java程序具体执行的过程: Java源代码文件(.java后缀)会被Java编译器编译为字节码文件(.class后缀) 由JVM中的类加载器加载各个类的字节码文 ...
- SpringBoot 非web项目简单架构
1.截图 2.DemoService package com.github.weiwei02.springcloudtaskdemo; import org.springframework.beans ...