翻譯自:https://docs.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-5.0ios
路由負責匹配 Http 請求,而後分發這些請求到應用程序的最終執行點。Endpoints 是應用程序可執行請求處理的代碼單元。Endpoints 在應用程序中定義並在應用程序啓動的時候配置。git
Endpoint 匹配處理能夠從請求的 URL 中提出值和爲請求處理提供值。使用從應用程序獲取的 Endpoint 信息,路由也能夠生成匹配 Endpoint 的 URLS。github
應用能夠經過如下方式配置路由:正則表達式
這篇文檔涵蓋了ASP.NET Core 路由的底層詳情。算法
這篇文檔中描述的 Endpoint 路由系統適用於 ASP.NET Core 3.0 或者更新的版本。數據庫
路由基礎express
全部的 ASP.NET Core 模板代碼中都包含路由。路由在 Startup.Configure 中註冊在中間件管道中。編程
下面代碼展現了一個路由的示例:json
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.MapGet("/", async context => { await context.Response.WriteAsync("Hello World!"); }); }); }
路由使用了一對中間件,經過 UseRouting 和 UseEndPoints 註冊:api
前面這個示例包含了一個單獨的路由到代碼的 Endpoint 使用 MapGet 方法:
Endpoint
MapGet 方法用來定義一個 Endpoint。一個 endpoint 能夠是如下狀況:
在 UseEndpoints 中配置的 Endpoints 能夠被 APP 匹配和執行。例如,MapGet, MapPost, 和一些相似於鏈接請求代理到路由系統的方法。更多的方法能夠被用於鏈接 ASP.NET Core 框架的特性到路由系統中:
下面這個例子展現了一個路由一個比較複雜的路由模板:
app.UseEndpoints(endpoints => { endpoints.MapGet("/hello/{name:alpha}", async context => { var name = context.Request.RouteValues["name"]; await context.Response.WriteAsync($"Hello {name}!"); }); });
字符串 /hello/{name:alpha} 是一個路有模板。被用來配置 endpoint 如何被匹配到。在這個例子中,模板匹配如下狀況:
{name:alpha}: 上面 URL 路徑中的第二段:
當前文檔中描述的 endpoint 路由系統是在 ASP.NET Core 3.0 中新添加的。然而,全部版本的 ASP.NET Core 都支持一樣的路由模板特性和路由約束的集合。
下面的示例展現了帶有 health checks 和 受權的路由:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } // Matches request to an endpoint. app.UseRouting(); // Endpoint aware middleware. // Middleware can use metadata from the matched endpoint. app.UseAuthentication(); app.UseAuthorization(); // Execute the matched endpoint. app.UseEndpoints(endpoints => { // Configure the Health Check endpoint and require an authorized user. endpoints.MapHealthChecks("/healthz").RequireAuthorization(); // Configure another endpoint, no authorization requirements. endpoints.MapGet("/", async context => { await context.Response.WriteAsync("Hello World!"); }); }); }
上面這個示例展現瞭如何:
MapHealthChecks 添加了一個 health check endpoint。接着又調用了 RequireAuthorization 附加了一個受權策略到這個 endpoint 上。
UseAuthentication 和 UseAuthorization 添加認證和受權中間件。這些中間件在 UseRouting 和 UseEndpoints 中間調用,所以能夠:
在前面這個例子中,有兩個 endpoints,可是隻有 health check 附加了一個受權策略。若是請求匹配了 health check, /healthz,受權檢查就會被執行。這說明 endpoints 能夠有額外的數據附加到他們上面。這寫額外的數據叫作 endpoint metadata:
路由系統經過添增強大的 endpoint 概念創建在中間件管道之上。Endpoints 表明了一組應用程序的功能,這些功能和路由,受權和 ASP.NET Core 核心系統功能是不一樣的。
ASP.NET Core endpoint:
下面的代碼展現瞭如何獲取和檢查匹配當前請求的 endpoint:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseRouting(); app.Use(next => context => { var endpoint = context.GetEndpoint(); if (endpoint is null) { return Task.CompletedTask; } Console.WriteLine($"Endpoint: {endpoint.DisplayName}"); if (endpoint is RouteEndpoint routeEndpoint) { Console.WriteLine("Endpoint has route pattern: " + routeEndpoint.RoutePattern.RawText); } foreach (var metadata in endpoint.Metadata) { Console.WriteLine($"Endpoint has metadata: {metadata}"); } return Task.CompletedTask; }); app.UseEndpoints(endpoints => { endpoints.MapGet("/", async context => { await context.Response.WriteAsync("Hello World!"); }); }); }
若是 endpoint 被選中了,那麼能夠從 HttpContext 中獲取到。它的屬性能夠被檢測到。Endpoint 對象是不可變的,建立以後就不能夠修改了。最多見的 endpoint 類型是 RouteEnpoint。RouteEndpoint 包含了它能夠被路由系統選擇的信息。
在前面的代碼中,app.Use 配置了一個行內的 middleware。
下面的代碼展現了因爲 app.Use 調用位置不一樣,可能就沒有一個 enpoint。
// Location 1: before routing runs, endpoint is always null here app.Use(next => context => { Console.WriteLine($"1. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}"); return next(context); }); app.UseRouting(); // Location 2: after routing runs, endpoint will be non-null if routing found a match app.Use(next => context => { Console.WriteLine($"2. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}"); return next(context); }); app.UseEndpoints(endpoints => { // Location 3: runs when this endpoint matches endpoints.MapGet("/", context => { Console.WriteLine( $"3. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}"); return Task.CompletedTask; }).WithDisplayName("Hello"); }); // Location 4: runs after UseEndpoints - will only run if there was no match app.Use(next => context => { Console.WriteLine($"4. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}"); return next(context); });
上面示例添加了 Console.WriteLine 語句,顯示了是否一個 endpoint 被選中。爲了清晰,示例中爲 / endpoint 增長了名稱顯示。
運行這段代碼,訪問 /,將會顯示:
1. Endpoint: (null) 2. Endpoint: Hello 3. Endpoint: Hello
若是訪問其餘 URL,則會顯示:
1. Endpoint: (null) 2. Endpoint: (null) 4. Endpoint: (null)
輸出結果說明了:
UseRouting 中間件使用 SetEndpoint 方法把 endpoint 附加到當前請求上下文。可使用自定義的邏輯替換掉 UseRouting 而且使用 endpoint 好處。Endpoints 是和中間件相似的低級別的原語,不和路由的實現耦合在一塊兒。大多數的應用程序不須要自定義邏輯替換 UseRouting。
UseEndpoints 中間件被設計用來和 UseRouting 中間件配合使用。執行一個 endpoint 的核心邏輯並不複雜。 使用 GetEndpoint 獲取 endpoint,而後調用它的 RequestDelegate 屬性。
下面的代碼展現了中間件如何對路由產生影響或者作出反應:
public class IntegratedMiddlewareStartup { public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } // Location 1: Before routing runs. Can influence request before routing runs. app.UseHttpMethodOverride(); app.UseRouting(); // Location 2: After routing runs. Middleware can match based on metadata. app.Use(next => context => { var endpoint = context.GetEndpoint(); if (endpoint?.Metadata.GetMetadata<AuditPolicyAttribute>()?.NeedsAudit == true) { Console.WriteLine($"ACCESS TO SENSITIVE DATA AT: {DateTime.UtcNow}"); } return next(context); }); app.UseEndpoints(endpoints => { endpoints.MapGet("/", async context => { await context.Response.WriteAsync("Hello world!"); }); // Using metadata to configure the audit policy. endpoints.MapGet("/sensitive", async context => { await context.Response.WriteAsync("sensitive data"); }) .WithMetadata(new AuditPolicyAttribute(needsAudit: true)); }); } } public class AuditPolicyAttribute : Attribute { public AuditPolicyAttribute(bool needsAudit) { NeedsAudit = needsAudit; } public bool NeedsAudit { get; } }
上面的示例展現了兩個重要的概念;
上面的代碼展現了一個自定義的支持爲每個 endpoint 添加策略的 endpoint。這個中間件輸出訪問敏感數據的 audit log 到控制檯。這個中間件可使用 AuditPolicyAttribute metadata 配置爲一個 audit enpoint。這個示例展現了一個選擇模式,只有 enpoints 被標記爲敏感的纔會被驗證。也能夠反向定義邏輯,例如驗證沒有被標記爲安全的一切。endpoint metadata 系統是靈活的。邏輯能夠被設計爲任何符合使用狀況的方式。
上面的示例代碼是爲了展現 endpoints 的基本概念。示例不是爲了用於生產環境。一個更完整的 audit log 中間件應該是這樣的:
audit 策略 metadata AuditPolicyAttribute 被定義爲一個 Attribute 是爲了在一個 class-based 的 framework 中更加容易使用,例如 controllers 和 SignalR。當使用路由編碼時:
對於 metadata 類型的最佳實踐是把它們定義爲接口或者屬性。接口和屬性容許代碼複用。metadata 系統是靈活的而且不強加任何限制。
下面的代碼展現了使用中間件和使用路由的差異:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } // Approach 1: Writing a terminal middleware. app.Use(next => async context => { if (context.Request.Path == "/") { await context.Response.WriteAsync("Hello terminal middleware!"); return; } await next(context); }); app.UseRouting(); app.UseEndpoints(endpoints => { // Approach 2: Using routing. endpoints.MapGet("/Movie", async context => { await context.Response.WriteAsync("Hello routing!"); }); }); }
中間件的方式展現的方法1:一個終端中間件。被叫作終端中間件是由於它匹配了如下操做:
被叫作終端中間件是由於它終結了搜索,執行了一些操做,而後就返回了
比較一個終端中間件和路由:
一個 endpoint 定義了:
終結中間件能夠是一個有效的工具,可是要求:
在編寫一個終結中間件以前,應該優先考慮使用集成到路由
現有的集成了 Map 或者 MapWhen 的終結中間件一般能夠在一個路由中實現 endpoint。MapHealthChecks 展現了 router-ware 的模型:
下面的代碼展現了 MapHealthChecks 的使用:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } // Matches request to an endpoint. app.UseRouting(); // Endpoint aware middleware. // Middleware can use metadata from the matched endpoint. app.UseAuthentication(); app.UseAuthorization(); // Execute the matched endpoint. app.UseEndpoints(endpoints => { // Configure the Health Check endpoint and require an authorized user. endpoints.MapHealthChecks("/healthz").RequireAuthorization(); // Configure another endpoint, no authorization requirements. endpoints.MapGet("/", async context => { await context.Response.WriteAsync("Hello World!"); }); }); }
上面的代碼展現了爲何返回一個建立後的對象是重要的。返回一個建立的對象運行應用程序開發者去配置策略,例如 endpoint 受權策略。在這個例子中,health check 中間件沒有直接集成受權系統。
metadata 系統的建立是爲了響應擴展性做者在使用終結中間件時所遇到的問題。對於每個中間件,它是不肯定的,是爲了實現中間件本身帶有受權系統的集成。
當一個路由中間件執行的時候,它設置一個 Endpoint 而且設置路由的值到一個從當前請求中獲取的一個 HttpContext 的請求特性中:
運行在路由中間件以後的中間件能夠檢測 endpoint 後決定執行的操做。舉個例子,一個受權中間件能夠查詢 endpoint 的 metadata 集合實現受權策略。在請求處理管道中的全部中間件執行完畢後,被選擇的 endpoint 的代理被調用。
基於 enpoint 路由的路由系統負責全部的請求分發的決定。由於中間件應用策略是基於被選擇的 endpoint,這是很重要的:
警告:爲了向後兼容,當一個控制器或者 Razor 頁面 endpoint 代理被執行的時候,目前基於執行的請求處理 RouteContext.RouteData 被設置爲合適的值。
RouteContext 類型在之後的版本中將被標記爲廢棄的在:
URL 匹配操做在一個可配置的分段集合中。在每個分段中,輸出是匹配的集合。經過下一個分段,匹配的集合會逐步縮小。路由的實現不保證匹配 endpoints 的處理順序。全部可能的匹配一次就會處理。URL的匹配遵循如下順序。
ASP.NET Core:
enpoints 列表的優先級遵循如下原則:
全部匹配的 endpoints 在每一個階段直到 EndpointSelector 執行。 EnpointSelector 是最後一個階段。它從全部匹配的 endpoints 中選擇優先級最高的 endpoint 做爲最佳匹配。若是有一樣優先級的匹配,一個模糊匹配的異常將會拋出。
路由的優先級是基於一個更加具體的路由模板被計算出來被賦予一個更高的優先級。例如,比較一個兩個路由模板 /hello 和 /{message}
一般的,在實際中,路由優先級已經作了一個從各類各樣的 URL schemes 中選擇最佳匹配的很好的工做。僅僅在爲了不歧義的時候使用 Order。
因爲路由提供了各類各樣類型的擴展性,路由系統不太可能花費大量的時間去計算有歧義的路由。考慮一下像 /{message:alpha} 和 /{message:int} 這兩個路由模板:
警告:
UseEnpoints 中的操做的順序不會影響路由的行爲,但有一個例外。 MapControllerRoute 和 MapAreaRoute 自動的會基於它們被調用的順序賦值一個排序的值給它們的 enpoints。這模擬了控制器的長期行爲,而這些控制器沒有路由器提供和舊的路由實現相同的保證。
在舊的路由實現中,是能夠實現依賴路由處理順序的擴展。ASP.NET Core 以及更新的版本中的 endpoint 路由:
路由模板優先是基於如何具體化一個路由模板,並給它賦予一個值得系統。路由模板優先級:
例如,模板 /Products/List 和 /Products/{id}。對於 URL path,系統將會認爲 /Products/List 比/Produts/{id} 更加匹配。這是由於字面值段 /List 被認爲比參數 /{id} 有更高的優先級。
優先級工做原理和路由模板如何定義相結合的詳情以下:
URL 生成:
Endpoint 裸遊包含 LinkGenerator API。LInkGenerator 做爲一個單利服務從依賴注入中獲取。LinkGenerator API 能夠在正在執行的請求的上下文以外執行。Mvc.IUrlHelper 和 scenarios 依賴於 IUrlHelper,例如 Tag Helpers,HTML Helpers,以及 Action Results,在內部使用 LinkGenerator API 提供生成連接的功能。
路由生成器由地址和地址架構的概念的支持。一個地址架構是一種決定哪些 endpoints 應該被用來生成連接的方式。例如,許多用戶熟悉的從控制器獲取路由名稱和路由值以及 Razor Pages 被用來實現做爲一種地址架構。
連接生成器能夠連接到控制器和 Razor Pages 經過如下擴展方法:
這些方法的重載的參數包含 HttpContext。這些方法在功能上等同於 Url.Action 和 Url.Page,可是提供更多的靈活性和選擇。
GetPath* 之類的方法和 Url.Action 以及 Url.Page 很類似,它們生成的 URI 包含一個絕對路徑。GetUrl* 方法老是生成一個包含一個架構和主機的絕對 URI。接受參數 HttpContext 參數的方法在正在執行的請求的 Context 中生成一個 URI。除非重寫,不然路由值將使用當前正在執行的請求中的 URI base path,架構以及主機。
LinkGenerator 被地址調用。生成一個 URI 在如下兩個步驟中出現:
LinkGenerator 提供的方法支持生成任何類型的標準的連接的能力。使用 link generator 最方便的方式是經過那些爲特定地址類型操做的擴展方法:
GetPathByAddress 基於提供的值生成一個絕對路徑的 URI
GetUriByAdderss 基於提供的值生成一個絕對的 URI
⚠️ 警告:
注意調用 LinkGenerator 會有如下影響:
在下面的例子中,一箇中間件使用了 LinkGenerator API 爲一個列出存儲產品的方法建立一個連接。經過注入 link generator 到一個類中,而後在任何一個應用程序中的類均可以調用 GenerateLink:
public class ProductsLinkMiddleware { private readonly LinkGenerator _linkGenerator; public ProductsLinkMiddleware(RequestDelegate next, LinkGenerator linkGenerator) { _linkGenerator = linkGenerator; } public async Task InvokeAsync(HttpContext httpContext) { var url = _linkGenerator.GetPathByAction("ListProducts", "Store"); httpContext.Response.ContentType = "text/plain"; await httpContext.Response.WriteAsync($"Go to {url} to see our products."); } }
{} 中的符號定義了路由匹配時綁定的路由參數。能夠在路由分段中定義多個路由參數,可是路由參數必須由字面值分割開來。例如, {controller=Home}{action=Index} 不是一個有效的路由,因爲在 {controller} 和 {action} 之間沒有字面值。路由參數必須有一個名稱,也可能有更多指定的屬性。
字面值而不是路由參數(例如 {id}) 和路徑分隔符 / 必須匹配 URL 中的文本。文本匹配區分大小寫而且基於 URL's 路由的編碼。根據定界符 { 或者 } 匹配一個字面上的路由參數,經過重複字符轉義定界符。例如: {{ 或者 }}
星號 * 或者 兩個星號 **:
Catch-all 參數也能夠匹配空的字符串。
當路由被用來生成一個 URL,catch-all 參數會轉義合適的字符,包括路徑分隔符 /。例如,帶有參數 { path = "my/path" } 的路由 foo/{*path} 生成 foo/my%2Fpath。注意轉義的斜槓。爲了保留路徑分隔符,使用 ** 做爲路徑參數的前綴。路由 foo/{**path} 賦值 { path="my/path" } 生成 foo/my/path。
視圖捕捉一個帶有可選的文件擴展名的文件名稱的 URL 模式的時候,須要有更多的考慮。例如,模板 files/{filename}.{ext?}。當參數 filename 和 ext 都有值得時候,兩個值都會被填充。若是隻有 filename 的值存在於 URL 中,路由將會匹配,由於尾部的 . 這是就是可選的。下面的 URLs 會匹配這兩種路由:
路由參數能夠提供默認值,經過在參數名稱後面添加等號(=)給路由參數指定。例如, {controller=Home},爲 controller 指定了 Home 做爲默認的值。默認的值在 URL 中沒有爲參數提供值的時候使用。經過在路由參數名稱後面添加一個問號 (?) 來指定這個參數是可選的。例如,id?。可選參數和默認參數不一樣的是:
路由參數可能包含必須匹配綁定到 URL 中的路由值的約束。在路由參數名稱後面添加 : 和約束名稱在行內指定參數約束。若是約束要求帶有參數,它們被包括在圓括號(...)中跟在約束名稱後面。多個行內約束能夠經過添加另一個 : 和約束名稱來指定。
約束名稱和參數被傳遞給 IlnlineConstraintResolver 用來建立一個 IRouteConstratin 實例,這個實例在 URL 處理過程當中使用。例如,路由模板 blog/(article:minlength(10) 指定了一個值爲 10 的 minlength 約束。更多的關於路由約束和框架提供的一系列的約束,請查看 Route constraint reference 部分。
路由參數可能會有參數轉換。路由參數在生成連接和匹配方法以及pages到URLs的時候會轉換參數的值。和約束同樣,參數轉換能夠經過在路由參數名稱後面添加一個 : 和轉換名稱到路由參數。例如,路由模板 blog/{article:slugify} 指定一個名稱爲 slugify 的轉換。關於更過關於參數轉換的信息,請查看 Parameter transformer reference 部分。
下面的表格展現了路由模板和他們的行爲:
路由模板 | 匹配的 URI 示例 | 請求 URI... |
hello | /hello | 只匹配一個路徑 /hello |
{Page=Home} | / | 匹配而且設置 Page 爲 Home |
{Page=Home} | /Contact | 匹配而且設置 Page 爲 Contact |
{controller}/{action}/{id?} | /Products/List | 映射 Products 到 controller,List 到 action |
{controller}/{action}/{id?} | /Products/Details/123 | 映射 Products 到 controller,Details 到 action,id 的值被設置爲 123 |
{controller=Home}/{action=Index}/{id?} | / | 映射到 Home controller 和 Index 方法. id 參數被忽略 |
{controller=Home}/{action=Index}/{id?} | /Products | 映射到 Products controller 和 Index 方法,id 參數被忽略 |
一般的,使用模板是最簡單的獲取路由的方式。約束和默認值也能夠被指定在路由模板的外部。
複雜的分段是經過以非貪婪的方式從右到左匹配文字分割的方式來處理的。例如,[Route("/a{b}c{d}")] 是一個複雜的分段路由。複雜的分段以一種特殊的方式工做,必須理解以可以正確的使用它們。這部分的示例展現了爲何複雜的分段只有在分界文本不在參數值中才能正常工做的緣由。使用正則表達式,而後對於更加複雜的例子須要手動提取其中的值。
⚠️ 警告:
當使用 System.Text.RegularExpressions 處理不受信任的輸入的時候,傳入一個超時時間。一個惡意的用戶提供給 RegularExpressions 的輸入可能會引發 Denial-of-Service attack. ASP.NET Core 框架的 APIs 使用 RegularExpressions 的時候傳入了超時時間。
下面總結了路由處理模板 /a{b}c{d} 匹配 URL path /abcd 的步驟。| 用來使算法是怎麼工做的更加形象:
這裏舉例一個使用相同模板 /a{b}c{d},不一樣 URL 路徑匹配不成功的例子。| 用來更形象的展現算法的工做。這個例子使用一樣的算法解釋了沒有匹配成功:
因爲匹配算法是非貪婪的:
正則表達式提供了更多的匹配行爲。
貪婪匹配,也叫作懶匹配,匹配最大可能的字符串。非貪婪模式匹配最小的字符串。
路由約束在匹配入站 URL 和 URL path 被路由值標記進入的時候會執行。路由約束經過路由模板檢測路由值,而後確認值是否能夠被接受。一些路由約束使用路由值以外的數據去考慮是否一個請求能夠被路由。例如,HttpMethodRouteConstraint 能夠基於它的 HTTP 謂詞接受或者拒絕一個請求。約束被用於路由請求和連接生成中。
⚠️ 警告:
不要使用約束驗證輸入。若是約束被用於輸入驗證,無效的輸入將會致使 404 Not Found 被返回。無效的輸入應該產生一個帶有合適錯誤信息的 400 Bad Request。路由約束用來消除類似路由的歧義,而不是用來驗證一個特定的路由。
下面的表格展現了示例路由約束和它們指望的行爲:
約束 | 示例 | 匹配的示例 | 備註 |
int | {id:int} | 123456789,-123456789 | 匹配任何的整型數據 |
bool | {active:bool} | true,False | 匹配 true,false。不區分大小寫 |
datetime | {dob:datetime} | 2020-01-02,2020-01-02 14:27pm | 在固定區域中匹配一個有效的 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 的過程當中用來強制一個非空參數值 |
⚠️ 警告:
當使用 System.Text.RegularExpressions 處理不被信任的輸入時,傳入一個超時時間。一個惡意的用戶可能會提供一個引發 Denial-of-Service attack 的 RegularExpressions。ASP.NET Core 框架的 APIs 使用 RegularExpressions 時都會傳入一個超時時間。
多個冒號分隔符能夠應用於單個的參數。例如,下面的約束限制一個最小爲1的整型:
[Route("users/{id:int:min(1)}")] public User GetUserById(int id) {}
⚠️ 警告:
路由約束驗證 URL 而且老是使用固定的區域轉換爲一個 CLR 類型。例如,轉換爲 CLR 類型中的 int 或者 DateTime。這些約束假設 URL 不是本地化的。框架提供的路由約束不修改保存在路由值中的值。全部的路由值從 URL 中解析出來被保存爲字符串。例如,float 約束試圖轉換一個路由值爲 float 類型,可是轉換隻有在驗證能夠被轉換的時候纔會使用到。
⚠️ 警告:
當使用 System.Text.RegularExpressions 處理不被信任的輸入時,傳入一個超時時間。一個惡意的用戶可能會提供一個引發 Denial-of-Service attack 的 RegularExpressions。ASP.NET Core 框架的 APIs 使用 RegularExpressions 時都會傳入一個超時時間。
約束中的正則表達式可使用 regex(...) 在行內指定。 MapControllerRoute 一類的方法也接受對象字面值。若是使用了這種格式,字符串的值被解釋爲正則表達式。
下面的代碼使用了行內正則表達式約束:
app.UseEndpoints(endpoints => { endpoints.MapGet("{message:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)}", context => { return context.Response.WriteAsync("inline-constraint match"); }); });
下面的代碼使用了字面對象指定一個正則表達式約束:
app.UseEndpoints(endpoints => { endpoints.MapControllerRoute( name: "people", pattern: "People/{ssn}", constraints: new { ssn = "^\\d{3}-\\d{2}-\\d{4}$", }, defaults: new { controller = "People", action = "List", }); });
ASP.NET Core 框架在正則表達式的構造方法中添加了 RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant 參數。查看 RegexOptions 瞭解這些成員的描述。
正則表達式使用分隔符和符號和路由以及 C# 語言類似。正則表達式的符號必須被轉義。在約束行內使用正則表達式 ^\d{3}-\d{2}-\d{4}$,可使用如下任意一種方法:
爲了轉義路由參數分隔符 {,},[,],在表達式中使用重複的字符,例如,{{,}},[[,]]。下面的表格展現了正則表達式和它的轉義版本:
正則表達式 | 轉義後的正則表達式 |
^\d{3}-\d{2}-\d{4}$ | ^\\d{{3}}-\\d{{2}}-\\d{{4}}$ |
^[a-z]{2}$ | ^[[a-z]]{{2}}$ |
路由中的正則表達式常常是 ^ 字符開頭去匹配字符串的起始位置。正則表達式老是以 $ 結尾來匹配字符串的結尾。 ^ 和 $ 字符保證了正則表達式可以匹配所有的路由參數值。若是沒有 ^ 和 $,正則表達式匹配任意的子字符串,這不是咱們指望獲得的。下面的表格列出了一些例子,而且解釋了爲何可以匹配或者匹配失敗:
表達式 | 字符串 | 是否匹配 | 備註 |
[a-z]{2} | hello | YES | 子字符串匹配 |
[a-z]{2} | 123abc456 | YES | 子字符串匹配 |
[a-z]{2} | mz | YES | 匹配正則表達式 |
[a-z]{2} | MZ | YES | 不區分大小寫 |
^[a-z]{2}$ | hello | NO | 查看上面 ^ 和 $ |
^[a-z]{2}$ | 123abc456 | NO | 查看上面 ^ 和 $ |
更多關於正則表達式語法的信息,查看 .NET Framework Regular Expressions.
使用正則表達式能夠約束參數到一些已知的可能的值上面。例如,{action:regex(^(list|get|create)$)} 僅僅匹配 action 的路由值到 list,get 或者 create。若是傳遞到約束字典中,字符串 ^(list|get|create)$) 是等同的。傳入約束字典的約束若是不匹配任意一個一直的約束,那麼任然被認爲是一個正則表達式。使用模板傳入的約束若是不匹配任意一個已知的約束將不被認爲是正則表達式。
經過實現 IRouteConstraint 接口能夠建立自定義的路由約束。接口 IRouteConstraint 包含 Match,當知足約束的時候它會返回 true,不然返回 false。自定義約束不多被用到。在實現一個自定義約束以前,考慮更直接的方法,好比模型綁定。
ASP.ENT Core 約束文件夾提供了一個很好的建立約束的例子。例如,GuidRouteConstraint。
爲了使用一個自定義的 IRouteConstraint,路由約束的類型必須使用 ConstraintMap 在服務容器中註冊。一個 CostraintMap 是一個映射路由約束鍵值和 驗證這些約束 IRouteConstraint 實現的字典。應用程序的 ConstraintMap 能夠在 Startup.ConfigureServices中或者做爲 services.AddRouting 調用的一部分或者直接經過 services.Configure<RouteOptions>配置 RouteOptions 來更新。例如:
public void ConfigureServices(IServiceCollection services) { services.AddControllers(); services.AddRouting(options => { options.ConstraintMap.Add("customName", typeof(MyCustomConstraint)); }); }
上面添加的約束在下面的代碼中使用:
[Route("api/[controller]")] [ApiController] public class TestController : ControllerBase { // GET /api/test/3 [HttpGet("{id:customName}")] public IActionResult Get(string id) { return ControllerContext.MyDisplayRouteInfo(id); } // GET /api/test/my/3 [HttpGet("my/{id:customName}")] public IActionResult Get(int id) { return ControllerContext.MyDisplayRouteInfo(id); } }
MyDisplayRouteInfo 經過 Rick.Docs.Samples.RouteInf NuGet 包提供,用來顯示路由信息。
MyCustomConstraint 約束的實現阻止 0 被賦值給路由參數:
class MyCustomConstraint : IRouteConstraint { private Regex _regex; public MyCustomConstraint() { _regex = new Regex(@"^[1-9]*$", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase, TimeSpan.FromMilliseconds(100)); } public bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection) { if (values.TryGetValue(routeKey, out object value)) { var parameterValueString = Convert.ToString(value, CultureInfo.InvariantCulture); if (parameterValueString == null) { return false; } return _regex.IsMatch(parameterValueString); } return false; } }
⚠️ 警告:
當使用 System.Text.RegularExpressions 處理不被信任的輸入時,傳入一個超時時間。一個惡意的用戶可能會提供一個引發 Denial-of-Service attack 的 RegularExpressions。ASP.NET Core 框架的 APIs 使用 RegularExpressions 時都會傳入一個超時時間。
前面的代碼:
下面的代碼展現了一個更好的禁止0賦值給 id 的處理過程:
[HttpGet("{id}")] public IActionResult Get(string id) { if (id.Contains('0')) { return StatusCode(StatusCodes.Status406NotAcceptable); } return ControllerContext.MyDisplayRouteInfo(id); }
上面的代碼比自定義的約束 MyCustomConstraint 有如下優點:
參數轉換:
例如,一個在模型 blog\{article:slugify} 中的自定義的 slugfy 參數轉換時使用 Url.Action(new { artical = "MyTestArtical" }) 生成 blog\my-test-artical。
考慮下面 IOutboundParameterTransformer 的實現:
public class SlugifyParameterTransformer : IOutboundParameterTransformer { public string TransformOutbound(object value) { if (value == null) { return null; } return Regex.Replace(value.ToString(), "([a-z])([A-Z])", "$1-$2", RegexOptions.CultureInvariant, TimeSpan.FromMilliseconds(100)).ToLowerInvariant(); } }
在參數模型中爲了使用一個參數轉換,須要在 Startup.ConfigureServices 中使用 ConstraintMap 配置。以下:
public void ConfigureServices(IServiceCollection services) { services.AddControllers(); services.AddRouting(options => { options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer); }); }
ASP.NET Core 框架在一個 enpoint 被解析到的時候會使用參數轉換去轉換 URI。例如,參數轉換會轉換被用來匹配一個 area, controller,action,和page 的路由值。
例如如下代碼:
routes.MapControllerRoute( name: "default", template: "{controller:slugify=Home}/{action:slugify=Index}/{id?}");
對於上面這個路由模板,方法 SubscriptionManagementController.GetAll 匹配了 URI /subscription-management/get-all。參數轉換並無更改用來生成一個連接的路由值。例如,Url.Action("GetAll", "SubscriptionManagement")
輸出 /subscription-management/get-all.
ASP.NET Core 提供了供生成的路由使用的參數轉換 API 的約定:
這部分包含了 URL 生成算法的參考。在實際中,大多複雜的 URL 生成使用控制器或者 Razor Pages。查看 routing in controllers 獲取更多信息。
URL 生成過程一開始調用 LinkGenerator.GetPathByAddress 或者一個類似的方法。這個方法提供一個地址,一組路由值和關於從 HttpContext 獲取到的當前請求的可選的信息。
第一步就是使用地址去解析一組候選的 endpoints,這些 endpoints 使用 IEndpointAddressScheme<TAddress> 去匹配地址的類型。
一旦根據地址架構獲取到了一組候選 endpoints,endpoints 將會被排序,而後迭代處理直到一個 URL 生成的操做成功。URL 生成不檢查歧義性,第一個返回的結果就是最終的結果。
跟蹤 URL 生成的第一步就是設置日誌等級由 Microsoft.AspNetCore.Routing 到 TRACE。LinkGenerator 記錄了不少關於對解決問題有用的處理過程的詳細信息。
查看 URL generation reference 關於 URL 生成的詳細信息。
地址的概念是 URL 生成用來綁定一個連接生成器中的一個調用到一組候選的 enpoints。
地址是隨兩個默認實現擴展出來的概念:
地址架構的做用是經過任意條件在地址和 enpoints 匹配之間創建關聯。
從當前請求中,路由從 HttpContext.Request.RouteValues 中獲取路由值。和當前請求關聯的值被稱爲環境值。爲了更清晰,文檔中把路由值中傳遞給方法的值稱爲顯式值。
下面的例子展現了環境值和顯式值。它提供了從當前請求中獲取的環境值和顯式值: { id = 17 }
public class WidgetController : Controller { private readonly LinkGenerator _linkGenerator; public WidgetController(LinkGenerator linkGenerator) { _linkGenerator = linkGenerator; } public IActionResult Index() { var url = _linkGenerator.GetPathByAction(HttpContext, null, null, new { id = 17, }); return Content(url); }
上面代碼:
下面的例子展現了不提供環境值,提供顯式值: { controller = "Home", action = "Subscribe", Id = 17 }:
public IActionResult Index2() { var url = _linkGenerator.GetPathByAction("Subscribe", "Home", new { id = 17, }); return Content(url); }
上面的方法返回 /Home/Subscribe/17
WidgetController 中下面的代碼返回 /Widget/Subscribe/17:
var url = _linkGenerator.GetPathByAction("Subscribe", null, new { id = 17, });
下面的代碼提供了從當前請求的環境值中獲取的控制器,顯式值: { action = "Edit", id = 17 }:
public class GadgetController : Controller { public IActionResult Index() { var url = Url.Action("Edit", new { id = 17, }); return Content(url); }
在上面的代碼中:
下面的代碼提供了從當前請求中獲取的環境值,以及顯式值: { page = ".Edit", id = 17 }:
public class IndexModel : PageModel { public void OnGet() { var url = Url.Page("./Edit", new { id = 17, }); ViewData["URL"] = url; } }
上面的代碼在當 Edit Razor Page 包含如下指令的時候會設置 url 爲 /Edit/17:
@page "{id:int}"
若是 Edit page 不包含 "{id:int}" 路由模板,url 就是 /Edit?id=17.
MVC 的 IUrlHelper 又增長了一層複雜性,除了下面描述的規則外:
用戶老是對環境值的詳細行爲感到驚訝,由於 MVC 彷佛不跟隨它本身的規則。因爲歷史和兼容性的緣由,肯定的路由值,例如 action,controller,page 和 handler 都有它們本身特定的行爲。
LinkGenerator.GetPathByAction 和 LinkGenerator.GetPathByPage 提供的相同功能爲了兼容性複製了 IUrlHelper 的這些異常。
一旦一組候選的 enpoints 被發現了,接下來就是 URL 生成算法:
處理過程的第一不叫作路由值驗證。路由值驗證經過路由決定從環境值獲取到的路由值哪一個應該被使用和哪一個應該被忽略。每個環境值都會被考慮是結合顯示值仍是被忽略。
理解環境值得最好方式是在一般狀況下認爲它試圖節省應用程序開發者的輸入。傳統的,環境值使用的場景對相關的 MVC 是很是有用的:
調用 LinkGenerator 或者 IUrlHelper 返回 null 的狀況一般是因爲沒有經過路由值驗證。調試路由值驗證,能夠經過顯式指定更多的路由值來查看問題是否解決。
路由值無效的前提是假設應用程序 URL 架構是分層的,擁有一個從左到右的分層結構。考慮一個基本的路由模板 {controller}/{action}/{id?} 能夠直觀的感覺在實際中它是怎麼工做的。對一個值的更改會使得出如今右邊的全部路由值失效。這反映了關於層次結構的假設。若是應用程序中 id 有一個環境值,而且操做給控制器指定了一個不一樣的值:
一些示例展現了這個原則:
對於現存的屬性路由和專用常規路由,這一處理過程更加複雜。控制器常規路由,例如 {controller}/{action}/{id?} 使用路由參數指定了一個分層結構。 控制器和 Razor Pages 中的常規路由和屬性路由:
對於這些狀況, URL 生成定義了 required values 的概念。controllers 和 Razor Pages 建立的 endpoints 能夠指定容許路由值驗證工做的 required values。
路由值驗證算法的詳細信息:
這是,URL 生成操做已經準備好開始評估路由約束。接受的值的集合與提供給約束的默認的參數值相結合。若是約束所有經過,操做將會繼續。
下一步,被接受的參數能夠用來展開路由模板。路由模板的處理過程以下:
不匹配路由分段的顯式的值被添加到 query 字符串中。下面的表格展現了使用路由模板 {controller}/{action}/{id?} 的狀況:
環境值 | 顯式值 | 結果 |
controller = "Home" | action = "About" | /Home/About |
controller = "Home" | controller = "Order",action="About" | /Order/About |
controller="Home",color="Red" | action="About" | /Home/About |
controller="Home" | action="About",color="Red" | /Home/About?color=Red |
ASP.NET Core 3.0 中一些 URL 生成的架構在早期的 ASP.ENT Core 的版本中 URL 生成工做的並很差。ASP.NET Core 團隊計劃在將來的發佈版本中添加新的特性的需求。目前,最好的解決方法就是使用傳統路由。
下面的代碼展現了 URL 生成架構不被路由支持的示例:
app.UseEndpoints(endpoints => { endpoints.MapControllerRoute("default", "{culture}/{controller=Home}/{action=Index}/{id?}"); endpoints.MapControllerRoute("blog", "{culture}/{**slug}", new { controller = "Blog", action = "ReadPost", }); });
在上面的代碼中,culture 路由參數被用來本地化。指望的是 culture 參數老是被接受爲一個環境值。然而,culture 參數不被接受爲一個環境值,由於 required values 的工做方式。
下面的連接提供了配置 enpoint metadata 的更多信息:
RequireHost 應用一個約束到須要指定主機的路由。RequireHost 或者 [Host] 參數能夠是:
使用 RequireHost 或者 [Host] 能夠指定多個參數。匹配主機的約束驗證任意的參數。例如,[Host("domain.com","*domain.com")] 匹配 domain.com,www.domain.com 和 subdomain.domain.com。
下面的代碼使用 RequireHost 要求在路由中指定主機:
public void Configure(IApplicationBuilder app) { app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.MapGet("/", context => context.Response.WriteAsync("Hi Contoso!")) .RequireHost("contoso.com"); endpoints.MapGet("/", context => context.Response.WriteAsync("AdventureWorks!")) .RequireHost("adventure-works.com"); endpoints.MapHealthChecks("/healthz").RequireHost("*:8080"); }); }
下面的代碼在 controller 上使用 [Host] 屬性要求任意指定的主機:
[Host("contoso.com", "adventure-works.com")] public class ProductController : Controller { public IActionResult Index() { return ControllerContext.MyDisplayRouteInfo(); } [Host("example.com:8080")] public IActionResult Privacy() { return ControllerContext.MyDisplayRouteInfo(); } }
當 [Host] 屬性在 controller 和 action 方法上都應用了的狀況:
大部分的路由在 ASP.NET Core 3.0 被更新提升了性能。
當一個應用程序出現性能問題的時候,路由老是被懷疑是問題所在。路由被懷疑的緣由是像 controllers 和 Razor Pages 這樣的框架在它們的日誌信息中報告了在框架內部花費了大量的時間。當 controllers 報告的時間和請求的總的時間有很大不一樣的時候:
路由使用了成千上萬的 enpoints 來測試性能。一個典型的應用程序不太可能僅僅由於太大而遇到應能問題。路由性能緩慢最多見的根本緣由是因爲很差的自定義的中間件引發的。
下面的代碼展現了縮小延遲來源的基本技術:
public void Configure(IApplicationBuilder app, ILogger<Startup> logger) { app.Use(next => async context => { var sw = Stopwatch.StartNew(); await next(context); sw.Stop(); logger.LogInformation("Time 1: {ElapsedMilliseconds}ms", sw.ElapsedMilliseconds); }); app.UseRouting(); app.Use(next => async context => { var sw = Stopwatch.StartNew(); await next(context); sw.Stop(); logger.LogInformation("Time 2: {ElapsedMilliseconds}ms", sw.ElapsedMilliseconds); }); app.UseAuthorization(); app.Use(next => async context => { var sw = Stopwatch.StartNew(); await next(context); sw.Stop(); logger.LogInformation("Time 3: {ElapsedMilliseconds}ms", sw.ElapsedMilliseconds); }); app.UseEndpoints(endpoints => { endpoints.MapGet("/", async context => { await context.Response.WriteAsync("Timing test."); }); }); }
對於時間路由:
這是一種最基本的縮小延遲的方法,當延遲很嚴重的時候,例如超過 10ms。Time 2 減去 Time1 就是 UseRouting 中間件花費的時間。
相比前面的代碼,下面的代碼使用了一個更加緊湊的方法:
public sealed class MyStopwatch : IDisposable { ILogger<Startup> _logger; string _message; Stopwatch _sw; public MyStopwatch(ILogger<Startup> logger, string message) { _logger = logger; _message = message; _sw = Stopwatch.StartNew(); } private bool disposed = false; public void Dispose() { if (!disposed) { _logger.LogInformation("{Message }: {ElapsedMilliseconds}ms", _message, _sw.ElapsedMilliseconds); disposed = true; } } }
public void Configure(IApplicationBuilder app, ILogger<Startup> logger) { int count = 0; app.Use(next => async context => { using (new MyStopwatch(logger, $"Time {++count}")) { await next(context); } }); app.UseRouting(); app.Use(next => async context => { using (new MyStopwatch(logger, $"Time {++count}")) { await next(context); } }); app.UseAuthorization(); app.Use(next => async context => { using (new MyStopwatch(logger, $"Time {++count}")) { await next(context); } }); app.UseEndpoints(endpoints => { endpoints.MapGet("/", async context => { await context.Response.WriteAsync("Timing test."); }); }); }
下面列表展現了相比基本路由模板開銷更多的路由特性分析:
這部分包含了創建在路由之上的庫編寫者指南。這些細節目的是爲了保證應用程序的開發者在使用庫和框架擴展路由的時候能有一個好的體驗。
建立一個使用路由實現 URL 匹配的框架,開始須要定義一個創建在 UesEnpoints 之上的用戶體驗。
保證 在 IEndpointRouteBuilder 之上開始創建。這運行用戶把你的框架和其它 ASP.NET Core 特性很好的構造在一塊兒。每個 ASP.NET Core 模板都包含路由。假設路由已經存在而且對用戶來講很熟悉。
app.UseEndpoints(endpoints => { // Your framework endpoints.MapMyFramework(...); endpoints.MapHealthChecks("/healthz"); });
保證 調用 MapMyFramework(...) 返回一個具體的實現了 IEndpointConventionBuilder 的類型。大多數的框架 Map... 方法遵循這個模型。IEndpointConventionBuilder 接口;
聲明你本身的類型容許你添加你本身框架特有的功能到 builder 中。封裝一個框架聲明的 builder 而後去調用它是可行的。
app.UseEndpoints(endpoints => { // Your framework endpoints.MapMyFramework(...).RequireAuthorization() .WithMyFrameworkFeature(awesome: true); endpoints.MapHealthChecks("/healthz"); });
考慮編寫你本身的 EndpointDataSource. EndpointDataSource 用來聲明和更新 endpoints 集合的低級原語。EndpointDataSource 是一個功能強大的 API,被 controllers 和 Razor Pages 使用。
路由測試包含一個不更新 data source 的基本示例。
不要試圖默認註冊一個 EndpointDataSource。要求用戶在 UseEndpoint 中註冊你的框架。路由的哲學就是默認什麼都不包含, UseEndpoints 就是註冊 endpoints 的地方。
考慮定義 metadata 類型做爲一個接口
保證 可以在類和方法上面使用 metadata 類型。
public interface ICoolMetadata { bool IsCool { get; } } [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] public class CoolMetadataAttribute : Attribute, ICoolMetadata { public bool IsCool => true; }
像 controllers 和 Razor Pages 這樣的框架支持應用 metadata 屬性到類型和方法。若是你聲明瞭 metadata 類型:
聲明 metadata 類型爲一個接口增長了另一層靈活性:
保證 metadata 可以被重寫,就像下面展現的例子同樣:
public interface ICoolMetadata { bool IsCool { get; } } [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] public class CoolMetadataAttribute : Attribute, ICoolMetadata { public bool IsCool => true; } [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] public class SuppressCoolMetadataAttribute : Attribute, ICoolMetadata { public bool IsCool => false; } [CoolMetadata] public class MyController : Controller { public void MyCool() { } [SuppressCoolMetadata] public void Uncool() { } }
遵照這些指南的最好的方式是避免定義 maker metadata:
metadata 集合是根據優先級排序和支持重寫的。在控制器的狀況下,action 上的 metadata 是最肯定的。
保證 不論有沒有路由中間件都應該有用。
app.UseRouting(); app.UseAuthorization(new AuthorizationPolicy() { ... }); app.UseEndpoints(endpoints => { // Your framework endpoints.MapMyFramework(...).RequireAuthorization(); });
做爲這個指南的一個例子,考慮使用 UseAuthorization 中間件。authorization 中間件容許你傳遞一個反饋策略。若是反饋策略被指定了,將會應用到:
這使得 authorization 中間件在路由上下文以外也有做用。authorization 中間件能夠被用作傳統中間件編程。
對於更詳細的路由調試輸出,設置 Logging:LogLevel:Microsoft 爲 Debug。在開發環境中,在 appsettings.Development.json 中設置日誌等級:
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Debug", "Microsoft.Hosting.Lifetime": "Information" } } }