1.前言 中間件(middleware)是一種裝配到應用管道以處理請求和響應的組件。每個組件:●可選擇是否將請求傳遞到管道中的下一個組件。●可在管道中的下一個組件前後執行工作。請求委托(request delegates)用於建立請求管道(request pipeline),請求委托處理每個HTTP ...
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”的響應。所以應儘早在管道中調用異常處理委托,這樣它們就能捕獲在管道的後期階段發生的異常。
現在我們來演示下用一個簡單的ASP.NET Core應用程式建立單個請求委托處理每個HTTP請求(這種情況不包括實際請求管道):
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中間件