很久之前曾介紹過 OWIN - 微軟重新定義的開放網站介面標準,讓 ASP.NET WebAPI 跟 SignalR 不再侷限 IIS ( System.Web.dll),可輕鬆放進 Console Application / Windows Service 執行。而 OWIN 採取 Host/Server/Middleware/Application 分層架構,不管是極度要求效能的情境,或是包含複雜處理邏輯的場合,都可透過抽換組裝各層模組符合要求。

ASP.NET Core 跟 Katana 一樣,都是依據 OWIN 規格實作網站架構(參考:http://owin.org/ 列舉的 OWIN 實作包含 Katana、Freya(for F#)、ASP.NET vNext,其中 ASP.NET vNext 就是現在的 ASP.NET Core)),Request / Response 是交給一到多個 Middleware 組成的 Pipeline 處理。

在這篇文章我們將實地觀察及實驗 Middleware 的基本運作原理。

首先準備測試環境,開啟一個 ASP.NET Core 3.1 的空白專案(範本請選 Empty):

新建好專案的 Startup.cs Configure() 長這樣:

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
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!");
        });
    });
}

註解提到 Configure 方法用來設定 HTTP Request Pipeline,而程式碼中的 app.UseDeveloperExceptionPage()、app.UseRouting()、app.UseEndpoints() 便是在組裝 Middleware。預設邏輯為 URL / 時顯示 Hello World!,其餘路徑顯示 HTTP 404。

動手實驗之前,先簡單說明 Middleware 處理 Request 的原理,Request 會通過一連串 Middleware 組成的 Pipeline,每個 Middleware 在經手 Request 時可以決定接手處理傳回 Response 或呼叫 next() 交給下一個 Middleware 處理。而 next() 執行完下一個 Middleware 邏輯後,主導權又回到上層 Middleware。(如下圖) 換言之,註冊的先後順序很重要,Middleware 1 可優先決定挑選哪些 Request 留下來處理,吃剩的再交給 Middleware 2 發落;而 Middleware 3、Middleware 2 執行完要傳 Response 給使用者之前,Middleware 1 也有權利再跑一段程式對 Response 內容修改加料。


圖檔來源:ASP.NET Core 中介軟體

做個實驗來驗證。我加入一段 Middleware 邏輯,當 URL 為 /darkthread 時顯示 "ASP.NET Core Rocks!",否則呼叫 next() 交給下一個 Middleware 處理,但 next() 完要透過 context.Response.WriteAsync(" Powered by ASP.NET Core") 補上一段輸出內容。UseEndpoints() 則加入一段 MapGet("/darkthread", ...) 印出 "Handled by UseEndpoints" 以識別 Request 是由哪一個 Middleware 處理:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    
    app.Use(async (context, next) =>
    {
        if (context.Request.Path == "/darkthread")
            await context.Response.WriteAsync("ASP.NET Core Rocks!");
        else
        {
            await next();
            await context.Response.WriteAsync(" Powered by ASP.NET Core");
        }
    });    

    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapGet("/", async context =>
        {
            await context.Response.WriteAsync("Hello World!");
        });
        endpoints.MapGet("/darkthread", async context =>
        {
            await context.Response.WriteAsync("Handled by UseEndpoints");
        });
    });
}

猜猜瀏覽 / 與 /darkthread 各看到什麼結果?

一如我們預期,/ 時除了顯示 Hello World! 後面還被加料 Powered by ASP.NET Core 字樣,而 /darkthread 則被我們的 Middleware 邏輯接下傳回 ASP.NET Core Rocks!。

前面說到 Middleware 的註冊順序很重要,如果我們把的這段自訂 Middleware 程式邏移到最後,會發生什麼事?

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!");
        });
        endpoints.MapGet("/darkthread", async context =>
        {
            await context.Response.WriteAsync("Handled by UseEndpoints");
        });
    });

    app.Use(async (context, next) =>
    {
        if (context.Request.Path == "/darkthread")
            await context.Response.WriteAsync("ASP.NET Core Rocks!");
        else
        {
            await next();
            await context.Response.WriteAsync(" Powered by ASP.NET Core");
        }
    });
}

答案是完全沒作用! / 與 /darkthread 都在 UseEndpoints() 時被處理掉,不再經過最後一段 Use() 邏輯,由於 / 的 Response 回傳結果不會經過它,無從加料。

做完這個小實驗,相信大家對 ASP.NET Core Middleware 的運作原理又多一分了解囉。

Using a experiment to demostrate the basic principle of ASP.NET Core middleware.


Comments

# by 凱大

其實理想的情況下 應該是 NextRequest => HttpContext => { ..... } 因為實際上他在運行的時候 是把 內部的 HttpContxt => { } 作為上一步的 next 使用 兩個參數的寫法可能會需要sdk自行創造 Func<httpcontext, Task> 怕會有不如預期的情況 例如 已你寫的自訂middleware 可能可以在配置middleware時就完成if 判斷的情況下 就可以減少每一次request都做一次這個判斷 寫在一起的話 其實會變得有點難去切割這件事情

Post a comment