.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。
0x06 相關文章
.NET Core中介軟體的註冊和管道的構建(1)---- 註冊和構建原理
.NET Core中介軟體的註冊和管道的構建(2)---- 用UseMiddleware擴充套件方法註冊中介軟體類
.NET Core中介軟體的註冊和管道的構建(3) ---- 使用Map/MapWhen擴充套件方法
更多內容歡迎訪問我的部落格:http://www.durow.vip