1. 程式人生 > >理解 ASP.NET Core: 處理管道

理解 ASP.NET Core: 處理管道

# 理解 ASP.NET Core 處理管道 在 ASP.NET Core 的管道處理部分,實現思想已經不是傳統的面向物件模式,而是切換到了函數語言程式設計模式。這導致程式碼的邏輯大大簡化,但是,對於熟悉面向物件程式設計,而不是函數語言程式設計思路的開發者來說,是一個比較大的挑戰。 ## 處理請求的函式 在 ASP.NET Core 中,一次請求的完整表示是通過一個 HttpContext 物件來完成的,通過其 Request 屬性可以獲取當前請求的全部資訊,通過 Response 可以獲取對響應內容進行設定。 對於一次請求的處理可以看成一個函式,函式的處理引數就是這個 HttpContext 物件,處理的結果並不是輸出結果,結果是通過 Response 來完成的,從程式排程的角度來看,函式的輸出結果是一個任務 Task。 這樣的話,具體處理 Http 請求的函式可以使用如下的 RequestDelegate 委託進行定義。 ```csharp public delegate Task RequestDelegate(HttpContext context); ``` 在函式引數 HttpContext 中則提供了此次請求的所有資訊,context 的 Request 屬性中提供了所有關於該次請求的資訊,而處理的結果則在 context 的 Response 中表示。通常我們會修改 Response 的響應頭,或者響應內容來表達處理的結果。 需要注意的是,該函式的返回結果是一個 Task,表示非同步處理,而不是真正處理的結果。 參見:[在 Doc 中檢視 RequestDelegate 定義](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.requestdelegate?view=aspnetcore-3.1) 我們從 ASP.NET Core 的原始碼中選取一段作為參考,這就是在沒有我們自定義的處理時,ASP.NET Core 最終的處理方式,返回 404。這裡使用函式式定義。 ```csharp RequestDelegate app = context => { // ...... context.Response.StatusCode = StatusCodes.Status404NotFound; return Task.CompletedTask; }; ``` 來源:[在 GitHub 中檢視 ApplicationBuilder 原始碼](https://github.com/dotnet/aspnetcore/blob/master/src/Http/Http/src/Builder/ApplicationBuilder.cs) 把它翻譯成熟悉的方法形式,就是下面這個樣子: ```csharp public Task app(HttpContext context) { // ...... context.Response.StatusCode = StatusCodes.Status404NotFound; return Task.CompletedTask; }; ``` 這段程式碼只是設定了 Http 的響應狀態碼為 404,並直接返回了一個已經完成的任務物件。 為了脫離 ASP.NET Core 複雜的環境,可以簡單地進行後繼的演示,我們自定義一個模擬 HttpContext 的型別 HttpContextSample 和相應的 RequestDelegate 委託型別。 在模擬請求的 HttpContextSample 中,我們內部定義了一個 StringBuilder 來儲存處理的結果,以便進行檢查。其中的 Output 用來模擬 Response 來處理輸出。 而 RequestDelegate 則需要支援現在的 HttpContextSample。 ```csharp using System.Threading.Tasks; using System.Text; public class HttpContextSample { public StringBuilder Output { get; set; } public HttpContextSample() { Output = new StringBuilder(); } } public delegate Task RequestDelegate(HttpContextSample context); ``` 這樣,我們可以定義一個基礎的,使用 RequestDelegate 的示例程式碼。 ```csharp // 定義一個表示處理請求的委託物件 RequestDelegate app = context => { context.Output.AppendLine("End of output."); return Task.CompletedTask; }; // 建立模擬當前請求的物件 var context1 = new HttpContextSample(); // 處理請求 app(context1); // 輸出請求的處理結果 Console.WriteLine(context1.Output.ToString()); ``` 執行之後,可以得到如下的輸出 > End of output. ### 處理管道中介軟體 所謂的處理管道是使用多箇中間件串聯起來實現的。每個中介軟體當然需要提供處理請求的 RequestDelegate 支援。在請求處理管道中,通常會有多箇中間件串聯起來,構成處理管道。 ![](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/index/_static/request-delegate-pipeline.png?view=aspnetcore-3.1) 但是,如何將多箇中間件串聯起來呢? 可以考慮兩種實現方式:函式式和方法式。 方法式就是再通過另外的方法將註冊的中介軟體組織起來,構建一個處理管道,以後通過呼叫該方法來實現管道。而函式式是將整個處理管道看成一個高階函式,以後通過呼叫該函式來實現管道。 方法式的問題是在後繼中介軟體處理之前需要一個方法,後繼中介軟體處理之後需要一個方法,這就是為什麼 ASP.NET Web Form 有那麼多事件的原因。 如果我們只是把後繼的中介軟體中的處理看成一個函式,那麼,每個中介軟體只需要分成 3 步即可: * 前置處理 * 呼叫後繼的中介軟體 * 後置處理 在 ASP.NET Core 中是使用函式式來實現請求的處理管道的。 在函數語言程式設計中,函式本身是可以作為一個引數來進行傳遞的。這樣可以實現高階函式。也就是說函式的組合結果還是一個函式。 對於整個處理管道,我們最終希望得到的形式還是一個 RequestDelegate,也就是一個對當前請求的 HttpContext 進行處理的函式。 本質上來講,中介軟體就是一個用來生成 RequestDelegate 物件的生成函式。 為了將多個管道中介軟體串聯起來,每個中介軟體需要接收下一個中介軟體的處理請求的函式作為引數,中介軟體本身返回一個處理請求的 RequestDelegate 委託物件。所以,中介軟體實際上是一個生成器函式。 使用 C# 的委託表示出來,就是下面的一個型別。所以,在 ASP.NET Core 中,中介軟體的型別就是這個 Func。 ```csharp Func ``` [在 Doc 中檢視 Func 的文件](https://docs.microsoft.com/zh-cn/dotnet/api/system.func-2?view=net-5.0) 這個概念比較抽象,與我們所熟悉的面向物件程式設計方式完全不同,下面我們使用一個示例進行說明。 我們通過一箇中間件來演示它的模擬實現程式碼。下面的程式碼定義了一箇中間件,該中介軟體接收一個表示後繼處理的函式,中介軟體的返回結果是建立的另外一個 RequestDelegate 物件。它的內部通過呼叫下一個處理函式來完成中介軟體之間的級聯。 ```csharp // 定義中介軟體 Func middleware1 = next => { // 中介軟體返回一個 RequestDelegate 物件 return (HttpContextSample context) => { // 中介軟體 1 的處理內容 context.Output.AppendLine("Middleware 1 Processing."); // 呼叫後繼的處理函式 return next(context); }; }; ``` 把它和我們前面定義的 app 委託結合起來如下所示,注意呼叫中介軟體的結果是返回一個新的委託函式物件,它就是我們的處理管道。 ```csharp // 最終的處理函式 RequestDelegate app = context => { context.Output.AppendLine("End of output."); return Task.CompletedTask; }; // 定義中介軟體 1 Func middleware1 = next => { return (HttpContextSample context) => { // 中介軟體 1 的處理內容 context.Output.AppendLine("Middleware 1 Processing."); // 呼叫後繼的處理函式 return next(context); }; }; // 得到一個有一個處理步驟的管道 var pipeline1 = middleware1(app); // 準備一個表示當前請求的物件 var context2 = new HttpContextSample(); // 通過管道處理當前請求 pipeline1(context2); // 輸出請求的處理結果 Console.WriteLine(context2.Output.ToString()); ``` 可以得到如下的輸出 > Middleware 1 Processing. > End of output. 繼續增加第二個中介軟體來演示多箇中間件的級聯處理。 ```csharp RequestDelegate app = context => { context.Output.AppendLine("End of output."); return Task.CompletedTask; }; // 定義中介軟體 1 Func middleware1 = next => { return (HttpContextSample context) => { // 中介軟體 1 的處理內容 context.Output.AppendLine("Middleware 1 Processing."); // 呼叫後繼的處理函式 return next(context); }; }; // 定義中介軟體 2 Func middleware2 = next => { return (HttpContextSample context) => { // 中介軟體 2 的處理 context.Output.AppendLine("Middleware 2 Processing."); // 呼叫後繼的處理函式 return next(context); }; }; // 構建處理管道 var step1 = middleware1(app); var pipeline2 = middleware2(step1); // 準備當前的請求物件 var context3 = new HttpContextSample(); // 處理請求 pipeline2(context3); // 輸出處理結果 Console.WriteLine(context3.Output.ToString()); ``` 當前的輸出 > Middleware 2 Processing. > Middleware 1 Processing. > End of output. 如果我們把這些中介軟體儲存到幾個列表中,就可以通過迴圈來構建處理管道。下面的示例重複使用了前面定義的 app 變數。 ```csharp List> _components = new List>(); _components.Add(middleware1); _components.Add(middleware2); // 構建處理管道 foreach (var component in _components) { app = component(app); } // 構建請求上下文物件 var context4 = new HttpContextSample(); // 使用處理管道處理請求 app(context4); // 輸出處理結果 Console.WriteLine(context4.Output.ToString()); ``` 輸出結果與上一示例完全相同 > Middleware 2 Processing. > Middleware 1 Processing. > End of output. 但是,有一個問題,我們後加入到列表中的中介軟體 2 是先執行的,而先加入到列表中的中介軟體 1 是後執行的。如果希望實際的執行順序與加入的順序一致,只需要將這個列表再反轉一下即可。 ```csharp // 反轉此列表 _components.Reverse(); foreach (var component in _components) { app = component(app); } var context5 = new HttpContextSample(); app(context5); Console.WriteLine(context5.Output.ToString()); ``` 輸出結果如下 > Middleware 1 Processing. > Middleware 2 Processing. > End of output. 現在,我們可以回到實際的 ASP.NET Core 程式碼中,把 ASP.NET Core 中 ApplicationBuilder 的核心程式碼 Build() 方法抽象之後,可以得到如下的關鍵程式碼。 注意 Build() 方法就是構建我們的請求處理管道,它返回了一個 RequestDelegate 物件,該物件實際上是一個委託物件,代表了一個處理當前請求的處理管道函式,它就是我們所謂的處理管道,以後我們將通過該委託來處理請求。 ```csharp public RequestDelegate Build() { RequestDelegate app = context => { // ...... context.Response.StatusCode = StatusCodes.Status404NotFound; return Task.CompletedTask; }; foreach (var component in _components.Reverse()) { app = component(app); } return app; } ``` 完整的 ApplicationBuilder 程式碼如下所示: ```csharp // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.Internal; namespace Microsoft.AspNetCore.Builder { public class ApplicationBuilder : IApplicationBuilder { private const string ServerFeaturesKey = "server.Features"; private const string ApplicationServicesKey = "application.Services"; private readonly IList> _components = new List>(); public ApplicationBuilder(IServiceProvider serviceProvider) { Properties = new Dictionary(StringComparer.Ordinal); ApplicationServices = serviceProvider; } public ApplicationBuilder(IServiceProvider serviceProvider, object server) : this(serviceProvider) { SetProperty(ServerFeaturesKey, server); } private ApplicationBuilder(ApplicationBuilder builder) { Properties = new CopyOnWriteDictionary(builder.Properties, StringComparer.Ordinal); } public IServiceProvider ApplicationServices { get { return GetProperty(ApplicationServicesKey)!; } set { SetProperty(ApplicationServicesKey, value); } } public IFeatureCollection ServerFeatures { get { return GetProperty(ServerFeaturesKey)!; } } public IDictionary Properties { get; } private T? GetProperty(string key) { return Properties.TryGetValue(key, out var value) ? (T)value : default(T); } private void SetProperty(string key, T value) { Properties[key] = value; } public IApplicationBuilder Use(Func middleware) { _components.Add(middleware); return this; } public IApplicationBuilder New() { return new ApplicationBuilder(this); } public RequestDelegate Build() { RequestDelegate app = context => { // If we reach the end of the pipeline, but we have an endpoint, then something unexpected has happened. // This could happen if user code sets an endpoint, but they forgot to add the UseEndpoint middleware. var endpoint = context.GetEndpoint(); var endpointRequestDelegate = endpoint?.RequestDelegate; if (endpointRequestDelegate != null) { var message = $"The request reached the end of the pipeline without executing the endpoint: '{endpoint!.DisplayName}'. " + $"Please register the EndpointMiddleware using '{nameof(IApplicationBuilder)}.UseEndpoints(...)' if using " + $"routing."; throw new InvalidOperationException(message); } context.Response.StatusCode = StatusCodes.Status404NotFound; return Task.CompletedTask; }; foreach (var component in _components.Reverse()) { app = component(app); } return app; } } } ``` 見:[在 GitHub 中檢視 ApplicationBuilder 原始碼](https://github.com/dotnet/aspnetcore/blob/master/src/Http/Http/src/Builder/ApplicationBuilder.cs) ### 強型別的中介軟體 函式形式的中介軟體使用比較方便,可以直接在管道定義中使用。但是,如果我們希望能夠定義獨立的中介軟體,使用強型別的類來定義會更加方便一些。 ```csharp public interface IMiddleware { public System.Threading.Tasks.Task InvokeAsync ( Microsoft.AspNetCore.Http.HttpContext context, Microsoft.AspNetCore.Http.RequestDelegate next); } ``` [在 Doc 中檢視 IMiddleware 定義](https://docs.microsoft.com/zh-cn/dotnet/api/microsoft.aspnetcore.http.imiddleware?view=aspnetcore-3.1) 我們定義的強型別中介軟體可以選擇實現裝個介面。 next 表示請求處理管道中的下一個中介軟體,處理管道會將它提供給你定義的中介軟體。這是將各個中介軟體連線起來的關鍵。 如果當前中介軟體需要將請求繼續分發給後繼的中介軟體繼續處理,只需要呼叫這個委託物件即可。否則,應用程式針對該請求的處理到此為止。 例如,增加一個可以新增自定義響應頭的中介軟體,如下所示: ```csharp using System.Threading.Tasks; public class CustomResponseHeader: IMiddleware { // 使用建構函式完成服務依賴的定義 public CustomResponseHeader() { } public Task InvodeAsync(HttpContextSample context, RequestDelegate next) { context.Output.AppendLine("From Custom Middleware."); return next(context); } } ``` 這更好看懂了,可是它怎麼變成那個 Func\ 呢? 在演示程式中使用該中介軟體。 ```csharp List> _components = new List>(); _components.Add(middleware1); _components.Add(middleware2); var middleware3 = new CustomResponseHeader(); Func middleware3 = next => { return (HttpContextSample context) => { // 中介軟體 3 的處理 var result = middleware3.InvodeAsync(context, next); return result; }; }; _components.Add(middleware3); ``` 這樣開發者可以使用熟悉的物件方式開發中間件,而系統內部自動根據你的定義,生成出來一個 Func\ 形式的中介軟體。 ASP.NET Core 使用該型別中介軟體的形式如下所示,這是提供了一個方便的擴充套件方法來完成這個工作。 ```csharp .UseMiddleware(); ``` ### 按照約定定義中介軟體 除了實現 IMiddleware 這個介面,還可以使用約定方式來建立中介軟體。 按照約定定義中介軟體不需要實現某個預定義的介面或者繼承某個基類,而是需要遵循一些約定即可。約定主要體現在如下幾個方面: * 中介軟體需要一個公共的有效建構函式,該建構函式必須包含一個型別為 RequestDelegate 型別的引數。它代表後繼的中介軟體處理函式。建構函式不僅可以包含任意其它引數,對 RequestDelegate 引數出現的位置也沒有任何限制。 * 針對請求的處理實現再返回型別為 Task 的 InvokeAsync() 方法或者同步的 Invoke() 方法中,方法的第一個引數表示當前的請求上下文 HttpContext 物件,對於其他引數,雖然約定並未進行限制,但是由於這些引數最終由依賴注入框架提供,所以,相應的服務註冊必須提供。 建構函式和 Invoke/InvokeAsync 的其他引數由依賴關係注入 (DI) 填充。 ```csharp using System.Threading.Tasks; public class RequestCultureMiddleware { private readonly RequestDelegate _next; public RequestCultureMiddleware (RequestDelegate next) { _next = next; } public async Task InvokeAsync (HttpContextSample context) { context.Output.AppendLine("Middleware 4 Processing."); // Call the next delegate/middleware in the pipeline await _next (context); } } ``` 在演示程式中使用按照約定定義的中介軟體。 ```csharp Func middleware4 = next => { return (HttpContextSample context) => { var step4 = new RequestCultureMiddleware(next); // 中介軟體 4 的處理 var result = step4.InvokeAsync (context); return result; }; }; _components.Add (middleware4); ``` 在 ASP.NET Core 中使用按照約定定義的中介軟體語法與使用強型別方式相同: ```csharp .UseMiddleware(); ``` ### 中介軟體的順序 中介軟體安裝一定順尋構造成為請求處理管道,常見的處理管道如下所示: ![](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/index/_static/middleware-pipeline.svg?view=aspnetcore-3.1) ### 實現 BeginRequest 和 EndRequest 理解了請求處理管道的原理,下面看它的一個應用。 在 ASP.NET 中我們可以使用預定義的 Begin_Request 和 EndRequest 處理步驟。 現在整個請求處理管道都是我們自己來進行構建了,那麼怎麼實現 Begin_Request 和 EndRequest 呢?使用中介軟體可以很容易實現它。 首先,這兩個步驟是請求處理的第一個和最後一個步驟,顯然,該中介軟體必須是第一個註冊到管道中的。 所謂的 Begin_Request 就是在呼叫 next() 之間的處理了,而 End_Request 就是在呼叫 next() 之後的處理了。在 https://stackoverflow.com/questions/40604609/net-core-endrequest-middleware 中就有一個示例,我們將它修改一下,如下所示: ```csharp public class BeginEndRequestMiddleware { private readonly RequestDelegate _next; public BeginEndRequestMiddleware(RequestDelegate next) { _next = next; } public void Begin_Request(HttpContext context) { // do begin request } public void End_Request(HttpContext context) { // do end request } public async Task Invoke(HttpContext context) { // Do tasks before other middleware here, aka 'BeginRequest' Begin_Request(context); // Let the middleware pipeline run await _next(context); // Do tasks after middleware here, aka 'EndRequest' End_Request(); } } ``` Register ```csharp public void Configure(IApplicationBuilder app) { // 第一個註冊 app.UseMiddleware(); // Register other middelware here such as: app.UseMvc()