翻译自: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" } } }