(4)ASP.NET Core 中介軟體
1.前言
中介軟體(middleware)是一種裝配到應用管道以處理請求和響應的元件。每個元件:
●可選擇是否將請求傳遞到管道中的下一個元件。
●可在管道中的下一個元件前後執行工作。
請求委託(request delegates)用於建立請求管道(request pipeline),請求委託處理每個HTTP請求。
請求委託通過使用IApplicationBuilder型別的Run、Map和Use擴充套件方法來配置,並在Strartup類中傳給Configure方法。每個單獨的請求委託都可以被指定為一個內嵌匿名方法(稱為並行中介軟體,in-line middleware),或者其定義在一個可重用的類中。這些可重用的類被稱作“中介軟體”或“中介軟體元件”。請求管道中的每個中介軟體元件負責呼叫管道中的下一個元件,或使管道短路。當中間件短路時,它被稱為“終端中介軟體”(terminal middleware),因為它阻止中介軟體進一步處理請求。
2.使用 IApplicationBuilder 建立中介軟體管道
ASP.NET Core請求管道包含一系列請求委託,依次呼叫。下圖演示了這一概念。沿黑色箭頭執行。
每個委託(中介軟體)均可在下一個委託前後執行操作。任何委託都能選擇停止傳遞到下一個委託,轉而自己處理該請求,這就是請求管道的短路(下面會舉例說明)。而且是一種有意義的設計,因為它可以避免不必要的工作。比如,一個授權(authorization)中介軟體只有通過身份驗證之後才能呼叫下一個委託,否則它就會被短路,並返回“Not Authorized”的響應。所以應儘早在管道中呼叫異常處理委託,這樣它們就能捕獲在管道的後期階段發生的異常。
public class Startup { public void Configure(IApplicationBuilder app) { app.Run(async context => { await context.Response.WriteAsync("Hello, World!"); }); } }
響應結果:
由上面我們可以看到,執行時輸出的是Run委託訊息,然後我們再定義多一個請求委託看看效果,請看如下程式碼:
public void Configure(IApplicationBuilder app) { //第一個委託Run app.Run(async context => { await context.Response.WriteAsync("Hello, World!"); }); //第二個委託Run app.Run(async context => { await context.Response.WriteAsync("Hey, World!"); }); }
響應結果:
由上述程式碼可以看到,我們定義兩個Run委託,但是執行第一個Run委託的時候就已經終止了管道,這是為什麼呢?
因為Run方法又稱為短路管道(它不會呼叫next請求委託)。因此,Run方法一般在管道尾部被呼叫。Run是一種約定,有些中介軟體元件可能會暴露他們自己的Run方法,而這些方法只能在管道末尾處執行。
讓我們再來看看如下程式碼:
public void Configure(IApplicationBuilder app) { app.Use(async (context, next) => { context.Response.ContentType = "text/plain; charset=utf-8"; await context.Response.WriteAsync("進入第一個委託 執行下一個委託之前\r\n"); //呼叫管道中的下一個委託 await next.Invoke(); await context.Response.WriteAsync("結束第一個委託 執行下一個委託之後\r\n"); }); app.Run(async context => { await context.Response.WriteAsync("進入第二個委託\r\n"); await context.Response.WriteAsync("Hello from 2nd delegate.\r\n"); await context.Response.WriteAsync("結束第二個委託\r\n"); }); }
響應結果:
通過響應結果,我們可以看到Use方法將多個請求委託連結在一起。而next引數表示管道中的下一個委託。可通過不呼叫next 引數使管道短路,通常可在下一個委託前後執行操作。
3.順序
向Startup.Configure方法新增中介軟體元件的順序定義了在請求上呼叫它們的順序,以及響應的相反順序。此排序對於安全性、效能和功能至關重要。
以下Startup.Configure方法將為常見應用方案新增中介軟體元件:
●異常/錯誤處理(Exception/error handling)
●HTTP嚴格傳輸安全協議(HTTP Strict Transport Security Protocol)
●HTTPS重定向(HTTPS redirection)
●靜態檔案伺服器(Static file server)
●Cookie策略實施(Cookie policy enforcement)
●身份驗證(Authentication)
●會話(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(); } // Use HTTPS Redirection Middleware to redirect HTTP requests to HTTPS. app.UseHttpsRedirection(); // Return static files and end the pipeline. app.UseStaticFiles(); // Use Cookie Policy Middleware to conform to EU General Data // Protection Regulation (GDPR) regulations. app.UseCookiePolicy(); // Authenticate before the user accesses secure resources. app.UseAuthentication(); // If the app uses session state, call Session Middleware after Cookie // Policy Middleware and before MVC Middleware. app.UseSession(); // Add MVC to the request pipeline. app.UseMvc(); }View Code
從上述示例程式碼中,每個中介軟體擴充套件方法都通過Microsoft.AspNetCore.Builder名稱空間在 IApplicationBuilder上公開。但是為什麼我們要按照這個順序去新增中介軟體元件呢?下面我們挑幾個中介軟體來了解下。
●UseExceptionHandler(異常/錯誤處理)是新增到管道的第一個中介軟體元件。因此我們可以捕獲在應用程式呼叫中發生的任何異常。那為什麼要將異常/錯誤處理放在第一位呢?那是因為這樣我們就不用擔心因前面中介軟體短路而導致捕獲不到整個應用程式所有異常資訊。
●UseStaticFiles(靜態檔案)中介軟體在管道中提前呼叫,方便它可以處理請求和短路,而無需通過剩餘中間元件。也就是說靜態檔案中介軟體不用經過UseAuthentication(身份驗證)檢查就可以直接訪問,即可公開訪問由靜態檔案中介軟體服務的任何檔案,包括wwwroot下的檔案。
●UseAuthentication(身份驗證)僅在MVC選擇特定的Razor頁面或Controller和Action之後才會發生。
經過上面描述,大家都瞭解中介軟體順序的重要性了吧。以下示例演示中介軟體的排序,其中靜態檔案的請求在響應壓縮中介軟體之前由靜態檔案中介軟體進行處理。靜態檔案不會按照中介軟體的順序進行壓縮。可以壓縮來自 UseMvcWithDefaultRoute的 MVC 響應。示例:
public void Configure(IApplicationBuilder app) { // Static files not compressed by Static File Middleware. app.UseStaticFiles(); app.UseResponseCompression(); app.UseMvcWithDefaultRoute(); }
4.Use、Run和Map方法
你可以使用Use、Run和Map配置HTTP管道。
●Use:Use方法可使管道短路(即不呼叫 next 請求委託)。第二節點有示例程式碼演示。
●Run:Run是一種約定,並且某些中介軟體元件可公開在管道末尾執行的Run[Middleware]方法。第二節點有示例程式碼演示。
●Map:Map擴充套件用作建立管道分支。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的請求和響應。
請求 |
響應 |
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中刪除匹配的路徑段,並針對每個請求將該路徑追加到HttpRequest.PathBase。
MapWhen基於給定謂詞的結果建立請求管道分支。Func<HttpContext, bool>型別的任何謂詞均可用於將請求對映到管道的新分支(HandleBranch)。在以下示例中,謂詞用於檢測查詢字串變數branch是否存在:
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的請求和響應。
請求 |
響應 |
http://localhost:5001 |
Hello from non-Map delegate. <p> |
https://localhost:5001/?branch=master |
Branch used = master |
Map支援巢狀,例如:
public void Configure(IApplicationBuilder app) { app.Map("/level1", level1App => { level1App.Map("/level2a", level2AApp => { // "/level1/level2a" processing }); level1App.Map("/level2b", level2BApp => { // "/level1/level2b" processing }); }); }
此外Map 還可同時匹配多個段:
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."); }); } }
5.編寫中介軟體(重點)
雖然ASP.NET Core為我們提供了一組豐富的內建中介軟體元件,但在某些情況下,你可能需要寫入自定義中介軟體。
5.1中介軟體類
通常,中介軟體應該封裝在自定義類中,並且通過擴充套件方法公開。
下面我們自定義一個查詢當前區域性的中介軟體:
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}"); }); } }
可通過傳入區域性引數測試該中介軟體。例如 http://localhost:7997/?culture=zh、http://localhost:7997/?culture=en。
但是為了更好管理程式碼,我們應該把委託函式移到自定義類去:
//自定義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); } }
5.2中介軟體擴充套件方法
中介軟體擴充套件方法可以通過IApplicationBuilder公開中介軟體。示例建立一個RequestCultureMiddlewareExtensions擴充套件類並通過IApplicationBuilder公開:
public static class RequestCultureMiddlewareExtensions { public static IApplicationBuilder UseRequestCulture(this IApplicationBuilder builder) { return builder.UseMiddleware<RequestCultureMiddleware>(); } }
再通過Startup.Configure方法呼叫中介軟體:
public class Startup { public void Configure(IApplicationBuilder app) { app.UseRequestCulture(); app.Run(async (context) => { await context.Response.WriteAsync( $"Hello {CultureInfo.CurrentCulture.DisplayName}"); }); } }
響應結果:
由此整個自定義ASP.NET Core中介軟體完成。
6.按請求依賴項
因為中介軟體是在應用程式啟動時構建的,而不是每個請求時構建,所以在每個請求期間,中介軟體建構函式使用的範圍內生命週期服務不與其他依賴關係注入型別共享。如果您必須在中介軟體和其他型別之間共享作用域服務,請將這些服務新增到Invoke方法的簽名中。Invoke方法可以接受由依賴注入(DI)填充的其他引數。示例:
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服務 services.AddScoped<IMyScopedService, MyScopedService>(); }
響應結果:
參考文獻:
ASP.NET Core中介軟體
寫入自定義ASP.NET Core中介軟體