(4)ASP.NET Core 中間件

1.前言

整個HTTP Request請求跟HTTP Response返回結果之間的處理流程是一個請求管道(request pipeline)。而中間件(middleware)則是一種裝配到請求管道以處理請求和響應的組件。每一個組件:
可選擇是否將請求傳遞到管道中的下一個組件。
可在管道中的下一個組件先後執行工做。
中間件(middleware)處理流程以下圖所示:瀏覽器

2.使用中間件

ASP.NET Core請求管道中每一箇中間件都包含一系列的請求委託(request delegates)來處理每一個HTTP請求,依次調用。請求委託經過使用IApplicationBuilder類型的Run、Use和Map擴展方法在Strartup.Configure方法中配置。下面咱們經過配置Run、Use和Map擴展方法示例來了解下中間件。安全

2.1 Run

public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
    //第一個請求委託Run
    app.Run(async context =>//內嵌匿名方法
    {
        await context.Response.WriteAsync("Hello, World!");
    });
    //第二個請求委託Run
    app.Run(async context =>//內嵌匿名方法
    {
        await context.Response.WriteAsync("Hey, World!");
    });
    }
}

響應結果:服務器

由上述代碼可知,Run方法指定爲一個內嵌匿名方法(稱爲並行中間件,in-line middleware),而內嵌匿名方法中並無指定執行下一個請求委託,這一個過程叫管道短路,而該中間件又叫「終端中間件」(terminal middleware),由於它阻止中間件下一步處理請求。因此在Run第一個請求委託的時候就已經終止請求,並無執行第二個請求委託直接返回Hello, World!輸出文本。而根據官網解釋,Run是一種約定,有些中間件組件可能會暴露他們本身的Run方法,而這些方法只能在管道末尾處運行(也就是說Run方法只在中間件執行最後一個請求委託時才使用)。app

2.2 Use

public void Configure(IApplicationBuilder app)
{
    app.Use(async (context, next) =>
    {
        context.Response.ContentType = "text/plain; charset=utf-8";
        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");
    });
}

響應結果:async

由上述代碼可知,Use方法將多個請求委託連接在一塊兒。而next參數表示管道中的下一個委託。若是不調用next參數調用下一個請求委託則會使管道短路。好比,一個受權(authorization)中間件只有經過身份驗證以後才能調用下一個委託,不然它就會被短路,並返回「Not Authorized」的響應。因此應儘早在管道中調用異常處理委託,這樣它們就能捕獲在管道的後期階段發生的異常。性能

2.3 Map和MapWhen

●Map:Map擴展基於請求路徑建立管道分支。
●MapWhen:MapWhen擴展基於請求條件建立管道分支。
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:5001的請求和響應。ui

 

請求this

響應spa

localhost:5001

Hello from non-Map delegate.

localhost:5001/map1

Map Test 1

localhost:5001/map2

Map Test 2

localhost:5001/map3

Hello from non-Map delegate.

由上述代碼可知,Map方法將從HttpRequest.Path中刪除匹配的路徑段,並針對每一個請求將該路徑追加到HttpRequest.PathBase。也就是說當咱們在瀏覽器上輸入map1請求地址的時候,系統會執行map1分支管道輸出其請求委託信息,同理執行map2就會輸出對應請求委託信息。
MapWhen示例:

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:5001的請求和響應。

請求

響應

http://localhost:5001

Hello from non-Map delegate. <p>

https://localhost:5001/?branch=master

Branch used = master

由上述代碼可知,MapWhen是基於branch條件而建立管道分支的,咱們在branch條件上輸入master就會建立其對應管道分支。也就是說,branch條件上輸入任何一個字符串條件,都會建立一個新的管理分支。
並且還Map支持嵌套,例如:

public void Configure(IApplicationBuilder app)
{
    app.Map("/level1", level1App => {
        level1App.Map("/level2a", level2AApp => {
            // "/level1/level2a" processing
        });
        level1App.Map("/level2b", level2BApp => {
            // "/level1/level2b" processing
        });
    });
}

還可同時匹配多個段:

public class Startup
{
    private static void HandleMultiSeg(IApplicationBuilder app)
    {
        app.Run(async context =>
        {
            await context.Response.WriteAsync("Map multiple segments.");
        });
    }
    public void Configure(IApplicationBuilder app)
    {
        app.Map("/map1/seg1", HandleMultiSeg);

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

3.順序

向Startup.Configure方法添加中間件組件的順序定義了在請求上調用它們的順序,以及響應的相反順序。此排序對於安全性、性能和功能相當重要。
如下Startup.Configure方法將爲常見應用方案添加中間件組件:
●異常/錯誤處理(Exception/error handling)
●HTTP嚴格傳輸安全協議(HTTP Strict Transport Security Protocol)
●HTTPS重定向(HTTPS redirection)
●靜態文件服務器(Static file server)
●Cookie策略實施(Cookie policy enforcement)
●身份驗證(Authentication)
●會話(Session)
●MVC
請看以下代碼:

public void Configure(IApplicationBuilder app)
{
    if (env.IsDevelopment())
    {
        // When the app runs in the Development environment:
        //   Use the Developer Exception Page to report app runtime errors.
        //   Use the Database Error Page to report database runtime errors.
        app.UseDeveloperExceptionPage();
        app.UseDatabaseErrorPage();
    }
    else
    {
        // When the app doesn't run in the Development environment:
        //   Enable the Exception Handler Middleware to catch exceptions
        //     thrown in the following middlewares.
        //   Use the HTTP Strict Transport Security Protocol (HSTS)
        //     Middleware.
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }
    // Return static files and end the pipeline.
    app.UseStaticFiles();
    // Authenticate before the user accesses secure resources.
    app.UseAuthentication();
}

從上述示例代碼中,每一箇中間件擴展方法都經過Microsoft.AspNetCore.Builder命名空間在 IApplicationBuilder上公開。可是爲何咱們要按照這個順序去添加中間件組件呢?下面咱們挑幾個中間件來了解下。
UseExceptionHandler(異常/錯誤處理)是添加到管道的第一個中間件組件。所以咱們能夠捕獲在應用程序調用中發生的任何異常。那爲何要將異常/錯誤處理放在第一位呢?那是由於這樣咱們就不用擔憂因前面中間件短路而致使捕獲不到整個應用程序全部異常信息。
UseStaticFiles(靜態文件)中間件在管道中提早調用,方便它能夠處理請求和短路,而無需經過剩餘中間組件。也就是說靜態文件中間件不用通過UseAuthentication(身份驗證)檢查就能夠直接訪問,便可公開訪問由靜態文件中間件服務的任何文件,包括wwwroot下的文件。
UseAuthentication(身份驗證)僅在MVC選擇特定的Razor頁面或Controller和Action以後纔會發生。
通過上面描述,你們都瞭解中間件順序的重要性了吧。

4.編寫中間件(重點)

雖然ASP.NET Core爲咱們提供了一組豐富的內置中間件組件,但在某些狀況下,你可能須要寫入自定義中間件。

4.1中間件類

下面咱們自定義一個查詢當前區域性的中間件:

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

可經過傳入區域性參數條件進行測試。例如http://localhost:7997/?culture=zh、http://localhost:7997/?culture=en。
可是若是每一個自定義中間件都在Startup.Configure方法中編寫如上一大堆代碼,那麼對於程序來講,將是災難性的(不利於維護和調用)。爲了更好管理代碼,咱們應該把內嵌匿名方法封裝到新建的自定義類(示例自定義RequestCultureMiddleware類)裏面去:

public class RequestCultureMiddleware
{
    private readonly RequestDelegate _next;
    public RequestCultureMiddleware(RequestDelegate next)
    {
        _next = next;
    }
    public async Task InvokeAsync(HttpContext context)
    {
        context.Response.ContentType = "text/plain; charset=utf-8";
        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
        await _next(context);
    }
}

經過Startup.Configure方法調用中間件:

public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.UseMiddleware<RequestCultureMiddleware>();
        app.Run(async (context) =>
        {
            await context.Response.WriteAsync(
                $"Hello {CultureInfo.CurrentCulture.DisplayName}");
        });
    }
}

4.2中間件擴展方法

Startup.Configure方法調用中間件設置能夠經過自定義的擴展方法將其公開(調用內置IApplicationBuilder公開中間件)。示例建立一個RequestCultureMiddlewareExtensions擴展類並經過IApplicationBuilder公開:

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

再經過Startup.Configure方法調用中間件:

public class Startup
{
    public void Configure(IApplicationBuilder app)
{
        app.UseRequestCulture();
        app.Run(async (context) =>
        {
            await context.Response.WriteAsync(
                $"Hello {CultureInfo.CurrentCulture.DisplayName}");
        });
    }
}

響應結果:

經過委託構造中間件,應用程序在運行時建立這個中間件,並將它添加到管道中。這裏須要注意的是,中間件的建立是單例的,每一箇中間件在應用程序生命週期內只有一個實例。那麼問題來了,若是咱們業務邏輯須要多個實例時,該如何操做呢?請繼續往下看。

6.按每次請求建立依賴注入(DI)

在中間件的建立過程當中,內置的IOC容器會爲咱們建立一箇中間件實例,而且整個應用程序生命週期中只會建立一個該中間件的實例。一般咱們的程序不容許這樣的注入邏輯。其實,咱們能夠把中間件理解成業務邏輯的入口,真正的業務邏輯是經過Application Service層實現的,咱們只須要把應用服務注入到Invoke方法中便可。ASP.NET Core爲咱們提供了這種機制,容許咱們按照請求進行依賴的注入,也就是每次請求建立一個服務。示例:

public class CustomMiddleware
{
    private readonly RequestDelegate _next;
    public CustomMiddleware(RequestDelegate next)
    {
        _next = next;
    }
    // IMyScopedService is injected into Invoke
    public async Task Invoke(HttpContext httpContext, IMyScopedService svc)
    {
        svc.MyProperty(1000);
        await _next(httpContext);
    }
}
public static class CustomMiddlewareExtensions
{
    public static IApplicationBuilder UseCustomMiddleware(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<CustomMiddleware>();
    }
}
public interface IMyScopedService
{
    void MyProperty(decimal input);
}
public class MyScopedService : IMyScopedService
{
    public void MyProperty(decimal input)
    {
        Console.WriteLine("MyProperty is " + input);
    }
}
public void ConfigureServices(IServiceCollection services)
{
    //注入DI服務
    services.AddScoped<IMyScopedService, MyScopedService>();
}

響應結果:

 

參考文獻:
ASP.NET Core中間件
寫入自定義ASP.NET Core中間件

相關文章
相關標籤/搜索