1. 什麼是中間件 在ASP.NET Core中,中間件(Middleware)是一個可以處理HTTP請求或響應的軟體管道。 ASP.NET Core中給中間件組件的定位是具有非常特定的用途。例如,我們可能有需要一個中間件組件驗證用戶,另一個中間件來處理錯誤,另一個中間件來提供靜態文件,如JavaS ...
在ASP.NET Core中,中間件(Middleware)是一個可以處理HTTP請求或響應的軟體管道。 ASP.NET Core中給中間件組件的定位是具有非常特定的用途。例如,我們可能有需要一個中間件組件驗證用戶,另一個中間件來處理錯誤,另一個中間件來提供靜態文件,如JavaScript文件,CSS文件,圖片等等。
中間件就是用於組成應用程式管道來處理請求和響應的組件 。
中間件可以認為有兩個基本的職責:
-
選擇是否將請求傳遞給管道中的下一個中間件。
-
可以在管道中的下一個中間件前後執行一些工作。
我們使用這些中間件組件在ASP.NET Core中設置請求處理管道,而正是這管道決定瞭如何處理請求。 而請求管道是由Startup.cs
文件中的Configure()
方法進行配置,它也是應用程式啟動的一個重要部分。
// 配置http 請求管道,由運行時調用 public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); // 渲染錯誤頁中間件 } else { app.UseExceptionHandler("/Home/Error"); } app.UseStaticFiles(); // 使用靜態文件中間件 app.UseRouting(); // 使用路由中間件 app.UseAuthorization(); // 使用授權中間件 // 終端節點中間件 app.UseEndpoints(endpoints => { endpoints.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); }); }
2. 中間件處理流程-請求管道
.Net Core管道(pipeline)是什麼?
簡單來說,就是從發起請求到返回結果的一個過程,在.Net Core中這裡面的處理是由中間件(middleware)來完成。 管道機制解釋 用戶在發起請求後,系統會自動生成一個請求管道(request pipeline),在這個請求管道中,可以通過run、map和use方法來配置請求委托(RequestDelegate),而在單獨的請求委托中定義的可重用的類和並行的匿名方法即為中間件,也叫做中間件組件。當發起請求後,系統會創建一個請求管道,在這個管道中,每一個中間件都會按順序處理(可能會執行,也可能不會被執行,取決於具體的業務邏輯),等最後一個中間件處理完後,又會按照相反的方向返回最終的處理結果。
例如,如果您有一個日誌記錄中間件,它可能只是記錄請求的時間,它處理完畢後將請求傳遞給下一個中間件以進行進一步處理。
public void ConfigureServices(IServiceCollection services) { services.AddControllersWithViews(); // 添加控制器服務 } public void Configure(IApplicationBuilder app, IWebHostEnvironment env,ILogger<Startup> logger) { app.UseRouting();// 添加路由服務 app.Use(next => { logger.LogInformation("第1個中間件"); // 啟動時執行,只執行一次 // 每次請求都會執行一次 return async context => { logger.LogInformation("第1個中間件1-before"); await next(context); logger.LogInformation("第1個中間件1-after"); }; }); app.Use(next => { logger.LogInformation("第2個中間件");// 啟動時執行,只執行一次 // 每次請求都會執行一次 return async context => { logger.LogInformation("第2個中間件2-before"); await next(context); logger.LogInformation("第2個中間件2-after"); }; }); app.Use(next => { logger.LogInformation("第3個中間件");// 啟動時執行,只執行一次 // 每次請求都會執行一次 return async context => { logger.LogInformation("第3個中間件3-before"); await next(context); logger.LogInformation("第3個中間件3-after"); }; }); // 使用終端節點中間件會短路後面的中件間,所以,這個中間件最好放在最後 app.UseEndpoints(endpoints => { endpoints.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); }); } info: Step3.Empty.Startup[0] 第3個中間件 info: Step3.Empty.Startup[0] 第2個中間件 info: Step3.Empty.Startup[0] 第1個中間件 // 發起請求之後 info: Step3.Empty.Startup[0] 第1個中間件1-before info: Step3.Empty.Startup[0] 第2個中間件2-before info: Step3.Empty.Startup[0] 第3個中間件3-before info: Step3.Empty.Startup[0] 第3個中間件3-after info: Step3.Empty.Startup[0] 第2個中間件2-after info: Step3.Empty.Startup[0] 第1個中間件1-after
中間件順序
下圖顯示了 ASP.NET Core MVC 和 Razor Pages 應用的完整請求處理管道。 你可以在典型應用中瞭解現有中間件的順序,以及在哪裡添加自定義中間件。 你可以完全控制如何重新排列現有中間件,或根據場景需要註入新的自定義中間件。
3. 什麼是短路
中間件組件可以處理請求, 並決定不調用管道中的下一個中間件,從而使管道短路,官方微軟給了一個英文的名字叫“terminal middleware ”
,翻譯為“終端中間件”。短路通常是被允許的,因為它可以避免一些不必要的工作。 例如, 如果請求的是像圖像或 css 文件這樣的靜態文件, 則 StaticFiles 中間件可以處理和服務該請求並使管道中的其餘部分短路。這個意思就是說,在我們的示例中, 如果請求是針對靜態文件, 則 Staticile 中間件不會調用 MVC 中間件,避免一些無謂的操作。
public void Configure(IApplicationBuilder app, IWebHostEnvironment env,ILogger<Startup> logger) { app.UseRouting(); app.Use(next => { logger.LogInformation("第1個中間件"); return async context => { logger.LogInformation("第1個中間件1-before"); await next(context); logger.LogInformation("第1個中間件1-after"); }; }); // 後面的中件間將不會再執行了 app.Use(next => { logger.LogInformation("第2個中間件"); return async context => { logger.LogInformation("短路了"); // await next(context); // 沒有調用即表示短路了 }; }); app.Use(next => { logger.LogInformation("第3個中間件"); return async context => { logger.LogInformation("第3個中間件3-before"); await next(context); logger.LogInformation("第3個中間件3-after"); }; }); app.UseEndpoints(endpoints => { endpoints.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); }); } 輸出結果: info: Step3.Empty.Startup[0] 第3個中間件 info: Step3.Empty.Startup[0] 第2個中間件 info: Step3.Empty.Startup[0] 第1個中間件 // 發起請求之後 info: Step3.Empty.Startup[0] 第1個中間件1-before info: Step3.Empty.Startup[0] 短路了 info: Step3.Empty.Startup[0] 第1個中間件1-after
app.Use 與 app.Run 的區別
它倆都可以添加一個中間件至請求管道中。
-
Use 有權決定是否執行下一個中間件,如果不執行,則出現短路情況
-
Run 是直接短路,不會執行後面的中間件。
4. 常用的系統中間件
1. 路由中間件
ASP.NET Core 控制器使用路由
// 添加控制器與視圖服務 builder.Services.AddControllersWithViews(); // ....上面省略一些代碼 app.UseRouting();
路由模板:
-
在啟動時
Program.cs
或在屬性中定義。 -
描述 URL 路徑如何與
-
用於生成鏈接的 URL。 生成的鏈接通常在響應中返回。
操作既支持
路由模板 | 示例匹配 URI | 請求 URI… |
---|---|---|
hello |
/hello |
僅匹配單個路徑 /hello 。 |
{Page=Home} |
/ |
匹配並將 Page 設置為 Home 。 |
{Page=Home} |
/Contact |
匹配並將 Page 設置為 Contact 。 |
{controller}/{action}/{id?} |
/Products/List |
映射到 Products 控制器和 List 操作。 |
{controller}/{action}/{id?} |
/Products/Details/123 |
映射到 Products 控制器和 Details 操作,並將 id 設置為 123。 |
{controller=Home}/{action=Index}/{id?} |
/ |
映射到 Home 控制器和 Index 方法。 id 將被忽略。 |
{controller=Home}/{action=Index}/{id?} |
/Products |
映射到 Products 控制器和 Index 方法。 id 將被忽略。 |
設置傳統路由
app.UseRouting(); app.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); app.Run(); // 必須添加一個終節點 路由模板 "{controller=Home}/{action=Index}/{id?}": 匹配 URL 路徑,例如 /Products/Details/5 通過標記路徑來提取路由值 { controller = Products, action = Details, id = 5 }。 如果應用有一個名為 ProductsController 的控制器和一個 Details 操作,則提取路由值會導致匹配: public class ProductsController : Controller { public IActionResult Details(int id) { return ControllerContext.MyDisplayRouteInfo(id); } } MyDisplayRouteInfo 由 Rick.Docs.Samples.RouteInfo NuGet 包提供,會顯示路由信息。 /Products/Details/5 模型綁定 id = 5 的值,以將 id 參數設置為 5。 有關更多詳細信息,請參閱模型綁定。 {controller=Home} 將 Home 定義為預設 controller。 {action=Index} 將 Index 定義為預設 action。 {id?} 中的 ? 字元將 id 定義為可選。 預設路由參數和可選路由參數不必包含在 URL 路徑中進行匹配。 有關路由模板語法的詳細說明,請參閱路由模板參考。 匹配 URL 路徑 /。 生成路由值 { controller = Home, action = Index }。 controller 和 action 的值使用預設值。 id 不會生成值,因為 URL 路徑中沒有相應的段。 / 僅在存在 HomeController 和 Index 操作時匹配: public class HomeController : Controller { public IActionResult Index() { ... } } 使用前面的控制器定義和路由模板,為以下 URL 路徑運行 HomeController.Index 操作: /Home/Index/17 /Home/Index /Home / URL 路徑 / 使用路由模板預設 Home 控制器和 Index 操作。 URL 路徑 /Home 使用路由模板預設 Index 操作。 簡便方法 MapDefaultControllerRoute: app.MapDefaultControllerRoute(); 替代: app.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); 屬性路由 // 添加控制器與視圖服務 builder.Services.AddControllersWithViews(); // ....上面省略一些代碼 // app.UseRouting(); // 此行代碼已經不需要了 app.MapControllers(); // 映射控制器 MapControllers 調用它來映射屬性路由控制器。 在以下示例中: HomeController 匹配一組類似於預設傳統路由 {controller=Home}/{action=Index}/{id?} 匹配的 URL。 public class HomeController : Controller { [Route("")] [Route("Home")] [Route("Home/Index")] [Route("Home/Index/{id?}")] public IActionResult Index(int? id) { return ControllerContext.MyDisplayRouteInfo(id); } [Route("Home/About")] [Route("Home/About/{id?}")] public IActionResult About(int? id) { return ControllerContext.MyDisplayRouteInfo(id); } } 將針對任意 URL 路徑 /、/Home、/Home/Index 或 /Home/Index/3 執行 HomeController.Index 操作。 此示例重點介紹屬性路由與傳統路由之間的主要編程差異。 屬性路由需要更多輸入才能指定路由。 傳統預設路由會更簡潔地處理路由。 但是,屬性路由允許並需要精確控制應用於每項操作的路由模板。 對於屬性路由,控制器和操作名稱在操作匹配中不起作用,除非使用標記替換。 以下示例匹配與上一個示例相同的 URL: public class MyDemoController : Controller { [Route("")] [Route("Home")] [Route("Home/Index")] [Route("Home/Index/{id?}")] public IActionResult MyIndex(int? id) { return ControllerContext.MyDisplayRouteInfo(id); } [Route("Home/About")] [Route("Home/About/{id?}")] public IActionResult MyAbout(int? id) { return ControllerContext.MyDisplayRouteInfo(id); } } 以下代碼對 action 和 controller 使用標記替換: public class HomeController : Controller { [Route("")] [Route("Home")] [Route("[controller]/[action]")] public IActionResult Index() { return ControllerContext.MyDisplayRouteInfo(); } [Route("[controller]/[action]")] public IActionResult About() { return ControllerContext.MyDisplayRouteInfo(); } } 以下代碼將 [Route("[controller]/[action]")] 應用於控制器: [Route("[controller]/[action]")] public class HomeController : Controller { [Route("~/")] [Route("/Home")] [Route("~/Home/Index")] public IActionResult Index() { return ControllerContext.MyDisplayRouteInfo(); } public IActionResult About() { return ControllerContext.MyDisplayRouteInfo(); } }
在前面的代碼中,Index
方法模板必須將 /
或 ~/
預置到路由模板。 應用於操作的以 /
或 ~/
開頭的路由模板不與應用於控制器的路由模板合併。
有關路由模板選擇的信息,請參閱
路由約束(可選)
路由約束在傳入 URL 發生匹配時執行,URL 路徑標記為路由值。 路徑約束通常檢查通過路徑模板關聯的路徑值,並對該值是否為可接受做出對/錯決定。 某些路由約束使用路由值以外的數據來考慮是否可以路由請求。 例如,
警告
請勿將約束用於輸入驗證。 如果約束用於輸入驗證,則無效的輸入將導致 404
(找不到頁面)響應。 無效輸入可能生成包含相應錯誤消息的 400
錯誤請求。 路由約束用於消除類似路由的歧義,而不是驗證特定路由的輸入。
下表演示示例路由約束及其預期行為:
約束 | 示例 | 匹配項示例 | 說明 |
---|---|---|---|
int |
{id:int} |
123456789 , -123456789 |
匹配任何整數 |
bool |
{active:bool} |
true , FALSE |
匹配 true 或 false 。 不區分大小寫 |
datetime |
{dob:datetime} |
2016-12-31 , 2016-12-31 7:32pm |
在固定區域性中匹配有效的 DateTime 值。 請參閱前面的警告。 |
decimal |
{price:decimal} |
49.99 , -1,000.01 |
在固定區域性中匹配有效的 decimal 值。 請參閱前面的警告。 |
double |
{weight:double} |
1.234 , -1,001.01e8 |
在固定區域性中匹配有效的 double 值。 請參閱前面的警告。 |
float |
{weight:float} |
1.234 , -1,001.01e8 |
在固定區域性中匹配有效的 float 值。 請參閱前面的警告。 |
guid |
{id:guid} |
CD2C1638-1638-72D5-1638-DEADBEEF1638 |
匹配有效的 Guid 值 |
long |
{ticks:long} |
123456789 , -123456789 |
匹配有效的 long 值 |
minlength(value) |
{username:minlength(4)} |
Rick |
字元串必須至少為 4 個字元 |
maxlength(value) |
{filename:maxlength(8)} |
MyFile |
字元串不得超過 8 個字元 |
length(length) |
{filename:length(12)} |
somefile.txt |
字元串必須正好為 12 個字元 |
length(min,max) |
{filename:length(8,16)} |
somefile.txt |
字元串必須至少為 8 個字元,且不得超過 16 個字元 |
min(value) |
{age:min(18)} |
19 |
整數值必須至少為 18 |
max(value) |
{age:max(120)} |
91 |
整數值不得超過 120 |
range(min,max) |
{age:range(18,120)} |
91 |
整數值必須至少為 18,且不得超過 120 |
alpha |
{name:alpha} |
Rick |
字元串必須由一個或多個字母字元組成,a -z ,並區分大小寫。 |
regex(expression) |
{ssn:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)} |
123-45-6789 |
字元串必須與正則表達式匹配。 請參閱有關定義正則表達式的提示。 |
required |
{name:required} |
Rick |
用於強制在 URL 生成過程中存在非參數值 |
警告
如果使用
可向單個參數應用多個用冒號分隔的約束。 例如,以下約束將參數限製為大於或等於 1 的整數值:
[Route("users/{id:int:min(1)}")] public User GetUserById(int id) { }
警告
驗證 URL 的路由約束並將轉換為始終使用固定區域性的 CLR 類型。 例如,轉換為 CLR 類型
int
或DateTime
。 這些約束假定 URL 不可本地化。 框架提供的路由約束不會修改存儲於路由值中的值。 從 URL 中分析的所有路由值都將存儲為字元串。 例如,float
約束會嘗試將路由值轉換為浮點數,但轉換後的值僅用來驗證其是否可轉換為浮點數。
疑惑解答:
1. 當訪問一個Web 應用地址時,Asp.Net Core 是怎麼執行到Controller
的Action
的呢?
答:程式啟動的時候會把所有的Controller 中的Action 映射存儲到routeOptions
的集合中,Action 映射成Endpoint
終結者 的RequestDelegate
委托屬性,最後通過UseEndPoints
添加EndpointMiddleware
中間件進行執行,同時這個中間件中的Endpoint
終結者路由已經是通過Rouing
匹配後的路由。
2. EndPoint
跟普通路由又存在著什麼樣的關係?
答:Ednpoint
終結者路由是普通路由map 轉換後的委托路由,裡面包含了路由方法的所有元素信息EndpointMetadataCollection
和RequestDelegate
委托。
3. UseRouing()
、UseAuthorization()
、UseEndpoints()
這三個中間件的關係是什麼呢?
答:UseRouing
中間件主要是路由匹配,找到匹配的終結者路由Endpoint
;UseEndpoints
中間件主要針對UseRouing
中間件匹配到的路由進行 委托方法的執行等操作。 UseAuthorization
中間件主要針對 UseRouing
中間件中匹配到的路由進行攔截 做授權驗證操作等,通過則執行下一個中間件UseEndpoints()
,具體的關係可以看下麵的流程圖:
上面流程圖中省略了一些部分,主要是把UseRouing 、UseAuthorization 、UseEndpoint 這三個中間件的關係突顯出來。
2. 異常中間件
UseExceptionHandler : 將中間件添加到管道,該中間件將捕獲異常,記錄異常,併在備用管道中重新執行請求。如果響應已啟動,則不會重新執行請求。
UseDeveloperExceptionPage: 從管道捕獲同步和非同步
if (!app.Environment.IsDevelopment()) // 非開發環境下,可以顯示自定義錯誤頁 { app.UseExceptionHandler("/Home/Error"); } else { app.UseDeveloperExceptionPage(); // 開發人員錯誤頁 }
3. 靜態資源中間件
預設情況下,靜態文件(如 HTML、CSS、圖像和 JavaScript)是 ASP.NET Core 應用直接提供給客戶端的資產。
靜態文件存儲在項目的
Web 應用程式項目模板包含 wwwroot
文件夾中的多個文件夾:
-
wwwroot
-
css
樣式文件 -
js
腳本文件 -
lib
第三方前端庫 -
images
圖片文件
-
預設 Web 應用模板在 Program.cs
中調用