ASP.NET Core 2.0 : 八.圖說管道,唐僧掃塔的故事

来源:https://www.cnblogs.com/FlyLolo/archive/2018/03/27/ASPNETCore2_8.html
-Advertisement-
Play Games

本文通過一張GIF動圖來繼續聊一下ASP.NET Core的請求處理管道,從管道的配置、構建以及請求處理流程等方面做一下詳細的研究。(ASP.NET Core系列目錄) 一、概述 上文說到,請求是經過 Server監聽=>處理成httpContext=>Application處理生成Response ...


  本文通過一張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:

 1 public class ApplicationBuilder : IApplicationBuilder
 2 {
 3     private readonly IList<Func<RequestDelegate, RequestDelegate>> _components = new List<Func<RequestDelegate, RequestDelegate>>();
 5     public IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware)
 6     {
 7         _components.Add(middleware);
 8         return this;
 9     }
11     public RequestDelegate Build()
12     {
13         RequestDelegate app = context =>
14         {
15             context.Response.StatusCode = 404;
16             return Task.CompletedTask;
17         }; 
19         foreach (var component in _components.Reverse())20         {
21             app = component(app);
22         }
24         return app;
25     }
26 }

   ApplicationBuilder有個集合 IList<Func<RequestDelegate, RequestDelegate>> _components 和一個用於向這個集合中添加內容的  Use(Func<RequestDelegate, RequestDelegate> middleware) 方法,通過它們的類型可以看出來它們是用來添加和存儲中間件的。現在說一下大概的流程:

  1. 調用startupFilters和_startup的Configure方法,調用其中定義的多個UseXXX(進一步調用ApplicationBuilder的Use方法)將一個個中間件middleware按照順序寫入上文的集合_components(記住這個_components)
  2. 定義了一個 context.Response.StatusCode = 404 的RequestDelegate。
  3. 將集合_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個為例通過一個動圖形象的描述一下整個過程:

圖1

  一個“中規中矩”的管道就是這樣構建並運行的,通過上圖可以看到各個中間件在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?}");
            });
        }

運行一下看日誌:

 1 CoreMiddleware> Use FloorOneMiddleware
 2 CoreMiddleware> Use FloorTwoMiddleware
 3 CoreMiddleware> Hosting environment: Development
 4 CoreMiddleware> Content root path: C:\Users\FlyLolo\Desktop\CoreMiddleware\CoreMiddleware
 5 CoreMiddleware> Now listening on: http://localhost:10757
 6 CoreMiddleware> Application started. Press Ctrl+C to shut down.
 7 CoreMiddleware> info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
 8 CoreMiddleware>       Request starting HTTP/1.1 GET http://localhost:56440/  
 9 CoreMiddleware> FloorOneMiddleware In
10 CoreMiddleware> FloorTwoMiddleware In
11 CoreMiddleware> FloorThreeMiddleware In
12 CoreMiddleware> FloorFourMiddleware In
13 CoreMiddleware> info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[1]
14 CoreMiddleware>       Executing action method CoreMiddleware.Controllers.HomeController.Index (CoreMiddleware) with arguments ((null)) - ModelState is Valid
15 CoreMiddleware> info: Microsoft.AspNetCore.Mvc.ViewFeatures.Internal.ViewResultExecutor[1]
16 CoreMiddleware>       Executing ViewResult, running view at path /Views/Home/Index.cshtml.
17 CoreMiddleware> info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[2]
18 CoreMiddleware>       Executed action CoreMiddleware.Controllers.HomeController.Index (CoreMiddleware) in 9896.6822ms
19 CoreMiddleware> FloorFourMiddleware Out
20 CoreMiddleware> FloorThreeMiddleware Out
21 CoreMiddleware> FloorTwoMiddleware Out
22 CoreMiddleware> FloorOneMiddleware Out
23 CoreMiddleware> info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
24 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個中間件外全部註釋掉,再次運行

 1 //上面沒變化  省略
 2 CoreMiddleware> FloorOneMiddleware In
 3 CoreMiddleware> FloorTwoMiddleware In
 4 CoreMiddleware> FloorThreeMiddleware In
 5 CoreMiddleware> FloorFourMiddleware In
 6 CoreMiddleware> FloorFourMiddleware Out
 7 CoreMiddleware> FloorThreeMiddleware Out
 8 CoreMiddleware> FloorTwoMiddleware Out
 9 CoreMiddleware> FloorOneMiddleware Out
10 CoreMiddleware> info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
11 CoreMiddleware>       Request finished in 218.7216ms 404 

可以看到,MVC處理的部分沒有了,因為該中間件已被註釋,而最後一條可以看到系統返回了狀態404。

 那麼既然MVC可以正常處理請求沒有進入404, 我們怎麼做可以這樣呢?是不是不調用下一個中間件就可以了? 試著把FloorFour改一下

1 app.Use(async (context, next) =>
2 {
3     Console.WriteLine("FloorFourMiddleware  In");
4     //await next.Invoke();
5     await context.Response.WriteAsync("Danger!");
6     Console.WriteLine("FloorFourMiddleware  Out");
7 });

再次運行,查看輸出和上文的沒有啥太大改變, 只是最後的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) 。

 1 public class TestStartupFilter : IStartupFilter
 2 {
 3     public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
 4     {
 5         return app => { 
 6             app.Use(async (context, next1) =>
 7             {
 8                 Console.WriteLine("filter.Use1.begin");
 9                 await next1.Invoke();
10                 Console.WriteLine("filter.Use1.end");
11             });
12             next(app);
13         };
14     }
15 }

在複製一個,去startup的ConfigureServices註冊一下:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
    services.AddSingleton<IStartupFilter,TestStartupFilter>();
    services.AddSingleton<IStartupFilter, TestStartupFilter2>();
}

這樣的配置就生效了,現在剖析一下他的生效機制。回顧一下WebHost的BuildApplication方法:

 1 private RequestDelegate BuildApplication()
 2 {
 3  //....省略
 4     var startupFilters = _applicationServices.GetService<IEnumerable<IStartupFilter>>();
 5     Action<IApplicationBuilder> configure = _startup.Configure;
 6     foreach (var filter in startupFilters.Reverse())
 7     {
 8         configure = filter.Configure(configure);
 9     }
10 
11     configure(builder);
12 
13     return builder.Build();
14 }

  仔細看這段代碼,其實這和構建管道的流程非常相似,對比著說一下:

  1. 首先通過GetService獲取到註冊的IStartupFilter集合startupFilters(類比_components)
  2. 然後獲取Startup的Configure(類比404的RequestDelegate)
  3. 翻轉startupFilters,foreach它並且與Startup的Configure鏈接在一起。
  4. 上文強調要記得最後調用 next(app),這個是不是和 next.Invoke() 類似。

  是不是感覺和圖一的翻轉拼接過程非常類似,是不是想到了拼接先後順序的問題。對比著管道構建後中間件的執行順序,體會一下後,這時應該可以想到各個IStartupFilter和Startup的Configure的執行順序了吧。沒錯就是按照依賴註入的順序:TestStartupFilter=>TestStartupFilter2=>Startup。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 在Word文檔中,對於有多條併列的信息內容或者段落時,我們常以添加項目標號的形式來使文檔條理化,在閱讀時,文檔也更具美觀性。另外,對於在邏輯上存在一定層級結構的內容時,也可以通過多級編號列表來標明文檔內容的層次,並且,在修改、編輯文檔時也增加了靈活性。因此,在本篇文檔中,將介紹如何在C#中通過使用類 ...
  • 複合索引 一、概念: 單一索引是指索引列為一列的情況,即新建索引的語句只實施在一列上; 用戶可以在多個列上建立索引,這種索引叫做複合索引(組合索引); 複合索引在資料庫操作期間所需的開銷更小,可以代替多個單一索引; 同時有兩個概念叫做窄索引和寬索引,窄索引是指索引列為1-2列的索引,寬索引也就是索引 ...
  • 一、抽象類:抽象類是特殊的類,只是不能被實例化;除此以外,具有類的其他特性;重要的是抽象類可以包括抽象方法,這是普通類所不能的。抽象方法只能聲明於抽象類中,且不包含任何實現,派生類必須覆蓋它們。另外,抽象類可以派生自一個抽象類,可以覆蓋基類的抽象方法也可以不覆蓋,如果不覆蓋,則其派生類必須覆蓋它們。 ...
  • 概述 UWP Community Toolkit 是一個 UWP App 自定義控制項、應用服務和幫助方法的集合,能夠很大程度的簡化和指引開發者的開發工作,相信廣大 UWPer 並不陌生。 下麵是截取自 GitHub 的項目概覽,可以看出這個工具包的影響力和更新頻率都是比較理想的: 開發者可以通過 V ...
  • private static string logPath = @"D:\LogS\Logs\"; public static string FloderPath { get { return logPath; } set { logPath = value; } } private static ...
  • 寫程式時一直報題中所示的錯誤,提示定義的某個靜態資源(staticResource)無法找到。百思不得其解,百度了一下才意識到時資源定義順序的問題。 App.xaml定義如下: 如上所示,定義了兩個資源字典:Templete.xaml和Style.xaml。發生錯誤的原因是Templete.xaml ...
  • 日常生活中,上班下班坐地鐵已經是常事,每當我想去某一個遠一點的地方,如果有地鐵首選就是地鐵,因為方便嘛!每次坐地鐵,我們都是憑肉眼去得出我們心中最佳的換乘方案,但是,如果對於線路較少的城市來說,這個方法是最快的,但是如果對於線路較多的城市,例如北京或者上海,十幾條線路交叉穿梭,我們可能看到都暈了,怎 ...
  • 一、定義日誌類型 public enum LogType { Debug = 0, Error = 1, Info = 2, Warn = 3 } 二、添加靜態LogHelper 類 public static class LogHelper { public static void Write(s ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...