理解ASP.NET Core 中間件(Middleware)
中間件
先借用微軟官方文檔的一張圖:
可以看到,中間件實(shí)際上是一種配置在HTTP請(qǐng)求管道中,用來處理請(qǐng)求和響應(yīng)的組件。它可以:
- 決定是否將請(qǐng)求傳遞到管道中的下一個(gè)中間件
- 可以在管道中的下一個(gè)中間件處理之前和之后進(jìn)行操作
此外,中間件的注冊(cè)是有順序的,書寫代碼時(shí)一定要注意!
中間件管道
Run
該方法為HTTP請(qǐng)求管道添加一個(gè)中間件,并標(biāo)識(shí)該中間件為管道終點(diǎn),稱為終端中間件。也就是說,該中間件就是管道的末尾,在該中間件之后注冊(cè)的中間件將永遠(yuǎn)都不會(huì)被執(zhí)行。所以,該方法一般只會(huì)書寫在Configure
方法末尾。
public class Startup { public void Configure(IApplicationBuilder app) { app.Run(async context => { await context.Response.WriteAsync("Hello, World!"); }); } }
Use
通過該方法快捷的注冊(cè)一個(gè)匿名的中間件
public class Startup { public void Configure(IApplicationBuilder app) { app.Use(async (context, next) => { // 下一個(gè)中間件處理之前的操作 Console.WriteLine("Use Begin"); await next(); // 下一個(gè)中間件處理完成后的操作 Console.WriteLine("Use End"); }); } }
注意:
- 1.如果要將請(qǐng)求發(fā)送到管道中的下一個(gè)中間件,一定要記得調(diào)用
next.Invoke / next()
,否則會(huì)導(dǎo)致管道短路,后續(xù)的中間件將不會(huì)被執(zhí)行 - 2.在中間件中,如果已經(jīng)開始給客戶端發(fā)送
Response
,請(qǐng)千萬不要調(diào)用next.Invoke / next()
,也不要對(duì)Response
進(jìn)行任何更改,否則,將拋出異常。 - 3.可以通過
context.Response.HasStarted
來判斷響應(yīng)是否已開始。
以下都是錯(cuò)誤的代碼寫法
錯(cuò)誤1:
public class Startup { public void Configure(IApplicationBuilder app) { app.Use(async (context, next) => { await context.Response.WriteAsync("Use"); await next(); }); app.Run(context => { // 由于上方的中間件已經(jīng)開始 Response,此處更改 Response Header 會(huì)拋出異常 context.Response.Headers.Add("test", "test"); return Task.CompletedTask; }); } }
錯(cuò)誤2:
public class Startup { public void Configure(IApplicationBuilder app) { app.Use(async (context, next) => { await context.Response.WriteAsync("Use"); // 即使沒有調(diào)用 next.Invoke / next(),也不能在 Response 開始后對(duì) Response 進(jìn)行更改 context.Response.Headers.Add("test", "test"); }); } }
UseWhen
通過該方法針對(duì)不同的邏輯條件創(chuàng)建管道分支。需要注意的是:
進(jìn)入了管道分支后,如果管道分支不存在管道短路或終端中間件,則會(huì)再次返回到主管道。
當(dāng)使用PathString
時(shí),路徑必須以“/”開頭,且允許只有一個(gè)'/'
字符
支持嵌套,即UseWhen中嵌套UseWhen等
支持同時(shí)匹配多個(gè)段,如 /get/user
public class Startup { public void Configure(IApplicationBuilder app) { // /get 或 /get/xxx 都會(huì)進(jìn)入該管道分支 app.UseWhen(context => context.Request.Path.StartsWithSegments("/get"), app => { app.Use(async (context, next) => { Console.WriteLine("UseWhen:Use"); await next(); }); }); app.Use(async (context, next) => { Console.WriteLine("Use"); await next(); }); app.Run(async context => { Console.WriteLine("Run"); await context.Response.WriteAsync("Hello World!"); }); } }
當(dāng)訪問 /get 時(shí),輸出如下:
UseWhen:Use
Use
Run
如果你發(fā)現(xiàn)輸出了兩遍,別慌,看看是不是瀏覽器發(fā)送了兩次請(qǐng)求,分別是 /get 和 /favicon.ico
Map
- 通過該方法針對(duì)不同的請(qǐng)求路徑創(chuàng)建管道分支。需要注意的是:
- 一旦進(jìn)入了管道分支,則不會(huì)再回到主管道。
- 使用該方法時(shí),會(huì)將匹配的路徑從
HttpRequest.Path
中刪除,并將其追加到HttpRequest.PathBase
中。 - 路徑必須以“/”開頭,且不能只有一個(gè)
'/'
字符 - 支持嵌套,即Map中嵌套Map、MapWhen(接下來會(huì)講)等
- 支持同時(shí)匹配多個(gè)段,如 /post/user
public class Startup { public void Configure(IApplicationBuilder app) { // 訪問 /get 時(shí)會(huì)進(jìn)入該管道分支 // 訪問 /get/xxx 時(shí)會(huì)進(jìn)入該管道分支 app.Map("/get", app => { app.Use(async (context, next) => { Console.WriteLine("Map get: Use"); Console.WriteLine($"Request Path: {context.Request.Path}"); Console.WriteLine($"Request PathBase: {context.Request.PathBase}"); await next(); }); app.Run(async context => { Console.WriteLine("Map get: Run"); await context.Response.WriteAsync("Hello World!"); }); }); // 訪問 /post/user 時(shí)會(huì)進(jìn)入該管道分支 // 訪問 /post/user/xxx 時(shí)會(huì)進(jìn)入該管道分支 app.Map("/post/user", app => { // 訪問 /post/user/student 時(shí)會(huì)進(jìn)入該管道分支 // 訪問 /post/user/student/1 時(shí)會(huì)進(jìn)入該管道分支 app.Map("/student", app => { app.Run(async context => { Console.WriteLine("Map /post/user/student: Run"); Console.WriteLine($"Request Path: {context.Request.Path}"); Console.WriteLine($"Request PathBase: {context.Request.PathBase}"); await context.Response.WriteAsync("Hello World!"); }); }); app.Use(async (context, next) => { Console.WriteLine("Map post/user: Use"); Console.WriteLine($"Request Path: {context.Request.Path}"); Console.WriteLine($"Request PathBase: {context.Request.PathBase}"); await next(); }); app.Run(async context => { Console.WriteLine("Map post/user: Run"); await context.Response.WriteAsync("Hello World!"); }); }); } }
當(dāng)你訪問 /get/user 時(shí),輸出如下:
Map get: Use
Request Path: /user
Request PathBase: /get
Map get: Run
當(dāng)你訪問 /post/user/student/1 時(shí),輸出如下:
Map /post/user/student: Run
Request Path: /1
Request PathBase: /post/user/student
其他情況交給你自己去嘗試?yán)玻?/p>
MapWhen
與Map
類似,只不過MapWhen
不是基于路徑,而是基于邏輯條件創(chuàng)建管道分支。注意事項(xiàng)如下:
- 一旦進(jìn)入了管道分支,則不會(huì)再回到主管道。
- 當(dāng)使用
PathString
時(shí),路徑必須以“/”開頭,且允許只有一個(gè)'/'
字符 HttpRequest.Path
和HttpRequest.PathBase
不會(huì)像Map
那樣進(jìn)行特別處理- 支持嵌套,即MapWhen中嵌套MapWhen、Map等
- 支持同時(shí)匹配多個(gè)段,如 /get/user
public class Startup { public void Configure(IApplicationBuilder app) { // /get 或 /get/xxx 都會(huì)進(jìn)入該管道分支 app.MapWhen(context => context.Request.Path.StartsWithSegments("/get"), app => { app.MapWhen(context => context.Request.Path.ToString().Contains("user"), app => { app.Use(async (context, next) => { Console.WriteLine("MapWhen get user: Use"); await next(); }); }); app.Use(async (context, next) => { Console.WriteLine("MapWhen get: Use"); await next(); }); app.Run(async context => { Console.WriteLine("MapWhen get: Run"); await context.Response.WriteAsync("Hello World!"); }); }); } }
當(dāng)你訪問 /get/user 時(shí),輸出如下:
MapWhen get user: Use
可以看到,即使該管道分支沒有終端中間件,也不會(huì)回到主管道。
Run & Use & UseWhen & Map & Map
一下子接觸了4個(gè)命名相似的、與中間件管道有關(guān)的API,不知道你有沒有暈倒,沒關(guān)系,我來幫大家總結(jié)一下:
Run
用于注冊(cè)終端中間件,Use
用來注冊(cè)匿名中間件,UseWhen
、Map
、MapWhen
用于創(chuàng)建管道分支。UseWhen
進(jìn)入管道分支后,如果管道分支中不存在短路或終端中間件,則會(huì)返回到主管道。Map
和MapWhen
進(jìn)入管道分支后,無論如何,都不會(huì)再返回到主管道。UseWhen
和MapWhen
基于邏輯條件來創(chuàng)建管道分支,而Map
基于請(qǐng)求路徑來創(chuàng)建管道分支,且會(huì)對(duì)HttpRequest.Path
和HttpRequest.PathBase
進(jìn)行處理。
編寫中間件并激活
上面已經(jīng)提到過的Run
和Use
就不再贅述了。
基于約定的中間件
“約定大于配置”,先來個(gè)約法三章:
- 1.擁有公共(public)構(gòu)造函數(shù),且該構(gòu)造函數(shù)至少包含一個(gè)類型為
RequestDelegate
的參數(shù) - 2.擁有名為
Invoke
或InvokeAsync
的公共(public)方法,必須包含一個(gè)類型為HttpContext
的方法參數(shù),且該參數(shù)必須位于第一個(gè)參數(shù)的位置,另外該方法必須返回Task
類型。 - 3.構(gòu)造函數(shù)中的其他參數(shù)可以通過依賴注入(DI)填充,也可以通過
UseMiddleware
傳參進(jìn)行填充。
通過DI填充時(shí),只能接收 Transient 和 Singleton 的DI參數(shù)。這是由于中間件是在應(yīng)用啟動(dòng)時(shí)構(gòu)造的(而不是按請(qǐng)求構(gòu)造),所以當(dāng)出現(xiàn) Scoped 參數(shù)時(shí),構(gòu)造函數(shù)內(nèi)的DI參數(shù)生命周期與其他不共享,如果想要共享,則必須將Scoped DI參數(shù)添加到Invoke/InvokeAsync
來進(jìn)行使用。
通過UseMiddleware
傳參時(shí),構(gòu)造函數(shù)內(nèi)的DI參數(shù)和非DI參數(shù)順序沒有要求,傳入UseMiddleware
內(nèi)的參數(shù)順序也沒有要求,但是我建議將非DI參數(shù)放到前面,DI參數(shù)放到后面。(這一塊感覺微軟做的好牛皮)
- 4.
Invoke/InvokeAsync
的其他參數(shù)也能夠通過依賴注入(DI)填充,可以接收 Transient、Scoped 和 Singleton 的DI參數(shù)。
一個(gè)簡(jiǎn)單的中間件如下:
public class MyMiddleware { // 用于調(diào)用管道中的下一個(gè)中間件 private readonly RequestDelegate _next; public MyMiddleware( RequestDelegate next, ITransientService transientService, ISingletonService singletonService) { _next = next; } public async Task InvokeAsync( HttpContext context, ITransientService transientService, IScopedService scopedService, ISingletonService singletonService) { // 下一個(gè)中間件處理之前的操作 Console.WriteLine("MyMiddleware Begin"); await _next(context); // 下一個(gè)中間件處理完成后的操作 Console.WriteLine("MyMiddleware End"); } }
然后,你可以通過UseMiddleware
方法將其添加到管道中
public class Startup { public void Configure(IApplicationBuilder app) { app.UseMiddleware<MyMiddleware>(); } }
不過,一般不推薦直接使用UseMiddleware
,而是將其封裝到擴(kuò)展方法中
public static class AppMiddlewareApplicationBuilderExtensions { public static IApplicationBuilder UseMy(this IApplicationBuilder app) => app.UseMiddleware<MyMiddleware>(); } public class Startup { public void Configure(IApplicationBuilder app) { app.UseMy(); } }
基于工廠的中間件
優(yōu)勢(shì):
- 按照請(qǐng)求進(jìn)行激活。這個(gè)就是說,上面基于約定的中間件實(shí)例是單例的,但是基于工廠的中間件,可以在依賴注入時(shí)設(shè)置中間件實(shí)例的生命周期。
- 使中間件強(qiáng)類型化(因?yàn)槠鋵?shí)現(xiàn)了接口
IMiddleware
)
該方式的實(shí)現(xiàn)基于IMiddlewareFactory
和IMiddleware
。先來看一下接口定義:
public interface IMiddlewareFactory { IMiddleware? Create(Type middlewareType); void Release(IMiddleware middleware); } public interface IMiddleware { Task InvokeAsync(HttpContext context, RequestDelegate next); }
你有沒有想過當(dāng)我們調(diào)用UseMiddleware
時(shí),它是如何工作的呢?事實(shí)上,UseMiddleware
擴(kuò)展方法會(huì)先檢查中間件是否實(shí)現(xiàn)了IMiddleware
接口。 如果實(shí)現(xiàn)了,則使用容器中注冊(cè)的IMiddlewareFactory
實(shí)例來解析該IMiddleware
的實(shí)例(這下你知道為什么稱為“基于工廠的中間件”了吧)。如果沒實(shí)現(xiàn),那么就使用基于約定的中間件邏輯來激活中間件。
注意,基于工廠的中間件,在應(yīng)用的服務(wù)容器中一般注冊(cè)為 Scoped 或 Transient 服務(wù)。
這樣的話,咱們就可以放心的將 Scoped 服務(wù)注入到中間件的構(gòu)造函數(shù)中了。
接下來,咱們就來實(shí)現(xiàn)一個(gè)基于工廠的中間件:
public class YourMiddleware : IMiddleware { public async Task InvokeAsync(HttpContext context, RequestDelegate next) { // 下一個(gè)中間件處理之前的操作 Console.WriteLine("YourMiddleware Begin"); await next(context); // 下一個(gè)中間件處理完成后的操作 Console.WriteLine("YourMiddleware End"); } } public static class AppMiddlewareApplicationBuilderExtensions { public static IApplicationBuilder UseYour(this IApplicationBuilder app) => app.UseMiddleware<YourMiddleware>(); }
然后,在ConfigureServices
中添加中間件依賴注入
public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddTransient<YourMiddleware>(); } }
最后,在Configure
中使用中間件
public class Startup { public void Configure(IApplicationBuilder app) { app.UseYour(); } }
微軟提供了IMiddlewareFactory
的默認(rèn)實(shí)現(xiàn):
public class MiddlewareFactory : IMiddlewareFactory { // The default middleware factory is just an IServiceProvider proxy. // This should be registered as a scoped service so that the middleware instances // don't end up being singletons. // 默認(rèn)的中間件工廠僅僅是一個(gè) IServiceProvider 的代理 // 該工廠應(yīng)該注冊(cè)為 Scoped 服務(wù),這樣中間件實(shí)例就不會(huì)成為單例 private readonly IServiceProvider _serviceProvider; public MiddlewareFactory(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } public IMiddleware? Create(Type middlewareType) { return _serviceProvider.GetRequiredService(middlewareType) as IMiddleware; } public void Release(IMiddleware middleware) { // The container owns the lifetime of the service // DI容器來管理服務(wù)的生命周期 } }
可以看到,該工廠使用過DI容器來解析出服務(wù)實(shí)例的。因此,當(dāng)使用基于工廠的中間件時(shí),是無法通過UseMiddleware
向中間件的構(gòu)造函數(shù)傳參的。
基于約定的中間件 VS 基于工廠的中間件
- 基于約定的中間件實(shí)例都是 Singleton;而基于工廠的中間件實(shí)例可以是 Singleton、Scoped 和 Transient(當(dāng)然,不建議注冊(cè)為 Singleton)
- 基于約定的中間件實(shí)例構(gòu)造函數(shù)中可以通過依賴注入傳參,也可以用過
UseMiddleware
傳參;而基于工廠的中間件只能通過依賴注入傳參 - 基于約定的中間件實(shí)例可以在
Invoke/InvokeAsync
中添加更多的依賴注入?yún)?shù);而基于工廠的中間件只能按照IMiddleware
的接口定義進(jìn)行實(shí)現(xiàn)。
到此這篇關(guān)于理解ASP.NET Core 中間件(Middleware)的文章就介紹到這了,更多相關(guān)ASP.NET Core Middleware內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
淺談.NET中加密和解密的實(shí)現(xiàn)方法分享
這篇文章介紹了.NET中加密和解密的實(shí)現(xiàn)方法,有需要的朋友可以參考一下2013-11-11asp.net自定義控件中注冊(cè)Javascript問題解決方案
這篇文章主要介紹了asp.net自定義控件中注冊(cè)Javascript的問題,需要的朋友可以參考下2014-05-05ASP.NET實(shí)現(xiàn)用圖片進(jìn)度條顯示投票結(jié)果
ASP.NET實(shí)現(xiàn)用圖片進(jìn)度條顯示投票結(jié)果...2007-06-06Debian 8或Debian 9(64 位)安裝 .NET Core
這篇文章主要為大家詳細(xì)介紹了Debian 8或Debian 9(64 位)安裝 .NET Core,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-03-03asp.net SqlDataReader綁定Repeater
asp.net SqlDataReader綁定Repeater2009-04-04a.sp.net清除ListBox的列表項(xiàng)(刪除所有項(xiàng)目)
在網(wǎng)上搜索相關(guān)資料,相當(dāng)多用戶有相同要求,一次移除ListBox的列表所有項(xiàng)2012-01-01ASP.NET Core奇淫技巧之動(dòng)態(tài)WebApi的實(shí)現(xiàn)
這篇文章主要介紹了ASP.NET Core奇淫技巧之動(dòng)態(tài)WebApi的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-08-08