iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 8
1

簡介

中介層會組成應用程式的 pipeline,用來處理 Web 的請求和回應。每個中介層元件都可以:

  • 選擇是否要把請求傳到 pipeline 中下一個元件。
  • 在呼叫 pipeline 中下一個元件的之前和之後執行某些工作。

請求委任 (request delegates) 會用來建立請求 pipeline,而且會處理每個 HTTP 請求。

請求委任是透過 IApplicationBuilder 類別的 RunMapUse 三個擴充方法來設定。每個請求委任都可以寫成匿名方法或者獨立成一個類別。

用 IApplicationBuilder 來建立請求 pipeline

請求 pipeline 是由請求委任的序列所組成,會一個接著一個被呼叫。https://ithelp.ithome.com.tw/upload/images/20181022/201078753gNCVCe2lt.png

每一個委任都可以在呼叫下一個委任之前或之後執行某些工作。在上圖中可以看到,呼叫下個委任前的邏輯是針對 Request 處理;呼叫後則是針對 Response 處理。也可以不把請求傳到下個委任中,讓請求 pipeline 發生短路(short-circuiting),如此可以讓應用程式不需要執行多餘的工作。例如用來做授權驗證 (authorization) 的中介層中,應該只有通過驗證後才需要呼叫下一個委任,否則就要發生短路,傳回 403 狀態。

新增一個空白的 Web 應用程式,可以看到最簡單的請求委任設定範例。起始類別中,只設定一個委任來處理所有請求。第一個 Run 就會終止請求 pipeline。

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

在下面的範例中,只有第一個委任會被執行,因為在這個委任中沒有呼叫下一個委任。

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

        app.Run(async context =>
        {
            await context.Response.WriteAsync("Hello from 2nd delegate.");
        });
    }
}

如果要把多個委任串在一起,需要使用 Use 方法。next 參數表示 pipeline 中的下個委任。如果不呼叫下一個委任,便會發生短路。

public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.Use(async (context, next) =>
        {
            // 呼叫委任前
            await next.Invoke();
            // 呼叫委任後
        });

        app.Run(async context =>
        {
            await context.Response.WriteAsync("Hello from 2nd delegate.");
        });
    }
}

執行順序

Startup.Configure 方法中新增中介層元件的順序代表請求時呼叫中介層元件的順序及回應的相反順序。對安全性、效能與商業邏輯而言,此順序相當重要。

由 MVC 範本建立的專案中,在 Startup.Configure 方法中新增中介層元件的順序如下:

  1. 例外/錯誤處理
  2. HTTP 重新導向至 HTTPS
  3. 靜態檔案
  4. Cookie policy
  5. MVC
public void Configure(IApplicationBuilder app)
{
    if (_env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
        app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseStaticFiles();
    app.UseCookiePolicy();

    app.UseMvc(routes =>
    {
        routes.MapRoute(
            name: "default",
            template: "{controller=Home}/{action=Index}/{id?}");
    });
}

通常會第一個新增例外處理的中介層,才能讓此中介層攔截到所有後續委任中發生的例外狀況。

Use、Run 和 Map 方法

我們會用 RunMapUse 三個擴充方法來設定請求 pipeline。Run 方法會讓 pipeline 發生短路,因為此方法中沒有提供 next 參數,有些中介層元件會將 Run{Middleware} 方法曝露出來。Use 方法如果沒有呼叫下一個元件,也會發生短路。

在下面兩個範例中,RunUse 的效果是一樣的,因為在 Use 中沒有使用 next 參數來呼叫下一個中介層。

public void ConfigureWithRun(IApplicationBuilder app)
{
    app.Run(async context =>
    {
        await context.Response.WriteAsync("Hello from Run");
    });
}
public void ConfigureWithUse(IApplicationBuilder app)
{
    app.Use(async (context, next) =>
    {
        await context.Response.WriteAsync("Hello from Use");
    });
}

Map 方法則是用來為請求 pipeline 建立分支。有 MapMapWhen 兩個分法可以使用。

Map 方法第一個參數是指定請求路徑的開頭,如果符合就會執行第二個參數傳入的動作。

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

    public void Configure(IApplicationBuilder app)
    {
        app.Map("/ironman", HandleMapIronman);

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

使用上面的起始類別後,來自 http://localhost:44315 的請求及回應的對應表格如下:

Request Response
/ Hello from non-Map delegate
/ironman Map Ironman
/another-branch Hello from non-Map delegate

除了用路徑來判斷分支外,也有一個 MapWhen 方法可以自定判斷邏輯。此方法的第一個參數是 Func<HttpContext,Boolean>,回傳的 bool 用來判斷 pipeline 是否要走這個分支。

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

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

使用上面的起始類別後,來自 http://localhost:44315 的請求及回應的對應表格如下:

Request Response
/ Hello from non-Map delegate
/?branch=ironman Branch Name = ironman

Map 也支援巢狀設定,例如:

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

參考資料

今天說明 ASP.NET Core 中介層的觀念,明天我們再來實作中介層~


上一篇
Dependency Injection 依賴注入
下一篇
Middleware 中介層 - 2/2
系列文
.Net Core 網站開發 10131

尚未有邦友留言

立即登入留言