ASP.NET?Core中間件用法與官方常用中間件介紹
一、什么是中間件
我們都知道,任何的一個(gè)web框架都是把http請求封裝成一個(gè)管道,每一次的請求都是經(jīng)過管道的一系列操作,最終才會(huì)到達(dá)我們寫的代碼中。而中間件就是用于組成應(yīng)用程序管道來處理請求和響應(yīng)的組件。管道內(nèi)的每一個(gè)組件都可以選擇是否將請求轉(zhuǎn)交給下一個(gè)組件,并在管道中調(diào)用下一個(gè)組件之前和之后執(zhí)行某些操作。請求委托被用來建立請求管道,請求委托處理每一個(gè)HTTP請求。
中間件可以認(rèn)為有兩個(gè)基本的職責(zé):
- 選擇是否將請求傳遞給管道中的下一個(gè)中間件。
- 可以在管道中的下一個(gè)中間件前后執(zhí)行一些工作。
請求委托通過使用IApplicationBuilder類型的Run、Map以及Use擴(kuò)展方法來配置,并在Startup類中傳給Configure方法。每個(gè)單獨(dú)的請求委托都可以被指定為一個(gè)內(nèi)嵌匿名方法,或其定義在一個(gè)可重用的類中。這些可以重用的類被稱作“中間件”或“中間件組件”。每個(gè)位于請求管道內(nèi)的中間件組件負(fù)責(zé)調(diào)用管道中下一個(gè)組件,或適時(shí)短路調(diào)用鏈。中間件是一個(gè)典型的AOP應(yīng)用。
ASP.NET Core請求管道由一系列的請求委托所構(gòu)成,它們一個(gè)接著一個(gè)的被調(diào)用,看下面一張微軟官方的中間件請求管道圖(圖中執(zhí)行線程按黑色箭頭的順序執(zhí)行):
中間件短路:每一個(gè)委托在下一個(gè)委托之前和之后都有機(jī)會(huì)執(zhí)行操作。任何委托都能選擇停止傳遞到下一個(gè)委托,而是結(jié)束請求并開始響應(yīng),這就是請求管道的短路,這是一種有意義的設(shè)計(jì),因?yàn)樗梢员苊庖恍┎槐匾墓ぷ鳌1热缯f,一個(gè)授權(quán)(authorization)中間件只有在通過身份驗(yàn)證之后才能調(diào)用下一個(gè)委托,否則它就會(huì)被短路,并返回“Not Authorized”的響應(yīng)。異常處理委托需要在管道的早期被調(diào)用,這樣它們就能夠捕捉到發(fā)生在管道內(nèi)更深層次出現(xiàn)的異常了。短路可以用下面這張圖來表示:
在上圖中,我們可以把中間件1認(rèn)為是身份認(rèn)證的中間件,HTTP請求發(fā)送過來,首先經(jīng)過身份認(rèn)證中間件,如果身份認(rèn)證失敗,那么就直接給出響應(yīng)并返回,不會(huì)再把請求傳遞給下面的中間件2和中間件3.
中間件的執(zhí)行跟調(diào)用的順序有關(guān),然后在響應(yīng)時(shí)則以相反的順序返回。
請求在每一步都可能被短路,所以我們要以正確的順序添加中間件,如異常處理中間件,我們要添加在最開始的地方,這樣就能第一時(shí)間捕獲異常,以及后續(xù)中間可能發(fā)生的異常,然后最終做處理返回。
我們來看看Configure方法里面提供了哪些中間件:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { // 異常中間件 app.UseDeveloperExceptionPage(); } // 路由中間件 app.UseRouting(); // 授權(quán)中間件 app.UseAuthorization(); // 終結(jié)點(diǎn)中間件 app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); }
中間件和過濾器的區(qū)別
中間件和過濾器都是一種AOP的思想,他們的功能類似,那么他們有什么區(qū)別呢?
過濾器更加貼合業(yè)務(wù),它關(guān)注于應(yīng)用程序本身,關(guān)注的是如何實(shí)現(xiàn)業(yè)務(wù),比如對輸出結(jié)果進(jìn)行格式化,對請求的ViewModel進(jìn)行數(shù)據(jù)校驗(yàn),這時(shí)就肯定要使用過濾器了。過濾器是MVC的一部分,它可以攔截到你Action上下文的一些信息,而中間件是沒有這個(gè)能力的??梢哉J(rèn)為過濾器是附加性的一種功能,它只是中間件附帶表現(xiàn)出來的特征。中間件是管道模型里重要的組成部分,不可或缺,而過濾器可以沒有。
二、中間件常用方法
中間件中定義了Run、Use、Map、MapWhen幾種方法,我們下面一一講解這幾種方法。
1、Run方法
我們先來看到Run()方法的定義:
中定義中可以看出:Run()方法中只有一個(gè)RequestDelegate委托類型的參數(shù),沒有Next參數(shù),所以Run()方法也叫終端中間件,不會(huì)將請求傳遞給下一個(gè)中間件,也就是發(fā)生了“短路”??聪旅娴拇a:
// Run方法向應(yīng)用程序的請求管道中添加一個(gè)RequestDelegate委托 // 放在管道最后面,終端中間件 app.Run(handler: async context => { await context.Response.WriteAsync(text: "Hello World1\r\n"); }); app.Run(handler: async context => { await context.Response.WriteAsync(text: "Hello World2\r\n"); });
程序運(yùn)行結(jié)果:
可以看到:只輸出了中間件1的信息,沒有輸出中間件2的信息,說明發(fā)生了短路。
注意:Run()方法被稱為終端中間件,要放在所有中間件的最后面,否則在Run()方法后面的中間件將不會(huì)被執(zhí)行。
2、Use方法
我們先來看看Use()方法的定義:
可以看出:Use方法的參數(shù)是一個(gè)Func委托,輸入?yún)?shù)是一個(gè)RequestDelegate類型的委托,返回參數(shù)也是一個(gè)RequestDelegate類型的委托,這里表示調(diào)用下一個(gè)中間件,我們在來看看RequestDelegate委托的定義:
可以看出:RequestDelegate是一個(gè)委托,有一個(gè)HttpContext類型的參數(shù),HttPContext表示Http請求上下文,可以獲取請求信息,返回值是Task類型,明白了Use()方法的參數(shù)以后,我們寫一個(gè)自定義的Use()方法:
// 向應(yīng)用程序的請求管道中添加一個(gè)Func委托,這個(gè)委托其實(shí)就是所謂的中間件。 // context參數(shù)是HttpContext,表示HTTP請求的上下文對象 // next參數(shù)表示管道中的下一個(gè)中間件委托,如果不調(diào)用next,則會(huì)使管道短路 // 用Use可以將多個(gè)中間件鏈接在一起 app.Use(async (context, next) => { await context.Response.WriteAsync(text: "hello Use1\r\n"); // 調(diào)用下一個(gè)委托 await next(); }); app.Use(async (context, next) => { await context.Response.WriteAsync(text: "hello Use2\r\n"); // 調(diào)用下一個(gè)委托 await next(); });
程序運(yùn)行結(jié)果:
我們在上面說過,可以在調(diào)用中間件之前和之后做一些工作,看下面的代碼:
// 向應(yīng)用程序的請求管道中添加一個(gè)Func委托,這個(gè)委托其實(shí)就是所謂的中間件。 // context參數(shù)是HttpContext,表示HTTP請求的上下文對象 // next參數(shù)表示管道中的下一個(gè)中間件委托,如果不調(diào)用next,則會(huì)使管道短路 // 用Use可以將多個(gè)中間件鏈接在一起 app.Use(async (context, next) => { // 解決中文亂碼問題 context.Response.ContentType = "text/plain; charset=utf-8"; await context.Response.WriteAsync(text: "中間件1:傳入請求\r\n"); // 調(diào)用下一個(gè)委托 await next(); await context.Response.WriteAsync(text: "中間件1:傳出響應(yīng)\r\n"); }); app.Use(async (context, next) => { await context.Response.WriteAsync(text: "中間件2:傳入請求\r\n"); // 調(diào)用下一個(gè)委托 await next(); await context.Response.WriteAsync(text: "中間件2:傳出響應(yīng)\r\n"); }); app.Run(handler:async context => { await context.Response.WriteAsync(text: "中間件3:處理請求并生成響應(yīng)\r\n"); });
程序運(yùn)行結(jié)果:
我們可以總結(jié)上面代碼的執(zhí)行順序:
- 請求先到達(dá)中間件1,然后輸出(中間件1:傳入請求)
- 然后中間件1調(diào)用next()。next()會(huì)調(diào)用管道中的中間件2。
- 中間件2輸出(中間件2:傳入請求)。
- 然后中間件2會(huì)調(diào)用next()。next()在調(diào)用管道中的中間件3。
- 中間件3處理請求并生成響應(yīng),不在調(diào)用下一個(gè)中間件,所以我們看到輸出(中間件3:處理請求并生成響應(yīng))。
- 這時(shí)管理開始發(fā)生逆轉(zhuǎn)。
- 此時(shí)控制器將交回到中間件2,并將中間件3生成的響應(yīng)傳遞給它。中間件2輸出(中間件2:傳出響應(yīng))。
- 最后,中間件2在將控制權(quán)交給中間件1。
- 中間件1最后輸出(中間件1:傳出響應(yīng)),這就是我們最后看的的結(jié)果。
我們知道:Use()方法中有兩個(gè)參數(shù),next參數(shù)表示調(diào)用管道中的下一個(gè)中間件,如果不調(diào)用next,那么也會(huì)使管道發(fā)生短路,相當(dāng)于Run()方法,看下面的代碼:
// 向應(yīng)用程序的請求管道中添加一個(gè)Func委托,這個(gè)委托其實(shí)就是所謂的中間件。 // context參數(shù)是HttpContext,表示HTTP請求的上下文對象 // next參數(shù)表示管道中的下一個(gè)中間件委托,如果不調(diào)用next,則會(huì)使管道短路 // 用Use可以將多個(gè)中間件鏈接在一起 app.Use(async (context, next) => { // 解決中文亂碼問題 context.Response.ContentType = "text/plain; charset=utf-8"; await context.Response.WriteAsync(text: "中間件1:傳入請求\r\n"); // 調(diào)用下一個(gè)委托 await next(); await context.Response.WriteAsync(text: "中間件1:傳出響應(yīng)\r\n"); }); app.Use(async (context, next) => { await context.Response.WriteAsync(text: "中間件2:傳入請求\r\n"); // 調(diào)用下一個(gè)委托 await next(); await context.Response.WriteAsync(text: "中間件2:傳出響應(yīng)\r\n"); }); //app.Run(handler:async context => //{ // await context.Response.WriteAsync(text: "中間件3:處理請求并生成響應(yīng)\r\n"); //}); // Use方法也可以不調(diào)用next,表示發(fā)生短路 app.Use(async (context, next) => { await context.Response.WriteAsync(text: "中間件3:處理請求并生成響應(yīng)\r\n"); });
程序運(yùn)行結(jié)果:
可以看出:如果使用Use()方法,不調(diào)用next,實(shí)現(xiàn)的效果跟使用Run()方法一樣,都會(huì)使管道發(fā)生短路。
3、Map方法
Map作為慣例,將管道分流。Map根據(jù)給定請求路徑匹配將請求管道分流。如果請求路徑以指定路徑開始,則執(zhí)行分支??匆幌翸ap()方法的定義:
可以看到Map方法有兩個(gè)參數(shù):第一個(gè)參數(shù)是匹配規(guī)則,第二個(gè)參數(shù)是Action泛型委托,泛型委托參數(shù)是IApplicationBuilder類型,和Configure方法的第一個(gè)參數(shù)類型相同。這就表示可以把實(shí)現(xiàn)了Action泛型委托的方法添加到中間件管道中執(zhí)行。
我們首先定義一個(gè)方法,該方法的參數(shù)是IApplicationBuilder類型:
/// <summary> /// 自定義方法 /// </summary> /// <param name="app">IApplicationBuilder</param> private void HandleMap1(IApplicationBuilder app) { app.Run(handler: async context => { await context.Response.WriteAsync(text: "Hello Map1"); }); } /// <summary> /// 自定義方法 /// </summary> /// <param name="app">IApplicationBuilder</param> private void HandleMap2(IApplicationBuilder app) { app.Run(handler: async context => { await context.Response.WriteAsync(text: "Hello Map2"); }); }
然后看一下使用Map方法的代碼:
// Map可以根據(jù)匹配的URL來選擇執(zhí)行,簡單來說就是根據(jù)URL進(jìn)行分支選擇執(zhí)行 // 有點(diǎn)類似于MVC中的路由 // 匹配的URL:http://localhost:5000/Map1 app.Map(pathMatch: "/Map1", configuration: HandleMap1); // 匹配的URL:http://localhost:5000/Map2 app.Map(pathMatch: "/Map2", configuration: HandleMap2);
運(yùn)行程序,然后在瀏覽器地址欄里面輸入:http://localhost:5000/Map1,輸出結(jié)果:
在地址欄里面在輸入:http://localhost:5000/Map2,輸出結(jié)果:
Map還支持嵌套,看下面的代碼:
// 嵌套Map app.Map(pathMatch: "/Map1", configuration: App1 => { // App1.Map("/Map2",action=> { action.Run(async context => { await context.Response.WriteAsync("This is /Map1/Map2"); }); }); App1.Run(async context => { await context.Response.WriteAsync("This is no-map"); }); });
訪問http://localhost:5000/Map1/123輸出結(jié)果:
訪問http://localhost:5000/Map1輸出結(jié)果:
訪問http://localhost:5000/Map1/Map2輸出結(jié)果:
Map也可以同時(shí)匹配多個(gè)段,看下面的代碼:
運(yùn)行程序,輸出結(jié)果:
訪問http://localhost:5000/Map1/Map2輸出結(jié)果:
4、Mapwhen方法
MapWhen是基于給定的謂詞分支請求管道。任何使Func<HttpContext,bool>返回true的謂詞的請求都被映射到新的管道分支。
我們先來看看Mapwhen方法的定義:
可以看出:MapWhen方法有兩個(gè)參數(shù):第一個(gè)參數(shù)是Func類型的委托,輸入?yún)?shù)是HttpContext,輸出參數(shù)是bool類型。第二個(gè)參數(shù)是Action委托,參數(shù)是IApplicationBuilder類型,表示也可以把實(shí)現(xiàn)Action委托的方法添加到中間件管道中執(zhí)行。
看下面的例子,如果url中包括name查詢參數(shù),則執(zhí)行HandleName方法,如果包含age查詢參數(shù),則執(zhí)行HandleAge方法,否則執(zhí)行Run()方法。
HandleName和HandleAge方法定義如下:
private void HandleName(IApplicationBuilder app) { app.Run(handler: async context => { await context.Response.WriteAsync(text: $"This name is: {context.Request.Query["name"]}"); }); } private void HandleAge(IApplicationBuilder app) { app.Run(handler: async context => { await context.Response.WriteAsync(text: $"This age is: {context.Request.Query["age"]}"); }); }
對應(yīng)的MapWhen方法定義如下:
// 如果訪問的url參數(shù)中包含name,則執(zhí)行HandleName app.MapWhen( // Func委托,輸入?yún)?shù)是HttpContext,返回bool值 predicate: context => { // 判斷url參數(shù)中是否包含name return context.Request.Query.ContainsKey("name"); }, configuration: HandleName); // 如果訪問的url參數(shù)中包含name,則執(zhí)行HandleAge app.MapWhen( // Func委托,輸入?yún)?shù)是HttpContext,返回bool值 predicate: context => { // 判斷url參數(shù)中是否包含age return context.Request.Query.ContainsKey("age"); }, configuration: HandleAge); app.Run(async context => { await context.Response.WriteAsync("There is non-Map delegate \r\n"); });
運(yùn)行程序,輸出結(jié)果:
在url里面添加name查詢參數(shù)輸出結(jié)果:
在url里面添加age查詢參數(shù)輸出結(jié)果:
三、自定義中間件
在上面的例子中,我們都是使用的官方中間件自動(dòng)的方法,其實(shí)我們也可以自己編寫一個(gè)中間件。
中間件遵循顯示依賴原則,并在其構(gòu)造函數(shù)中暴露所有依賴項(xiàng)。中間件能夠利用UseMiddleware<T>擴(kuò)展方法的優(yōu)勢,直接通過它們的構(gòu)造函數(shù)注入服務(wù)。依賴注入服務(wù)是自動(dòng)完成填充的。
ASP.NET Core約定中間件類必須包括以下內(nèi)容:
- 具有類型為RequestDelegate參數(shù)的公共構(gòu)造函數(shù)。
- 必須有名為Invoke或InvokeAsync的公共方法,此方法必須滿足兩個(gè)條件:方法返回類型是Task、方法的第一個(gè)參數(shù)必須是HttpContext類型。
我們自定義一個(gè)記錄IP的中間件,新建一個(gè)類RequestIPMiddleware,代碼如下:
using Microsoft.AspNetCore.Http; using System.Threading.Tasks; namespace MiddlewareDemo.Middleware { /// <summary> /// 記錄IP地址的中間件 /// </summary> public class RequestIPMiddleware { // 私有字段 private readonly RequestDelegate _next; /// <summary> /// 公共構(gòu)造函數(shù),參數(shù)是RequestDelegate類型 /// 通過構(gòu)造函數(shù)進(jìn)行注入,依賴注入服務(wù)會(huì)自動(dòng)完成注入 /// </summary> /// <param name="next"></param> public RequestIPMiddleware(RequestDelegate next) { _next = next; } /// <summary> /// Invoke方法 /// 返回值是Task,參數(shù)類型是HttpContext /// </summary> /// <param name="context">Http上下文</param> /// <returns></returns> public async Task Invoke(HttpContext context) { await context.Response.WriteAsync($"User IP:{context.Connection.RemoteIpAddress.ToString()}\r\n"); // 調(diào)用管道中的下一個(gè)委托 await _next.Invoke(context); } } }
然后創(chuàng)建一個(gè)擴(kuò)展方法,對IApplicationBuilder進(jìn)行擴(kuò)展:
using Microsoft.AspNetCore.Builder; namespace MiddlewareDemo.Middleware { public static class RequestIPExtensions { /// <summary> /// 擴(kuò)展方法,對IApplicationBuilder進(jìn)行擴(kuò)展 /// </summary> /// <param name="builder"></param> /// <returns></returns> public static IApplicationBuilder UseRequestIP(this IApplicationBuilder builder) { // UseMiddleware<T> return builder.UseMiddleware<RequestIPMiddleware>(); } } }
最后在Startup類的Configure方法中使用自定義中間件:
// 使用自定義中間件 app.UseRequestIP();
運(yùn)行程序,查看結(jié)果:
這樣就完成了一個(gè)自定義中間件。
四、官方常用中間件
1、異常處理中間件
當(dāng)應(yīng)用程序在開發(fā)環(huán)境中運(yùn)行時(shí),開發(fā)人員異常頁中間件( UseDeveloperExceptionPage )報(bào)告應(yīng)用程序運(yùn)行時(shí)的錯(cuò)誤。
當(dāng)應(yīng)用程序在生產(chǎn)環(huán)境中運(yùn)行時(shí),異常處理中間件( UseExceptionHandler )捕獲下面中間件中引發(fā)的異常。
2、HTTPS重定向中間件
HTTPS重定向中間件( UseHttpsRedirection )會(huì)將HTTP請求重定向到HTTPS。
3、靜態(tài)文件中間件
靜態(tài)文件中間件( UseStaticFiles )返回靜態(tài)文件,并簡化進(jìn)一步請求處理。
4、Cookie中間件
Cookie策略中間件( UseCookiePolicy )使應(yīng)用符合歐盟一般數(shù)據(jù)保護(hù)條例的規(guī)定。
5、路由中間件
路由中間件( UseRouting )用于路由的請求。
6、身份認(rèn)證中間件
身份認(rèn)證中間件( UseAuthentication )嘗試對用戶進(jìn)行身份驗(yàn)證,驗(yàn)證通過之后才會(huì)允許用戶訪問安全資源。
7、授權(quán)中間件
授權(quán)中間件( UseAuthorization )用于授權(quán)驗(yàn)證通過的用戶可以訪問哪些資源。
8、會(huì)話中間件
會(huì)話中間件( UseSession )建立和維護(hù)會(huì)話狀態(tài)。如果應(yīng)用程序使用會(huì)話狀態(tài),請?jiān)贑ookie策略中間件之后和MVC中間件之前調(diào)用會(huì)話中間件。
9、終結(jié)點(diǎn)路由中間件
終結(jié)點(diǎn)路由中間件( UseEndpoints )用于將 Razor Pages 終結(jié)點(diǎn)添加到請求管道。
更多中間件組件可以到aspnet 的GitHub倉庫中查看:https://github.com/aspnet。
示例代碼GitHub地址:https://github.com/jxl1024/Middleware
到此這篇關(guān)于ASP.NET Core中間件用法與官方常用中間件的文章就介紹到這了。希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
ASP.Net頁面生命周期與Page_Load方法的工作原理介紹
這篇文章介紹了ASP.Net頁面生命周期與Page_Load方法的工作原理,文中通過示例代碼介紹的非常詳細(xì)。對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-05-05WPF使用ValidationRules對MVVM架構(gòu)數(shù)據(jù)驗(yàn)證
這篇文章介紹了WPF使用ValidationRules對MVVM架構(gòu)數(shù)據(jù)驗(yàn)證的方法,文中通過示例代碼介紹的非常詳細(xì)。對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-01-01ASP.NET2.0+SQL Server2005構(gòu)建多層應(yīng)用
ASP.NET2.0+SQL Server2005構(gòu)建多層應(yīng)用...2006-12-12