理解 ASP.NET Core: 處理管道

理解 ASP.NET Core 處理管道

在 ASP.NET Core 的管道處理部分,實現思想已經不是傳統的面向對象模式,而是切換到了函數式編程模式。這導致程式碼的邏輯大大簡化,但是,對於熟悉面向對象編程,而不是函數式編程思路的開發者來說,是一個比較大的挑戰。

處理請求的函數

在 ASP.NET Core 中,一次請求的完整表示是通過一個 HttpContext 對象來完成的,通過其 Request 屬性可以獲取當前請求的全部資訊,通過 Response 可以獲取對響應內容進行設置。

對於一次請求的處理可以看成一個函數,函數的處理參數就是這個 HttpContext 對象,處理的結果並不是輸出結果,結果是通過 Response 來完成的,從程式調度的角度來看,函數的輸出結果是一個任務 Task。

這樣的話,具體處理 Http 請求的函數可以使用如下的 RequestDelegate 委託進行定義。

public delegate Task RequestDelegate(HttpContext context);

在函數參數 HttpContext 中則提供了此次請求的所有資訊,context 的 Request 屬性中提供了所有關於該次請求的資訊,而處理的結果則在 context 的 Response 中表示。通常我們會修改 Response 的響應頭,或者響應內容來表達處理的結果。

需要注意的是,該函數的返回結果是一個 Task,表示非同步處理,而不是真正處理的結果。

參見:在 Doc 中查看 RequestDelegate 定義

我們從 ASP.NET Core 的源程式碼中選取一段作為參考,這就是在沒有我們自定義的處理時,ASP.NET Core 最終的處理方式,返回 404。這裡使用函數式定義。

RequestDelegate app = context =>
{
	  // ......

    context.Response.StatusCode = StatusCodes.Status404NotFound;
    return Task.CompletedTask;
};

來源:在 GitHub 中查看 ApplicationBuilder 源碼

把它翻譯成熟悉的方法形式,就是下面這個樣子:

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。

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 的示例程式碼。

// 定義一個表示處理請求的委託對象
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 支援。在請求處理管道中,通常會有多個中間件串聯起來,構成處理管道。

但是,如何將多個中間件串聯起來呢?

可以考慮兩種實現方式:函數式和方法式。

方法式就是再通過另外的方法將註冊的中間件組織起來,構建一個處理管道,以後通過調用該方法來實現管道。而函數式是將整個處理管道看成一個高階函數,以後通過調用該函數來實現管道。

方法式的問題是在後繼中間件處理之前需要一個方法,後繼中間件處理之後需要一個方法,這就是為什麼 ASP.NET Web Form 有那麼多事件的原因。

如果我們只是把後繼的中間件中的處理看成一個函數,那麼,每個中間件只需要分成 3 步即可:

  • 前置處理
  • 調用後繼的中間件
  • 後置處理

在 ASP.NET Core 中是使用函數式來實現請求的處理管道的。

在函數式編程中,函數本身是可以作為一個參數來進行傳遞的。這樣可以實現高階函數。也就是說函數的組合結果還是一個函數。

對於整個處理管道,我們最終希望得到的形式還是一個 RequestDelegate,也就是一個對當前請求的 HttpContext 進行處理的函數。

本質上來講,中間件就是一個用來生成 RequestDelegate 對象的生成函數。

為了將多個管道中間件串聯起來,每個中間件需要接收下一個中間件的處理請求的函數作為參數,中間件本身返回一個處理請求的 RequestDelegate 委託對象。所以,中間件實際上是一個生成器函數。

使用 C# 的委託表示出來,就是下面的一個類型。所以,在 ASP.NET Core 中,中間件的類型就是這個 Func<T, TResult>。

Func<RequestDelegate, RequestDelegate>

在 Doc 中查看 Func<T, TResult> 的文檔

這個概念比較抽象,與我們所熟悉的面向對象編程方式完全不同,下面我們使用一個示例進行說明。

我們通過一個中間件來演示它的模擬實現程式碼。下面的程式碼定義了一個中間件,該中間件接收一個表示後繼處理的函數,中間件的返回結果是創建的另外一個 RequestDelegate 對象。它的內部通過調用下一個處理函數來完成中間件之間的級聯。

// 定義中間件
Func<RequestDelegate, RequestDelegate> middleware1 = next => {
      // 中間件返回一個 RequestDelegate 對象
    return (HttpContextSample context) => {
        // 中間件 1 的處理內容
        context.Output.AppendLine("Middleware 1 Processing.");

        // 調用後繼的處理函數
        return next(context);
    };
};

把它和我們前面定義的 app 委託結合起來如下所示,注意調用中間件的結果是返回一個新的委託函數對象,它就是我們的處理管道。

// 最終的處理函數
RequestDelegate app = context =>
{
    context.Output.AppendLine("End of output.");
    return Task.CompletedTask;
};

// 定義中間件 1
Func<RequestDelegate, RequestDelegate> 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.

繼續增加第二個中間件來演示多個中間件的級聯處理。

RequestDelegate app = context =>
{
    context.Output.AppendLine("End of output.");
    return Task.CompletedTask;
};

// 定義中間件 1
Func<RequestDelegate, RequestDelegate> middleware1 = next =>
{
    return (HttpContextSample context) =>
    {
        // 中間件 1 的處理內容
        context.Output.AppendLine("Middleware 1 Processing.");

        // 調用後繼的處理函數
        return next(context);
    };
};

// 定義中間件 2
Func<RequestDelegate, RequestDelegate> 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 變數。

List<Func<RequestDelegate, RequestDelegate>> _components
    = new List<Func<RequestDelegate, RequestDelegate>>();
_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 是後執行的。如果希望實際的執行順序與加入的順序一致,只需要將這個列表再反轉一下即可。

// 反轉此列表
_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 對象,該對象實際上是一個委託對象,代表了一個處理當前請求的處理管道函數,它就是我們所謂的處理管道,以後我們將通過該委託來處理請求。

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 程式碼如下所示:

// 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<Func<RequestDelegate, RequestDelegate>> _components = new List<Func<RequestDelegate, RequestDelegate>>();

        public ApplicationBuilder(IServiceProvider serviceProvider)
        {
            Properties = new Dictionary<string, object?>(StringComparer.Ordinal);
            ApplicationServices = serviceProvider;
        }

        public ApplicationBuilder(IServiceProvider serviceProvider, object server)
            : this(serviceProvider)
        {
            SetProperty(ServerFeaturesKey, server);
        }

        private ApplicationBuilder(ApplicationBuilder builder)
        {
            Properties = new CopyOnWriteDictionary<string, object?>(builder.Properties, StringComparer.Ordinal);
        }

        public IServiceProvider ApplicationServices
        {
            get
            {
                return GetProperty<IServiceProvider>(ApplicationServicesKey)!;
            }
            set
            {
                SetProperty<IServiceProvider>(ApplicationServicesKey, value);
            }
        }

        public IFeatureCollection ServerFeatures
        {
            get
            {
                return GetProperty<IFeatureCollection>(ServerFeaturesKey)!;
            }
        }

        public IDictionary<string, object?> Properties { get; }

        private T? GetProperty<T>(string key)
        {
            return Properties.TryGetValue(key, out var value) ? (T)value : default(T);
        }

        private void SetProperty<T>(string key, T value)
        {
            Properties[key] = value;
        }

        public IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> 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 源碼

強類型的中間件

函數形式的中間件使用比較方便,可以直接在管道定義中使用。但是,如果我們希望能夠定義獨立的中間件,使用強類型的類來定義會更加方便一些。

public interface IMiddleware {
  public System.Threading.Tasks.Task InvokeAsync (
    Microsoft.AspNetCore.Http.HttpContext context, 
    Microsoft.AspNetCore.Http.RequestDelegate next);
}

在 Doc 中查看 IMiddleware 定義

我們定義的強類型中間件可以選擇實現裝個介面。

next 表示請求處理管道中的下一個中間件,處理管道會將它提供給你定義的中間件。這是將各個中間件連接起來的關鍵。

如果當前中間件需要將請求繼續分發給後繼的中間件繼續處理,只需要調用這個委託對象即可。否則,應用程式針對該請求的處理到此為止。

例如,增加一個可以添加自定義響應頭的中間件,如下所示:

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<RequestDelegate, RequestDelegate> 呢?

在演示程式中使用該中間件。

List<Func<RequestDelegate, RequestDelegate>> _components
    = new List<Func<RequestDelegate, RequestDelegate>>();
_components.Add(middleware1);
_components.Add(middleware2);

var middleware3 = new CustomResponseHeader();
Func<RequestDelegate, RequestDelegate> middleware3 = next =>
{
    return (HttpContextSample context) =>
    {
        // 中間件 3 的處理
        var result = middleware3.InvodeAsync(context, next);
        return result;
    };
};
_components.Add(middleware3);

這樣開發者可以使用熟悉的對象方式開發中間件,而系統內部自動根據你的定義,生成出來一個 Func<RequestDelegate, RequestDelegate> 形式的中間件。

ASP.NET Core 使用該類型中間件的形式如下所示,這是提供了一個方便的擴展方法來完成這個工作。

	.UseMiddleware<CustomResponseHeader>();

按照約定定義中間件

除了實現 IMiddleware 這個介面,還可以使用約定方式來創建中間件。

按照約定定義中間件不需要實現某個預定義的介面或者繼承某個基類,而是需要遵循一些約定即可。約定主要體現在如下幾個方面:

  • 中間件需要一個公共的有效構造函數,該構造函數必須包含一個類型為 RequestDelegate 類型的參數。它代表後繼的中間件處理函數。構造函數不僅可以包含任意其它參數,對 RequestDelegate 參數出現的位置也沒有任何限制。
  • 針對請求的處理實現再返回類型為 Task 的 InvokeAsync() 方法或者同步的 Invoke() 方法中,方法的第一個參數表示當前的請求上下文 HttpContext 對象,對於其他參數,雖然約定並未進行限制,但是由於這些參數最終由依賴注入框架提供,所以,相應的服務註冊必須提供。

構造函數和 Invoke/InvokeAsync 的其他參數由依賴關係注入 (DI) 填充。

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);
    }
}

在演示程式中使用按照約定定義的中間件。

Func<RequestDelegate, RequestDelegate> middleware4 = next => {
    return (HttpContextSample context) => {
        
        var step4 = new RequestCultureMiddleware(next);
        // 中間件 4 的處理
        var result = step4.InvokeAsync (context);
        return result;
    };
};
_components.Add (middleware4);

在 ASP.NET Core 中使用按照約定定義的中間件語法與使用強類型方式相同:

	.UseMiddleware<RequestCultureMiddleware >();

中間件的順序

中間件安裝一定順尋構造成為請求處理管道,常見的處理管道如下所示:

實現 BeginRequest 和 EndRequest

理解了請求處理管道的原理,下面看它的一個應用。

在 ASP.NET 中我們可以使用預定義的 Begin_Request 和 EndRequest 處理步驟。
現在整個請求處理管道都是我們自己來進行構建了,那麼怎麼實現 Begin_Request 和 EndRequest 呢?使用中間件可以很容易實現它。
首先,這兩個步驟是請求處理的第一個和最後一個步驟,顯然,該中間件必須是第一個註冊到管道中的。
所謂的 Begin_Request 就是在調用 next() 之間的處理了,而 End_Request 就是在調用 next() 之後的處理了。在 //stackoverflow.com/questions/40604609/net-core-endrequest-middleware 中就有一個示例,我們將它修改一下,如下所示:

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

public void Configure(IApplicationBuilder app)
{
    // 第一個註冊
    app.UseMiddleware<BeginEndRequestMiddleware>();

    // Register other middelware here such as:
    app.UseMvc();
}