咱們知道在 ASP.NET 中,有一個面向切面的請求管道,由22個主要的事件構成,可以讓咱們在往預約的執行順序裏面添加本身的處理邏輯。通常採起兩種方式:一種是直接在 Global.asax 中對應的方法中直接添加代碼。一種是是在 web.config 中經過註冊 HttpModule 來實現對請求管道事件監聽,並經過 HttpHandler 進入到咱們的應用程序中。而在 ASP.NET Core 中,對請求管道進行了從新設計,經過使用一種稱爲中間件的方式來進行管道的註冊,同時也變得更加簡潔和強大。關於 HTTP請求流程和管道模型能夠看這篇博客 http://www.javashuo.com/article/p-nfkrnuxg-u.htmlweb
中間件 (Middleware) 是組裝到應用程序管道中以處理請求和響應的組件。 管道內的每一個組件均可以選擇是否將請求交給下一個組件,並在管道中調用下一個組件以前和以後執行某些操做。請求委託被用來創建請求管道,請求委託處理每個HTTP請求。跨域
請求委託經過使用 IApplicationBuilder 類型的 Run、Map 以及 Use 擴展方法來配置,並在 Startup 類中傳給 Configure方法。每一個單獨的清求委託均可以被指定爲一個內嵌匿名方法,或其定義在一個可重用的類中。這些可重用的類被稱做 「中間件」或「中間件組件」。每一個位於請求管道內的中間件組件負責調用管道中的下一個組件,或適時短路調用鏈。數組
ASP.NET請求管道由一系列的請求委託所構成,它們一個接着一個被調用,如圖所示(該執行線程按黑色箭頭的順序執行)。瀏覽器
每一個委託:緩存
短路是一種有意義的設計,由於它能夠避免沒必要要的工做。好比說:安全
先在管道中調用異常處理委託,以便它們能夠捕獲在管道的後期階段所發生的異常。服務器
返回頂部session
在筆記一中,咱們就簡單介紹過 IApplicationBuilder,在 Startup 類的 Configure方法中,第一個參數即是 IApplicationBuilder。app
首先,IApplicationBuilder 是用來構建請求管道的,而所謂請求管道,本質上就是對 HttpContext 的一系列操做,即經過對 Request 的處理,來生成 Reponse。在 ASP.NET Core 中定義了一個 RequestDelegate 委託,來表示請求管道中的一個步驟,而對請求管道的註冊是經過 Func<RequestDelegate, RequestDelegate> 類型的委託(也就是中間件)來實現的。
能夠先看一下 VisualStudio2017 中默認Web(MVC)站點模板關於請求管道設置的例子。
在 Startup.cs 的 Configure 方法默認增長了下列這些中間件組件:
public void Configure(IApplicationBuilder app, IHostingEnvironment env) {
if (env.IsDevelopment())
{
// 開發環境
app.UseDeveloperExceptionPage();
}
else
{
// 非開發環境
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();// Https重定向
app.UseStaticFiles(); // 靜態文件
app.UseCookiePolicy(); // 使用Cookie策略
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
}); // MVC
}
上面的代碼在非開發環境中,UseExceptionHandler 是第一個被加入到管道中的中間件,所以將會捕獲以後調用組件中出現的任何異常,而後跳轉到設置的異常頁(/Home/Error)。
使用 UseHttpsRedirection 中間件,能夠輕鬆實施HTTPS,將HTTP重定向到HTTPS(ASP.NET Core 2.1具備新功能)。
而後就是靜態文件中間件 UseStaticFiles,靜態文件中間件不提供受權檢查,由它提供的任何文件,包括那些位於wwwroot下的文件都是公開可被訪問的。UseCookiePolicy 是使用ASP.NET Core中的Cookie策略(詳解Microsoft.AspNetCore.CookiePolicy),管道的最後執行的也就是MVC框架。
順序
向 Startup.Configure 方法添加中間件組件的順序定義了針對請求調用這些組件的順序,以及響應的相反順序。 此排序對於安全性、性能和功能相當重要。請求在每一步均可能被短路,因此咱們要以正確的順序添加中間件,如異常處理,咱們須要添加在最開始,這樣咱們就能第一時間捕獲異常,以及後續中間可能發生的異常,而後最終作處理返回。
最簡單的ASP.NET應用程序(空白Web模板)是使用單個請求委託來處理全部請求。事實上,在這種狀況下,並不存在所謂的「管道」,針對每一個HTTP請求都調用單個匿名函數來處理。
public void Configure(IApplicationBuilder app) {
// 使用單個請求委託來處理全部請求
app.Run(async (context) =>
{
await context.Response.WriteAsync("Hello World!");
});
}
上方代碼中 app.Run() 會中斷管道,調用單個匿名函數來處理HTTP請求。在下面的例子中,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("Welcome!");
});
}
經過運行項目,發現確實只有在第一個委託執行了,app.Run 終止了管道。
你能夠將多個請求委託 app.Use 鏈接在一塊兒,next參數表示管道內的下一個請求委託。在管道中,能夠經過不調用next參數來終止或短路管道。一般能夠在執行下一個委託以前和以後執行一些操做,以下例所示:
public void Configure(IApplicationBuilder app) {
app.Use(async (context, next) =>
{
await context.Response.WriteAsync("Handing Request.\r\n");
// 調用管道中的下一個委託
await next.Invoke();
await context.Response.WriteAsync("Finish Handing Request.\r\n");
});
app.Run(async (context) =>
{
await context.Response.WriteAsync("Hello from Middleware\r\n");
});
}
運行項目,在瀏覽器中訪問出現以下結果:
你可使用 Run、Map 和 Use 方法配置 HTTP 管道。
Run 方法會短路管道,由於它不會調用 next 請求委託。所以 Run 方法通常只在管道底部被調用。Run 方法是一種慣例約定,有些中間件組件可能會暴露它們本身的 Run[Middleware]方法,而這些方法只能在管道末尾處運行。
Use 前面已經簡單介紹經過 Use 構建請求管道的例子,Use 方法亦可以使管道短路(即不調用 next 請求委託)。
Map 擴展方法用匹配基於請求路徑的請求委託,Map只接受路徑,並配置單獨的中間件管道的功能。 Map* 方法能夠基於給定請求路徑的匹配項來建立請求管道分支。 若是請求路徑以給定路徑開頭,則執行分支。以下面例子中:
public void Configure(IApplicationBuilder app) {
app.Map("/map1", HandleMapTest1);
app.Map("/map2", HandleMapTest2);
app.Run(async context =>
{
await context.Response.WriteAsync("<p> Hello from non-Map delegate. </p>");
});
}
private static void HandleMapTest1(IApplicationBuilder app) {
app.Run(async context =>
{
await context.Response.WriteAsync("<p> Map Test 1 </p>");
});
}
private static void HandleMapTest2(IApplicationBuilder app) {
app.Run(async context =>
{
await context.Response.WriteAsync("<p> Map Test 2 </p>");
});
}
任何基於路徑 /map1 的請求都會被管道中所配置 HandleMapTest1 方法處理。基於路徑 /map2 的請求都會被管道中所配置 HandleMapTest2 方法處理。
下表顯示上例來自 http://localhost:52831 的請求和響應。
請求 | 響應 |
---|---|
http://localhost:52831 | Hello from non-Map delegate. |
http://localhost:52831/map1 | Map Test 1 |
http://localhost:52831/map2 | Map Test 2 |
http://localhost:52831/map3 | Hello from non-Map delegate. |
除了基於路徑的映射外,MapWhen 方法還支持基於謂語的中間件分支,容許以很是靈活的方式構建單獨的管道。任何 Func<HttpContext, bool> 類型的謂語均可以被用於將請求映射到新的管道分支。下例中使用了一個簡單的謂語來檢測查洵字符串中變量 branch 是否存在:
public class Startup {
public void Configure(IApplicationBuilder app) {
app.MapWhen(context => context.Request.Query.ContainsKey("branch"), HandleBranch);
app.Run(async context =>
{
await context.Response.WriteAsync("<p> Hello from non-Map delegate. </p>");
});
}
private static void HandleBranch(IApplicationBuilder app) {
app.Run(async context =>
{
var branchVer = context.Request.Query["branch"];
await context.Response.WriteAsync($"Branch used = {branchVer}");
});
}
}
使用了上面的設置後,任何包含請求字符 branch 的請求將使用定義於 HandleBranch 方法內的管道,若是沒有包含查詢字符串 branch 的請求,將被 app.Run 所定義的委託處理。
請求 | 響應 |
---|---|
http://localhost:52831 | Hello from non-Map delegate. |
http://localhost:52831/?branch=1 | Branch used = 1 |
http://localhost:52831/?branch=2 | Branch used = 2 |
http://localhost:52831/?branch | Branch used = |
另外還能夠嵌套 Maps:
app.Map("/level1", level1App => {
level1App.Map("/level2a", level2AApp => {
// "/level1/level2a"
//...
});
level1App.Map("/level2b", level2BApp => {
// "/level1/level2b"
//...
});
});
Map 也能夠一次匹配多個片斷,例如:
app.Map("/level1/level2", HandleMultiSeg); // "/level1/level2"
如下 Startup.Configure 方法將爲常見應用方案添加中間件組件:
中間件 | 描述 |
---|---|
身份驗證(Authentication) | 提供身份驗證支持 |
跨域資源共享(CORS) | 配置跨域資源共享 |
響應支持(Response Caching) | 提供緩存響應支持 |
響應壓縮(Response Compression) | 提供響應壓縮支持 |
路由(Routing) | 定義和約束請求路由 |
會話(Session) | 提供對管理用戶會話(session)的支持 |
靜態文件(Static Files) | 爲靜態文件和目錄瀏覽提供服務提供支持 |
URL Rewriting Middleware | 用於重寫 Url,並將請求重定向的支持 |
更多中間件組件能夠到 Microsoft 文檔 上查看。
如何實現一箇中間件呢,下面咱們來實際操做。
中間件遵循 顯式依賴原則 ,並在其構造函數中暴露全部的依賴項。中間件可以利用 UseMiddleware<T> 擴展方法的優點,直接經過它們的構造函數注入服務。依賴注入服務是自動完成填充的,擴展所用到的 params 參數數組被用於非注入參數。
下面來實現一個記錄IP的中間件。
① 新建一個 ASP.NET Core WebApplication 項目,選擇空的模板。
而後爲項目添加—個 Microsoft.Extensions.Logging.Console。
NuGet命令行執行(請使用官方源):
Install-Package Microsoft.Extensions.Logging.Console
② 新建一個類 RequestIPMiddleware.es,將中間件委託移動到類:
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace middlewareDemo
{
public class RequestIPMiddleware {
private readonly RequestDelegate _next;
private readonly ILogger _logger;
public RequestIPMiddleware(RequestDelegate next, ILoggerFactory loggerFactory) {
_next = next;
_logger = loggerFactory.CreateLogger<RequestIPMiddleware>();
}
public async Task Invoke(HttpContext context) {
_logger.LogInformation("User IP:" + context.Connection.RemoteIpAddress.ToString());
await _next.Invoke(context);
}
}
}
③ 再新建—個 RequestIPExtensions.cs,如下擴展方法經過 IApplicationBuilder 公開中間件:
using Microsoft.AspNetCore.Builder;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace middlewareDemo
{
public static class RequestIPExtensions {
public static IApplicationBuilder UseRequestIP(this IApplicationBuilder builder) {
return builder.UseMiddleware<RequestIPMiddleware>();
}
}
}
這樣就編寫好了一箇中間件。
④ 使用中間件。在 Startup.cs 中添加 app.UseRequestIP() 來使用中間件:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) {
loggerFactory.AddConsole();
app.UseRequestIP();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.Run(async (context) =>
{
await context.Response.WriteAsync("Hello World!");
});
}
而後運行程序,這裏選擇使用 Kestrel
訪問:http://localhost:5000/
成功運行,到這裏咱們還能夠對這個中間件進行進一步改進,增長更多的功能,如限制訪問等。
Microsoft 文檔 ASP.NET Core 中間件
ASP.NET Core 中間件(Middleware)詳解
《ASP.NET Core 跨平臺開發從入門到實戰》