本章將和大家分享ASP.NET Core 中間件(Middleware)的使用及其源碼解析。 ...
中間件是一種裝配到應用管道以處理請求和響應的軟體。每個組件:
1、選擇是否將請求傳遞到管道中的下一個組件。
2、可在管道中的下一個組件前後執行工作。
請求委托用於生成請求管道。請求委托處理每個 HTTP 請求。
請求管道中的每個中間件組件負責調用管道中的下一個組件,或使管道短路。當中間件短路時,它被稱為“終端中間件”,因為它阻止中間件進一步處理請求。
廢話不多說,我們直接來看一個Demo,Demo的目錄結構如下所示:
本Demo的Web項目為ASP.NET Core Web 應用程式(目標框架為.NET Core 3.1) MVC項目。
其中 Home 控制器代碼如下:
using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace NETCoreMiddleware.Controllers { public class HomeController : Controller { private readonly ILogger<HomeController> _logger; public HomeController(ILogger<HomeController> logger) { _logger = logger; } public IActionResult Index() { Console.WriteLine(""); Console.WriteLine($"This is {typeof(HomeController)} Index"); Console.WriteLine(""); return View(); } } }
其中 Startup.cs 類的代碼如下:
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace NETCoreMiddleware { public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } //服務註冊(往容器中添加服務) // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { services.AddControllersWithViews(); } /// <summary> /// 配置Http請求處理管道 /// Http請求管道模型---就是Http請求被處理的步驟 /// 所謂管道,就是拿著HttpContext,經過多個步驟的加工,生成Response,這就是管道 /// </summary> /// <param name="app"></param> /// <param name="env"></param> // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { #region 環境參數 if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Home/Error"); } #endregion 環境參數 //靜態文件中間件 app.UseStaticFiles(); #region Use中間件 //中間件1 app.Use(next => { Console.WriteLine("middleware 1"); return async context => { await Task.Run(() => { Console.WriteLine(""); Console.WriteLine("===================================Middleware==================================="); Console.WriteLine($"This is middleware 1 Start"); }); await next.Invoke(context); await Task.Run(() => { Console.WriteLine($"This is middleware 1 End"); Console.WriteLine("===================================Middleware==================================="); }); }; }); //中間件2 app.Use(next => { Console.WriteLine("middleware 2"); return async context => { await Task.Run(() => Console.WriteLine($"This is middleware 2 Start")); await next.Invoke(context); //可通過不調用 next 參數使請求管道短路 await Task.Run(() => Console.WriteLine($"This is middleware 2 End")); }; }); //中間件3 app.Use(next => { Console.WriteLine("middleware 3"); return async context => { await Task.Run(() => Console.WriteLine($"This is middleware 3 Start")); await next.Invoke(context); await Task.Run(() => Console.WriteLine($"This is middleware 3 End")); }; }); #endregion Use中間件 #region 最終把請求交給MVC app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllerRoute( name: "areas", pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}"); endpoints.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); }); #endregion 最終把請求交給MVC } } }
用 Use 將多個請求委托鏈接在一起,next 參數表示管道中的下一個委托(下一個中間件)。
下麵我們使用命令行(CLI)方式啟動我們的網站,如下所示:
可以發現控制台依次輸出了“middleware 3” 、“middleware 2”、“middleware 1”,這是怎麼回事呢?此處我們先留個疑問,該點在後面的講解中會再次提到。
啟動成功後,我們來訪問一下 “/home/index” ,控制台輸出結果如下所示:
請求管道包含一系列請求委托,依次調用,下圖演示了這一過程:
每個委托均可在下一個委托前後執行操作。應儘早在管道中調用異常處理委托,這樣它們就能捕獲在管道的後期階段發生的異常。
此外,可通過不調用 next 參數使請求管道短路,如下所示:
/// <summary> /// 配置Http請求處理管道 /// Http請求管道模型---就是Http請求被處理的步驟 /// 所謂管道,就是拿著HttpContext,經過多個步驟的加工,生成Response,這就是管道 /// </summary> /// <param name="app"></param> /// <param name="env"></param> // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { #region 環境參數 if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Home/Error"); } #endregion 環境參數 //靜態文件中間件 app.UseStaticFiles(); #region Use中間件 //中間件1 app.Use(next => { Console.WriteLine("middleware 1"); return async context => { await Task.Run(() => { Console.WriteLine(""); Console.WriteLine("===================================Middleware==================================="); Console.WriteLine($"This is middleware 1 Start"); }); await next.Invoke(context); await Task.Run(() => { Console.WriteLine($"This is middleware 1 End"); Console.WriteLine("===================================Middleware==================================="); }); }; }); //中間件2 app.Use(next => { Console.WriteLine("middleware 2"); return async context => { await Task.Run(() => Console.WriteLine($"This is middleware 2 Start")); //await next.Invoke(context); //可通過不調用 next 參數使請求管道短路 await Task.Run(() => Console.WriteLine($"This is middleware 2 End")); }; }); //中間件3 app.Use(next => { Console.WriteLine("middleware 3"); return async context => { await Task.Run(() => Console.WriteLine($"This is middleware 3 Start")); await next.Invoke(context); await Task.Run(() => Console.WriteLine($"This is middleware 3 End")); }; }); #endregion Use中間件 #region 最終把請求交給MVC app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllerRoute( name: "areas", pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}"); endpoints.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); }); #endregion 最終把請求交給MVC }
此處我們註釋掉了 中間件2 的 next 參數調用,使請求管道短路。下麵我們重新編譯後再次訪問 “/home/index” ,控制台輸出結果如下所示:
當委托不將請求傳遞給下一個委托時,它被稱為“讓請求管道短路”。 通常需要短路,因為這樣可以避免不必要的工作。 例如,靜態文件中間件可以處理對靜態文件的請求,並讓管道的其餘部分短路,從而起到終端中間件的作用。
對於終端中間件,框架專門為我們提供了一個叫 app.Run(...) 的擴展方法,其實該方法的內部也是調用 app.Use(...) 這個方法的,下麵我們來看個示例:
/// <summary> /// 配置Http請求處理管道 /// Http請求管道模型---就是Http請求被處理的步驟 /// 所謂管道,就是拿著HttpContext,經過多個步驟的加工,生成Response,這就是管道 /// </summary> /// <param name="app"></param> /// <param name="env"></param> // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { #region 環境參數 if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Home/Error"); } #endregion 環境參數 //靜態文件中間件 app.UseStaticFiles(); #region Use中間件 //中間件1 app.Use(next => { Console.WriteLine("middleware 1"); return async context => { await Task.Run(() => { Console.WriteLine(""); Console.WriteLine("===================================Middleware==================================="); Console.WriteLine($"This is middleware 1 Start"); }); await next.Invoke(context); await Task.Run(() => { Console.WriteLine($"This is middleware 1 End"); Console.WriteLine("===================================Middleware==================================="); }); }; }); //中間件2 app.Use(next => { Console.WriteLine("middleware 2"); return async context => { await Task.Run(() => Console.WriteLine($"This is middleware 2 Start")); await next.Invoke(context); //可通過不調用 next 參數使請求管道短路 await Task.Run(() => Console.WriteLine($"This is middleware 2 End")); }; }); //中間件3 app.Use(next => { Console.WriteLine("middleware 3"); return async context => { await Task.Run(() => Console.WriteLine($"This is middleware 3 Start")); await next.Invoke(context); await Task.Run(() => Console.WriteLine($"This is middleware 3 End")); }; }); #endregion Use中間件 #region 終端中間件 //app.Use(_ => handler); app.Run(async context => { await Task.Run(() => Console.WriteLine($"This is Run")); }); #endregion 終端中間件 #region 最終把請求交給MVC app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllerRoute( name: "areas", pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}"); endpoints.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); }); #endregion 最終把請求交給MVC }
我們重新編譯後再次訪問 “/home/index” ,控制台輸出結果如下所示:
Run 委托不會收到 next 參數。第一個 Run 委托始終為終端,用於終止管道。Run 是一種約定。某些中間件組件可能會公開在管道末尾運行 Run[Middleware] 方法。
此外,app.Use(...) 方法還有另外一個重載,如下所示(中間件4):
/// <summary> /// 配置Http請求處理管道 /// Http請求管道模型---就是Http請求被處理的步驟 /// 所謂管道,就是拿著HttpContext,經過多個步驟的加工,生成Response,這就是管道 /// </summary> /// <param name="app"></param> /// <param name="env"></param> // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { #region 環境參數 if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Home/Error"); } #endregion 環境參數 //靜態文件中間件 app.UseStaticFiles(); #region Use中間件 //中間件1 app.Use(next => { Console.WriteLine("middleware 1"); return async context => { await Task.Run(() => { Console.WriteLine(""); Console.WriteLine("===================================Middleware==================================="); Console.WriteLine($"This is middleware 1 Start"); }); await next.Invoke(context); await Task.Run(() => { Console.WriteLine($"This is middleware 1 End"); Console.WriteLine("===================================Middleware==================================="); }); }; }); //中間件2 app.Use(next => { Console.WriteLine("middleware 2"); return async context => { await Task.Run(() => Console.WriteLine($"This is middleware 2 Start")); await next.Invoke(context); //可通過不調用 next 參數使請求管道短路 await Task.Run(() => Console.WriteLine($"This is middleware 2 End")); }; }); //中間件3 app.Use(next => { Console.WriteLine("middleware 3"); return async context => { await Task.Run(() => Console.WriteLine($"This is middleware 3 Start")); await next.Invoke(context); await Task.Run(() => Console.WriteLine($"This is middleware 3 End")); }; }); //中間件4 //Use方法的另外一個重載 app.Use(async (context, next) => { await Task.Run(() => Console.WriteLine($"This is middleware 4 Start")); await next(); await Task.Run(() => Console.WriteLine($"This is middleware 4 End")); }); #endregion Use中間件 #region 終端中間件 //app.Use(_ => handler); app.Run(async context => { await Task.Run(() => Console.WriteLine($"This is Run")); }); #endregion 終端中間件 #region 最終把請求交給MVC app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllerRoute( name: "areas", pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}"); endpoints.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); }); #endregion 最終把請求交給MVC }
我們重新編譯後再次訪問 “/home/index” ,控制台輸出結果如下所示:
下麵我們結合ASP.NET Core源碼來分析下其實現原理:
首先我們通過調試來看下 IApplicationBuilder 的實現類到底是啥?如下所示:
可以看出它的實現類是 Microsoft.AspNetCore.Builder.ApplicationBuilder ,我們找到 ApplicationBuilder 類的源碼,如下所示:
// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.Internal; namespace Microsoft.AspNetCore.Builder { public class ApplicationBuilder : IApplicationBuilder { private const string ServerFeaturesKey = "server.Features"; private const string ApplicationServicesKey = "application.Services"; private readonly IList<Func<RequestDelegate, RequestDelegate>> _components = new List<Func<RequestDelegate, RequestDelegate>>(); public ApplicationBuilder(IServiceProvider serviceProvider) { Properties = new Dictionary<string, object>(StringComparer.Ordinal); ApplicationServices = serviceProvider; } public ApplicationBuilder(IServiceProvider serviceProvider, object server) : this(serviceProvider) { SetProperty(ServerFeaturesKey, server); } private ApplicationBuilder(ApplicationBuilder builder) { Properties = new CopyOnWriteDictionary<string, object>(builder.Properties, StringComparer.Ordinal); } public IServiceProvider ApplicationServices { get { return GetProperty<IServiceProvider>(ApplicationServicesKey); } set { SetProperty<IServiceProvider>(ApplicationServicesKey, value); } } public IFeatureCollection ServerFeatures { get { return GetProperty<IFeatureCollection>(ServerFeaturesKey); } } public IDictionary<string, object> Properties { get; } private T GetProperty<T>(string key) { object value; return Properties.TryGetValue(key, out value) ? (T)value : default(T); } private void SetProperty<T>(string key, T value) { Properties[key] = value; } public IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware) { _components.Add(middleware); return this; } public IApplicationBuilder New() { return new ApplicationBuilder(this); } public RequestDelegate Build() { RequestDelegate app = context => { // If we reach the end of the pipeline, but we have an endpoint, then something unexpected has happened. // This could happen if user code sets an endpoint, but they forgot to add the UseEndpoint middleware. var endpoint = context.GetEndpoint(); var endpointRequestDelegate = endpoint?.RequestDelegate; if (endpointRequestDelegate != null) { var message = $"The request reached the end of the pipeline without executing the endpoint: '{endpoint.DisplayName}'. " + $"Please register the EndpointMiddleware using '{nameof(IApplicationBuilder)}.UseEndpoints(...)' if using " + $"routing."; throw new InvalidOperationException(message); } context.Response.StatusCode = 404; return Task.CompletedTask; }; foreach (var component in _components.Reverse()) { app = component(app); } return app; } } }
其中 RequestDelegate 委托的聲明,如下:
// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Threading.Tasks; namespace Microsoft.AspNetCore.Http { /// <summary> /// A function that can process an HTTP request. /// </summary> /// <param name="context">The <see cref="HttpContext"/> for the request.</param> /// <returns>A task that represents the completion of request processing.</returns> public delegate Task RequestDelegate(HttpContext context); }
仔細閱讀後可以發現其實 app.Use(...) 這個方法就只是將 Func<RequestDelegate, RequestDelegate> 類型的委托參數添加到 _components 這個集合中。
最終程式會調用 ApplicationBuilder 類的 Build() 方法去構建Http請求處理管道,接下來我們就重點來關註一下這個 Build() 方法,如下:
public RequestDelegate Build() { RequestDelegate app = context => { // If we reach the end of the pipeline, but we have an endpoint, then something unexpected has happened. // This could happen if user code sets an endpoint, but they forgot to add the UseEndpoint middleware. var endpoint = context.GetEndpoint(); var endpointRequestDelegate = endpoint?.RequestDelegate; if (endpointRequestDelegate != null) { var message = $"The request reached the end of the pipeline without executing the endpoint: '{endpoint.DisplayName}'. " + $"Please register the EndpointMiddleware using '{nameof(IApplicationBuilder)}.UseEndpoints(...)' if using " + $"routing."; throw new InvalidOperationException(message); } context.Response.StatusCode = 404; return Task.CompletedTask; }; foreach (var component in _components.Reverse()) { app = component(app); } return app; }
仔細觀察上面的源碼後我們可以發現:
1、首先它是將 _components 這個集合反轉(即:_components.Reverse()),然後依次調用裡面的中間件(Func<RequestDelegate, RequestDelegate>委托),這也就解釋了為什麼網站啟動時我們的控制台會依次輸出 “middleware 3” 、“middleware 2”、“middleware 1” 的原因。
2、調用反轉後的第一個中間件(即:註冊的最後一個中間件)時傳入的參數是狀態碼為404的 RequestDelegate 委托,作為預