ASP.NET Core 基礎 - Middleware
1 |
很久之前曾介紹過 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都做一次這個判斷 寫在一起的話 其實會變得有點難去切割這件事情