客製化.NET 6.0的Middleware中介軟體

2022-12-19 12:01:12

大家好,我是張飛洪,感謝您的閱讀,我會不定期和你分享學習心得,希望我的文章能成為你成長路上的墊腳石,讓我們一起精進。

在本文中,我們將學習中介軟體,以及如何使用它進一步客製化應用程式。我們將快速學習中介軟體的基礎知識,然後探討如何使用它做的一些特殊事情。
本文涵蓋的主題包括:

  • 中介軟體簡介
  • 編寫自定義中介軟體
  • 中介軟體的潛力
  • 如何使用中介軟體

本章所處的位置,如下圖所示:

技術準備

我們使用控制檯、shell或Bash終端先建立一個ASP.NET Core MVC應用程式,然後切換到工作目錄:

dotnet new web -n MiddlewaresDemo -o MiddlewaresDemo

然後用VS開啟專案:

cd MiddlewaresDemo code .

注意在.NET 6.0中,web專案模板發生了變化。Microsoft引入了minimal API,專案模板預設使用minimal API。

中介軟體簡介

大多數人可能已經知道中介軟體是什麼,但有些人可能不知道,即使你已經在使用ASP.NET Core有一段時間了。我們一般不需要詳細瞭解中介軟體範例,因為它們大多隱藏在擴充套件方法後面,例如UseMvc()、UseAuthentication()、UseDeveloperExceptionPage()等。每次在Configure方法中,我們預設將隱式地使用至少一個或更多箇中介軟體元件。

中介軟體元件是處理請求管道的一段程式碼。我們可以將請求流程想象成一串管道,每次請求呼叫,都會返回一個響應。中介軟體負責建立回聲——它操縱請求上下文,加工處理、疊加邏輯、豐富資訊。

中介軟體元件按設定順序執行。設定的第一個中介軟體元件是第一個執行的元件。我們可以把中介軟體看成迴旋鏢,出去的時候第一個執行,回來的時候最後一個執行。

在ASP.NET Core web應用程式,如果使用者端請求的是影象或任何其他靜態檔案,StaticFileMiddleware將負責查詢該資源,如果找到該資源,則返回該資源。如果沒有,這個中介軟體除了呼叫下一個之外什麼都不做。

MvcMiddleware元件檢查請求的資源,將其對映到已設定的路由,執行控制器,建立檢視,並返回HTML或Web API結果。如果MvcMiddleware沒有找到匹配的控制器,它無論如何都會返回一個結果——通常是一個404狀態的結果,這就是為什麼MvcMiddleware是最後設定的中介軟體。

例外處理中介軟體通常是設定的第一批的中介軟體之一,不是因為它是第一個執行的,而是因為它是最後一個執行的。例外處理驗證結果,並以使用者端友好的方式在瀏覽器中顯示可能的異常。以下過程描述了執行時發生的500錯誤狀態:

var builder = WebApplication.CreateBuilder(args); 
var app = builder.Build(); 
app.MapGet("/", () => "Hello World!"); 
app.Run();

在ASP.NET Core 6.0,Microsoft引入了minimal API,它簡化了應用設定,並隱藏了許多預設設定,比如隱式的using宣告,因此,在頭部我們看不到任何using語句,以上就是我們看到的ASP.NET Core 6.0中的Program.cs 檔案內容。
在這裡,lambda中介軟體系結到預設路由,只有一句簡單的「Hello World!」響應流。這個特殊的中介軟體會終止管道並返回響應內容。因此,它是最後一個執行的中介軟體。

下面我們把app.MapGet()做個替換,如下所示:

app.Use(async (context, next) =>{     
    await context.Response.WriteAsync("===");     
    await next();     
    await context.Response.WriteAsync("==="); 
}); 
app.Use(async (context, next) => { 
    await context.Response.WriteAsync(">>>>>> ");     
    await next();     
    await context.Response.WriteAsync(" <<<<<<");
}); 
app.Run(async context => { 
    await context.Response.WriteAsync("Hello World!"); 
});

這裡呼叫兩個app.Use()方法,並且建立了兩個lambda中介軟體,除了做簡單的處理外,中介軟體還呼叫了它們的後繼元件,每個中介軟體的呼叫鏈很明確很清晰。在呼叫下一個中介軟體之前,處理實際的請求,在呼叫下箇中介軟體之後,處理響應。以上就是管道的工作機制。
如果現在執行程式(使用dotnet run)並在瀏覽器中開啟URL,我們應該會看到這樣的純文字結果

===>>>>>> Hello World! <<<<<<===

不知道您理解了沒?如果理解了,我們往下學習,看看如何使用這個概念向請求管道新增一些附加功能。

編寫自定義中介軟體

中介軟體可以說是ASP.NET Core的基座,在請求期間執行的所有邏輯都基於此機制。因此,我們可以使用它向web新增自定義功能。在下面案例,我們希望找出通過請求管道的每個請求的執行時間:

我們可以在呼叫下一個中介軟體之前建立並啟動秒錶,然後在呼叫下箇中介軟體之後停止測量執行時間,如下所示:

app.Use(async (context, next) => {     
    var s = new Stopwatch();     
    s.Start();     
    //其他操作 
    await next();     
    s.Stop(); 
    //結束度量     
    var result = s.ElapsedMilliseconds;     
    //統計耗時     
    await context.Response.WriteAsync($"耗時:{result} 秒。"); 
});

記得為System.Diagnostics新增using語句。
之後,我們將經過的毫秒返回到響應流。
如果您編寫的中介軟體元件很多,Program.cs將變得非常混亂。所以大多數中介軟體元件將被編寫為獨立的類,如下所示:

using System.Diagnostics; 
public class StopwatchMiddleware {    
    private readonly RequestDelegate _next;     
    public StopwatchMiddleware(RequestDelegate next)  
    {  
        _next = next;  
    }     

    public async Task Invoke(HttpContext context) {         
        var s = new Stopwatch();         
        s.Start();         
        //其他操作          
        await _next(context);         
        s.Stop(); 
        //結束度量         
        var result = s.ElapsedMilliseconds;         
        //統計耗時     
        await context.Response.WriteAsync($"耗時:{result} 秒。");    
    }  
}

在Invoke方法中的,我們獲得建構函式和當前上下文獲得要執行的下一個中介軟體元件。

注意:
中介軟體在應用程式啟動時初始化,建構函式在應用程式生命週期內僅執行一次。另一方面,每個請求呼叫一次Invoke方法。
要使用此中介軟體,您可以使用一個通用的UseMiddleware方法:

app.UseMiddleware<StopwatchMiddleware>();

然而,更優雅的方法是建立一個封裝此呼叫的擴充套件方法:

public static class StopwatchMiddlewareExtension {     
    public static IApplicationBuilder  UseStopwatch(this IApplicationBuilder app)     
    {         
        app.UseMiddleware<StopwatchMiddleware>();         
        return app;    
     }
 }

然後就可以這樣使用:

app.UseStopwatch();

這樣,您可以通過請求管道向ASP.NET Core應用程式提供其他功能。中介軟體中提供了整個HttpContext。這樣,您可以使用中介軟體操縱請求和響應。

例如,AuthenticationMiddleware嘗試從請求中收集使用者資訊。如果找不到任何資訊,它將通過向用戶端傳送特定的響應來請求資訊。如果它找到,它會將其新增到請求上下文中,並以這種方式將其提供給整個應用程式。

中介軟體的潛力

使用中介軟體還可以做許多其他事情。例如,可以將請求管道拆分為兩個或多個管道,我們將在這裡討論如何做到這一點。

使用/map分支管道

下一段程式碼顯示瞭如何基於特定路徑建立請求管道的分支:

app.Map("/map1", app1 => {     
    // 其他中介軟體     
    app1.Run(async context =>     {         
            await context.Response.WriteAsync("Map Test 1");     
    }); 
}); 
app.Map("/map2", app2 => {     
     // 其他中介軟體     
    app2.Run(async context => {         
        await context.Response.WriteAsync("Map Test 2");     
    }); 
}); 
// 其他中介軟體

/map1路徑是一個特定的分支,它在內部繼續請求管道,/map2與此相同。這兩個map都有自己內部的中介軟體設定。所有其他未指定的路徑都遵循該主分支。

使用MapWhen分支管道

還有一個MapWhen方法可以根據條件分支管道,而不是根據路徑分支:

public void Configure(IApplicationBuilder app) {     
    app.MapWhen(context =>context.Request.Query.ContainsKey("分支"),         
        app1 => {            
             // 其他中介軟體           
            app1.Run(async context =>  {  
                await context.Response.WriteAsync( "MapBranch Test"); 
            });     
    });     
    //其他中介軟體     
    app.Run(async context =>    { 
        await context.Response.WriteAsync("Hello non-Map.");     
    });
}

使用中介軟體構造條件

我們一般可以根據設定值建立條件,或者根據請求上下文的屬性建立條件。在前面的範例中,我們使用了查詢字串屬性作為條件。當然,你也可以使用HTTP檔頭、表單屬性或請求上下文的任何其他屬性。

如果需要,還可以巢狀map以建立子分支和孫分支。
我們再看下健康檢查中介軟體,ASP.NET Core HealthCheck API的工作原理如下:
首先,它使用MapWhen指定要使用的埠,然後,它使用Map設定HealthCheck API路徑(如果未指定埠則使用Map)。最後,使用了HealthCheckMiddleware。我們看下面的程式碼範例:

private static void UseHealthChecksCore(IApplicationBuilder app, PathString path, int? port, object[] args) 
{     
    if (port == null)    
    {         
        app.Map(path, b => b.UseMiddleware<HealthCheckMiddleware>(args)); 
    }     
    else  {        
        app.MapWhen(c => c.Connection.LocalPort == port,
            b0 => b0.Map(path, b1 =>b1.UseMiddleware<HealthCheckMiddleware>(args)));     
    }; 
}

這裡,我們可以使用Map或MapWhen分別基於特定路徑或特定條件提供特殊的API或資源。
接下來,讓我們看看如何在更新版本的ASP.NET Core中使用終止中介軟體元件。

在ASP.NET Core 3.0及更高版本中使用中介軟體

ASP.NET Core 3.0及更高版本,有兩種新的中介軟體,它們被稱為UseRoutingUseEndpoints

public void Configure(IApplicationBuilder app, IWebHostEnvironment env) {     
    if (env.IsDevelopment())     
    {  
        app.UseDeveloperExceptionPage();     
    }     
    app.UseRouting();     
    app.UseEndpoints(endpoints =>  {         
        endpoints.MapGet("/", async context => { 
            await context.Response.WriteAsync("Hello  World!");         
        });     
    }); 
}

第一個是使用路由的中介軟體UseRouting,另一個是存取地址的UseEndpoints

這是新的端點路由。以前,路由是MVC的一部分,它只適用於MVC、Web API和基於MVC的框架。然而在ASP.NET Core 3.0及更高版本,路由不再是MVC框架中的一部分。現在,MVC和其他框架都可以被對映到特定的路由或端點。
在前面的程式碼段中,GET請求被對映到頁面根URL。在下一個程式碼片段中,MVC被對映到路由模式,RazorPages被對映到基於RazorPage的特定檔案結構的路由:

app.UseEndpoints(endpoints => {     
    endpoints.MapControllerRoute(name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); 
    endpoints.MapRazorPages(); 
});

現在已經沒有UseMvc方法了,即使它仍然存在並在IApplicationBuilder物件級別上工作,以防止現有程式碼中斷。現在,啟用ASP.NET Core功能的方法更為精細。

  • Areas for MVC and web APIendpoints.MapAreaControllerRoute(...);
  • MVC and web APIendpoints.MapControllerRoute(...);
  • Blazor server-sideendpoints.MapBlazorHub(...);
  • SignalRendpoints.MapHub(...);
  • Razor Pagesendpoints.MapRazorPages(...);
  • Health checksendpoints.MapHealthChecks(...);

這些是ASP最常用的新Map方法。
還有很多方法可以定義回退地址,比如將路由和HTTP方法對映到代理,以及中介軟體元件。
你可以建立適用於所有請求的中介軟體,例如StopWatchMiddleware,你也可以編寫中介軟體以在特定路徑或路由上工作,例如建立一個Map方法,以將其對映到該路由。

注意事項
不再建議在中介軟體內部處理路由。相反,您應該使用新的地址路由。使用這種方法,中介軟體更加通用,它可以通過單一的設定就可以在多個路由上工作。

重寫終止中介軟體

接下來,我們建立小型虛擬中介軟體,將應用程式狀態寫入特定路由。在此範例中,沒有自定義路由處理:

namespace MiddlewaresSample; 
public class AppStatusMiddleware {     
    private readonly RequestDelegate _next;     
    private readonly string _status;
    public AppStatusMiddleware(RequestDelegate next, string status)     
    {        
        _next = next;         
        _status = status;    
    }     
    public async Task Invoke(HttpContext context)  {         
        await context.Response.WriteAsync($"Hello {_status}!");     
    } 
}

我們需要做的是在IEndpointRouteBuilder物件上編寫一個擴充套件方法。此方法將路由模式作為可選引數,並返回IEndpointConventionBuilder物件以啟用跨域資源共用(CORS)、身份驗證或路由的其他條件。

現在,我們應該新增一個擴充套件方法,以便更容易地使用中介軟體:

public static class MapAppStatusMiddlewareExtension {     
    public static IEndpointConventionBuilder MapAppStatus(this IEndpointRouteBuilder routes, string pattern = "/", string name = "World") 
     {         
        var pipeline = routes.CreateApplicationBuilder().UseMiddleware<AppStatusMiddleware>(name).Build();         
        return routes.Map(pattern, pipeline).WithDisplayName("AppStatusMiddleware");     
    } 
}

完成後,我們可以使用MapAppStatus方法將其對映到特定路線:

app.UseRouting(); 
app.UseEndpoints(endpoints => {     
    endpoints.MapGet("/", () => "Hello World!");     
    endpoints.MapAppStatus("/status", "Status"); 
});

現在,我們可以通過輸入以下地址在瀏覽器中呼叫路由: http://localhost:5000/status

總結

大多數ASP.NET Core功能基於中介軟體,在本章中,我們學習了中介軟體的工作原理以及如何建立自己的中介軟體元件來擴充套件ASP.NET框架。我們還學習瞭如何使用新路由向自定義的終止中介軟體新增路由。

在下一章中,我們將瞭解ASP.NET Core中的新端點路由,它允許我們以簡單靈活的方式建立自己的託管端點。