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) { // Middleware A app.Use(async (context, next) => { Console.WriteLine("A (in)"); await next(); Console.WriteLine("A (out)"); }); // Middleware B app.Use(async (context, next) => { Console.WriteLine("B (in)"); await next(); Console.WriteLine("B (out)"); }); // Middleware C 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)
{
// Middleware A
app.Use(async (context, next) =>
{
Console.WriteLine("A (in)");
await next();
Console.WriteLine("A (out)");
});
// Middleware B
app.Map(
new PathString("/foo"),
a => a.Use(async (context, next) =>
{
Console.WriteLine("B (in)");
await next();
Console.WriteLine("B (out)");
}));
// Middleware C
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.UseMiddleware<CustomMiddleware>();
//等價於下述呼叫方式
app.UseCustomMiddle();
// Middleware C
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標準的“管道式”設計