1.前言
整個HTTP Request請求跟HTTP Response返回結果之間的處理流程是一個請求管道(request pipeline)。而中間件(middleware)則是一種裝配到請求管道以處理請求和響應的組件。每個組件:
●可選擇是否將請求傳遞到管道中的下一個組件。
●可在管道中的下一個組件前后執行工作。
中間件(middleware)處理流程如下圖所示:
2.使用中間件
ASP.NET Core請求管道中每個中間件都包含一系列的請求委托(request delegates)來處理每個HTTP請求,依次調用。請求委托通過使用IApplicationBuilder類型的Run、Use和Map擴展方法在Strartup.Configure方法中配置。下面我們通過配置Run、Use和Map擴展方法示例來了解下中間件。
2.1 Run
public class Startup { 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方法指定為一個內嵌匿名方法(稱為並行中間件,in-line middleware),而內嵌匿名方法中並沒有指定執行下一個請求委托,這一個過程叫管道短路,而該中間件又叫“終端中間件”(terminal middleware),因為它阻止中間件下一步處理請求。所以在Run第一個請求委托的時候就已經終止請求,並沒有執行第二個請求委托直接返回Hello, World!輸出文本。而根據官網解釋,Run是一種約定,有些中間件組件可能會暴露他們自己的Run方法,而這些方法只能在管道末尾處運行(也就是說Run方法只在中間件執行最后一個請求委托時才使用)。
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("進入第一個委托 執行下一個委托之前\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參數調用下一個請求委托則會使管道短路。比如,一個授權(authorization)中間件只有通過身份驗證之后才能調用下一個委托,否則它就會被短路,並返回“Not Authorized”的響應。所以應盡早在管道中調用異常處理委托,這樣它們就能捕獲在管道的后期階段發生的異常。
2.3 Map和MapWhen
●Map:Map擴展基於請求路徑創建管道分支。
●MapWhen:MapWhen擴展基於請求條件創建管道分支。
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。也就是說當我們在瀏覽器上輸入map1請求地址的時候,系統會執行map1分支管道輸出其請求委托信息,同理執行map2就會輸出對應請求委托信息。
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的請求和響應。
請求 |
響應 |
http://localhost:5001 |
Hello from non-Map delegate. <p> |
https://localhost:5001/?branch=master |
Branch used = master |
由上述代碼可知,MapWhen是基於branch條件而創建管道分支的,我們在branch條件上輸入master就會創建其對應管道分支。也就是說,branch條件上輸入任何一個字符串條件,都會創建一個新的管理分支。
而且還Map支持嵌套,例如:
public void Configure(IApplicationBuilder app) { app.Map("/level1", level1App => { level1App.Map("/level2a", level2AApp => { // "/level1/level2a" processing }); level1App.Map("/level2b", level2BApp => { // "/level1/level2b" processing }); }); }
還可同時匹配多個段:
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方法添加中間件組件的順序定義了在請求上調用它們的順序,以及響應的相反順序。此排序對於安全性、性能和功能至關重要。
以下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(); } // Return static files and end the pipeline. app.UseStaticFiles(); // Authenticate before the user accesses secure resources. app.UseAuthentication(); }
從上述示例代碼中,每個中間件擴展方法都通過Microsoft.AspNetCore.Builder命名空間在 IApplicationBuilder上公開。但是為什么我們要按照這個順序去添加中間件組件呢?下面我們挑幾個中間件來了解下。
●UseExceptionHandler(異常/錯誤處理)是添加到管道的第一個中間件組件。因此我們可以捕獲在應用程序調用中發生的任何異常。那為什么要將異常/錯誤處理放在第一位呢?那是因為這樣我們就不用擔心因前面中間件短路而導致捕獲不到整個應用程序所有異常信息。
●UseStaticFiles(靜態文件)中間件在管道中提前調用,方便它可以處理請求和短路,而無需通過剩余中間組件。也就是說靜態文件中間件不用經過UseAuthentication(身份驗證)檢查就可以直接訪問,即可公開訪問由靜態文件中間件服務的任何文件,包括wwwroot下的文件。
●UseAuthentication(身份驗證)僅在MVC選擇特定的Razor頁面或Controller和Action之后才會發生。
經過上面描述,大家都了解中間件順序的重要性了吧。
4.編寫中間件(重點)
雖然ASP.NET Core為我們提供了一組豐富的內置中間件組件,但在某些情況下,你可能需要寫入自定義中間件。
4.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。
但是如果每個自定義中間件都在Startup.Configure方法中編寫如上一大堆代碼,那么對於程序來說,將是災難性的(不利於維護和調用)。為了更好管理代碼,我們應該把內嵌匿名方法封裝到新建的自定義類(示例自定義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方法調用中間件:
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中間件擴展方法
Startup.Configure方法調用中間件設置可以通過自定義的擴展方法將其公開(調用內置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}"); }); } }
響應結果:
通過委托構造中間件,應用程序在運行時創建這個中間件,並將它添加到管道中。這里需要注意的是,中間件的創建是單例的,每個中間件在應用程序生命周期內只有一個實例。那么問題來了,如果我們業務邏輯需要多個實例時,該如何操作呢?請繼續往下看。
6.按每次請求創建依賴注入(DI)
在中間件的創建過程中,內置的IOC容器會為我們創建一個中間件實例,並且整個應用程序生命周期中只會創建一個該中間件的實例。通常我們的程序不允許這樣的注入邏輯。其實,我們可以把中間件理解成業務邏輯的入口,真正的業務邏輯是通過Application Service層實現的,我們只需要把應用服務注入到Invoke方法中即可。ASP.NET Core為我們提供了這種機制,允許我們按照請求進行依賴的注入,也就是每次請求創建一個服務。示例:
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>(); }
響應結果: