ASP.NET Core 2.0 : 图说管道,唐僧扫塔的故事
本文通過一張GIF動圖來繼續聊一下ASP.NET Core的請求處理管道,從管道的配置、構建以及請求處理流程等方面做一下詳細的研究。(ASP.NET Core系列目錄)
一、概述
上文說到,請求是經過 Server監聽=>處理成httpContext=>Application處理生成Response。?這個Application的類型RequestDelegate本質是?public?delegate?Task RequestDelegate (HttpContext context);?,即接收HttpContext并返回Task, 它是由一個個中間件?Func<RequestDelegate, RequestDelegate> middleware?嵌套在一起構成的。它的構建是由ApplicationBuilder完成的,先來看一下這個ApplicationBuilder:
public class ApplicationBuilder : IApplicationBuilder
{
? ? private readonly IList<Func<RequestDelegate, RequestDelegate>> _components = new List<Func<RequestDelegate, RequestDelegate>>();
? ? public IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware)
? ? {
? ? ? ? _components.Add(middleware);
? ? ? ? return this;
? ? }
? ? public RequestDelegate Build()
? ? {
? ? ? ? RequestDelegate app = context =>
? ? ? ? {
? ? ? ? ? ? context.Response.StatusCode = 404;
? ? ? ? ? ? return Task.CompletedTask;
? ? ? ? };?
? ? ? ? foreach (var component in _components.Reverse())20? ? ? ? ?{
? ? ? ? ? ? app = component(app);
? ? ? ? }
? ? ? ? return app;
? ? }
}
ApplicationBuilder有個集合?IList<Func<RequestDelegate, RequestDelegate>> _components?和一個用于向這個集合中添加內容的??Use(Func<RequestDelegate, RequestDelegate> middleware)?方法,通過它們的類型可以看出來它們是用來添加和存儲中間件的。現在說一下大概的流程:
調用startupFilters和_startup的Configure方法,調用其中定義的多個UseXXX(進一步調用ApplicationBuilder的Use方法)將一個個中間件middleware按照順序寫入上文的集合_components(記住這個_components)。
定義了一個?context.Response.StatusCode =?404?的RequestDelegate。
將集合_components顛倒一下, 然后遍歷其中的middleware,一個個的與新創建的404?RequestDelegate 連接在一起,組成一個新的RequestDelegate(即Application)返回。
這個最終返回的RequestDelegate類型的Application就是對HttpContext處理的管道了,這個管道是多個中間件按照一定順序連接在一起組成的,startupFilters先不說,以我們非常熟悉的Startup為例,它的Configure方法默認情況下已經依次進行了UseBrowserLink、UseDeveloperExceptionPage、UseStaticFiles、UseMvc了等方法,請求進入管道后,請求也會按照這個順序來經過各個中間件處理,首先進入UseBrowserLink,然后UseBrowserLink會調用下一個中間件UseDeveloperExceptionPage,依次類推到達UseMVC后被處理生成Response開始逆向返回再依次反向經過這幾個中間件,正常情況下,請求到達MVC中間件后被處理生成Response開始逆向返回,而不會到達最終的404,這個404是為了防止其他層未配置或未能處理的時候的一個保險操作。
胡扯兩句:這個管道就像一座塔,話說唐僧路過金光寺去掃金光塔,從前門進入第一層開始掃,然后從前門的樓梯進入第二層、第三層、第四層,然后從第四層的后門掃下來直至后門出去,卻不想妖怪沒處理好, 被唐僧掃到了第五層(頂層)去,發現佛寶被奔波兒灞和霸波爾奔偷走了,大喊:悟空悟空,佛寶被妖怪偷走啦!(404...)
下面就以這4個為例通過一個動圖形象的描述一下整個過程:
一個“中規中矩”的管道就是這樣構建并運行的,通過上圖可以看到各個中間件在Startup文件中的配置順序與最終構成的管道中的順序的關系,下面我們自己創建幾個中間件體驗一下,然后再看一下不“中規中矩”的長了杈子的管道。
二、自定義中間件
先仿照系統現有的寫一個
public class FloorOneMiddleware
? ? {
? ? ? ? private readonly RequestDelegate _next;
? ? ? ? public FloorOneMiddleware(RequestDelegate next)
? ? ? ? {
? ? ? ? ? ? _next = next;
? ? ? ? }
? ? ? ? public async Task InvokeAsync(HttpContext context)
? ? ? ? {
? ? ? ? ? ? Console.WriteLine("FloorOneMiddleware In");
? ? ? ? ? ? //Do Something
? ? ? ? ? ? //To FloorTwoMiddleware
? ? ? ? ? ? await _next(context);
? ? ? ? ? ? //Do Something
? ? ? ? ? ? Console.WriteLine("FloorOneMiddleware Out");
? ? ? ? }
? ? }
這是塔的第一層,進入第一層后的?//Do Something?表示在第一層需要做的工作, 然后通過?_next(context)?進入第二層,再下面的?//Do Something?是從第二層出來后的操作。同樣第二層調用第三層也是一樣。再仿寫個UseFloorOne的擴展方法:
public static class FloorOneMiddlewareExtensions
? ? {
? ? ? ? public static IApplicationBuilder UseFloorOne(this IApplicationBuilder builder)
? ? ? ? {
? ? ? ? ? ? Console.WriteLine("Use FloorOneMiddleware");
? ? ? ? ? ? return builder.UseMiddleware<FloorOneMiddleware>();
? ? ? ? }
? ? }
這樣在Startup的Configure方法中就也可以寫?app.UseFloorOne();?將這個中間件作為管道的一部分了。
通過上面的例子仿照系統默認的中間件完成了一個簡單的中間件的編寫,這里也可以用簡要的寫法,直接在Startup的Configure方法中這樣寫:
app.Use(async (context,next) =>
{
? ? Console.WriteLine("FloorThreeMiddleware In");
? ? //Do Something
? ? //To FloorThreeMiddleware
? ? await next.Invoke();
? ? //Do Something
? ? Console.WriteLine("FloorThreeMiddleware Out");
});
同樣可以實現上一種例子的工作,但還是建議按照那樣的寫法,在Startup這里體現的簡潔并且可讀性好的多。
復制一下第一種和第二種的例子,形成如下代碼:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
? ? ? ? {
? ? ? ? ? ? app.UseFloorOne();
? ? ? ? ? ? app.UseFloorTwo();
? ? ? ? ? ? app.Use(async (context,next) =>
? ? ? ? ? ? {
? ? ? ? ? ? ? ? Console.WriteLine("FloorThreeMiddleware In");
? ? ? ? ? ? ? ? //Do Something
? ? ? ? ? ? ? ? //To FloorThreeMiddleware
? ? ? ? ? ? ? ? await next.Invoke();
? ? ? ? ? ? ? ? //Do Something
? ? ? ? ? ? ? ? Console.WriteLine("FloorThreeMiddleware Out");
? ? ? ? ? ? });
? ? ? ? ? ? app.Use(async (context, next) =>
? ? ? ? ? ? {
? ? ? ? ? ? ? ? Console.WriteLine("FloorFourMiddleware In");
? ? ? ? ? ? ? ? //Do Something
? ? ? ? ? ? ? ? await next.Invoke();
? ? ? ? ? ? ? ? //Do Something
? ? ? ? ? ? ? ? Console.WriteLine("FloorFourMiddleware Out");
? ? ? ? ? ? });
? ? ? ? ? ? if (env.IsDevelopment())
? ? ? ? ? ? {
? ? ? ? ? ? ? ? app.UseBrowserLink();
? ? ? ? ? ? ? ? app.UseDeveloperExceptionPage();
? ? ? ? ? ? }
? ? ? ? ? ? else
? ? ? ? ? ? {
? ? ? ? ? ? ? ? app.UseExceptionHandler("/Home/Error");
? ? ? ? ? ? }
? ? ? ? ? ? app.UseStaticFiles();
? ? ? ? ? ? app.UseMvc(routes =>
? ? ? ? ? ? {
? ? ? ? ? ? ? ? routes.MapRoute(
? ? ? ? ? ? ? ? ? ? name: "default",
? ? ? ? ? ? ? ? ? ? template: "{controller=Home}/{action=Index}/{id?}");
? ? ? ? ? ? });
? ? ? ? }
運行一下看日志:
CoreMiddleware> Use FloorOneMiddleware
CoreMiddleware> Use FloorTwoMiddleware
CoreMiddleware> Hosting environment: Development
CoreMiddleware> Content root path: C:\Users\FlyLolo\Desktop\CoreMiddleware\CoreMiddleware
CoreMiddleware> Now listening on: http://localhost:10757
CoreMiddleware> Application started. Press Ctrl+C to shut down.
CoreMiddleware> info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
CoreMiddleware>? ? ? ?Request starting HTTP/1.1 GET http://localhost:56440/??
CoreMiddleware> FloorOneMiddleware In
CoreMiddleware> FloorTwoMiddleware In
CoreMiddleware> FloorThreeMiddleware In
CoreMiddleware> FloorFourMiddleware In
CoreMiddleware> info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[1]
CoreMiddleware>? ? ? ?Executing action method CoreMiddleware.Controllers.HomeController.Index (CoreMiddleware) with arguments ((null)) - ModelState is Valid
CoreMiddleware> info: Microsoft.AspNetCore.Mvc.ViewFeatures.Internal.ViewResultExecutor[1]
CoreMiddleware>? ? ? ?Executing ViewResult, running view at path /Views/Home/Index.cshtml.
CoreMiddleware> info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[2]
CoreMiddleware>? ? ? ?Executed action CoreMiddleware.Controllers.HomeController.Index (CoreMiddleware) in 9896.6822ms
CoreMiddleware> FloorFourMiddleware Out
CoreMiddleware> FloorThreeMiddleware Out
CoreMiddleware> FloorTwoMiddleware Out
CoreMiddleware> FloorOneMiddleware Out
CoreMiddleware> info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
CoreMiddleware>? ? ? ?Request finished in 10793.8944ms 200 text/html; charset=utf-8
可以看到,前兩行的Use?FloorOneMiddleware和Use FloorTwoMiddleware是將對應的中間件寫入集合_components,而中間件本身并未執行,然后10至12行是依次經過我們自定義的例子的處理,第13-18就是在中間件MVC中的處理了,找到并調用對應的Controller和View,然后才是19-22的逆向返回, 最終Request finished返回狀態200, 這個例子再次驗證了請求在管道中的處理流程。
那么我們試一下404的情況, 把Configure方法中除了自定義的4個中間件外全部注釋掉,再次運行
//上面沒變化? 省略
CoreMiddleware> FloorOneMiddleware In
CoreMiddleware> FloorTwoMiddleware In
CoreMiddleware> FloorThreeMiddleware In
CoreMiddleware> FloorFourMiddleware In
CoreMiddleware> FloorFourMiddleware Out
CoreMiddleware> FloorThreeMiddleware Out
CoreMiddleware> FloorTwoMiddleware Out
CoreMiddleware> FloorOneMiddleware Out
CoreMiddleware> info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
CoreMiddleware>? ? ? ?Request finished in 218.7216ms 404
可以看到,MVC處理的部分沒有了,因為該中間件已被注釋,而最后一條可以看到系統返回了狀態404。
?那么既然MVC可以正常處理請求沒有進入404, 我們怎么做可以這樣呢?是不是不調用下一個中間件就可以了? 試著把FloorFour改一下
app.Use(async (context, next) =>
{
? ? Console.WriteLine("FloorFourMiddleware? In");
? ? //await next.Invoke();
? ? await context.Response.WriteAsync("Danger!");
? ? Console.WriteLine("FloorFourMiddleware? Out");
});
再次運行,查看輸出和上文的沒有啥太大改變, 只是最后的404變為了200, 網頁上的“404 找不到。。”也變成了我們要求輸出的"Danger!", 達到了我們想要的效果。
但一般情況下我們不這樣寫,ASP.NET Core 提供了Use、Run和Map三種方法來配置管道。
三、Use、Run和Map
Use上面已經用過就不說了,對于上面的問題, 一般用Run來處理,Run主要用來做為管道的末尾,例如上面的可以改成這樣:
app.Run(async (context) =>{ ?? ?await context.Response.WriteAsync("Danger!"); });
因為本身他就是作為管道末尾,也就省略了next參數,雖然用use也可以實現, 但還是建議用Run。
? ? Map:
?static?IApplicationBuilder Map(this?IApplicationBuilder app, PathString pathMatch, Action<IApplicationBuilder> configuration);?pathMatch用于匹配請求的path, 例如“/Home”, 必須以“/”開頭, 判斷path是否是以pathMatch開頭。
若是,則進入?Action<IApplicationBuilder> configuration)?,?這個參數是不是長得很像startup的Configure方法? 這就像進入了我們配置的另一個管道,它是一個分支,如下圖
圖2
做個例子:
app.UseFloorOne();
app.Map("/Manager", builder =>
{
? ? builder.Use(async (context, next) =>
? ? {
? ? ? ? await next.Invoke();
? ? });
? ? builder.Run(async (context) =>
? ? {
? ? ? ? await context.Response.WriteAsync("Manager.");
? ? });
});
app.UseFloorTwo();
進入第一層后, 添加了一個Map, 作用是當我們請求?localhost:56440/Manager/index?這樣的地址的時候(是不是有點像Area), 會進入這個Map創建的新分支, 結果也就是頁面顯示"Manager." 不會再進入下面的FloorTwo。若不是“/Manager”開頭的, 這繼續進入FloorTwo。雖然感覺這個Map靈活了我們的管道配置, 但這個只能匹配path開頭的方法太局限了,不著急, 我們看一下MapWhen。
Map When:
MapWhen方法就是一個靈活版的Map,它將原來的PathMatch替換為一個?Func<HttpContext,?bool> predicate?,這下就開放多了,它返回一個bool值,現在舉個栗子隨便改一下
app.MapWhen(context=> {return context.Request.Query.ContainsKey("XX");}, builder =>{ ? ?//...TODO...}當根據請求的參數是否包含“XX”的時候進入這個分支。
從圖2可知,一旦進入分支,是無法回到原分支的,?如果只是想在某種情況下進入某些中間件,但執行完后還可以繼續后續的中間件怎么辦呢?對比MapWhen,Use也有個UseWhen。
UseWhen:
?它和MapWhen一樣,當滿足條件的時候進入一個分支,在這個分支完成之后再繼續后續的中間件,當然前提是這個分支中沒有Run等短路行為。
app.UseWhen(context=> {return context.Request.Query.ContainsKey("XX");}, builder =>{ ? ?//...TODO...}四、IStartupFilter
我們只能指定一個Startup類作為啟動類,那么還能在其他的地方定義管道么? 文章開始的時候說到,構建管道的時候,會調用startupFilters和_startup的Configure方法,調用其中定義的多個UseXXX方法來將中間件寫入_components。自定義一個StartupFilter,實現IStartupFilter的Configure方法,用法和Startup的Configure類似,不過要記得最后調用?next(app)?。
public class TestStartupFilter : IStartupFilter
{
? ? public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
? ? {
? ? ? ? return app => {?
? ? ? ? ? ? app.Use(async (context, next1) =>
? ? ? ? ? ? {
? ? ? ? ? ? ? ? Console.WriteLine("filter.Use1.begin");
? ? ? ? ? ? ? ? await next1.Invoke();
? ? ? ? ? ? ? ? Console.WriteLine("filter.Use1.end");
? ? ? ? ? ? });
? ? ? ? ? ? next(app);
? ? ? ? };
? ? }
}
在復制一個,去startup的ConfigureServices注冊一下:
public void ConfigureServices(IServiceCollection services)
{
? ? services.AddMvc();
? ? services.AddSingleton<IStartupFilter,TestStartupFilter>();
? ? services.AddSingleton<IStartupFilter, TestStartupFilter2>();
}
這樣的配置就生效了,現在剖析一下他的生效機制。回顧一下WebHost的BuildApplication方法:
private RequestDelegate BuildApplication()
{
?//....省略
? ? var startupFilters = _applicationServices.GetService<IEnumerable<IStartupFilter>>();
? ? Action<IApplicationBuilder> configure = _startup.Configure;
? ? foreach (var filter in startupFilters.Reverse())
? ? {
? ? ? ? configure = filter.Configure(configure);
? ? }
? ? configure(builder);
? ? return builder.Build();
仔細看這段代碼,其實這和構建管道的流程非常相似,對比著說一下:
首先通過GetService獲取到注冊的IStartupFilter集合startupFilters(類比_components)
然后獲取Startup的Configure(類比404的RequestDelegate)
翻轉startupFilters,foreach它并且與Startup的Configure鏈接在一起。
上文強調要記得最后調用?next(app),這個是不是和?next.Invoke()?類似。
是不是感覺和圖一的翻轉拼接過程非常類似,是不是想到了拼接先后順序的問題。對比著管道構建后中間件的執行順序,體會一下后,這時應該可以想到各個IStartupFilter和Startup的Configure的執行順序了吧。沒錯就是按照依賴注入的順序:TestStartupFilter=>TestStartupFilter2=>Startup。
}
原文地址 https://www.cnblogs.com/FlyLolo/p/ASPNETCore2_8.html
.NET社區新聞,深度好文,歡迎訪問公眾號文章匯總 http://www.csharpkit.com
總結
以上是生活随笔為你收集整理的ASP.NET Core 2.0 : 图说管道,唐僧扫塔的故事的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 把旧系统迁移到.Net Core 2.0
- 下一篇: ASP.NET MVC应用迁移到ASP.