前言 由於是第一次寫博客,如果您看到此文章,希望大家抱著找錯誤、批判的心態來看。 sky! 何為中間件? 在 ASP.NET Framework 中應該都知道請求管道。可參考: "淺談 ASP.NET 的內部機制" 系列,個人感覺超詳細。 _題外話: 說到請求管道,就想以前還 ...
前言
由於是第一次寫博客,如果您看到此文章,希望大家抱著找錯誤、批判的心態來看。 sky!
何為中間件?
在 ASP.NET Framework 中應該都知道請求管道。可參考:淺談 ASP.NET 的內部機制 系列,個人感覺超詳細。
題外話:
說到請求管道,就想以前還是超菜鳥時有次面試被問到這個問題,一臉懵逼只說了 Controller→Action→View。臉紅啊!!
ASP.NET Core 中的中間件就是.net framework 請求管道的實現。下圖演示了 Middlerware 的概念。 沿黑色箭頭執行。
每一個中間件(Middleware1、Middleware2...)都是一個委托,這一系列委托就組成了整個管道。
中間件的寫法
直接在Startup.cs類的Configure方法里寫
app.Use(async (context, next) => { logger.LogInformation("中間件開始..."); await next.Invoke(); //執行下一個中間件 logger.LogInformation("中間件完成..."); });
結合上圖:
//logic對應logger.LogInformation("中間件開始...");
next();對應await next.Invoke();
//more logic對應logger.LogInformation("中間件完成...");
其中//logic(即請求)是順序執行。即:Middleware1→Middleware2→...→Middlewaren
而//more logic(即響應)是倒序執行。即:Middlewaren→...→Middleware2→Middleware1
同 1,只是不用 Use 而是用 Run:
app.Run(async context => { await context.Response.WriteAsync("請求終止了,下一步將會執行已執行過的Middleware的 //more logic"); });
Run 會終止請求,即管道中最後一個中間件,後面詳細剖析!
下麵這種寫法應該是比較合理的,也是比較優雅的
新建一個類如下(該類是有強制規範的,詳細見下文):
public class RequestTestMiddleware { private readonly RequestDelegate _next; public RequestTestMiddleware(RequestDelegate next) { _next = next; } public async Task InvokeAsync(HttpContext context) { //中間件開始 logic await _next(context);//執行下一個中間件 //中間件完成 more logic } }
在Startup.cs類的Configure方法里添加如下代碼,效果和 1 相同:
app.UseMiddleware<RequestTestMiddleware>(); //app.UseMiddleware<RequestTestMiddleware>(params object[] parameters);//參數說明見下麵
不知發現了沒,上面的InvokeAsync方法不是用的列印日誌,而是用的註釋。
因為我們沒有引用logger對象,瞭解過 ASP.NET Core 的肯定知道依賴註入,我們只需要把ILogger註入進來就行了,改造如下:public class RequestTestMiddleware { private readonly RequestDelegate _next; public RequestTestMiddleware(RequestDelegate next) { _next = next; } public async Task InvokeAsync(HttpContext context, ILogger<TestMiddleware> logger) { logger.LogInformation("中間件開始 logic"); await _next(context); logger.LogInformation("中間件完成 more logic"); } }
通過依賴註入方法添加中間件:
新建類 TestMiddleware.cs 註意依賴註入的位置和 3 不同public class TestMiddleware : IMiddleware { private readonly ILogger _logger; public TestMiddleware(ILogger<TestMiddleware> logger) { _logger = logger; } public async Task InvokeAsync(HttpContext context, RequestDelegate next) { _logger.LogInformation("中間件開始"); await next(context); _logger.LogInformation("中間件完成"); } }
在Startup.cs類的ConfigureServices方法里添加如下代碼:
services.AddTransient<TestMiddleware>();
在Startup.cs類的Configure方法里添加如下代碼:
app.UseMiddleware<TestMiddleware>();
還有一種第三方容器激活中間件
源代碼分析(部分)
Run和Use的實現
直接放出源代碼:
public static void Run(this IApplicationBuilder app, RequestDelegate handler) { if (app == null) { throw new ArgumentNullException(nameof(app)); } if (handler == null) { throw new ArgumentNullException(nameof(handler)); } app.Use(_ => handler); }
public static IApplicationBuilder Use(this IApplicationBuilder app, Func<HttpContext, Func<Task>, Task> middleware) { return app.Use(next => { return context => { Func<Task> simpleNext = () => next(context); return middleware(context, simpleNext); }; }); }
2 個方法最終調用的都是app.Use(),我們看下代碼:
public IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware) { _components.Add(middleware); return this; }
_components是IList<Func<RequestDelegate, RequestDelegate>>類型,其實就是把我們的Middleware添加到 _components 中,繼續看代碼:
public RequestDelegate Build() { RequestDelegate app = context => { context.Response.StatusCode = 404; return Task.CompletedTask; }; foreach (var component in _components.Reverse()) { app = component(app); } return app; }
該方法會在Program.cs中Main方法的 CreateWebHostBuilder(args).Build().Run(); 的 Run() 方法執行。
此方法把我們所有的Middleware再次組裝成 1 個新的RequestDelegate,最終的順序將會是:
Middleware1() { next()=>Middleware2() { next()=>Middleware3() { next()=>最後的那個返回404的委托 } } }
不知道寫清楚了沒( ╯□╰ ). 其中next()=>Middleware2()的意思為:next()就是 Middleware2()
繼承 IMiddleware 和沒繼承 IMiddleware(根據規範必須要有 InvokeAsync 或 Invoke 方法等)的區別:
按功能實現方面來說是沒區別的,但按性能方面應該是繼承了 IMiddleware 的方式要好很多,因為沒繼承 IMiddleware 的方式會用到反射。(未測試,由於繼承 IMiddleware 還需要用依賴註入這裡只是猜測)
代碼見:
Microsoft.AspNetCore.Http.Abstractions\Extensions\UseMiddlewareExtensions.cs 的 UseMiddleware 方法。未繼承 IMiddleware 時的約定,直接看代碼吧:
//1.在middleware中必須存在public且有返回值的方法 var methods = middleware.GetMethods(BindingFlags.Instance | BindingFlags.Public); //2.必須有‘Invoke’或‘InvokeAsync’方法 var invokeMethods = methods.Where(m => string.Equals(m.Name, "Invoke", StringComparison.Ordinal) || string.Equals(m.Name, "InvokeAsync", StringComparison.Ordinal) ).ToArray(); //3.‘Invoke’和‘InvokeAsync’只能有1個 if (invokeMethods.Length > 1) {} //4.‘Invoke’和‘InvokeAsync’必須要存在 if (invokeMethods.Length == 0) {} var methodInfo = invokeMethods[0]; //5.返回結果類型必須為Task if (!typeof(Task).IsAssignableFrom(methodInfo.ReturnType)){}
中間件傳參
直接上代碼:public class RequestTestMiddleware { private readonly RequestDelegate _next; private int _i; public RequestTestMiddleware(RequestDelegate next, int i) { _next = next; _i = i; } public async Task InvokeAsync(HttpContext context, ILogger<TestMiddleware> logger) { logger.LogInformation($"通過參數傳遞i值:{_i}"); logger.LogInformation("中間件開始"); await _next(context); logger.LogInformation("中間件完成"); } }
在Startup.cs類的Configure方法里:
//參數類型為: params object[] args app.UseMiddleware<RequestTestMiddleware>(1);
具體實現方式同樣在 Microsoft.AspNetCore.Http.Abstractions\Extensions\UseMiddlewareExtensions.cs 的 UseMiddleware 方法中
高級用法 Map MapWhen
Map
app.Map("/map", _app => { _app.Run(async context => { await context.Response.WriteAsync("Test Map!"); }); });
當訪問https://localhost:5001/map時將返回 Test Map!
這裡說一下,代碼中並沒有 MapController....
MapWhen
app.MapWhen(context => context.Request.Query.ContainsKey("branch"), _app => { _app.Run(async context => { await context.Response.WriteAsync("Test Map!"); }); });
看源代碼會發現,MapWhen 的第二個參數(委托)並不是上面Use()中next(),而是存在MapOptions的Branch屬性中,也是RequestDelegate委托
其他說明
- Middleware 的執行的有順序的,在合適的 Middleware 返回請求可時管道更短,速度更快。
比如 UseStaticFiles(),靜態資源不必走驗證、MVC 中間件,所以該方法在中間件的前面執行。 我們看到有很多內置的中間件的用法是*Use**,其實是加了個擴展:
public static class RequestCultureMiddlewareExtensions { public static IApplicationBuilder UseRequestCulture( this IApplicationBuilder builder) { return builder.UseMiddleware<RequestCultureMiddleware>(); } }
總結
第一次寫博客,最大的感觸就是慢,然後就是思維邏輯有點混亂,總想用最簡單的語言來表達,就是掌握不好。最後看起來還是太啰嗦了點。最後說明,以上很可能有錯誤的說法,希望大家以批判的角度來看,有任何問題可在留言區留言!Thanks!