ASP.NET Core中間件
1.前言
整個(gè)HTTP Request請求跟HTTP Response返回結(jié)果之間的處理流程是一個(gè)請求管道(request pipeline)。而中間件(middleware)則是一種裝配到請求管道以處理請求和響應(yīng)的組件。每個(gè)組件:
- 可選擇是否將請求傳遞到管道中的下一個(gè)組件。
- 可在管道中的下一個(gè)組件前后執(zhí)行工作。
中間件(middleware)處理流程如下圖所示:
2.使用中間件
ASP.NET Core請求管道中每個(gè)中間件都包含一系列的請求委托(request delegates)來處理每個(gè)HTTP請求,依次調(diào)用。請求委托通過使用IApplicationBuilder類型的Run、Use和Map擴(kuò)展方法在Strartup.Configure方法中配置。下面我們通過配置Run、Use和Map擴(kuò)展方法示例來了解下中間件。
2.1 Run
public class Startup { public void Configure(IApplicationBuilder app) { //第一個(gè)請求委托Run app.Run(async context =>//內(nèi)嵌匿名方法 { await context.Response.WriteAsync("Hello, World!"); }); //第二個(gè)請求委托Run app.Run(async context =>//內(nèi)嵌匿名方法 { await context.Response.WriteAsync("Hey, World!"); }); } }
響應(yīng)結(jié)果:
由上述代碼可知,Run方法指定為一個(gè)內(nèi)嵌匿名方法(稱為并行中間件,in-line middleware),而內(nèi)嵌匿名方法中并沒有指定執(zhí)行下一個(gè)請求委托,這一個(gè)過程叫管道短路,而該中間件又叫“終端中間件”(terminal middleware),因?yàn)樗柚怪虚g件下一步處理請求。所以在Run第一個(gè)請求委托的時(shí)候就已經(jīng)終止請求,并沒有執(zhí)行第二個(gè)請求委托直接返回Hello, World!輸出文本。而根據(jù)官網(wǎng)解釋,Run是一種約定,有些中間件組件可能會(huì)暴露他們自己的Run方法,而這些方法只能在管道末尾處運(yùn)行(也就是說Run方法只在中間件執(zhí)行最后一個(gè)請求委托時(shí)才使用)。
2.2 Use
public void Configure(IApplicationBuilder app) { app.Use(async (context, next) => { context.Response.ContentType = "text/plain; charset=utf-8"; await context.Response.WriteAsync("進(jìn)入第一個(gè)委托 執(zhí)行下一個(gè)委托之前\r\n"); //調(diào)用管道中的下一個(gè)委托 await next.Invoke(); await context.Response.WriteAsync("結(jié)束第一個(gè)委托 執(zhí)行下一個(gè)委托之后\r\n"); }); app.Run(async context => { await context.Response.WriteAsync("進(jìn)入第二個(gè)委托\(zhòng)r\n"); await context.Response.WriteAsync("Hello from 2nd delegate.\r\n"); await context.Response.WriteAsync("結(jié)束第二個(gè)委托\(zhòng)r\n"); }); }
響應(yīng)結(jié)果:
由上述代碼可知,Use方法將多個(gè)請求委托鏈接在一起。而next參數(shù)表示管道中的下一個(gè)委托。如果不調(diào)用next參數(shù)調(diào)用下一個(gè)請求委托則會(huì)使管道短路。比如,一個(gè)授權(quán)(authorization)中間件只有通過身份驗(yàn)證之后才能調(diào)用下一個(gè)委托,否則它就會(huì)被短路,并返回“Not Authorized”的響應(yīng)。所以應(yīng)盡早在管道中調(diào)用異常處理委托,這樣它們就能捕獲在管道的后期階段發(fā)生的異常。
2.3 Map和MapWhen
- Map:Map擴(kuò)展基于請求路徑創(chuàng)建管道分支。
- MapWhen:MapWhen擴(kuò)展基于請求條件創(chuàng)建管道分支。
Map示例:
public class Startup { private static void HandleMapTest1(IApplicationBuilder app) { app.Run(async context => { await context.Response.WriteAsync("Map Test 1"); }); } private static void HandleMapTest2(IApplicationBuilder app) { app.Run(async context => { await context.Response.WriteAsync("Map Test 2"); }); } public void Configure(IApplicationBuilder app) { app.Map("/map1", HandleMapTest1); app.Map("/map2", HandleMapTest2); app.Run(async context => { await context.Response.WriteAsync("Hello from non-Map delegate. <p>"); }); } }
下面表格使用前面的代碼顯示來自http://localhost:5001的請求和響應(yīng)。
請求 | 響應(yīng) |
localhost:5001 | Hello from non-Map delegate. |
localhost:5001/map1 | Map Test 1 |
localhost:5001/map2 | Map Test 2 |
localhost:5001/map3 | Hello from non-Map delegate. |
由上述代碼可知,Map方法將從HttpRequest.Path中刪除匹配的路徑段,并針對(duì)每個(gè)請求將該路徑追加到HttpRequest.PathBase。也就是說當(dāng)我們在瀏覽器上輸入map1請求地址的時(shí)候,系統(tǒng)會(huì)執(zhí)行map1分支管道輸出其請求委托信息,同理執(zhí)行map2就會(huì)輸出對(duì)應(yīng)請求委托信息。
MapWhen示例:
public class Startup { private static void HandleBranch(IApplicationBuilder app) { app.Run(async context => { var branchVer = context.Request.Query["branch"]; await context.Response.WriteAsync($"Branch used = {branchVer}"); }); } public void Configure(IApplicationBuilder app) { app.MapWhen(context => context.Request.Query.ContainsKey("branch"), HandleBranch); app.Run(async context => { await context.Response.WriteAsync("Hello from non-Map delegate. <p>"); }); } }
下面表格使用前面的代碼顯示來自http://localhost:5001的請求和響應(yīng)。
請求 | 響應(yīng) |
http://localhost:5001 | Hello from non-Map delegate. <p> |
https://localhost:5001/?branch=master | Branch used = master |
由上述代碼可知,MapWhen是基于branch條件而創(chuàng)建管道分支的,我們在branch條件上輸入master就會(huì)創(chuàng)建其對(duì)應(yīng)管道分支。也就是說,branch條件上輸入任何一個(gè)字符串條件,都會(huì)創(chuàng)建一個(gè)新的管理分支。
而且還Map支持嵌套,例如:
public void Configure(IApplicationBuilder app) { app.Map("/level1", level1App => { level1App.Map("/level2a", level2AApp => { // "/level1/level2a" processing }); level1App.Map("/level2b", level2BApp => { // "/level1/level2b" processing }); }); }
還可同時(shí)匹配多個(gè)段:
public class Startup { private static void HandleMultiSeg(IApplicationBuilder app) { app.Run(async context => { await context.Response.WriteAsync("Map multiple segments."); }); } public void Configure(IApplicationBuilder app) { app.Map("/map1/seg1", HandleMultiSeg); app.Run(async context => { await context.Response.WriteAsync("Hello from non-Map delegate."); }); } }
3.順序
向Startup.Configure方法添加中間件組件的順序定義了在請求上調(diào)用它們的順序,以及響應(yīng)的相反順序。此排序?qū)τ诎踩?、性能和功能至關(guān)重要。
以下Startup.Configure方法將為常見應(yīng)用方案添加中間件組件:
- 異常/錯(cuò)誤處理(Exception/error handling)
- HTTP嚴(yán)格傳輸安全協(xié)議(HTTP Strict Transport Security Protocol)
- HTTPS重定向(HTTPS redirection)
- 靜態(tài)文件服務(wù)器(Static file server)
- Cookie策略實(shí)施(Cookie policy enforcement)
- 身份驗(yàn)證(Authentication)
- 會(huì)話(Session)
- MVC
請看如下代碼:
public void Configure(IApplicationBuilder app) { if (env.IsDevelopment()) { // When the app runs in the Development environment: // Use the Developer Exception Page to report app runtime errors. // Use the Database Error Page to report database runtime errors. app.UseDeveloperExceptionPage(); app.UseDatabaseErrorPage(); } else { // When the app doesn't run in the Development environment: // Enable the Exception Handler Middleware to catch exceptions // thrown in the following middlewares. // Use the HTTP Strict Transport Security Protocol (HSTS) // Middleware. app.UseExceptionHandler("/Error"); app.UseHsts(); } // Return static files and end the pipeline. app.UseStaticFiles(); // Authenticate before the user accesses secure resources. app.UseAuthentication(); }
從上述示例代碼中,每個(gè)中間件擴(kuò)展方法都通過Microsoft.AspNetCore.Builder命名空間在 IApplicationBuilder上公開。但是為什么我們要按照這個(gè)順序去添加中間件組件呢?下面我們挑幾個(gè)中間件來了解下。
- UseExceptionHandler(異常/錯(cuò)誤處理)是添加到管道的第一個(gè)中間件組件。因此我們可以捕獲在應(yīng)用程序調(diào)用中發(fā)生的任何異常。那為什么要將異常/錯(cuò)誤處理放在第一位呢?那是因?yàn)檫@樣我們就不用擔(dān)心因前面中間件短路而導(dǎo)致捕獲不到整個(gè)應(yīng)用程序所有異常信息。
- UseStaticFiles(靜態(tài)文件)中間件在管道中提前調(diào)用,方便它可以處理請求和短路,而無需通過剩余中間組件。也就是說靜態(tài)文件中間件不用經(jīng)過UseAuthentication(身份驗(yàn)證)檢查就可以直接訪問,即可公開訪問由靜態(tài)文件中間件服務(wù)的任何文件,包括wwwroot下的文件。
- UseAuthentication(身份驗(yàn)證)僅在MVC選擇特定的Razor頁面或Controller和Action之后才會(huì)發(fā)生。
經(jīng)過上面描述,大家都了解中間件順序的重要性了吧。
4.編寫中間件(重點(diǎn))
雖然ASP.NET Core為我們提供了一組豐富的內(nèi)置中間件組件,但在某些情況下,你可能需要寫入自定義中間件。
4.1中間件類
下面我們自定義一個(gè)查詢當(dāng)前區(qū)域性的中間件:
public class Startup { public void Configure(IApplicationBuilder app) { app.Use((context, next) => { var cultureQuery = context.Request.Query["culture"]; if (!string.IsNullOrWhiteSpace(cultureQuery)) { var culture = new CultureInfo(cultureQuery); CultureInfo.CurrentCulture = culture; CultureInfo.CurrentUICulture = culture; } // Call the next delegate/middleware in the pipeline return next(); }); app.Run(async (context) => { await context.Response.WriteAsync( $"Hello {CultureInfo.CurrentCulture.DisplayName}"); }); } }
可通過傳入?yún)^(qū)域性參數(shù)條件進(jìn)行測試。例如http://localhost:7997/?culture=zh、http://localhost:7997/?culture=en。
但是如果每個(gè)自定義中間件都在Startup.Configure方法中編寫如上一大堆代碼,那么對(duì)于程序來說,將是災(zāi)難性的(不利于維護(hù)和調(diào)用)。為了更好管理代碼,我們應(yīng)該把內(nèi)嵌匿名方法封裝到新建的自定義類(示例自定義RequestCultureMiddleware類)里面去:
public class RequestCultureMiddleware { private readonly RequestDelegate _next; public RequestCultureMiddleware(RequestDelegate next) { _next = next; } public async Task InvokeAsync(HttpContext context) { context.Response.ContentType = "text/plain; charset=utf-8"; var cultureQuery = context.Request.Query["culture"]; if (!string.IsNullOrWhiteSpace(cultureQuery)) { var culture = new CultureInfo(cultureQuery); CultureInfo.CurrentCulture = culture; CultureInfo.CurrentUICulture = culture; } // Call the next delegate/middleware in the pipeline await _next(context); } }
通過Startup.Configure方法調(diào)用中間件:
public class Startup { public void Configure(IApplicationBuilder app) { app.UseMiddleware<RequestCultureMiddleware>(); app.Run(async (context) => { await context.Response.WriteAsync( $"Hello {CultureInfo.CurrentCulture.DisplayName}"); }); } }
4.2中間件擴(kuò)展方法
Startup.Configure方法調(diào)用中間件設(shè)置可以通過自定義的擴(kuò)展方法將其公開(調(diào)用內(nèi)置IApplicationBuilder公開中間件)。示例創(chuàng)建一個(gè)RequestCultureMiddlewareExtensions擴(kuò)展類并通過IApplicationBuilder公開:
public static class RequestCultureMiddlewareExtensions { public static IApplicationBuilder UseRequestCulture(this IApplicationBuilder builder) { return builder.UseMiddleware<RequestCultureMiddleware>(); } }
再通過Startup.Configure方法調(diào)用中間件:
public class Startup { public void Configure(IApplicationBuilder app) { app.UseRequestCulture(); app.Run(async (context) => { await context.Response.WriteAsync( $"Hello {CultureInfo.CurrentCulture.DisplayName}"); }); } }
響應(yīng)結(jié)果:
通過委托構(gòu)造中間件,應(yīng)用程序在運(yùn)行時(shí)創(chuàng)建這個(gè)中間件,并將它添加到管道中。這里需要注意的是,中間件的創(chuàng)建是單例的,每個(gè)中間件在應(yīng)用程序生命周期內(nèi)只有一個(gè)實(shí)例。那么問題來了,如果我們業(yè)務(wù)邏輯需要多個(gè)實(shí)例時(shí),該如何操作呢?請繼續(xù)往下看。
5.按每次請求創(chuàng)建依賴注入(DI)
在中間件的創(chuàng)建過程中,內(nèi)置的IOC容器會(huì)為我們創(chuàng)建一個(gè)中間件實(shí)例,并且整個(gè)應(yīng)用程序生命周期中只會(huì)創(chuàng)建一個(gè)該中間件的實(shí)例。通常我們的程序不允許這樣的注入邏輯。其實(shí),我們可以把中間件理解成業(yè)務(wù)邏輯的入口,真正的業(yè)務(wù)邏輯是通過Application Service層實(shí)現(xiàn)的,我們只需要把應(yīng)用服務(wù)注入到Invoke方法中即可。ASP.NET Core為我們提供了這種機(jī)制,允許我們按照請求進(jìn)行依賴的注入,也就是每次請求創(chuàng)建一個(gè)服務(wù)。示例:
public class CustomMiddleware { private readonly RequestDelegate _next; public CustomMiddleware(RequestDelegate next) { _next = next; } // IMyScopedService is injected into Invoke public async Task Invoke(HttpContext httpContext, IMyScopedService svc) { svc.MyProperty(1000); await _next(httpContext); } } public static class CustomMiddlewareExtensions { public static IApplicationBuilder UseCustomMiddleware(this IApplicationBuilder builder) { return builder.UseMiddleware<CustomMiddleware>(); } } public interface IMyScopedService { void MyProperty(decimal input); } public class MyScopedService : IMyScopedService { public void MyProperty(decimal input) { Console.WriteLine("MyProperty is " + input); } } public void ConfigureServices(IServiceCollection services) { //注入DI服務(wù) services.AddScoped<IMyScopedService, MyScopedService>(); }
響應(yīng)結(jié)果:
到此這篇關(guān)于ASP.NET Core中間件的文章就介紹到這了。希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
.Net結(jié)構(gòu)型設(shè)計(jì)模式之外觀模式(Facade)
這篇文章介紹了.Net結(jié)構(gòu)型設(shè)計(jì)模式之外觀模式(Facade),文中通過示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-05-05.Net行為型設(shè)計(jì)模式之命令模式(Command)
這篇文章介紹了.Net行為型設(shè)計(jì)模式之命令模式(Command),文中通過示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-05-05