原文:Middleware
作者:Steve Smith、Rick Anderson
翻譯:劉怡(AlexLEWIS)
校對:許登洋(Seay)
章節:
什么是中間件
中間件是用於組成應用程序管道來處理請求和響應的組件。管道內的每一個組件都可以選擇是否將請求交給下一個組件、並在管道中調用下一個組件之前和之后執行某些操作。請求委托被用來建立請求管道,請求委托處理每一個 HTTP 請求。
請求委托通過使用 IApplicationBuilder 類型的 Run、Map 以及 Use 擴展方法來配置,並在 Startup
類中傳給 Configure
方法 。每個單獨的請求委托都可以被指定為一個內嵌匿名方法,或其定義在一個可重用的類中。這些可重用的類被稱作 中間件 或 中間件組件。每個位於請求管道內的中間件組件負責調用管道中下一個組件,或適時短路調用鏈。
Migrating HTTP Modules to Middleware 解釋了請求管道在 ASP.NET Core 和之前版本之間的區別,並提供了更多中間件樣例。
用 IApplicationBuilder 創建中間件管道
ASP.NET 請求管道由一系列的請求委托所構成,它們一個接着一個被調用,如圖所示(該執行線程按黑色箭頭的順序執行):
每個委托在下一個委托之前和之后都有機會執行操作。任何委托都能選擇停止傳遞到下一個委托,轉而自己處理該請求。這被叫做請求管道的短路,而且是一種有意義的設計,因為它可以避免不必要的工作。比方說,一個授權(authorization)中間件只有在通過身份驗證之后才調用下一個委托,否則它就會被短路並返回 “Not Authorized” 的響應。異常處理委托需要在管道的早期被調用,這樣它們就能夠捕捉到發生在管道內更深層次出現的異常了。
你可以看一下 Visual Studio 2015 附帶的默認 Web 站點模板關於請求管道設置的例子。Configure
方法增加了下列這些中間件組件:
- 錯誤處理(同時針對於開發環境和非開發環境)
- 靜態文件服務器
- 身份驗證
- MVC
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();//手工高亮
app.UseDatabaseErrorPage();//手工高亮
app.UseBrowserLink();//手工高亮
}
else
{
app.UseExceptionHandler("/Home/Error");//手工高亮
}
app.UseStaticFiles();//手工高亮
app.UseIdentity();//手工高亮
// Add external authentication middleware below. To configure them please see http://go.microsoft.com/fwlink/?LinkID=532715
app.UseMvc(routes =>//手工高亮
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
上面的代碼中(在非開發環境時),UseExceptionHandler 是第一個被加入到管道中的中間件,因此將會捕獲之后調用中出現的任何異常。
靜態文件模塊 不提供授權檢查,由它提供的任何文件,包括那些位於wwwroot 下的文件都是公開的可被訪問的。如果你想基於授權來提供這些文件:
- 將它們存放在 wwwroot 外面以及任何靜態文件中間件都可訪問得到的目錄。
- 利用控制器操作來判斷授權是否允許,如果允許則通過返回 FileResult 來提供它們。
被靜態文件模塊處理的請求會在管道中被短路(參見 Working with Static Files)。如果該請求不是由靜態文件模塊處理,那么它就會被傳給 Identity 模塊 執行身份驗證。如果未通過身份驗證,則管道將被短路。如果請求的身份驗證沒有失敗,則管道的最后一站是 MVC 框架。
注意
你添加中間件組件的順序通常會影響到它們處理請求的順序,然后在響應時則以相反的順序返回。這對應用程序安全、性能和功能很關鍵。在上面的代碼中,靜態文件中間件 在管道的早期被調用,這樣就能處理並及時短路管道,以避免請求走到不必要的組件中。身份驗證中間件被添加在任何需要身份認證的處理請求的前面。異常處理必須被注冊在其它中間件之前以便捕獲其它組件的異常。
最簡單的 ASP.NET 應用程序是使用單個請求委托來處理所有請求。事實上在這種情況下並不存在所謂的“管道”,調用單個匿名函數以相應每個 HTTP 請求。
app.Run(async context =>
{
await context.Response.WriteAsync("Hello, World!");
});
第一個 App.Run
委托中斷了管道。在下面的例子中,只有第一個委托(“Hello, World!”)會被運行。
public void Configure(IApplicationBuilder app)
{
app.Run(async context =>
{
await context.Response.WriteAsync("Hello, World!");//手工高亮
});
app.Run(async context =>
{
await context.Response.WriteAsync("Hello, World, Again!");
});
將多個請求委托彼此鏈接在一起;next
參數表示管道內下一個委托。通過 不 調用 next 參數,你可以中斷(短路)管道。你通常可以在執行下一個委托之前和之后執行一些操作,如下例所示:
public void ConfigureLogInline(IApplicationBuilder app, ILoggerFactory loggerfactory)
{
loggerfactory.AddConsole(minLevel: LogLevel.Information);
var logger = loggerfactory.CreateLogger(_environment);
app.Use(async (context, next) =>//手工高亮
{
logger.LogInformation("Handling request.");
await next.Invoke();//手工高亮
logger.LogInformation("Finished handling request.");
});
app.Run(async context =>
{
await context.Response.WriteAsync("Hello from " + _environment);//手工高亮
});
}
警告
應當避免在修改了HttpResponse
之后還調用管道內下一個會修改響應的組件,從而導致它被送到客戶端處。
提示
當應用程序運行的環境設置為LogInline
時,這個ConfigureLogInline
方法就會被調動。要了解更多請訪問環境 Working with Multiple Environments 一章。本文剩下的篇幅將使用變化的Configure[Environment]
來展示不同的選項。 Visual Studio 中運行示例代碼的最簡單辦法是使用web
命令,該命令由 project.json 文件所配置。也可參考 Application Startup 。
在上例中,調用 await next.Invoke()
將會調用下一個委托await context.Response.WriteAsync("Hello from " + _environment);
。客戶端將收到預期的響應(“Hello from LogInline”),同時服務端這邊的控制台將先后輸出如下信息:
Run,Map 與 Use
你可以使用 Run、Map 和 Use 配置 HTTP 管道。Run
方法將會短路管道(因為它不會調用 next
請求委托)。因此,Run
應該只能在你的管道尾部被調用。Run
是一種慣例,有些中間件組件可能會暴露他們自己的 Run[Middleware] 方法,而這些方法只能在管道末尾處運行。下面這兩個中間件等價的,其中有用到 Use
的版本沒有使用 next
參數:
public void ConfigureEnvironmentOne(IApplicationBuilder app)
{
app.Run(async context =>//手工高亮
{
await context.Response.WriteAsync("Hello from " + _environment);
});
}
public void ConfigureEnvironmentTwo(IApplicationBuilder app)
{
app.Use(async (context, next) =>//手工高亮
{
await context.Response.WriteAsync("Hello from " + _environment);
});
}
注意
IApplicationBuilder 接口向外暴露了一個Use
方法,因此從技術上來說它們並不完全是 擴展 方法。
我們已經看了幾個關於如何通過 Use
構建請求管道的例子,同時約定了 Map*
擴展被用於分支管道。當前的實現已支持基於請求路徑或使用謂詞來進入分支。Map
擴展方法用於匹配基於請求路徑的請求委托。Map
只接受路徑,並配置單獨的中間件管道的功能。在下例中,任何基於路徑 /maptest
的請求都會被管道中所配置的 HandleMapTest
方法所處理。
private static void HandleMapTest(IApplicationBuilder app)
{
app.Run(async context =>
{
await context.Response.WriteAsync("Map Test Successful");
});
}
public void ConfigureMapping(IApplicationBuilder app)
{
app.Map("/maptest", HandleMapTest);//手工高亮
}
注意
當使用了Map
,每個請求所匹配的路徑段將從HttpRequest.Path
中移除,並附加到HttpRequest.PathBase
中。
除基於路徑的映射外,MapWhen
方法還支持基於謂詞的中間件分支,允許以非常靈活的方式構建單獨的管道。任何 Func<HttpContext, bool>
類型的謂語都被用於將請求映射到新的管到分支。在下例中使用了一個簡單的謂詞來檢測查詢字符串變量 branch
是否存在:
private static void HandleBranch(IApplicationBuilder app)
{
app.Run(async context =>
{
await context.Response.WriteAsync("Branch used.");//手工高亮
});
}
public void ConfigureMapWhen(IApplicationBuilder app)
{
app.MapWhen(context => {//手工高亮
return context.Request.Query.ContainsKey("branch");//手工高亮
}, HandleBranch);//手工高亮
app.Run(async context =>
{
await context.Response.WriteAsync("Hello from " + _environment);
});
}
使用了上述設置后,任何包含請求字符 branch
的請求將使用定義於HandleBranch
方法內的管道(其響應就將是“Branch used.”)。其他請求(即沒有為 branch
定義查詢字符串值)將被第 17 行所定義的委托處理。
你也可以嵌套映射:
app.Map("/level1", level1App => {
level1App.Map("/level2a", level2AApp => {
// "/level1/level2a"
//...
});
level1App.Map("/level2b", level2BApp => {
// "/level1/level2b"
//...
});
});
內置中間件
ASP.NET 帶來了下列中間件組件:
中間件 | 描述 |
---|---|
身份驗證(Authentication) | 提供身份驗證支持。 |
跨域資源共享(CORS) | 配置跨域資源共享。CORS 全稱為 Cross-Origin Resource Sharing。 |
路由(Routing) | 定義和約定請求路由。 |
會話(Session) | 提供對管理用戶會話(session)的支持。 |
靜態文件 | 提供對靜態文件服務於目錄瀏覽的支持。 |
編寫中間件
CodeLabs 中間件教程 提供了一個清晰介紹用於編寫中間件。
對於更復雜的請求處理功能,ASP.NET 團隊推薦在他們自己的類中實現中間件,並暴露 IApplicationBuilder
擴展方法,這樣就能通過 Configure
方法來被調用。之前演示的簡易日志中間件就能被轉換為一個中間件類(middleware class):只要在其構造函數中獲得下一個 RequestDelegate
並提供一個 Invoke
方法,如下所示:
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
namespace MiddlewareSample
{
public class RequestLoggerMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger _logger;
public RequestLoggerMiddleware(RequestDelegate next, ILoggerFactory loggerFactory)//手工高亮
{
_next = next;
_logger = loggerFactory.CreateLogger<RequestLoggerMiddleware>();
}
public async Task Invoke(HttpContext context)//手工高亮
{
_logger.LogInformation("Handling request: " + context.Request.Path);
await _next.Invoke(context);
_logger.LogInformation("Finished handling request.");
}
}
}
中間件遵循 顯式依賴原則 並在其構造函數中暴露所有依賴項。中間件能夠利用到 UseMiddleware
params
參數數組被用於非注入參數。
public static class RequestLoggerExtensions
{
public static IApplicationBuilder UseRequestLogger(this IApplicationBuilder builder)
{
return builder.UseMiddleware<RequestLoggerMiddleware>();//手工高亮
}
}
通過使用擴展方法和相關中間件類,Configure
方法變得非常簡潔和高可讀性。
public void ConfigureLogMiddleware(IApplicationBuilder app,
ILoggerFactory loggerfactory)
{
loggerfactory.AddConsole(minLevel: LogLevel.Information);
app.UseRequestLogger();//手工高亮
app.Run(async context =>
{
await context.Response.WriteAsync("Hello from " + _environment);
});
}
盡管 RequestLoggerMiddleware
在其構造函數中需要 ILoggerFactory
參數,但無論是 Startup
類還是 UseRequestLogger
擴展方法都不需要顯式依賴之。相反,它將自動地通過內置的 UseMiddleware<T>
來執行依賴注入以提供之。
測試中間件(通過給 LogMiddleware
設置 Hosting:Environment
環境變量)會輸出下圖的結果(當時用了 WebListener 時):
注意
UseStaticFiles 擴展方法(該方法會創建 StaticFileMiddleware)同樣也使用了UseMiddleware<T>
。所以除了StaticFileOptions
參數被傳入之外,構造函數的其他參數都由UseMiddleware<T>
和依賴注入所提供。