asp.net core之中介軟體

2023-07-26 18:01:10

中介軟體介紹

在asp.net core中,中介軟體中介軟體是一種裝配到應用管道以處理請求和響應的軟體。
每個元件:

  • 選擇是否將請求傳遞到管道中的下一個元件。
  • 可在管道中的下一個元件前後執行工作。

請求委託用於生成請求管道。 請求委託處理每個 HTTP 請求。
ASP.NET Core 請求管道包含一系列請求委託,依次呼叫。每個委託均可在下一個委託前後執行操作。 應儘早在管道中呼叫例外處理委託,這樣它們就能捕獲在管道的後期階段發生的異常。 如下圖所示:

編寫中介軟體

在asp.net core中已經內建了挺多的中介軟體,包括身份驗證,授權等等,詳細的可以看官方檔案內建中介軟體列表。
接下來主要講一下如何編寫我們自己的中介軟體,在前面的文章中我們也用到了自己寫的中介軟體,用的是最簡單的app.Use的方式。
Use 擴充套件可以使用兩個過載:

  • 一個過載採用 HttpContext 和 Func。 不使用任何引數呼叫 Func
  • 另一個過載採用 HttpContext 和 RequestDelegate。 通過傳遞 HttpContext 呼叫 RequestDelegate。

優先使用後面的過載,因為它省去了使用其他過載時所需的兩個內部每請求分配。

app.Use(async (context, next) =>
{
    // 下游中介軟體執行前
    await next.Invoke(); //往下執行中介軟體
    // 下游中介軟體執行後
});

上面寫法就是一個最簡單的沒有任何操作的中介軟體。
在呼叫await next.Invoke()前我們寫的操作就是在下游中介軟體執行之前做的事情,對應的,在之後寫的操作則是在下游中介軟體響應後做的事情。
舉個例子,當我們要在下游中介軟體執行之前,做一些引數的賦值,如我想在Headers中新增一個頭部,

app.Use(async (context, next) =>
{
    context.Request.Headers.Add("TestMiddlewareAdd", "Abc");
    await next.Invoke();
});

在新增之後,下游就可以獲取Headers中TestMiddlewareAdd的值。
我們來實操一下,建立一個WebApi專案,然後在Program中MapControllers()之前新增上述中介軟體。

可以看到,Headers中已經加上了我們之前加的內容。
對應的,如果寫在await next.Invoke()後面,則是不生效的,這個可以自行測試。那麼在await next.Invoke()後面我們可以做一些什麼操作呢?比如記錄請求響應完成後的內容,或對相應內容做進一步的處理等等,根據我們的實際需要去寫。

除了app.Use(),在asp.net core中還有幾種中介軟體的編寫方式。

app.Map();
app.MpaWhen();
app.Run();
app.UseMiddleware();

Map擴充套件用作約定來建立管道分支。 Map 基於給定請求路徑的匹配項來建立請求管道分支。 如果請求路徑以給定路徑開頭,則執行分支。
MapWhen基於給定謂詞的結果建立請求管道分支。 Func<HttpContext, bool> 型別的任何謂詞均可用於將請求對映到管道的新分支。
Run 委託不會收到 next 引數。 第一個 Run 委託始終為終端,用於終止管道。 Run 是一種約定。 某些中介軟體元件可能會公開在管道末尾執行的 Run[Middleware] 方法:

UseMiddleware

UseMiddleware是我們最常用的封裝中介軟體的方式,中介軟體類是基於約定編寫的。其約定如下:

  • 具有型別為 RequestDelegate 的引數的公共建構函式。
  • 名為 Invoke 或 InvokeAsync 的公共方法。 此方法必須:
    • 返回 Task。
    • 接受型別 HttpContext 的第一個引數。

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

接下來我們來實操基於約定編寫一個Middleware類

    public class AMiddleware
    {
        private readonly RequestDelegate _next;

        public AMiddleware(RequestDelegate next)
            => _next = next;

        public async Task InvokeAsync(HttpContext context, ILogger<AMiddleware> logger)
        {
            logger.LogInformation("AMiddleware Invoke");

            await _next(context);
        }
    }

在Program使用UseMiddleware把中介軟體加入管道

app.UseAuthorization();
app.Use(async (context, next) =>
{
    context.Request.Headers.Add("TestMiddlewareAdd", "Abc");
    await next.Invoke();
});
app.UseMiddleware<AMiddleware>();
app.MapControllers();

app.Run();

啟動專案發出請求。可以看到下圖結果:

需要注意的是,這裡的Middleware會自動註冊為一個單例,所以在構造器注入時,無法注入Scope生命週期的服務。
如果注入,啟動會直接報錯

public class AMiddleware
{
    private readonly RequestDelegate _next;
    private readonly TestMiddlewareDi _testMiddlewareDi;

    public AMiddleware(RequestDelegate next, TestMiddlewareDi testMiddlewareDi)
    {
        _next = next;
        _testMiddlewareDi = testMiddlewareDi;
    }

    public async Task InvokeAsync(HttpContext context, ILogger<AMiddleware> logger)
    {
        logger.LogInformation("AMiddleware Invoke");
        logger.LogInformation($"AMiddleware _testMiddlewareDi: {_testMiddlewareDi.Id}");

        await _next(context);
    }
}
builder.Services.AddScoped<TestMiddlewareDi>();


當我們需要注入Scope生命週期的服務時,直接在InvokeAsync方法中新增注入。

public class AMiddleware
{
    private readonly RequestDelegate _next;

    public AMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context, ILogger<AMiddleware> logger, TestMiddlewareDi testMiddleware)
    {
        logger.LogInformation("AMiddleware Invoke");
        logger.LogInformation($"AMiddleware _testMiddlewareDi: {testMiddleware.Id}");

        await _next(context);
    }
}

執行結果可以看到,正常執行,並且每次請求Id都是不一樣的。

IMiddleware

除了基於約定實現中介軟體,asp.net core還有一個基於工廠的中介軟體啟用擴充套件。
IMiddlewareFactory/IMiddleware 是中介軟體啟用的擴充套件點,具有以下優勢:

  • 按使用者端請求(作用域服務的注入)啟用
  • 讓中介軟體強型別化

UseMiddleware 擴充套件方法檢查中介軟體的已註冊型別是否實現 IMiddleware。 如果是,則使用在容器中註冊的 IMiddlewareFactory 範例來解析 IMiddleware 實現,而不使用基於約定的中介軟體啟用邏輯。 中介軟體在應用的服務容器中註冊為作用域或瞬態服務。
接下來我們來實現一個IMiddleware

public class FactoryMiddleware : IMiddleware
{
    private readonly ILogger _logger;
    private readonly TestMiddlewareDi _testMiddleware;

    public FactoryMiddleware(ILogger<FactoryMiddleware> logger, TestMiddlewareDi testMiddleware)
    {
        _logger = logger;
        _testMiddleware = testMiddleware;
    }

    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        _logger.LogInformation("FactoryMiddleware Invoke");
        _logger.LogInformation($"FactoryMiddleware _testMiddlewareDi: {_testMiddleware.Id}");
        await next(context);
    }
}
app.UseAuthorization();
app.Use(async (context, next) =>
{
    context.Request.Headers.Add("TestMiddlewareAdd", "Abc");
    await next.Invoke();
});
app.UseMiddleware<AMiddleware>();
app.UseMiddleware<FactoryMiddleware>();
app.MapControllers();

app.Run();

需要注意的是,這種方式必須把中介軟體註冊到依賴注入容器中,否則會出現以下錯誤:

註冊注入之後,我們再次啟動服務,並測試請求。

builder.Services.AddScoped<FactoryMiddleware>();


一切順利執行。

基於約定的中介軟體和基於工廠的中介軟體區別

基於約定的中介軟體無法通過建構函式注入Scope生命週期的服務,只能通過Invoke方法的引數進行注入。
基於工廠的中介軟體只能通過建構函式新增注入,Invoke無法注入(因為是基於IMiddleware介面的實現)。

基於約定的中介軟體無需手動註冊進依賴注入容器。
基於工廠的中介軟體必須註冊進依賴注入容器,且生命週期註冊為作用域或瞬態服務。

基於約定的中介軟體生命週期為單例
基於工廠的中介軟體生命週期為作用域

中介軟體順序

中間既然是一種管道的模式,那麼必然和順序有關係,管道前面的中介軟體先執行,後面的中介軟體後執行。
那麼這個順序會帶來哪種影響呢?
這裡盜官方檔案圖,下圖顯示了 ASP.NET Core MVC 和 Razor Pages 應用的完整請求處理管道。

這裡UseCors 和 UseStaticFiles 順序是最容易看出影響的。
若是UseStaticFiles在UseCors之前呼叫,則檢索靜態檔案時,不會檢查是否跨站點呼叫。所有靜態檔案可以直接檢索。
若是相反,則在跨站檢索靜態檔案時,則會優先檢查站點是否跨域,若是跨域則無法檢索靜態檔案。

由此我們可以想到,當我們需要做一些前置校驗的中介軟體時,可以把中介軟體順序放在前面,校驗不通過直接終止後續請求,可以提高應用的響應效率。

歡迎進群催更。