.NET Core中間件的註冊和管道的構建(2) 用UseMiddleware擴展方法註冊中間件類 0x00 為什麼要引入擴展方法 有的中間件功能比較簡單,有的則比較複雜,並且依賴其它組件。除了直接用ApplicationBuilder的Use()方法註冊中間件外,還可以使用ApplicationB ...
.NET Core中間件的註冊和管道的構建(2)---- 用UseMiddleware擴展方法註冊中間件類
0x00 為什麼要引入擴展方法
有的中間件功能比較簡單,有的則比較複雜,並且依賴其它組件。除了直接用ApplicationBuilder的Use()方法註冊中間件外,還可以使用ApplicationBuilder的擴展方法UseMiddleware()註冊中間件。這種情況下可以註冊類型,這個方法會通過反射解析這個類型,並把它包裝成Func<ReuqestDelegate,RequestDelegate>然後調用Use()方法註冊。
遇到這種情況一般直覺上是通過繼承一個抽象類並實現其中的方法在寫一個中間件。不過.NET Core不是這麼做的。中間件類使用約定而不是繼承來進行約束。這裡說的約定就是約定原本的意思,例如約定好了中間件類中必須包含一個叫Invoke的方法,叫別的就不行,有重載也不行。因為中間件類沒有任何繼承上的約束,在註冊過程中就是通過反射去尋找名字為Invoke的方法,然後把它包裝成RequestDelegate的。這篇文章就是要說一下寫一個中間件類都有哪些約定以及中間件類的註冊。
0x01 一個最簡單的例子
先看一個中間件類的最簡單的例子:
上一篇文章中說過了,中間件本質就是一個方法,這個方法接收一個HttpContext參數,返回Task。在上面這個中間件類中Invoke就是這個方法。為了能夠調用下一個中間件,當前中間件還需要保存下一個中間件的引用。這個引用是通過構造函數傳進來的,如果當前中間件不需要調用後面中間件的話,這個引用完全可以不保存。如果要註冊這個中間件,我們可以這樣做:
但如果我們這個中間件比較複雜,依賴很多其他模塊,那麼我們在註冊的時候需要構造依賴模塊的實例,併在中間件類的構造函數中把這些依賴傳進去。這加強了中間件和依賴模塊之間的耦合度。為了能減少這種耦合,同時享受到依賴註入帶來的便利,提供了UseMiddleware<T>擴展方法來註冊中間件類T。
UseMiddleware擴展方法會找到上面中間件類中的Invoke方法,創建上面類的實例,在創建實例時遇到需要註入的類型會嘗試註入,然後把Invoke方法包裝為ReuqestDelegate,進而包裝為Func<RequestDelegate,RequestDelegate>,然後通過ApplicatonBuilder的Use方法(上篇文章講過了)註冊到IList<Func<RequestDelegaet,RequestDelegate>中。
從上面的SimpleMiddleware我們可以看到這個類沒有任何顯示的繼承關係,那麼我們在寫一個中間件類時需要註意哪些約束呢?我們只要看一下UseMiddleware註冊中間件的過程就明白了。下麵是對UseMiddleware()方法的分析,對代碼分析不感興趣的可以跳過直接看後面的結論和測試。
0x02 擴展方法註冊中間件類的過程
使用UseMiddleware<T>擴展方法註冊中間件類T主要包含以下幾個關鍵步驟:
1.找到中間件類的Invoke方法。UseMiddleware方法會通過反射獲取註冊的中間件類的所有public且非static的方法列表,然後從其中找出名字叫Invoke的方法,確認Invoke方法沒有重載,確認Invoke方法返回Task,確認Invoke方法第一個參數是HttpContext,最後這兩個檢查是為了能把Invoke方法包裝為RequestDelegate。
2.選取最佳構造函數。把下一個中間件的引用next插入到從UseMiddleware傳入的參數列表的第一個,作為給定的參數列表。
然後獲取中間件類的所有構造函數,從給定的參數列表中依次取出參數,和構造函數的參數進行類型匹配,匹配最多的構造函數選為最佳構造函數。匹配相同的以代碼中排在前面的構造函數為準(這其中省略了很多匹配最佳構造函數的細節,感興趣的可以自行查看代碼)。
值得註意的是如果存在給定的參數列表中存在某個參數P,在當前構造函數參數列表中找不到與之匹配的類型,那麼這個構造函數不能作為最佳構造函數。也就是說選中的最佳構造函數的參數列表必須要是給定參數列表的超集。剛剛上面也說了,下一個中間件next被插入到了給定參數列表的第一個,因此選中的最佳構造函數參數中必須包含參數RequestDelegate。如果所有構造函數都不包含RequestDelegate,那麼會拋出異常。
3.構造中間件類的實例。找到了最佳構造函數後,接下來就使用該構造函數構造中間件類的實例。對於構造函數中的所有參數,能夠從給定的參數列表中找到類型匹配的,從給定的參數列表中獲取參數。從參數列表中找不到的,則嘗試從依賴註入容器中獲取,依賴註入容器中也找不到的檢查是不是有預設值,預設值也沒有就拋出異常。
4.實例構造完成後,如果Invoke方法只有一個參數(HttpContext)會把這個實例的Invoke方法包裝為RequestDelegate,進而包裝為Func<RequestDelegate,RequestDelegate>然後使用Use方法註冊。如果有多個參數,不符合RequestDelegate約束,則對Invoke進行二次包裝以符合RequestDelegate。在二次包裝中會嘗試從依賴註入容器中獲取Invoke參數中的依賴。
0x03一些結論
下麵總結一下中間件類的一些約定,主要是基於對代碼的理解,有錯誤或不全的地方請指正。
關於中間件的方法:
1.中間件的方法必須叫Invoke,且為public,非static。
2.Invoke方法第一個參數必須是HttpContext類型。
3.Invoke方法必須返回Task。
4.Invoke方法可以有多個參數,除HttpContext外其它參數會嘗試從依賴註入容器中獲取。
5.Invoke方法不能有重載。
關於構造函數:
1.構造函數必須包含RequestDelegate參數,該參數傳入的是下一個中間件。
2.構造函數參數中的RequestDelegate參數不是必須放在第一個,可以是任意位置。
3.構造函數可以有多個參數,參數會優先從給定的參數列表中找,其次會從依賴註入容器中獲取,獲取失敗會嘗試獲取預設值,都失敗會拋出異常。
4.構造函數可以有多個,屆時會根據構造函數參數列表和給定的參數列表選擇匹配度最高的一個。
個人建議,真的僅僅是個人的一些建議:
1.除及特殊情況外只保留一個構造函數,以省去多餘的構造函數匹配檢查。
2.在構造函數中註入所需依賴而不是Invoke中。
3.關於構造函數參數的順序,把RequestDelegate放在第一個;之後是UseMiddleware方法中給出的參數,而且構造函數中參數順序和給定參數列表中的順序最好也相同;然後是需要註入的參數;最後是有預設值的參數。以上除了預設值參數必須放在最後外其餘的順序都不是必須的,但按照上面的順序會比較清晰,而且能使實例創建的開銷最小。
4.Invoke方法只保留一個HttpContext參數。這樣可以省去對Invoke方法的二次包裝。
5.進一步擴展ApplicationBuilder,創建語義更加明確的方法代替Use/UseMiddleware,例如UseMVC、UseStaticFiles。
其中1中所說的及特殊的情況,我能想到的就是給UseMiddleware提供不同的參數列表,進而匹配到不同的構造函數創建實例。具體使用場景沒有想到。
0x04 測試
上篇文章中我們寫過一個記錄後面所有中間件耗時的中間件。當時直接用Use方法註冊的。現在我們把它寫為一個中間件類,並且把計時功能寫為一個StopWatch類,並添加到依賴註入容器中。
下麵是計時器類的代碼:
下麵是中間件類的代碼
下麵是向依賴註入容器中添加StopWatch
下麵是使用UseMiddleware擴展方法添加TimeMiddleware中間件代碼
當然,也可以不把StopWatch添加到依賴註入容器中,而是在UserMiddleware方法中直接給出參數。
如果既在依賴註入容器中添加了StopWatch,又在UseMiddleware註冊時提供了StopWatch,那麼按照參數匹配順序最終使用的是註冊時提供的StopWatch。
運行一下可以看到與上篇文章同樣的效果。
0x05 寫在最後
UseMiddleware方法使註冊中間件變得容易,同事也減小了中間件和其它依賴模塊間的耦合。不過不管哪種擴展方法,最終都是通過Use方法實現中間件的註冊。下一篇文章將寫一下註冊中間件的其它擴展方法Map、MapWhen和Run。