ASP.NET Core基本原理(2)-中間件
原文鏈接:Application Startup
什么是中間件
中間件是裝配到應用管道中用來處理請求和響應的軟件組件。管道中的每一個組件都可以選擇是否將請求移交給下一個組件,并且可以在管道中調用下一個組件之前或者之后執行指定的操作。請求委托被用于構建請求管道。請求委托會處理每一個HTTP請求。
請求委托通過在傳遞給Startup
類中的Configure
方法的IApplicationBuilder
類型上使用Run
,Map
,Use
擴展方法進行配置。一個單獨的請求委托可以被指定為一個內嵌的匿名方法,或定義在一個可重用的類中。這些可重用的類就是中間件或中間件組件。每個請求管道中的中間件負責調用管道中的下一個組件,或者在適當的時候將調用鏈短路。
通過IApplicationBuilder創建中間件管道
ASP.NET請求管道由一系列的請求委托所組成,一個接著一個被調用,如圖所示,執行線程跟隨著黑色箭頭:
每一個委托在下一個委托調用之前或之后都有機會去執行操作。任何委托都可以選擇停止傳遞請求到下一個委托,而自己處理該請求。這就是請求管道的短路,這種設計可以避免一些不必要的工作。例如,一個授權(Authorization)中間件只有在請求通過身份驗證之后才能調用下一個委托;否則它就會被短路并返回"Not Authorized"的響應。異常處理委托必須在管道的早期被調用,這樣它們就可以捕獲到發生在管道內更深層次的異常。
打開默認的網站模板,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();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
上述代碼(在非開發環境),UseExceptionHandler
是第一個被添加到管道的中間件,因此會捕獲之后調用中出現的任何異常。
Static File Module
提供了無需授權檢查的功能,由它服務的任何文件,包含在那些位于wwwroot文件夾中的文件都是可被公開訪問的。如果你想讓基于授權來提供這些文件:
- 將它們存放在wwwroot文件夾之外以及靜態文件中間件可以訪問的任何目錄。
- 交給控制器方法,通過返回一個
FileResult
表示授權被應用的地方。
被靜態文件模塊處理的請求會在管道中被短路。如果請求不是通過靜態文件模塊來處理,那么它會被傳給Identity module
來執行身份驗證。如果請求未通過驗證,則管道將被短路。否則,將會調用管道的最后一站-MVC框架。
注:你添加中間件的順序通常會影響它們對請求產生影響的順序,然后在響應時會以相反的順序。這對應用程序的安全、性能和功能都非常關鍵。在上面的代碼中,Static File Module
在管道的早期被調用,這樣可以及時短路,避免了請求進行到不必要的組件中。身份驗證中間件在任何需要身份驗證的處理請求之前被添加進來。異常處理必須在其它中間件之前被注冊以便捕獲其它組件的異常。
最簡單的ASP.NET應用設置一個簡單的請求委托來處理所有的請求。這樣就不是一個真實的請求管道,通過調用一個簡單的匿名函數來響應每一個HTTP請求。
app.Run(async context =>
{
await context.Response.WriteAsync("Hello, World!");
});
App.Run
委托會終止管道,下面的盒子中,只有第一個委托會被運行。
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);
});
}
當應用程序運行的環境設置成LogInline
時,ConfigureLogInline
方法會被調用。
在上述例子中,調用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);
});
}
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
的查詢字符串的請求)將被await context.Response.WriteAsync("Hello from " + _environment);
所定義的委托所處理。
內置中間件
ASP.NET帶來了下列中間件組件:
中間件 | ?描述 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |
---|---|
身份驗證(Authentication) | ?提供身份驗證支持 |
跨域資源共享(CORS) | ?配置跨域資源共享 |
路由(Routing) | ?定義和約定請求路由 |
會話(Session) | ?對管理用戶會話提供支持 |
靜態文件(Static Files) | ??對服務靜態文件和目錄瀏覽提供支持 |