ASP.NET Core 中的管道机制
首先,很感謝在上篇文章 C# 管道式編程?中給我有小額捐助和點贊的朋友們,感謝你們的支持與肯定。希望我的每一次分享都能讓彼此獲得一些收獲,當然如果我有些地方敘述的不正確或不當,還請不客氣的指出。好了,下面進入正文。
前言
在開始之前,我們需要明確的一個概念是,在 Web 程序中,用戶的每次請求流程都是線性的,放在 ASP.NET Core 程序中,都會對應一個?請求管道(request pipeline),在這個請求管道中,我們可以動態配置各種業務邏輯對應的?中間件(middleware),從而達到服務端可以針對不同用戶做出不同的請求響應。在 ASP.NET Core 中,管道式編程是一個核心且基礎的概念,它的很多中間件都是通過?管道式?的方式來最終配置到請求管道中的,所以理解這里面的管道式編程對我們編寫更加健壯的 DotNetCore 程序相當重要。
剖析管道機制
在上面的論述中,我們提到了兩個很重要的概念:請求管道(request pipeline)?和?中間件(middleware)。對于它倆的關系,我個人的理解是,首先,請求管道服務于用戶,其次,請求管道可以將多個相互獨立的業務邏輯模塊(即中間件)串聯起來,然后服務于用戶請求。這樣做的好處是可以將業務邏輯層級化,因為在實際的業務場景中,有些業務的處理即相互獨立,又依賴于其它的業務操作,各個業務模塊之間的關系實際上是動態不固定的。
下面,我們嘗試著來一步步解析 ASP.NET Core 中的管道機制。
理論解釋
首先,我們來看一下官方的圖例解釋:
從上圖中,我們不難看出,當用戶發出一起請求后,應用程序都會為其創建一個請求管道,在這個請求管道中,每一個中間件都會按順序進行處理(可能會執行,也可能不會被執行,取決于具體的業務邏輯),等最后一個中間件處理完畢后請求又會以相反的方向返回給用戶最終的處理結果。
代碼闡釋
為了驗證上述我們的理論解釋,我們開始創建一個 DotNetCore 的控制臺項目,然后引用如下包:
Microsoft.AspNetCore.App
編寫如下示例代碼:
class Program{ static void Main(string[] args) { CreateHostBuilder(args).Build().Run(); } private static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args).ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); });}public class Startup{ public void Configure(IApplicationBuilder app) { app.Use(async (context, next) => { Console.WriteLine("A (in)"); await next(); Console.WriteLine("A (out)"); }); app.Use(async (context, next) => { Console.WriteLine("B (in)"); await next(); Console.WriteLine("B (out)"); }); app.Run(async context => { Console.WriteLine("C"); await context.Response.WriteAsync("Hello World from the terminal middleware"); }); }}上述代碼段展示了一個最簡單的 ASP.NET Core Web 程序,嘗試 F5 運行我們的程序,然后打開瀏覽器訪問 http://127.0.0.1:5000?會看到瀏覽器顯示了 Hello World from the terminal middleware 的信息。對應的控制臺信息如下圖所示:
上述示例程序成功驗證了我們理論解釋中的一些設想,這說明在 Configure 函數中成功構建了一個完成的請求管道,那既然這樣,我們就可以將其修改為我們之前使用管道的方式,示例代碼如下所示:
public class Startup{ public void Configure(IApplicationBuilder app) { app.Use(async (context, next) => { Console.WriteLine("A (int)"); await next(); Console.WriteLine("A (out)"); }).Use(async (context, next) => { Console.WriteLine("B (int)"); await next(); Console.WriteLine("B (out)"); }).Run(async context => { Console.WriteLine("C"); await context.Response.WriteAsync("Hello World from the terminal middleware"); }); }}這兩個方式都能讓我們的請求管道正常運行,只是寫的方式不同。至于采用哪種方式完全看個人喜好。需要注意的是,最后一個控制臺中間件需要最后注冊,因為它的處理是單向的,不涉及將用戶請求修改后返回。
同樣的,我們也可以對我們的管道中間件進行條件式組裝(分叉路由),組裝條件可以依據具體的業務場景而定,這里我以路由為條件進行組裝,不同的訪問路由最終訪問的中間件是不一樣的,示例代碼如下所示:
public class Startup{ public void Configure(IApplicationBuilder app) { app.Use(async (context, next) => { Console.WriteLine("A (in)"); await next(); Console.WriteLine("A (out)"); }); app.Map( new PathString("/foo"), a => a.Use(async (context, next) => { Console.WriteLine("B (in)"); await next(); Console.WriteLine("B (out)"); })); app.Run(async context => { Console.WriteLine("C"); await context.Response.WriteAsync("Hello World from the terminal middleware"); }); }}當我們直接訪問 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("/foo"), a => a.Use(async (context, next) => { Console.WriteLine("B (in)"); await next(); await context.Response.WriteAsync("Hello World from the middleware B"); Console.WriteLine("B (out)"); }));這種方式不太推薦,因為它極易導致業務邏輯的不一致性,違反了?單一職責原則?的思想。
另一種解決辦法是通過路由匹配的方式,示例代碼如下所示:
app.UseWhen( context => context.Request.Path.StartsWithSegments(new PathString("/foo")), a => a.Use(async (context, next) => { Console.WriteLine("B (in)"); await next(); Console.WriteLine("B (out)"); }));通過使用?UseWhen?的方式,添加了一個業務中間件對應的業務條件,在該中間件執行完畢后會自動回歸到主的請求管道中。最終對應的日志輸出入下圖所示:
同樣的,我們也可以自定義一個中間件,示例代碼如下所示:
public class Startup{ public void Configure(IApplicationBuilder app) { app.UseCustomMiddle(); app.Run(async context => { Console.WriteLine("C"); await context.Response.WriteAsync("Hello World from the terminal middleware"); }); }}public class CustomMiddleware{ private readonly RequestDelegate _next; public CustomMiddleware(RequestDelegate next) { _next = next; } public async Task InvokeAsync(HttpContext httpContext) { Console.WriteLine("CustomMiddleware (in)"); await _next.Invoke(httpContext); Console.WriteLine("CustomMiddleware (out)"); }}public static class CustomMiddlewareExtension{ public static IApplicationBuilder UseCustomMiddle(this IApplicationBuilder builder) { return builder.UseMiddleware<CustomMiddleware>(); }}日志輸出如下圖所示:
由于 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 中的管道式編程的一些理解和拙見,如果有不正確或不當的地方,還請斧正。
望共勉!
相關參考
ASP.NET Core Middleware
UNDERSTANDING THE ASP.NET CORE MIDDLEWARE PIPELINE
ASP.NET Web API標準的“管道式”設計
原文:https://www.cnblogs.com/hippieZhou/p/11205573.html
.NET社區新聞,深度好文,歡迎訪問公眾號文章匯總?http://www.csharpkit.com?
總結
以上是生活随笔為你收集整理的ASP.NET Core 中的管道机制的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: gRPC in ASP.NET Core
- 下一篇: [译].Net中的内存-什么分配在了哪里