一.概述 中間件(也叫中間件組件)是一種裝配到應用管道以處理請求和響應的軟體。 每個組件:(1)選擇是否將請求傳遞到管道中的下一個組件;(2)可以在管道中的下一個組件之前和之後執行工作。 請求委托用於生成請求管道。 請求委托會處理每個 HTTP 請求。使用以下方法配置請求委托:Run, Map, U ...
一.概述
中間件(也叫中間件組件)是一種裝配到應用管道以處理請求和響應的軟體。 每個組件:(1)選擇是否將請求傳遞到管道中的下一個組件;(2)可以在管道中的下一個組件之前和之後執行工作。
請求委托用於生成請求管道。 請求委托會處理每個 HTTP 請求。使用以下方法配置請求委托:Run, Map, Use擴展方法。可以將單個請求委托作為匿名方法(稱為內聯中間件in-line middleware) 或者可以在可重用類中定義。這些可重用的類和內聯匿名方法是中間件,也稱為中間件組件。請求管道中的每個中間件組件負責調用管道中的下一個組件,或使管道短路。
(1) Run
//將終端中間件委托添加到應用程式的請求管道中。 public static class RunExtensions { public static void Run(this IApplicationBuilder app, RequestDelegate handler); }
(2) Map
// 根據給定請求路徑的匹配對請求管道進行分支。 public static class MapExtensions { public static IApplicationBuilder Map(this IApplicationBuilder app, PathString pathMatch, Action<IApplicationBuilder> configuration); }
(3) Use
// 提供配置應用程式請求的機制 public interface IApplicationBuilder { //.... // 將中間件委托添加到應用程式的請求管道中。 IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware); }
1.1 使用 IApplicationBuilder 創建中間件管道
在Startup. Configure方法中,使用IApplicationBuilder來創建中間件管理。每一個use開頭的擴展方法將一個中間件添加到IApplicationBuilder請求管道中。使用Use擴展方法來配置請求委托。每個use的中間件類似如下聲明:
public static IApplicationBuilder Use[Middleware] (this IApplicationBuilder app ) public static IApplicationBuilder Use[Middleware] (this IApplicationBuilder app , Action<T>)
ASP.NET Core 請求管道包含一系列請求委托,依次調用。 下圖演示了這一概念。 沿黑色箭頭執行。
在Startup. Configure代碼中,一系列use請求委托中間件如下所示:
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseMvc();
委托可以決定不將請求傳遞給下一個委托(中間件),這就是對請求管道進行短路。通常需要短路,因為這樣可以避免不必要的工作。
下麵示例 是一個最簡單的 ASP.NET core 應用程式,用run方法配置請求委托,設置單個委托處理處理所有請求。此案例不包括實際的請求管道。相反,調用單個匿名函數以響應每個 HTTP 請求。並用委托終止了管道。
public class Startup { public void Configure(IApplicationBuilder app) { app.Run(async context => { await context.Response.WriteAsync("Hello, World!"); }); } }
下麵示例用Use方法將多個請求委托鏈接在一起,next
參數表示管道中的下一個委托。 可通過不調用 next 參數使管道短路。
app.Use(async (context, next) => { //調用下一個委托(app.run) await next.Invoke(); }); app.Run(async context => { await context.Response.WriteAsync("Hello, World!"); });
1.2 中間件順序
向 Startup.Configure
方法添加中間件組件的順序定義了針對請求調用這些組件的順序,以及響應的相反順序。 此排序對於安全性、性能和功能至關重要。以下 Startup.Configure
方法將為常見應用方案添加中間件組件:
(1) 異常/錯誤處理
(2) HTTP 嚴格傳輸安全協議
(3) HTTPS 重定向
(4) 靜態文件伺服器
(5) Cookie 策略實施
(6) 身份驗證
(7) 會話
(8) 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(); }
(1) UseExceptionHandler 是添加到管道的第一個中間件組件。 該異常處理程式中間件可捕獲稍後調用中發生的任何異常。
(2) UseStaticFiles 靜態文件中間件,應該在管道的早期調用。這樣它就可以處理請求和短路,而不需要遍歷其餘組件。靜態文件中間件不提供授權檢查。 它提供的任何文件,包括wwwroot下的文件,都是公開可訪問的。
(3) UseAuthentication 身份驗證中間件。未經身份驗證的請求不會短路,但只有在特定的Razor頁面或MVC控制器操作之後,才會發生授權(和拒絕)。
1.3 Use、Run 和 Map
配置 HTTP 管道可以使用Use、Run 和 Map,但各方法針對構建的中間件作用不同:
(1) Use[Middleware]中間件負責調用管道中的下一個中間件,也可使管道短路(即不調用 next
請求委托)。
(2) Run[Middleware]是一種約定,一些中間件組件可能會公開在管道末端運行的Run[Middleware]方法。
(3) Map擴展用作約定來創建管道分支, Map*創建請求管道分支是基於給定請求路徑的匹配項。
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { app.Map("/Map1",HandleMapTest1); app.Map("/Map2", HandleMapTest2); //其它請求地址 app.Run(async context => { await context.Response.WriteAsync("Hello from non-Map delegate. <p>"); }); } 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"); }); }
Map 還支持嵌套,下麵的示例中,請求訪問/level1/level2a 和 /level1/level2b時進行不同邏輯處理:
app.Map("/level1", level1App => { level1App.Map("/level2a", level2AApp => { // "/level1/level2a" processing }); level1App.Map("/level2b", level2BApp => { // "/level1/level2b" processing }); });
1.4 MapWhen
MapWhen 基於url給定謂詞的結果創建請求管道分支。 Func<HttpContext, bool> 類型的任何謂詞均可用於將請求映射到管道的新分支。 在以下示例中,謂詞用於檢測查詢字元串變數 branch 是否存在,如果存在使用新分支(HandleBranch)。
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { //Func<HttpContext, bool> predicate, Action<IApplicationBuilder> configuration app.MapWhen(context => context.Request.Query.ContainsKey("branch"), HandleBranch); //非匹配branch其它請求地址 app.Run(async context => { await context.Response.WriteAsync("Hello from non-Map delegate. <p>"); }); } private static void HandleBranch(IApplicationBuilder app) { app.Run(async context => { var branchVer = context.Request.Query["branch"]; await context.Response.WriteAsync("Map Test 1"); }); }
二. 編寫中間件
上面演示在請求管道中使用use,map,run方法,來委托處理每個 HTTP 請求就是中間件。通常中間件會封裝在類中,並且通過擴展方法公開。下麵示例是如何編寫一個中間件組件。處理邏輯是該中間件通過查詢字元串設置當前請求的區域性。
/// <summary> /// 自定義中間件實現類 /// </summary> public class RequestCultureMiddleware { //using Microsoft.AspNetCore.Http private readonly RequestDelegate _next;/// <summary>
/// 程式啟動時調用
/// </summary>
/// <param name="next"></param>
public RequestCultureMiddleware(RequestDelegate next) { this._next = next; }/// <summary>
///每個頁面請求時自動調用,方法按約定命名,必需是Invoke或InvokeAsync
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public async Task InvokeAsync(HttpContext context) { var cultureQuery = context.Request.Query["culture"]; if (!string.IsNullOrWhiteSpace(cultureQuery)) { //using System.Globalization; var culture = new CultureInfo(cultureQuery); CultureInfo.CurrentCulture = culture; CultureInfo.CurrentUICulture = culture; } // Call the next delegate/middleware in the pipeline await _next(context); } } /// <summary> /// 通過擴展方法公開中間件 /// </summary> public static class RequestCultureMiddlewareExtensions { public static IApplicationBuilder UseRequestCulture(this IApplicationBuilder builder) { //在管道中添加一個use的中間件 return builder.UseMiddleware<RequestCultureMiddleware>(); } }
public void Configure(IApplicationBuilder app) { //調用中間件 app.UseRequestCulture(); app.Run(async (context) => { await ResponseAsync(context); }); } private async Task ResponseAsync(HttpContext context) { context.Response.ContentType = "text/html; charset=utf-8"; await context.Response.WriteAsync( //列印當前顯示的語言 $"Hello { CultureInfo.CurrentCulture.DisplayName }" ); }
2.1 請求依賴項
由於中間件是在應用啟動時構造的(實例),而不是在每個請求時的,因此在每個請求過程中,中間件構造函數使用的作用域生命周期服務,不會在每個請求期間與其他依賴註入類型共用。如果必須在中間件和其他類型之間共用一個範圍服務,請將這些服務添加到 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); } }
參考文獻:
官方文檔:ASP.NET Core 中間件