今天來討論一個ASP.NET Core 很重要概念管道和中間件,在ASP.NET Core中,針對HTTP請求採用pipeline也就是通常說的管道方式來處理,而管道容器內可以掛載很多中間件(處理邏輯)“串聯”來處理HTTP請求,每一個中間件都有權決定是否需要執行下一個中間件,或者直接做出響應。這樣... ...
今天來討論一個ASP.NET Core 很重要概念管道和中間件,在ASP.NET Core中,針對HTTP請求採用pipeline也就是通常說的管道方式來處理,而管道容器內可以掛載很多中間件(處理邏輯)“串聯”來處理HTTP請求,每一個中間件都有權決定是否需要執行下一個中間件,或者直接做出響應。這樣的機制使得HTTP請求能夠很好的被層層處理和控制,並且層次清晰處理起來甚是方便。 示意圖如下:
為了再次說明管道和中間件的概念,舉一個官方給出的許可權驗證的例子,中間件A,B分別按順序掛載在管道容器中,A為許可權驗證中間件,只有通過A的許可權驗證才能執行B,如果沒有通過A的驗證,A有權中斷管道處理直接返回相應的錯誤提示例如401等。這樣必須由上一節點來調用的串列遞歸執行方式就是pipeline,而每一個節點就是中間件或者叫中間組件。現在我們來看看如何在ASP.NET Core中使用中間件和管理自己的HTTP管道
環境配置與Startup
在瞭解中間件之前我們需要先知道Startup這個類具體運作方式,我們以下麵這段代碼為例:
/// <summary> /// web宿主的入口類 /// </summary> public class Startup { //加入服務項到容器, 這個方法將會被runtime調用 public void ConfigureServices(IServiceCollection services) { } /// <summary> /// 配置HTTP請求管道 /// </summary> /// <param name="app">被用於構建應用程式的請求管道 只可以在Startup中的Configure方法里使用</param> /// <param name="env">提供了訪問應用程式屬性,如環境變數</param> /// <param name="loggerFactory">提供了創建日誌的機制</param> public void Configure(IApplicationBuilder app,IHostingEnvironment env,ILoggerFactory loggerFactory) { loggerFactory.AddConsole(); if (env.IsDevelopment()) //根據配置的環境為開發環境,則會配置拋出異常錯誤界面 { app.UseDeveloperExceptionPage(); //拋出詳細的異常錯誤界面 } //管道斷路 app.Run(async (context) => { await context.Response.WriteAsync("Hello World!"); }); } }
可以看到 Startup.cs 內有兩個方法,一個是用來配置介面服務到管道容器中的ConfigureServices, 一個是用來配置管道中間件的Configure。
為什麼必須是這兩個方法名?
其實這兩個方法名並不是規定死的,但也不是任意規定的,他是根據容器的環境變數來判斷的,這裡先給出官方文檔《多環境下工作》。
我們可以在文檔中瞭解到,Core使用“ASPNETCORE_ENVIRONMENT”欄位來描述當前運行環境名稱這就是上文中提到的環境配置,官方預設了3個環境名分別是Development(開發環境), Staging(測試環境), Production(生產環境),如果您使用的是VSCode您可以在.vscode文件夾下的launch.json中找到“ASPNETCORE_ENVIRONMENT”欄位,可以發現預設情況下是Development,那說這些到底有什麼用呢?
在Startup中規定,配置服務和中間件兩個方法可以根據環境名稱來命名和選擇調用,命名規則為ConfigureServices{ENVIRONMENT}和Configure{ENVIRONMENT}。如 ASPNETCORE_ENVIRONMENT = “Development” 則ConfigureServices和Configure 可以寫成ConfigureServicesDevelopment 和 ConfigureDevelopment ,其他也是如此。這樣就可以通過配置ASPNETCORE_ENVIRONMENT 來決定該調用哪一個配置方法了。
ConfigureServices和Configure是什麼環境下的呢?
ConfigureServices和Configure就好像Switch 語句中的 default一樣的道理,如果沒有找到任何符合環境名的方法名,就會執行調用這兩個方法。如配置了Development,但卻沒有給出ConfigureServicesDevelopment ,這時就會執行ConfigureServices,如果都沒有就會拋出異常。
必須設置成預設環境名嗎?
環境名配置的參數名不必是預設值,你可以自己寫一個,比如LogEnv等等。
接下來我們看一下實現的代碼:
/// <summary> /// web宿主的入口類 /// </summary> public class Startup { //加入服務項到容器, 這個方法將會被runtime調用 public void ConfigureServices(IServiceCollection services) { } /// <summary> /// Log環境下配置HTTP請求管道 /// </summary> /// <param name="app"></param> public void ConfigureLogHelp(IApplicationBuilder app){ app.Run(async (context) => { await context.Response.WriteAsync("Hello World - ConfigureLogHelp"); }); } /// <summary> /// 開發環境下配置HTTP請求管道 /// </summary> /// <param name="app"></param> public void ConfigureDevelopment(IApplicationBuilder app){ app.Run(async (context) => { await context.Response.WriteAsync("Hello World - ConfigureDevelopment"); }); } /// <summary> /// 預設情況下配置HTTP請求管道 /// </summary> /// <param name="app">被用於構建應用程式的請求管道。只可以在 Startup 中的 Configure 方法里使用</param> /// <param name="env">提供了訪問應用程式屬性,如環境變數</param> /// <param name="loggerFactory">提供了創建日誌的機制</param> public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { //管道斷路 app.Run(async (context) => { await context.Response.WriteAsync("Hello World!"); }); }Startup.cs
當ASPNETCORE_ENVIRONMENT = “Development”
當ASPNETCORE_ENVIRONMENT = “LogHelp”
這樣做的好處就是你可以寫自己的測試配置而不會影響到其他人或者開發過程。當然環境的作用還在於前端應該引用什麼樣的CSS和JS,關於這些我們之後在MVC的章節再來討論, 想瞭解的博友可以看官方文檔
管道配置與Startup
說完環境配置和Startup的關係,我們回來接著聊管道的事情,現在我們來說說Configure{ENVIRONMENT}一下Configure簡稱這個方法。
Configure這個方法是用於配置中間件到中管道容器(IApplicationBuilder),所以這個方法必須要包含一個IApplicationBuilder參數用來接受管道容器,方便開發者配置。當然他還可以接受其他的可選參數供開發者使用如下:
(註:下圖來源於ASP.NET Core中文文檔)
需要提一下的是,剛剛我們上文中說的環境名在IHostingEnvironment中可以獲取,對於預設值官方還做了判斷封裝,當然你可以重構它來封裝自己的環境名判斷。
HTTP管道容器由三個擴展的方法來控制中間件的路由、掛載等等,分別是Run, Map, User。
a. Run方法會使得可以使管道短路,顧名思義就是終結管道向下執行不會調用next()委托,所以Run方法最好放在管道的最後來執行,如下麵的代碼:
/// <summary> /// 開發環境下配置HTTP請求管道 /// </summary> /// <param name="app"></param> public void ConfigureDevelopment(IApplicationBuilder app){ app.Run(async (context) => { await context.Response.WriteAsync("Hello World - ConfigureDevelopment"); }); app.Run(async (context) => { await context.Response.WriteAsync("Hello World - ConfigureDevelopment 不會被執行"); }); }
執行結果:
b. Use不會主動短路整個HTTP管道,但是也不會主動調用下一個中間件,必須自行調用await next.Invoke(); 如果不使用這個方法去調用下一個中間件那麼Use此時的效果其實和Run是相同的,我們來看正常的代碼:
/// <summary> /// 開發環境下配置HTTP請求管道 /// </summary> /// <param name="app"></param> public void ConfigureDevelopment(IApplicationBuilder app){ var order =""; app.Use(async (context, next) => { order = $"{order}|Use start"; await next.Invoke(); order = $"{order}|Use end"; }); app.Run(async context => { await context.Response.WriteAsync($"{order}|Run ext"); }); }
執行結果如下:
可以看到,Use end並沒有被執行到,因為在調用下一個中間件時採用了Run,管道被終止了。
再來看看如果不顯式調用next.Invoke()時的代碼:
/// <summary> /// 開發環境下配置HTTP請求管道 /// </summary> /// <param name="app"></param> public void ConfigureDevelopment(IApplicationBuilder app){ var order =""; app.Use(async (context, next) => { order = $"{order}|Use start"; //去掉顯示調用下一個中間件 //await next.Invoke(); order = $"{order}|Use end"; await context.Response.WriteAsync(order); }); app.Run(async context => { await context.Response.WriteAsync($"{order}|Run ext"); }); }
其結果如下:
可以發現Run這個中間件並沒有被執行,而只是單純的執行了Use這個中間件。所以說 在不顯式調用下一個中間件的情況下,效果和Run時一樣的會使管道短路。
c. Map可以根據提供的URL來路由中間件,如下代碼判斷URL中訪問"/test"時就會執行某個中間件邏輯:
/// <summary> /// 開發環境下配置HTTP請求管道 /// </summary> /// <param name="app"></param> public void ConfigureDevelopment(IApplicationBuilder app){ app.Map("/test", HandleMapTest); } /// <summary> /// maptest 處理方法 /// </summary> public void HandleMapTest(IApplicationBuilder app){ app.Run(async (context) => { await context.Response.WriteAsync("HandleMapTest Handler"); }); }
結果如下:
如果訪問/test就會執行相應的中間件,反之則不會執行。
MapWhen是Map的一個條件判斷的擴展方法,可以通過它來判斷某個條件適合的時候執行某一個中間件,如:當攜帶某一個參數名稱時,執行某一個中間件或者反之,代碼如下:
/// <summary> /// 開發環境下配置HTTP請求管道 /// </summary> /// <param name="app"></param> public void ConfigureDevelopment(IApplicationBuilder app){ app.MapWhen(context => { return context.Request.Query.ContainsKey("username"); }, HandleUserName); app.Run(async context => { await context.Response.WriteAsync("default ext"); }); } /// <summary> /// /// </summary> public void HandleUserName(IApplicationBuilder app){ app.Run(async context => { await context.Response.WriteAsync("UserName Map"); }); }
結果如下:
Map還可以進行嵌套路由中間件,這裡不再描述,大家可以參看這裡。
今天的管道介紹就寫到這裡,希望能對大家有幫助,如有不對望多加指正。