路由
大约 4 分钟
路由
路由的概念
- 路由使用一对由 UseRouting 和 UseEndpoints 注册的中间件
UseRouting
向中间件管道添加路由匹配。 此中间件会查看应用中定义的终结点集,并根据请求选择最佳匹配。UseEndpoints
向中间件管道添加终结点执行。 它会运行与所选终结点关联的委托。- 框架默认会配置
UseRouting
和UseEndpoints
,如果显示配置,则可以实现控制运行顺序 - 终结点在
UseEndpoints
包括:
终结点定义
- 可执行:具有 RequestDelegate。
- 可扩展:具有元数据集合。
- Selectable: 可选择性包含路由信息。
- 可枚举:可通过检索 EndpointDataSource 来列出终结点集合。
原理演示代码
// Location 1: 调用 UseRouting 之前,终结点始终为 null
app.Use(async (context, next) =>
{
Console.WriteLine($"1. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
await next(context);
});
app.UseRouting();
// Location 2: 如果找到匹配项,则 UseRouting 和 UseEndpoints 之间的终结点为非 null
app.Use(async (context, next) =>
{
Console.WriteLine($"2. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
await next(context);
});
// Location 3: 如果找到匹配,作为终结点运行
app.MapGet("/", (HttpContext context) =>
{
Console.WriteLine($"3. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
return "Hello World!";
}).WithDisplayName("Location 3");
// 如果找到匹配项,则 UseEndpoints 中间件即为终端
app.UseEndpoints(_ => { }); // 如果没有此句,执行顺序为 1-2-4-3
// Location 4: 仅当找不到匹配项时才执行 UseEndpoints 后的中间件
app.Use(async (context, next) =>
{
Console.WriteLine($"4. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
await next(context);
});
/*
使用 / URL 运行此代码
1. Endpoint: (1 --> null)
2. Endpoint: Location 3
3. Endpoint: Location 3
使用任何其他 URL 运行此代码
1. Endpoint: (1 --> null)
2. Endpoint: (2 --> null)
4. Endpoint: (4 --> null)
*/
终结点用处
- 中间件可以在
UseRouting
之前运行,以修改路由操作的数据。 - 中间件可以在
UseRouting
和UseEndpoints
之间运行,以便在执行终结点前处理路由结果。
终端中间件与路由对比
- 不再调用 next 的中间件成为终端中间件
- 这两种方法都允许终止处理管道
- 终端中间件允许在管道中的任意位置放置中间件;终结点在 UseEndpoints 位置执行
- 使用带有终结点接口的中间件,比如
UseAuthorization
或UseCors
,采用终端中间件时需要与授权系统进行手动交互。
URL 匹配
- 路由将传入请求匹配到终结点的过程
- 当路由中间件执行时,它会设置
Endpoint
,并将值从当前请求路由到 HttpContext 上的Endpoint
:- 调用 GetEndpoint 获取终结点。
HttpRequest.RouteValues
将获取路由值的集合。
- 任何可能影响发送或安全策略都在路由系统中
- 根据以下内容设置终结点列表的优先级:
路由模板
- 如果路由找到匹配项,
{}
内的令牌定义绑定的路由参数,多个路由参数必须用文本值隔开,比如{controller=Home}{action=Index}
没有文本分隔不正确 - 文本匹配区分大小写,花括号需要使用
{{
或}}
转义 - 星号
*
或双星号**
称为 catch-all 参数。 例如,blog/{**slug}
匹配以blog/
开头并在其后面包含任何值 URI - 通过在参数名称的末尾附加问号 (
?
) 可使路由参数成为可选项, 例如,id?
- 路由参数可指定的默认值,例如,
{controller=Home}
- 路由约束,例如,
blog/{article:minlength(10)}
使用参数10
指定minlength
约束 - 参数转换器,例如,
blog/{article:slugify}
指定slugify
转换器 - 复杂段通过非贪婪的方式从右到左匹配文字进行处理。 例如,
[Route("/a{b}c{d}")]
属性路由
为 Controller 或者 Action 添加 [Route] 特性
属性路由会自动拼接 Controller 和 Action 的路由
[Route("api/[controller]")] [ApiController] public class Test2Controller : ControllerBase { [HttpGet] // GET /api/test2 public IActionResult ListProducts() { /* ... */} }
使用绝对路径可以拒绝拼接 Controller 的路由
[HttpGet(~/{id:int})]
所有 Http 谓词都是路由模板
属性路由支持标记替换
[controller]、[action]、[area]
同一个 Action 可以应用多个属性路由进行组合
属性路由可以设置可选参数、默认值和约束
路由中与 RequireHost 匹配的主机
- RequireHost 将约束应用于需要指定主机的路由。
app.MapGet("/", () => "Contoso").RequireHost("contoso.com");
路由组
var inner = outer.MapGroup("/inner");
inner.AddEndpointFilter((context, next) =>
{
app.Logger.LogInformation("/inner group filter");
return next(context);
});
路由性能
- 路由性能缓慢的最常见根本原因通常在于性能不佳的自定义中间件。
- 可能比较昂贵的路由功能:正则表达式、复杂段 (
{x}-{y}-{z}
)、同步数据访问 - 改善路由:
- 路由约束
- 将参数移动到模板中的后几段
- 使用动态路由并动态执行到控制器/页面的映射,
MapDynamicControllerRoute
和MapDynamicPageRoute