首先,很感謝在上篇文章 C# 管道式編程 中給我有小額捐助和點讚的朋友們,感謝大家的支持與確定。但願個人每一次分享都能讓彼此得到一些收穫,固然若是我有些地方敘述的不正確或不當,還請不客氣的指出。好了,下面進入正文。html
在開始以前,咱們須要明確的一個概念是,在 Web 程序中,用戶的每次請求流程都是線性的,放在 ASP.NET Core 程序中,都會對應一個 請求管道(request pipeline),在這個請求管道中,咱們能夠動態配置各類業務邏輯對應的 中間件(middleware),從而達到服務端能夠針對不一樣用戶作出不一樣的請求響應。在 ASP.NET Core 中,管道式編程是一個核心且基礎的概念,它的不少中間件都是經過 管道式 的方式來最終配置到請求管道中的,因此理解這裏面的管道式編程對咱們編寫更加健壯的 DotNetCore 程序至關重要。git
在上面的論述中,咱們提到了兩個很重要的概念:請求管道(request pipeline) 和 中間件(middleware)。對於它倆的關係,我我的的理解是,首先,請求管道服務於用戶,其次,請求管道能夠將多個相互獨立的業務邏輯模塊(即中間件)串聯起來,而後服務於用戶請求。這樣作的好處是能夠將業務邏輯層級化,由於在實際的業務場景中,有些業務的處理即相互獨立,又依賴於其它的業務操做,各個業務模塊之間的關係其實是動態不固定的。github
下面,咱們嘗試着來一步步解析 ASP.NET Core 中的管道機制。web
首先,咱們來看一下官方的圖例解釋:編程
從上圖中,咱們不難看出,當用戶發出一塊兒請求後,應用程序都會爲其建立一個請求管道,在這個請求管道中,每個中間件都會按順序進行處理(可能會執行,也可能不會被執行,取決於具體的業務邏輯),等最後一箇中間件處理完畢後請求又會以相反的方向返回給用戶最終的處理結果。c#
爲了驗證上述咱們的理論解釋,咱們開始建立一個 DotNetCore 的控制檯項目,而後引用以下包:api
編寫以下示例代碼:瀏覽器
class Program { static void Main(string[] args) { CreateHostBuilder(args).Build().Run(); } private static IHostBuilder CreateHostBuilder(string[] args) =gt; Host.CreateDefaultBuilder(args).ConfigureWebHostDefaults(webBuilder =gt; { webBuilder.UseStartuplt;Startupgt;(); }); } public class Startup { public void Configure(IApplicationBuilder app) { // Middleware A app.Use(async (context, next) =gt; { Console.WriteLine(quot;A (in)quot;); await next(); Console.WriteLine(quot;A (out)quot;); }); // Middleware B app.Use(async (context, next) =gt; { Console.WriteLine(quot;B (in)quot;); await next(); Console.WriteLine(quot;B (out)quot;); }); // Middleware C app.Run(async context =gt; { Console.WriteLine(quot;Cquot;); await context.Response.WriteAsync(quot;Hello World from the terminal middlewarequot;); }); } }
上述代碼段展現了一個最簡單的 ASP.NET Core Web 程序,嘗試 F5 運行咱們的程序,而後打開瀏覽器訪問 http://127.0.0.1:5000 會看到瀏覽器顯示了 Hello World from the terminal middleware 的信息。對應的控制檯信息以下圖所示:架構
上述示例程序成功驗證了咱們理論解釋中的一些設想,這說明在 Configure 函數中成功構建了一個完成的請求管道,那既然這樣,咱們就能夠將其修改成咱們以前使用管道的方式,示例代碼以下所示:app
public class Startup { public void Configure(IApplicationBuilder app) { app.Use(async (context, next) =gt; { Console.WriteLine(quot;A (int)quot;); await next(); Console.WriteLine(quot;A (out)quot;); }).Use(async (context, next) =gt; { Console.WriteLine(quot;B (int)quot;); await next(); Console.WriteLine(quot;B (out)quot;); }).Run(async context =gt; { Console.WriteLine(quot;Cquot;); await context.Response.WriteAsync(quot;Hello World from the terminal middlewarequot;); }); } }
這兩個方式都能讓咱們的請求管道正常運行,只是寫的方式不一樣。至於採用哪一種方式徹底看我的喜愛。須要注意的是,最後一個控制檯中間件須要最後註冊,由於它的處理是單向的,不涉及將用戶請求修改後返回。
一樣的,咱們也能夠對咱們的管道中間件進行條件式組裝(分叉路由),組裝條件能夠依據具體的業務場景而定,這裏我以路由爲條件進行組裝,不一樣的訪問路由最終訪問的中間件是不同的,示例代碼以下所示:
public class Startup { public void Configure(IApplicationBuilder app) { // Middleware A app.Use(async (context, next) =gt; { Console.WriteLine(quot;A (in)quot;); await next(); Console.WriteLine(quot;A (out)quot;); }); // Middleware B app.Map( new PathString(quot;/fooquot;), a =gt; a.Use(async (context, next) =gt; { Console.WriteLine(quot;B (in)quot;); await next(); Console.WriteLine(quot;B (out)quot;); })); // Middleware C app.Run(async context =gt; { Console.WriteLine(quot;Cquot;); await context.Response.WriteAsync(quot;Hello World from the terminal middlewarequot;); }); } }
當咱們直接訪問 http://127.0.0.1:5000 時,對應的請求路由輸出以下:
對應的頁面會回顯 Hello World from the terminal middleware
當咱們直接訪問 httP://127.0.0.1:5000/foo 時,對應的請求路由輸出以下:
當咱們嘗試查看對應的請求頁面,發現對應的頁面倒是 HTTP ERROR 404 ,經過上述輸出咱們能夠找到緣由,是因爲最後一個註冊的終端路由未能成功調用,致使不能返回對應的請求結果。針對這種狀況有兩種解決方法。
一種是在咱們的 路由B 中直接返回請求結果,示例代碼以下所示:
app.Map( new PathString(quot;/fooquot;), a =gt; a.Use(async (context, next) =gt; { Console.WriteLine(quot;B (in)quot;); await next(); await context.Response.WriteAsync(quot;Hello World from the middleware Bquot;); Console.WriteLine(quot;B (out)quot;); }));
這種方式不太推薦,由於它極易致使業務邏輯的不一致性,違反了 單一職責原則 的思想。
另外一種解決辦法是經過路由匹配的方式,示例代碼以下所示:
app.UseWhen( context =gt; context.Request.Path.StartsWithSegments(new PathString(quot;/fooquot;)), a =gt; a.Use(async (context, next) =gt; { Console.WriteLine(quot;B (in)quot;); await next(); Console.WriteLine(quot;B (out)quot;); }));
經過使用 UseWhen 的方式,添加了一個業務中間件對應的業務條件,在該中間件執行完畢後會自動迴歸到主的請求管道中。最終對應的日誌輸出入下圖所示:
一樣的,咱們也能夠自定義一箇中間件,示例代碼以下所示:
public class Startup { public void Configure(IApplicationBuilder app) { // app.UseMiddlewarelt;CustomMiddlewaregt;(); //等價於下述調用方式 app.UseCustomMiddle(); // Middleware C app.Run(async context =gt; { Console.WriteLine(quot;Cquot;); await context.Response.WriteAsync(quot;Hello World from the terminal middlewarequot;); }); } } public class CustomMiddleware { private readonly RequestDelegate _next; public CustomMiddleware(RequestDelegate next) { _next = next; } public async Task InvokeAsync(HttpContext httpContext) { Console.WriteLine(quot;CustomMiddleware (in)quot;); await _next.Invoke(httpContext); Console.WriteLine(quot;CustomMiddleware (out)quot;); } } public static class CustomMiddlewareExtension { public static IApplicationBuilder UseCustomMiddle(this IApplicationBuilder builder) { return builder.UseMiddlewarelt;CustomMiddlewaregt;(); } }
日誌輸出以下圖所示:
因爲 ASP.NET Core 中的自定義中間件都是經過 依賴注入(DI) 的的方式來進行實例化的。因此對應的構造函數,咱們是能夠注入咱們想要的數據類型,不光是 RequestDelegate
;其次,咱們自定義的中間件還須要實現一個公有的 public void Invoke(HttpContext httpContext)
或 public async Task InvokeAsync(HttpContext httpContext)
的方法,該方法內部主要處理咱們的自定義業務,並進行中間件的鏈接,扮演着 樞紐中心 的角色。
因爲 ASP.NET Core 是徹底開源跨平臺的,因此咱們能夠很容易的在 Github 上找到其對應的託管倉庫。最後,咱們能夠看一下 ASP.NET Core 官方的一些實現代碼。以下圖所示:
官方開源了內置中間件的所有實現代碼,這裏我以 健康檢查(HeathChecks)
中間件爲例,來驗證一下咱們上面說的自定義中間件的實現。
經過查閱源碼,咱們能夠看出,咱們上述自定義的中間件是符合官方的實現標準的。一樣的,當咱們之後使用某個內置中間件時,若是對其具體實現感興趣,能夠經過這種方式來進行查看。
當咱們對 ASP.NET Core 的請求管道進行中間件配置的時候,有一個地方須要注意一下,就是中間件的配置必定要具體的業務邏輯順序進行,好比網關配置必定要先於路由配置,結合到代碼就是下述示例:
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { //...... app.UseAuthentication(); //...... app.UseMvc(); }
若是當咱們的中間件順序配置不當的話,極有可能致使相應的業務出現問題。
就 ASP.NET Core 的技術架構而言,管道式編程只是其中很小很基礎的一部分,整個技術框架設計與實現,用到了不少優秀的技術和架構思想。可是這些高大上的實現都是基於基礎技術衍化而來的,因此,基礎很重要,只有把基礎打紮實了,纔不會被技術浪潮所淘汰。
上述全部內容就是我我的對 ASP.NET Core 中的管道式編程的一些理解和拙見,若是有不正確或不當的地方,還請斧正。
望共勉!