重溫.NET下Assembly的加載過程 ASP.NET Core Web API下事件驅動型架構的實現(三):基於RabbitMQ的事件總線

重溫.NET下Assembly的加載過程

 

最近在工做中牽涉到了.NET下的一個古老的問題:Assembly的加載過程。雖然網上有不少文章介紹這部份內容,不少文章也是好久之前就已經出現了,但閱讀以後發現,並沒能解決個人問題,有些點寫的不是特別詳細,讓人看完以後感受仍是雲裏霧裏。最後,我決定從新複習一下這個經典而古老的問題,並將所得總結於此,而後會有一個實例對這個問題進行演示,但願可以幫助到你們。html

.NET下Assembly的加載過程

.NET下Assembly的加載,最主要的一步就是肯定Assembly的版本。在.NET下,託管的DLL和EXE都稱之爲Assembly,Assembly由AssemblyName來惟一標識,AssemblyName也就是你們所熟悉的Assembly.FullName,它是由五部分:名稱、版本、語言、公鑰Token、處理器架構組成的,這一點相信你們都知道。有關Assembly Name的詳細描述,請參考:https://docs.microsoft.com/en-us/dotnet/framework/app-domains/assembly-names。那麼版本,就是AssemblyName中的一個重要組成部分。其它四部分相同,版本若是不一樣的話,就不能算做是同一個Assembly。設計這樣一個Assembly的版本策略,微軟自己就是爲了解決最開始的DLL Hell的問題,在維基百科上着關於這段黑歷史的詳細描述,地址是:https://en.wikipedia.org/wiki/DLL_Hell,在此也就很少囉嗦了。git

Assembly版本的重定向和最終肯定

.NET下Assembly的加載過程,其實也是Assembly版本的肯定和Assembly文件的定位過程,步驟以下:github

  1. 在一個Assembly被編譯的時候,它所引用的Assembly的全名(FullName)就會被編譯器強行寫入Assembly的Metadata,這個值是死的,從ILSpy能夠看到,每一個Reference都有它的全名信息:
    image
    例如上圖,System.Data依賴System.Xml,它所須要的版本是4.0.0.0,那麼當CLR加載System.Data的時候,就能夠暫且認爲接下來須要加載的System.Xml版本是4.0.0.0。這裏強調「暫且認爲」,是由於這只是肯定Assembly版本的第一步,那麼最終System.Xml究竟是不是使用4.0.0.0的版本呢?就須要看接下來這步的處理結果,也就是Assembly版本的重定向

  2. 首先,檢查應用程序的配置文件,看是否存在Assembly版本重定向的設定。咱們暫時先討論應用程序配置文件就在AppDomain內的狀況(若是在AppDomain以外,則須要首先下載配置文件,再繼續,這裏先不深刻討論)。應用程序配置文件常見的有.exe.config和web.config兩種。在配置文件中,能夠在runtime節點下的assemblyBinding中進行配置。例如:
    image
    在這個例子中,asm6 Assembly的版本號被重定向到2.0.0.0。那麼假設這就是asm6的最終版本號,那麼接下來當CLR開始加載asm6的時候,若是2.0.0.0的版本沒有找到,則直接拋出FileLoadException(即便3.0.0.0的版本是存在的),整個Assembly加載過程結束。FileLoadException的詳細信息相似於:Could not load file or assembly 'asm6, Version=3.0.0.0, Culture=neutral, PublicKeyToken=c0305c36380ba429' or one of its dependencies. The located assembly's manifest definition does not match the assembly reference

  3. 若是在配置文件中找到了對應的版本重定向設定,那麼,再接着查看Publisher Policy文件。Publisher Policy文件是一個僅包含配置文件的.NET Assembly,被安裝到GAC裏。它的Assembly版本重定向配置內容跟上面的應用程序配置文件的配置內容相同,不一樣的是,它的做用域是全部使用了該Assembly的應用程序。這種作法對於開發系統級通用框架的Assembly升級很是有用,好比.NET Framework。下面就是安裝在GAC裏的Publisher Policy文件的樣本,須要注意:Publisher Policy會override應用程序配置信息中的版本重定向配置,而不是相反。換言之,假如asm6在上面這一步被肯定爲2.0.0.0,而所對應的Publisher Policy文件又將其肯定爲2.5.0.0,那麼,暫且認爲,CLR應該要加載2.5.0.0的版本。同理,「暫且認爲」這個詞表示,版本肯定的過程還未結束
    image

  4. 接下來,查找machine.config文件。同理,若是machine.config文件中存在版本重定向的設定,那麼就會使用machine.config文件中的這個值,做爲CLR應該去加載的Assembly的版本

至此,Assembly的最終版本已被肯定,接下來就是搜索Assembly文件並進行加載的過程了。web

Assembly文件的搜索和加載過程

如今,CLR已經開始加載肯定版本的Assembly了,接下來就是搜索Assembly文件的過程。這個過程也叫做Assembly Probing。CLR會作如下事情:sql

  1. 首先,查看所需的Assembly是否已經加載過,若是已經加載了,那就直接使用那個已經加載的Assembly的版本與當前所需的版本進行比對,若是匹配,則使用那個已經加載的Assembly,若是不匹配,則拋出FileLoadException,執行結束
  2. 而後,看Assembly是否已被強簽名(Strongly Named),若是是,則去GAC裏查找Assembly。若是找到,則直接加載,整個Assembly加載過程結束。若是沒有找到,那麼就進行下一步,繼續搜索Assembly文件。固然,若是Assembly沒有進行強簽名,那麼就跳過這一步,直接繼續
  3. 接着,CLR開始搜索(Probing)可能的Assembly位置,這又要分多種狀況:
    1. 首先,查看文件中是否有指定<codeBase>,codeBase配置容許應用程序針對Assembly的不一樣版本指定裝載地址,遵循以下規律:
      1. 若是所指定的Assembly文件位於當前應用程序域的啓動目錄(或其子目錄)下,則使用相對路徑指定href的值
      2. 若是所指定的Assembly文件位於其它目錄,或任何其它地方,則href必須給出全路徑,而且Assembly必須強簽名的
    2. 而後,CLR對應用程序域的根目錄以及相關的子目錄進行探索:
      1. 假設Assembly的名字是abc.dll,那麼CLR會探索如下目錄:
        1. [appdomain_base]\abc.dll
        2. [appdomain_base]\abc\abc.dll
      2. 假設abc.dll還有語言設置(culture不是neutral),那麼CLR會探索如下目錄:
        1. [appdomain_base]\[culture]\abc.dll
        2. [appdomain_base]\[culture]\abc\abc.dll
    3. 若是找到符合版本的Assembly,則加載,不然進入下一步
  4. 最後,CLR會查看應用程序配置文件中是否有<probling>節點,若是有,則按probling節點所指定的privatePath值進行逐一探索。這個過程也會考慮culture的因素,相似於上面這步這樣,對相應的子目錄進行搜索。若是找到對應的Assembly,則加載,不然拋出FileLoadException,整個加載過程結束。注意,這裏「逐一探索」的過程,不是遍歷並找最佳匹配的過程。CLR僅根據Assembly的名字(不帶版本號的名字)在privatePath下查找Assembly的文件,找到第一個名字匹配可是版本不匹配的話,就拋異常並終止加載了,它不會繼續搜索privatePath中餘下的其它路徑

在加載Assembly文件失敗的時候,AppDomain會觸發AssemblyResolve的事件,在這個事件的訂閱函數中,容許客戶程序自定義對加載失敗的Assembly的處理方式,好比,能夠經過Assembly.LoadFrom或者Assembly.LoadFile調用「手動地」將Assembly加載到AppDomain。shell

fuslogvw Assembly綁定日誌查看器

在.NET SDK中帶了一個fuslogvw.exe的應用程序,經過它能夠查看詳細的Assembly加載過程。使用方法很是簡單,使用管理員身份啓動Visual Studio 2017 Developer Command Prompt,而後在命令行輸入fuslogvw.exe,便可啓動日誌查看器。啓動以後,點擊Settings按鈕,以啓用日誌記錄功能:數據庫

image

日誌啓動以後,點擊Refresh按鈕,而後啓動你的.NET應用程序,就能夠看到當前應用程序所依賴的Assembly的加載過程日誌了:json

image

接下來,我會作一個例子程序,而後使用這個工具來分析Assembly的加載過程。架構

插件系統的實現與Assembly加載過程的分析

理論結合實際,看看如何經過實際代碼來詮釋以上所述Assembly的加載過程。一個比較好的例子就是設計一個簡單的插件系統,並經過觀察系統加載插件的過程,來了解Assembly加載的前因後果。爲了簡單直觀,我把這個插件系統稱爲PluginDemo。這個插件很簡單,主體程序是一個控制檯應用程序,而後咱們實現兩個插件:Earth和Mars,在不一樣的插件的Initialize方法中,會輸出不一樣的字符串。app

整個應用程序的項目結構以下:

image

該插件系統包含4個C#的項目:

  • PluginDemo.Common:它定義了AddIn抽象類,全部的插件實現都須要繼承於這個抽象類。此外,AddInDefinition類是一個用來保存插件Metadata的類。爲了演示,插件的Metadata僅僅包含插件類型的Assembly Qualified Name
  • PluginDemo.App:插件系統的應用程序。這個程序執行的時候,會掃描程序目錄下Modules目錄中的DLL,並根據module.xml的Metadata信息,加載相應的插件對象,並執行Initialize方法
  • PluginDemo.Plugins.Earth:其中的一個插件實現
  • PluginDemo.Plugins.Mars:另外一個插件實現

注意:除了PluginDemo.Common以外的其它三個項目,都對PluginDemo.Common有引用關係。而PluginDemo.App項目僅僅在項目自己依賴於PluginDemo.Plugins.Earth和PluginDemo.Plugins.Mars,它不會去引用這兩個項目。目的就是爲了當PluginDemo.App被編譯時,其他兩個插件項目也會同時被編譯並輸出到指定位置。

在Earth插件的CustomAddIn類中,咱們實現了Initialize方法,並在此輸出一個字符串:

1
2
3
4
5
6
7
8
9
public  class  CustomAddIn : AddIn
{
     public  override  string  Name => "Earth AddIn" ;
 
     public  override  void  Initialize()
     {
         Console.WriteLine( "Earth Plugin initialized." );
     }
}

在Mars插件的CustomAddIn類中,咱們也實現了Initialize方法,並在此輸出一個字符串:

1
2
3
4
5
6
7
8
9
public  class  CustomAddIn : AddIn
{
     public  override  string  Name => "Mars AddIn" ;
 
     public  override  void  Initialize()
     {
         Console.WriteLine( "Mars AddIn initialized." );
     }
}

那麼,在插件系統主程序中,就會掃描Modules子目錄下的module.xml文件,而後解析每一個module.xml文件得到每一個插件類的Assembly Qualified Name,而後經過Type.GetType方法得到插件類,進而建立實例、調用Initialize方法。代碼以下:

1
2
3
4
5
6
7
8
9
10
11
12
static  void  Main()
{
     var  directory = new  DirectoryInfo( "Modules" );
     foreach ( var  file in  directory.EnumerateFiles( "module.xml" , SearchOption.AllDirectories))
     {
         var  addinDefinition = AddInDefinition.ReadFromFile(file.FullName);
         var  addInType = Type.GetType(addinDefinition.FullName);
         var  addIn = (AddIn)Activator.CreateInstance(addInType);
         Console.WriteLine($ "{addIn.Id} - {addIn.Name}" );
         addIn.Initialize();
     }
}

接下來,修改App.config文件,修改成:

1
2
3
4
5
6
7
8
<? xml  version="1.0" encoding="utf-8" ?>
< configuration >
   < runtime >
     < assemblyBinding  xmlns="urn:schemas-microsoft-com:asm.v1">
       < probing  privatePath="Modules\Earth;Modules\Mars;" />
     </ assemblyBinding >
   </ runtime >
</ configuration >

此時,運行程序,能夠獲得:

image

目前沒有什麼問題。接下來,對兩個AddIn分別作一些修改。讓這兩個AddIn依賴於不一樣版本的Newtonsoft.Json,好比,Earth依賴於7.0.0.0的版本,Mars依賴於6.0.0.0的版本,而後分別修改兩個CustomAddIn的Initialize方法,在方法中各自調用一次JsonConvert.SerializeObject方法,以觸發Newtonsoft.Json這個Assembly的加載。此時再次運行程序,你將看到下面的異常:

image

如今,刷新fuslogvw.exe,找到Newtonsoft.Json的日誌:

image

雙擊打開日誌,能夠看到以下信息:

image

從整個過程能夠看出:

  1. PluginDemo.App.exe正在試圖加載PluginDemo.Plugins.Mars Assembly
  2. PluginDemo.Plugins.Mars開始調用Newtonsoft.Json
  3. 掃描應用程序配置文件、Host配置文件以及machine.config文件,均無找到Newtonsoft.Json的重定向信息,此時,Newtonsoft.Json版本肯定爲6.0.0.0
  4. GAC掃描失敗,繼續查找文件
  5. 首先查找應用程序當前目錄下有沒有Newtonsoft.Json,以及Newtonsoft.Json子目錄下有沒有Newtonsoft.Json.dll,發現都沒有,繼續
  6. 而後,經過App.config中的probing的privatePath設定,首先查找Modules\Earth目錄(由於這個目錄放在privatePath的第一個),找到了一個叫作Newtonsoft.Json.dll的Assembly,因而,判斷版本是否相同。結果,找到的是7.0.0.0,而它須要的倒是6.0.0.0,版本不匹配,因而就拋出異常,退出程序

那麼接下來,改一改App.config文件,將privatePath下的兩個值換個位置呢?

image

再試試:

image

此時,Earth AddIn又出錯了。那麼,咱們加上版本重定向的配置,指定當程序須要加載7.0.0.0版本的Newtonsoft.Json時,讓它重定向到6.0.0.0的版本:

image

再次執行,成功了:

image

看看日誌:

image

版本已經被重定向到6.0.0.0,而且在Mars目錄下找到了6.0.0.0的Newtonsoft.Json,加載成功了。

這個案例的源代碼能夠點擊此處下載

總結

本文詳細介紹了.NET下Assembly的版本肯定和加載過程,最後給出了一個實例,對這個過程進行了演示。

 
 
 
--

ASP.NET Core Web API下事件驅動型架構的實現(三):基於RabbitMQ的事件總線

 

在上文中,咱們討論了事件處理器中對象生命週期的問題,在進入新的討論以前,首先讓咱們總結一下,咱們已經實現了哪些內容。下面的類圖描述了咱們已經實現的組件及其之間的關係,貌似系統已經變得愈來愈複雜了。

class_diagram_chapter2

其中綠色的部分就是上文中新實現的部分,包括一個簡單的Event Store,一個事件處理器執行上下文的接口,以及一個基於ASP.NET Core依賴注入框架的執行上下文的實現。接下來,咱們打算淘汰PassThroughEventBus,而後基於RabbitMQ實現一套新的事件總線。

事件總線的重構

根據前面的結論,事件總線的執行須要依賴於事件處理器執行上下文,也就是上面類圖中PassThroughEventBus對於IEventHandlerExecutionContext的引用。更具體些,是在事件總線訂閱某種類型的事件時,須要將事件處理器註冊到IEventHandlerExecutionContext中。那麼在實現RabbitMQ時,也會有着相似的設計需求,即RabbitMQEventBus也須要依賴IEventHandlerExecutionContext接口,以保證事件處理器生命週期的合理性。

爲此,咱們新建一個基類:BaseEventBus,並將這部分公共的代碼提取出來,須要注意如下幾點:

  1. 經過BaseEventBus的構造函數傳入IEventHandlerExecutionContext實例,也就限定了全部子類的實現中,必須在構造函數中傳入IEventHandlerExecutionContext實例,這對於框架的設計很是有利:在實現新的事件總線時,框架的使用者無需查看API文檔,便可知道事件總線與IEventHandlerExecutionContext之間的關係,這符合SOLID原則中的Open/Closed Principle
  2. BaseEventBus的實現應該放在EdaSample.Common程序集中,更確切地說,它應該放在EdaSample.Common.Events命名空間下,由於它是屬於框架級別的組件,而且不會依賴任何基礎結構層的組件

BaseEventBus的代碼以下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public  abstract  class  BaseEventBus : IEventBus
{
     protected  readonly  IEventHandlerExecutionContext eventHandlerExecutionContext;
 
     protected  BaseEventBus(IEventHandlerExecutionContext eventHandlerExecutionContext)
     {
         this .eventHandlerExecutionContext = eventHandlerExecutionContext;
     }
 
     public  abstract  Task PublishAsync<TEvent>(TEvent @ event , CancellationToken cancellationToken = default ) where  TEvent : IEvent;
 
     public  abstract  void  Subscribe<TEvent, TEventHandler>()
         where  TEvent : IEvent
         where  TEventHandler : IEventHandler<TEvent>;
     
     // Disposable接口實現代碼省略
}

在上面的代碼中,PublishAsync和Subscribe方法是抽象方法,以便子類根據不一樣的須要來實現。

接下來就是調整PassThroughEventBus,使其繼承於BaseEventBus:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public  sealed  class  PassThroughEventBus : BaseEventBus
{
     private  readonly  EventQueue eventQueue = new  EventQueue();
     private  readonly  ILogger logger;
 
     public  PassThroughEventBus(IEventHandlerExecutionContext context,
         ILogger<PassThroughEventBus> logger)
         : base (context)
     {
         this .logger = logger;
         logger.LogInformation($ "PassThroughEventBus構造函數調用完成。Hash Code:{this.GetHashCode()}." );
 
         eventQueue.EventPushed += EventQueue_EventPushed;
     }
 
     private  async void  EventQueue_EventPushed( object  sender, EventProcessedEventArgs e)
         => await this .eventHandlerExecutionContext.HandleEventAsync(e.Event);
 
     public  override  Task PublishAsync<TEvent>(TEvent @ event , CancellationToken cancellationToken = default )
     {
         return  Task.Factory.StartNew(() => eventQueue.Push(@ event ));
     }
 
     public  override  void  Subscribe<TEvent, TEventHandler>()
     {
         if  (! this .eventHandlerExecutionContext.HandlerRegistered<TEvent, TEventHandler>())
         {
             this .eventHandlerExecutionContext.RegisterHandler<TEvent, TEventHandler>();
         }
     }
     
     // Disposable接口實現代碼省略
}

代碼都很簡單,也就很少作說明了,接下來,咱們開始實現RabbitMQEventBus。

RabbitMQEventBus的實現

首先須要新建一個.NET Standard 2.0的項目,使用.NET Standard 2.0的項目模板所建立的項目,能夠同時被.NET Framework 4.6.1或者.NET Core 2.0的應用程序所引用。建立新的類庫項目的目的,是由於RabbitMQEventBus的實現須要依賴RabbitMQ C#開發庫這個外部引用。所以,爲了保證框架核心的純淨和穩定,須要在新的類庫項目中實現RabbitMQEventBus。

Note:對於RabbitMQ及其C#庫的介紹,本文就再也不涉及了,網上有不少資料和文檔,博客園有不少朋友在這方面都有使用經驗分享,RabbitMQ官方文檔也寫得很是詳細,固然是英文版的,若是英語比較好的話,建議參考官方文檔。

如下就是在EdaSample案例中,RabbitMQEventBus的實現,咱們先讀一讀代碼,再對這部分代碼作些分析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
public  class  RabbitMQEventBus : BaseEventBus
{
     private  readonly  IConnectionFactory connectionFactory;
     private  readonly  IConnection connection;
     private  readonly  IModel channel;
     private  readonly  string  exchangeName;
     private  readonly  string  exchangeType;
     private  readonly  string  queueName;
     private  readonly  bool  autoAck;
     private  readonly  ILogger logger;
     private  bool  disposed;
 
     public  RabbitMQEventBus(IConnectionFactory connectionFactory,
         ILogger<RabbitMQEventBus> logger,
         IEventHandlerExecutionContext context,
         string  exchangeName,
         string  exchangeType = ExchangeType.Fanout,
         string  queueName = null ,
         bool  autoAck = false )
         : base (context)
     {
         this .connectionFactory = connectionFactory;
         this .logger = logger;
         this .connection = this .connectionFactory.CreateConnection();
         this .channel = this .connection.CreateModel();
         this .exchangeType = exchangeType;
         this .exchangeName = exchangeName;
         this .autoAck = autoAck;
 
         this .channel.ExchangeDeclare( this .exchangeName, this .exchangeType);
 
         this .queueName = this .InitializeEventConsumer(queueName);
 
         logger.LogInformation($ "RabbitMQEventBus構造函數調用完成。Hash Code:{this.GetHashCode()}." );
     }
 
     public  override  Task PublishAsync<TEvent>(TEvent @ event , CancellationToken cancellationToken = default (CancellationToken))
     {
         var  json = JsonConvert.SerializeObject(@ event , new  JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All });
         var  eventBody = Encoding.UTF8.GetBytes(json);
         channel.BasicPublish( this .exchangeName,
             @ event .GetType().FullName,
             null ,
             eventBody);
         return  Task.CompletedTask;
     }
 
     public  override  void  Subscribe<TEvent, TEventHandler>()
     {
         if  (! this .eventHandlerExecutionContext.HandlerRegistered<TEvent, TEventHandler>())
         {
             this .eventHandlerExecutionContext.RegisterHandler<TEvent, TEventHandler>();
             this .channel.QueueBind( this .queueName, this .exchangeName, typeof (TEvent).FullName);
         }
     }
 
     protected  override  void  Dispose( bool  disposing)
     {
         if  (!disposed)
         {
             if  (disposing)
             {
                 this .channel.Dispose();
                 this .connection.Dispose();
 
                 logger.LogInformation($ "RabbitMQEventBus已經被Dispose。Hash Code:{this.GetHashCode()}." );
             }
 
             disposed = true ;
             base .Dispose(disposing);
         }
     }
 
     private  string  InitializeEventConsumer( string  queue)
     {
         var  localQueueName = queue;
         if  ( string .IsNullOrEmpty(localQueueName))
         {
             localQueueName = this .channel.QueueDeclare().QueueName;
         }
         else
         {
             this .channel.QueueDeclare(localQueueName, true , false , false , null );
         }
 
         var  consumer = new  EventingBasicConsumer( this .channel);
         consumer.Received += async (model, eventArgument) =>
         {
             var  eventBody = eventArgument.Body;
             var  json = Encoding.UTF8.GetString(eventBody);
             var  @ event  = (IEvent)JsonConvert.DeserializeObject(json, new  JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All });
             await this .eventHandlerExecutionContext.HandleEventAsync(@ event );
             if  (!autoAck)
             {
                 channel.BasicAck(eventArgument.DeliveryTag, false );
             }
         };
 
         this .channel.BasicConsume(localQueueName, autoAck: this .autoAck, consumer: consumer);
 
         return  localQueueName;
     }
}

閱讀上面的代碼,須要注意如下幾點:

  1. 正如上面所述,構造函數須要接受IEventHandlerExecutionContext對象,並經過構造函數的base調用,將該對象傳遞給基類
  2. 構造函數中,queueName參數是可選參數,也就是說:
    1. 若是經過RabbitMQEventBus發送事件消息,則無需指定queueName參數,僅需指定exchangeName便可,由於在RabbitMQ中,消息的發佈方無需知道消息是發送到哪一個隊列中
    2. 若是經過RabbitMQEventBus接收事件消息,那麼也分兩種狀況:
      1. 若是兩個進程在使用RabbitMQEventBus時,同時指定了queueName參數,而且queueName的值相同,那麼這兩個進程將會輪流處理路由至queueName隊列的消息
      2. 若是兩個進程在使用RabbitMQEventBus時,同時指定了queueName參數,但queueName的值不相同,或者都沒有指定queueName參數,那麼這兩個進程將會同時處理路由至queueName隊列的消息
    3. 有關Exchange和Queue的概念,請參考RabbitMQ的官方文檔
  3. 在Subscribe方法中,除了將事件處理器註冊到事件處理器執行上下文以外,還經過QueueBind方法,將指定的隊列綁定到Exchange上
  4. 事件數據都經過Newtonsoft.Json進行序列化和反序列化,使用TypeNameHandling.All這一設定,使得序列化的JSON字符串中帶有類型名稱信息。在此處這樣作既是合理的,又是必須的,由於若是沒有帶上類型名稱的信息,JsonConvert.DeserializeObject反序列化時,將沒法斷定獲得的對象是否能夠轉換爲IEvent對象,這樣就會出現異常。但若是是實現一個更爲通用的消息系統,應用程序派發出去的事件消息可能還會被由Python或者Java所實現的應用程序所使用,那麼對於這些應用,它們並不知道Newtonsoft.Json是什麼,也沒法經過Newtonsoft.Json加入的類型名稱來獲知事件消息的初衷(Intent),Newtonsoft.Json所帶的類型信息又會顯得冗餘。所以,簡單地使用Newtonsoft.Json做爲事件消息的序列化、反序列化工具,實際上是欠妥的。更好的作法是,實現自定義的消息序列化、反序列化器,在進行序列化的時候,將.NET相關的諸如類型信息等,做爲Metadata(元數據)附着在序列化的內容上。理論上說,在序列化的數據中加上一些元數據信息是合理的,只不過咱們對這些元數據作一些標註,代表它是由.NET框架產生的,第三方系統若是不關心這些信息,能夠對元數據不作任何處理
  5. 在Dispose方法中,注意將RabbitMQ所使用的資源dispose掉

使用RabbitMQEventBus

在Customer服務中,使用RabbitMQEventBus就很是簡單了,只須要引用RabbitMQEventBus的程序集,而後在Startup.cs文件的ConfigureServices方法中,替換PassThroughEventBus的使用便可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public  void  ConfigureServices(IServiceCollection services)
{
     this .logger.LogInformation( "正在對服務進行配置..." );
 
     services.AddMvc();
 
     services.AddTransient<IEventStore>(serviceProvider =>
         new  DapperEventStore(Configuration[ "mssql:connectionString" ],
             serviceProvider.GetRequiredService<ILogger<DapperEventStore>>()));
 
     var  eventHandlerExecutionContext = new  EventHandlerExecutionContext(services,
         sc => sc.BuildServiceProvider());
     services.AddSingleton<IEventHandlerExecutionContext>(eventHandlerExecutionContext);
     // services.AddSingleton<IEventBus, PassThroughEventBus>();
 
     var  connectionFactory = new  ConnectionFactory { HostName = "localhost"  };
     services.AddSingleton<IEventBus>(sp => new  RabbitMQEventBus(connectionFactory,
         sp.GetRequiredService<ILogger<RabbitMQEventBus>>(),
         sp.GetRequiredService<IEventHandlerExecutionContext>(),
         RMQ_EXCHANGE,
         queueName: RMQ_QUEUE));
 
     this .logger.LogInformation( "服務配置完成,已註冊到IoC容器!" );
}

Note:一種更好的作法是經過配置文件來配置IoC容器,在曾經的Microsoft Patterns and Practices Enterprise Library Unity Container中,使用配置文件是很方便的。這樣只須要Customer服務可以經過配置文件來配置IoC容器,同時只須要讓Customer服務依賴(注意,不是程序集引用)於不一樣的事件總線的實現便可,無需對Customer服務從新編譯。

下面來驗證一下效果。首先確保RabbitMQ已經配置並啓動穩當,我是安裝在本地機器上,使用默認安裝。首先啓動ASP.NET Core Web API,而後經過Powershell發起兩次建立Customer的請求:

image

查看一下數據庫是否更新正常:

image

並檢查一下日誌信息:

image

RabbitMQ中Exchange的信息:

image

總結

本文提供了一種RabbitMQEventBus的實現,目前來講是夠用的,並且這種實現是可使用在實際項目當中的。在實際使用中,或許也會碰到一些與RabbitMQ自己有關的問題,這就須要具體問題具體分析了。此外,本文沒有涉及事件消息丟失、重發而後保證最終一致性的問題,這些內容會在後面討論。從下文開始,咱們着手逐步實現CQRS架構的領域事件和事件存儲部分。

源代碼的使用

本系列文章的源代碼在https://github.com/daxnet/edasample這個Github Repo裏,經過不一樣的release tag來區分針對不一樣章節的源代碼。本文的源代碼請參考chapter_3這個tag,以下:

image

歡迎訪問個人博客新站:http://sunnycoding.net

相關文章
相關標籤/搜索