MediatR 知多少

引言

首先不用查字典了辣卒,詞典查無此詞棚放。猜測是作者筆誤將Mediator寫成MediatR了照瘾。廢話少說涩拙,轉(zhuǎn)入正題。

先來簡單了解下這個開源項目MediatR(作者Jimmy Bogard耸采,也是開源項目AutoMapper的創(chuàng)建者兴泥,在此表示膜拜):

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中的簡單中介者模式實現(xiàn),一種進程內(nèi)消息傳遞機制(無其他外部依賴)虾宇。 支持以同步或異步的形式進行請求/響應(yīng)搓彻,命令,查詢嘱朽,通知和事件的消息傳遞旭贬,并通過C#泛型支持消息的智能調(diào)度。

如上所述燥翅,其核心是一個中介者模式的.NET實現(xiàn)骑篙,其目的是消息發(fā)送和消息處理的解耦。它支持以單播和多播形式使用同步或異步的模式來發(fā)布消息森书,創(chuàng)建和偵聽事件靶端。

中介者模式

既然是對中介者模式的一種實現(xiàn),那么我們就有必要簡要介紹下中介者這個設(shè)計模式凛膏,以便后續(xù)展開杨名。


中介者模式類圖

中介者模式:用一個中介對象封裝一系列的對象交互,中介者使各對象不需要顯示地相互作用猖毫,從而使耦合松散台谍,而且可以獨立地改變它們之間的交互。

看上面的官方定義可能還是有點繞吁断,那么下面這張圖應(yīng)該能幫助你對中介者模式有個直觀了解趁蕊。


使用中介模式,對象之間的交互將封裝在中介對象中仔役。對象不再直接相互交互(解耦)掷伙,而是通過中介進行交互。這減少了對象之間的依賴性又兵,從而減少了耦合任柜。

那其優(yōu)缺點也在圖中很容易看出:

優(yōu)點:中介者模式的優(yōu)點就是減少類間的依賴,把原有的一對多的依賴變成了一對一的依賴沛厨,同事類只依賴中介者宙地,減少了依賴,當(dāng)然同時也降低了類間的耦合
缺點:中介者模式的缺點就是中介者會膨脹得很大逆皮,而且邏輯復(fù)雜宅粥,原本N個對象直接的相互依賴關(guān)系轉(zhuǎn)換為中介者和同事類的依賴關(guān)系,同事類越多页屠,中介者的邏輯就越復(fù)雜粹胯。

Hello MeidatR

在開始之前蓖柔,我們先來了解下其基本用法。

單播消息傳輸

單播消息傳輸风纠,也就是一對一的消息傳遞况鸣,一個消息對應(yīng)一個消息處理。其通過IRequest來抽象單播消息竹观,用IRequestHandler進行消息處理镐捧。

//構(gòu)建 消息請求
public class Ping : IRequest<string> { }
//構(gòu)建 消息處理
public class PingHandler : IRequestHandler<Ping, string> {
    public Task<string> Handle(Ping request, CancellationToken cancellationToken) {
        return Task.FromResult("Pong");
    }
}
//發(fā)送 請求
var response = await mediator.Send(new Ping());
Debug.WriteLine(response); // "Pong"

多播消息傳輸

多播消息傳輸,也就是一對多的消息傳遞臭增,一個消息對應(yīng)多個消息處理懂酱。其通過INotification來抽象多播消息,對應(yīng)的消息處理類型為INotificationHandler誊抛。

//構(gòu)建 通知消息
public class Ping : INotification { }
//構(gòu)建 消息處理器1
public class Pong1 : INotificationHandler<Ping> {
    public Task Handle(Ping notification, CancellationToken cancellationToken) {
        Debug.WriteLine("Pong 1");
        return Task.CompletedTask;
    }
}
//構(gòu)建 消息處理器2
public class Pong2 : INotificationHandler<Ping> {
    public Task Handle(Ping notification, CancellationToken cancellationToken) {
        Debug.WriteLine("Pong 2");
        return Task.CompletedTask;
    }
}

//發(fā)布消息
await mediator.Publish(new Ping());

源碼解析

對MediatR有了基本認(rèn)識后列牺,我們來看看源碼,研究下其如何實現(xiàn)的拗窃。

類圖

從代碼圖中我們可以看到其核心的對象主要包括:

  1. IRequest Vs IRequestHandler
  2. INotification Vs INoticifaitonHandler
  3. IMediator Vs Mediator
  4. Unit
  5. IPipelineBehavior

IRequest Vs IRequestHandler

其中IRequestINotification分別對應(yīng)單播和多播消息的抽象瞎领。
對于單播消息可以決定是否需要返回值選用不同的接口:

  • IRequest<T> - 有返回值
  • IRequest - 無返回值

這里就不得不提到其中巧妙的設(shè)計,通過引入結(jié)構(gòu)類型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也是通過結(jié)構(gòu)類型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的包裝類型,而因此賦予了其具有以同步和異步的方式進行消息處理的能力宾毒。我們再看一下其以異步方式進行消息處理(無返回值)的默認(rèn)實現(xiàn)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);
}

從上面的代碼來看驼修,我們很容易看出這是裝飾模式的實現(xiàn)方式,是不是很巧妙的解決了無需返回值的場景诈铛。

最后我們來看下結(jié)構(gòu)類型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

MediatR 類圖

IMediator主要定義了兩個方法SendPublish乙各,分別用于發(fā)送消息和發(fā)布通知。其默認(rèn)實現(xiàn)Mediator中定義了兩個集合幢竹,分別用來保存請求與請求處理的映射關(guān)系觅丰。

//Mediator.cs
//保存request和requesthandler的映射關(guān)系,1對1妨退。
private static readonly ConcurrentDictionary<Type, object> _requestHandlers = new ConcurrentDictionary<Type, object>();
//保存notification與notificationhandler的映射關(guān)系,
private static readonly ConcurrentDictionary<Type, NotificationHandlerWrapper> _notificationHandlers = new ConcurrentDictionary<Type, NotificationHandlerWrapper>();

這里面其又引入了兩個包裝類:RequestHandlerWrapperNotificationHandlerWrapper蜕企。這兩個包裝類的作用就是用來傳遞ServiceFactory委托進行依賴解析咬荷。

所以說Mediator借助public delegate object ServiceFactory(Type serviceType);完成對Ioc容器的一層抽象。這樣就可以對接任意你喜歡用的Ioc容器轻掩,比如:Autofac幸乒、Windsor或ASP.NET Core默認(rèn)的Ioc容器,只需要在注冊IMediator時指定ServiceFactory類型的委托即可唇牧,比如ASP.NET Core中的做法:

ASP.NET Core注冊IMediatr

在使用ASP.NET Core提供的原生Ioc容器有些問題:Service registration crashes when registering generic handlers

IPipelineBehavior

處理管道

MeidatR支持按需配置請求管道進行消息處理罕扎。即支持在請求處理前和請求處理后添加額外行為聚唐。僅需實現(xiàn)以下兩個接口,并注冊到Ioc容器即可腔召。

  • IRequestPreProcessor<in TRequest> 請求處理前接口
  • IRequestPostProcessor<in TRequest, in TResponse> 請求處理后接口

其中IPipelineBehavior的默認(rèn)實現(xiàn):RequestPreProcessorBehaviorRequestPostProcessorBehavior分別用來處理所有實現(xiàn)IRequestPreProcessorIRequestPostProcessor接口定義的管道行為杆查。

而處理管道是如何構(gòu)建的呢?我們來看下RequestHandlerWrapperImpl的具體實現(xiàn):

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))();
    }
}

就這樣一個簡單的函數(shù)臀蛛,涉及的知識點還真不少亲桦,說實話我花了不少時間來理清這個邏輯。
那都涉及到哪些知識點呢浊仆?我們一個一個的來理一理客峭。

  1. C# 7.0的新特性 - 局部函數(shù)
  2. C# 6.0的新特性 - 表達式形式的成員函數(shù)
  3. Linq高階函數(shù) - Aggregate
  4. 匿名委托
  5. 構(gòu)造委托函數(shù)鏈

關(guān)于第1、2個知識點抡柿,請看下面這段代碼:

public delegate int SumDelegate();//定義委托
public static void Main()
{
    //局部函數(shù)(在函數(shù)內(nèi)部定義函數(shù))
    //表達式形式的成員函數(shù)舔琅, 相當(dāng)于 int Sum() { return 1 + 2;}
    int Sum() => 1 + 2;

    var sumDelegate = (SumDelegate)Sum;//轉(zhuǎn)換為委托
    Console.WriteLine(sumDelegate());//委托調(diào)用,輸出:3
}

再看第4個知識點洲劣,匿名委托:

public delegate int SumDelegate();

SumDelegate delegater1 = delegate(){ return 1+2; }
//也相當(dāng)于
SumDelegate delegater2 => 1+2;

下面再來介紹一下Aggregate這個Linq高階函數(shù)备蚓。Aggregate是對一個集合序列進行累加操作,通過指定初始值闪檬,累加函數(shù)星著,以及結(jié)果處理函數(shù)完成計算。

函數(shù)定義:

public static TResult Aggregate<TSource,TAccumulate,TResult>
(this IEnumerable<TSource> source, 
TAccumulate seed, 
Func<TAccumulate,TSource,TAccumulate> func, 
Func<TAccumulate,TResult> resultSelector);

根據(jù)函數(shù)定義我們來寫個簡單的demo:

var nums = Enumerable.Range(2, 3);//[2,3,4]
// 計算1到5的累加之和粗悯,再將結(jié)果乘以2
var sum = nums.Aggregate(1, (total, next) => total + next, result => result * 2);// 相當(dāng)于 (((1+2)+3)+4)*2=20
Console.WriteLine(sum);//20

和函數(shù)參數(shù)進行一一對應(yīng):

  1. seed : 1
  2. Func<TAccumulate,TSource,TAccumulate> func : (total, next) => total + next
  3. Func<TAccumulate,TResult> resultSelector : result => result * 2

基于上面的認(rèn)識虚循,我們再來回過頭梳理一下RequestHandlerWrapperImpl
其主要是借助委托:public delegate Task<TResponse> RequestHandlerDelegate<TResponse>();來構(gòu)造委托函數(shù)鏈來構(gòu)建處理管道样傍。

Aggregate函數(shù)了解后横缔,我們就不難理解處理管道的構(gòu)建了。請看下圖中的代碼解讀:

請求處理管道代碼解讀
構(gòu)建流程解析

那如何保證先執(zhí)行IRequestPreProcessor再執(zhí)行IRequestPostProcessor呢衫哥?
就是在注冊到Ioc容器時必須保證順序茎刚,先注冊IRequestPreProcessor再注冊IRequestPostProcessor。(這一點很重要3贩辍L哦А!)

看到這里有沒有想到ASP.NET Core中請求管道中中間件的構(gòu)建呢蚊荣?是不是很像俄羅斯套娃初狰?先由內(nèi)而外構(gòu)建管道,再由外而內(nèi)執(zhí)行互例!

至此奢入,MediatR的實現(xiàn)思路算是理清了。

應(yīng)用場景

如文章開頭提到:MediatR是一種進程內(nèi)消息傳遞機制媳叨。 支持以同步或異步的形式進行請求/響應(yīng)腥光,命令关顷,查詢,通知和事件的消息傳遞武福,并通過C#泛型支持消息的智能調(diào)度议双。

那么我們就應(yīng)該明白,其核心是消息的解耦艘儒。因為我們幾乎都是在與消息打交道聋伦,那因此它的應(yīng)用場景就很廣泛,比如我們可以基于MediatR實現(xiàn)CQRS界睁、EventBus等觉增。

另外,還有一種應(yīng)用場景:我們知道借助依賴注入的好處是翻斟,就是解除依賴逾礁,但我們又不得不思考一個問題,隨著業(yè)務(wù)邏輯復(fù)雜度的增加访惜,構(gòu)造函數(shù)可能要注入更多的服務(wù)嘹履,當(dāng)注入的依賴太多時,其會導(dǎo)致構(gòu)造函數(shù)膨脹债热。比如:

public DashboardController(
    ICustomerRepository customerRepository,
    IOrderService orderService,
    ICustomerHistoryRepository historyRepository,
    IOrderRepository orderRepository,
    IProductRespoitory productRespoitory,
    IRelatedProductsRepository relatedProductsRepository,
    ISupportService supportService,
    ILog logger
    )  

如果借助MediatR進行改造砾嫉,也許僅需注入IMediatR就可以了。

public DashboardController(IMediatR mediatr)  

總結(jié)

看到這里窒篱,也許你應(yīng)該明白MediatR實質(zhì)上并不是嚴(yán)格意義上的中介者模式實現(xiàn)焕刮,我更傾向于其是基于Ioc容器的一層抽象,根據(jù)請求定位相應(yīng)的請求處理器進行消息處理墙杯,也就是服務(wù)定位配并。
那到這里似乎也恍然大悟MediatR這個筆誤可能是有意為之了。序員高镐,你怎么看溉旋?

參考資料:
CQRS/MediatR implementation patterns
MediatR when and why I should use it? vs 2017 webapi
ABP CQRS 實現(xiàn)案例:基于 MediatR 實現(xiàn)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市嫉髓,隨后出現(xiàn)的幾起案子观腊,更是在濱河造成了極大的恐慌,老刑警劉巖算行,帶你破解...
    沈念sama閱讀 216,496評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件恕沫,死亡現(xiàn)場離奇詭異,居然都是意外死亡纱意,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評論 3 392
  • 文/潘曉璐 我一進店門鲸阔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來偷霉,“玉大人迄委,你說我怎么就攤上這事±嗌伲” “怎么了叙身?”我有些...
    開封第一講書人閱讀 162,632評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長硫狞。 經(jīng)常有香客問我信轿,道長,這世上最難降的妖魔是什么残吩? 我笑而不...
    開封第一講書人閱讀 58,180評論 1 292
  • 正文 為了忘掉前任财忽,我火速辦了婚禮,結(jié)果婚禮上泣侮,老公的妹妹穿的比我還像新娘即彪。我一直安慰自己,他們只是感情好活尊,可當(dāng)我...
    茶點故事閱讀 67,198評論 6 388
  • 文/花漫 我一把揭開白布隶校。 她就那樣靜靜地躺著,像睡著了一般蛹锰。 火紅的嫁衣襯著肌膚如雪深胳。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,165評論 1 299
  • 那天铜犬,我揣著相機與錄音舞终,去河邊找鬼。 笑死翎苫,一個胖子當(dāng)著我的面吹牛权埠,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播煎谍,決...
    沈念sama閱讀 40,052評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼攘蔽,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了呐粘?” 一聲冷哼從身側(cè)響起满俗,我...
    開封第一講書人閱讀 38,910評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎作岖,沒想到半個月后唆垃,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,324評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡痘儡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,542評論 2 332
  • 正文 我和宋清朗相戀三年辕万,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,711評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡渐尿,死狀恐怖醉途,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情砖茸,我是刑警寧澤隘擎,帶...
    沈念sama閱讀 35,424評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站凉夯,受9級特大地震影響货葬,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜劲够,卻給世界環(huán)境...
    茶點故事閱讀 41,017評論 3 326
  • 文/蒙蒙 一震桶、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧再沧,春花似錦尼夺、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽棍潘。三九已至赴魁,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間氮帐,已是汗流浹背隘截。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評論 1 269
  • 我被黑心中介騙來泰國打工扎阶, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人婶芭。 一個月前我還...
    沈念sama閱讀 47,722評論 2 368
  • 正文 我出身青樓东臀,卻偏偏與公主長得像,于是被迫代替她去往敵國和親犀农。 傳聞我的和親對象是個殘疾皇子惰赋,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,611評論 2 353

推薦閱讀更多精彩內(nèi)容