ASP.NET Core 中間件(Middleware)詳解

本文爲官方文檔譯文,官方文檔現已非機器翻譯 https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/middleware/?view=aspnetcore-2.1跨域

什麼是中間件(Middleware)?

中間件是組裝到應用程序管道中以處理請求和響應的軟件。 每一個組件:瀏覽器

  • 選擇是否將請求傳遞給管道中的下一個組件。
  • 能夠在調用管道中的下一個組件以前和以後執行工做。

請求委託(Request delegates)用於構建請求管道,處理每一個HTTP請求。緩存

請求委託使用RunMapUse擴展方法進行配置。單獨的請求委託能夠之內聯匿名方法(稱爲內聯中間件)指定,或者能夠在可重用的類中定義它。這些可重用的類和內聯匿名方法是中間件或中間件組件。請求流程中的每一箇中間件組件都負責調用流水線中的下一個組件,若是適當,則負責連接短路。安全

將HTTP模塊遷移到中間件解釋了ASP.NET Core和之前版本(ASP.NET)中的請求管道之間的區別,並提供了更多的中間件示例。app

使用 IApplicationBuilder 建立中間件管道

ASP.NET Core請求流程由一系列請求委託組成,以下圖所示(執行流程遵循黑色箭頭):async

每一個委託能夠在下一個委託以前和以後執行操做。委託還能夠決定不將請求傳遞給下一個委託,這稱爲請求管道的短路。短路一般是可取的,由於它避免了沒必要要的工做。例如,靜態文件中間件能夠返回一個靜態文件的請求,並使管道的其他部分短路。須要在管道早期調用異常處理委託,所以它們能夠捕獲後面管道的異常。函數

最簡單的多是ASP.NET Core應用程序創建一個請求的委託,處理全部的請求。此案例不包含實際的請求管道。相反,針對每一個HTTP請求都調用一個匿名方法。性能

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;

public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.Run(async context =>
        {
            await context.Response.WriteAsync("Hello, World!");
        });
    }
}

第一個 app.Run 委託終止管道。測試

有以下代碼:ui

經過瀏覽器訪問,發現確實在第一個app.Run終止了管道。

您能夠將多個請求委託與app.Use鏈接在一塊兒。 next參數表示管道中的下一個委託。 (請記住,您能夠經過不調用下一個參數來結束流水線。)一般能夠在下一個委託以前和以後執行操做,以下例所示:

public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.Use(async (context, next) =>
            {
                await context.Response.WriteAsync("進入第一個委託 執行下一個委託以前\r\n");
                //調用管道中的下一個委託
                await next.Invoke();
                await context.Response.WriteAsync("結束第一個委託 執行下一個委託以後\r\n");
            });
            app.Run(async context =>
            {
                await context.Response.WriteAsync("進入第二個委託\r\n");
                await context.Response.WriteAsync("Hello from 2nd delegate.\r\n");
                await context.Response.WriteAsync("結束第二個委託\r\n");
            });
    }
}

使用瀏覽器訪問有以下結果:

能夠看出請求委託的執行順序是遵循上面的流程圖的。

注意:
響應發送到客戶端後,請勿調用next.Invoke。 響應開始以後,對HttpResponse的更改將拋出異常。 例如,設置響應頭,狀態代碼等更改將會引起異常。在調用next以後寫入響應體。

  • 可能致使協議違規。 例如,寫入超過content-length所述內容長度。

  • 可能會破壞響應內容格式。 例如,將HTML頁腳寫入CSS文件。

HttpResponse.HasStarted是一個有用的提示,指示是否已發送響應頭和/或正文已寫入。

順序

Startup。Configure方法中添加中間件組件的順序定義了在請求上調用它們的順序,以及響應的相反順序。 此排序對於安全性,性能和功能相當重要。

Startup.Configure方法(以下所示)添加了如下中間件組件:

  1. 異常/錯誤處理
  2. 靜態文件服務
  3. 身份認證
  4. MVC
public void Configure(IApplicationBuilder app)
{
    app.UseExceptionHandler("/Home/Error"); // Call first to catch exceptions
                                                                            // thrown in the following middleware.

    app.UseStaticFiles();                   // Return static files and end pipeline.

    app.UseAuthentication();               // Authenticate before you access
                                                            // secure resources.

    app.UseMvcWithDefaultRoute();          // Add MVC to the request pipeline.
}

上面的代碼,UseExceptionHandler是添加到管道中的第一個中間件組件,所以它捕獲之後調用中發生的任何異常。

靜態文件中間件在管道中提早調用,所以能夠處理請求和短路,而無需經過剩餘的組件。 靜態文件中間件不提供受權檢查。 由其提供的任何文件,包括wwwroot下的文件都是公開的。

若是請求沒有被靜態文件中間件處理,它將被傳遞給執行身份驗證的Identity中間件(app.UseAuthentication)。 身份不會使未經身份驗證的請求發生短路。 雖然身份認證請求,但受權(和拒絕)僅在MVC選擇特定的Razor頁面或控制器和操做以後纔會發生。

受權(和拒絕)僅在MVC選擇特定的Razor頁面或Controller和Action以後纔會發生。

如下示例演示了中間件順序,其中靜態文件的請求在響應壓縮中間件以前由靜態文件中間件處理。 靜態文件不會按照中間件的順序進行壓縮。 來自UseMvcWithDefaultRoute的MVC響應能夠被壓縮。

public void Configure(IApplicationBuilder app)
{
    app.UseStaticFiles();         // Static files not compressed
    app.UseResponseCompression();
    app.UseMvcWithDefaultRoute();
}

Use, Run, 和 Map

你可使用UseRunMap配置HTTP管道。Use方法可使管道短路(即,能夠不調用下一個請求委託)。Run方法是一個約定, 而且一些中間件組件可能暴露在管道末端運行的Run [Middleware]方法。Map*擴展用做分支管道的約定。映射根據給定的請求路徑的匹配來分支請求流水線,若是請求路徑以給定路徑開始,則執行分支。

public class Startup
{
    private static void HandleMapTest1(IApplicationBuilder app)
    {
        app.Run(async context =>
        {
            await context.Response.WriteAsync("Map Test 1");
        });
    }

    private static void HandleMapTest2(IApplicationBuilder app)
    {
        app.Run(async context =>
        {
            await context.Response.WriteAsync("Map Test 2");
        });
    }

    public void Configure(IApplicationBuilder app)
    {
        app.Map("/map1", HandleMapTest1);

        app.Map("/map2", HandleMapTest2);

        app.Run(async context =>
        {
            await context.Response.WriteAsync("Hello from non-Map delegate. <p>");
        });
    }
}

下表顯示了使用之前代碼的 http://localhost:19219 的請求和響應:

請求 響應
localhost:1234 Hello from non-Map delegate.
localhost:1234/map1 Map Test 1
localhost:1234/map2 Map Test 2
localhost:1234/map3 Hello from non-Map delegate.

當使用Map時,匹配的路徑段將從HttpRequest.Path中刪除,併爲每一個請求追加到Http Request.PathBase

MapWhen根據給定謂詞的結果分支請求流水線。 任何類型爲Func<HttpContext,bool>的謂詞均可用於將請求映射到管道的新分支。 在如下示例中,謂詞用於檢測查詢字符串變量分支的存在:

public class Startup
{
    private static void HandleBranch(IApplicationBuilder app)
    {
        app.Run(async context =>
        {
            var branchVer = context.Request.Query["branch"];
            await context.Response.WriteAsync($"Branch used = {branchVer}");
        });
    }

    public void Configure(IApplicationBuilder app)
    {
        app.MapWhen(context => context.Request.Query.ContainsKey("branch"),
                               HandleBranch);

        app.Run(async context =>
        {
            await context.Response.WriteAsync("Hello from non-Map delegate. <p>");
        });
    }
}

如下下表顯示了使用上面代碼 http://localhost:19219 的請求和響應:

請求 響應
localhost:1234 Hello from non-Map delegate.
localhost:1234/?branch=1 Branch used = master

Map支持嵌套,例如:

app.Map("/level1", level1App => {
       level1App.Map("/level2a", level2AApp => {
           // "/level1/level2a"
           //...
       });
       level1App.Map("/level2b", level2BApp => {
           // "/level1/level2b"
           //...
       });
   });

Map也能夠一次匹配多個片斷,例如:

app.Map("/level1/level2", HandleMultiSeg);

內置中間件

ASP.NET Core附帶如下中間件組件:

中間件 描述
Authentication 提供身份驗證支持
CORS 配置跨域資源共享
Response Caching 提供緩存響應支持
Response Compression 提供響應壓縮支持
Routing 定義和約束請求路由
Session 提供用戶會話管理
Static Files 爲靜態文件和目錄瀏覽提供服務提供支持
URL Rewriting Middleware 用於重寫 Url,並將請求重定向的支持

編寫中間件

中間件一般封裝在一個類中,並使用擴展方法進行暴露。 查看如下中間件,它從查詢字符串設置當前請求的Culture:

public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.Use((context, next) =>
        {
            var cultureQuery = context.Request.Query["culture"];
            if (!string.IsNullOrWhiteSpace(cultureQuery))
            {
                var culture = new CultureInfo(cultureQuery);

                CultureInfo.CurrentCulture = culture;
                CultureInfo.CurrentUICulture = culture;
            }

            // Call the next delegate/middleware in the pipeline
            return next();
        });

        app.Run(async (context) =>
        {
            await context.Response.WriteAsync(
                $"Hello {CultureInfo.CurrentCulture.DisplayName}");
        });

    }
}

您能夠經過傳遞Culture來測試中間件,例如 http://localhost:19219/?culture=zh-CN

如下代碼將中間件委託移動到一個類:

using Microsoft.AspNetCore.Http;
using System.Globalization;
using System.Threading.Tasks;

namespace Culture
{
    public class RequestCultureMiddleware
    {
        private readonly RequestDelegate _next;

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

        public Task Invoke(HttpContext context)
        {
            var cultureQuery = context.Request.Query["culture"];
            if (!string.IsNullOrWhiteSpace(cultureQuery))
            {
                var culture = new CultureInfo(cultureQuery);

                CultureInfo.CurrentCulture = culture;
                CultureInfo.CurrentUICulture = culture;

            }

            // Call the next delegate/middleware in the pipeline
            return this._next(context);
        }
    }
}

如下經過IApplicationBuilder的擴展方法暴露中間件:

using Microsoft.AspNetCore.Builder;

namespace Culture
{
    public static class RequestCultureMiddlewareExtensions
    {
        public static IApplicationBuilder UseRequestCulture(
            this IApplicationBuilder builder)
        {
            return builder.UseMiddleware<RequestCultureMiddleware>();
        }
    }
}

如下代碼從Configure調用中間件:

public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.UseRequestCulture();

        app.Run(async (context) =>
        {
            await context.Response.WriteAsync(
                $"Hello {CultureInfo.CurrentCulture.DisplayName}");
        });

    }
}

中間件應該遵循顯式依賴原則,經過在其構造函數中暴露其依賴關係。 中間件在應用程序生命週期構建一次。 若是您須要在請求中與中間件共享服務,請參閱如下請求相關性。

中間件組件能夠經過構造方法參數來解析依賴注入的依賴關係。 UseMiddleware 也能夠直接接受其餘參數。

每一個請求的依賴關係

由於中間件是在應用程序啓動時構建的,而不是每一個請求,因此在每一個請求期間,中間件構造函數使用的做用域生命週期服務不會與其餘依賴注入類型共享。 若是您必須在中間件和其餘類型之間共享做用域服務,請將這些服務添加到Invoke方法的簽名中。 Invoke方法能夠接受由依賴注入填充的其餘參數。 例如:

public class MyMiddleware
{
    private readonly RequestDelegate _next;

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

    public async Task Invoke(HttpContext httpContext, IMyScopedService svc)
    {
        svc.MyProperty = 1000;
        await _next(httpContext);
    }
}
相關文章
相關標籤/搜索