终结点路由源码解析
大约 11 分钟约 3309 字
终结点路由源码解析
简介
ASP.NET Core 的终结点路由(Endpoint Routing)是请求分发的核心机制。理解路由中间件的工作原理、路由匹配算法和 LinkGenerator 的实现,有助于优化路由性能和解决路由问题。
特点
路由管道原理
双中间件架构
// ASP.NET Core 路由使用两个中间件:
// 1. EndpointRoutingMiddleware — 匹配路由,设置 IEndpointFeature
// 2. EndpointMiddleware — 执行匹配到的终结点
// 内部流程:
// Request → EndpointRoutingMiddleware → 匹配 Endpoint → EndpointMiddleware → 执行 Endpoint
// EndpointRoutingMiddleware 核心逻辑(简化)
public class EndpointRoutingMiddleware
{
private readonly RequestDelegate _next;
private readonly EndpointDataSource _endpointDataSource;
private readonly IRouter _router;
public async Task Invoke(HttpContext httpContext)
{
// 从数据源获取所有终结点
var endpoints = _endpointDataSource.Endpoints;
// 遍历匹配
foreach (var endpoint in endpoints)
{
var routeEndpoint = endpoint as RouteEndpoint;
if (routeEndpoint == null) continue;
// 匹配路由模板
if (TryMatch(httpContext.Request.Path, routeEndpoint.RoutePattern, out var routeValues))
{
// 检查约束
if (routeEndpoint.Metadata.GetMetadata<IRouteConstraint>()?.Match(/*...*/) != false)
{
// 检查 HTTP 方法
var httpMethodMetadata = routeEndpoint.Metadata.GetMetadata<IHttpMethodMetadata>();
if (httpMethodMetadata?.HttpMethods.Contains(httpContext.Request.Method) != false)
{
// 设置匹配的终结点
httpContext.SetEndpoint(routeEndpoint);
httpContext.Request.RouteValues = routeValues;
break;
}
}
}
}
await _next(httpContext);
}
}
// EndpointMiddleware 核心逻辑
public class EndpointMiddleware
{
private readonly RequestDelegate _next;
public async Task Invoke(HttpContext httpContext)
{
var endpoint = httpContext.GetEndpoint();
if (endpoint != null)
{
// 执行终结点(Controller Action 或 RequestDelegate)
var requestDelegate = endpoint.RequestDelegate;
await requestDelegate!(httpContext);
}
else
{
// 未匹配到路由 → 404
httpContext.Response.StatusCode = 404;
await httpContext.Response.WriteAsync("Not Found");
}
await _next(httpContext);
}
}路由注册方式
// 1. Minimal API 路由
app.MapGet("/api/users", () => "Users");
app.MapPost("/api/users", (CreateUserRequest req) => Results.Created());
app.MapPut("/api/users/{id}", (int id, UpdateUserRequest req) => Results.Ok());
app.MapDelete("/api/users/{id}", (int id) => Results.NoContent());
// 2. Controller 路由
app.MapControllers(); // 自动发现 [Route] 属性
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
[HttpGet] // GET /api/orders
public IActionResult GetAll() => Ok();
[HttpGet("{id:int}")] // GET /api/orders/123(约束:int)
public IActionResult Get(int id) => Ok();
[HttpGet("{id}/items")] // GET /api/orders/123/items
public IActionResult GetItems(int id) => Ok();
}
// 3. 混合路由
app.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");路由模板与匹配
路由模板语法
// 路由模板语法:
// {param} — 必选参数
// {param?} — 可选参数
// {param=default}— 默认值
// {param:constraint} — 约束
// {*catchall} — 捕获所有剩余段
// 示例
app.MapGet("/products/{id:int}", (int id) => $"Product {id}");
app.MapGet("/posts/{year:int}/{month:int:range(1,12)}", (int year, int month) => $"Posts {year}/{month}");
app.MapGet("/files/{*filepath}", (string filepath) => $"File: {filepath}");
// /files/a/b/c.txt → filepath = "a/b/c.txt"
// 路由约束
// :int — 整数
// :double — 双精度
// :float — 浮点数
// :guid — GUID
// :bool — 布尔值
// :datetime — 日期时间
// :decimal — 十进制
// :length(n) — 长度等于 n
// :minlength(n) — 最小长度
// :maxlength(n) — 最大长度
// :range(min,max) — 范围
// :alpha — 纯字母
// :regex(pattern) — 正则匹配
// :required — 必填
// 自定义约束
public class ApiVersionConstraint : IRouteConstraint
{
public bool Match(HttpContext? httpContext, IRouter? route, string routeKey,
RouteValueDictionary values, RouteDirection routeDirection)
{
if (!values.TryGetValue(routeKey, out var value)) return false;
var version = value?.ToString();
return version is "v1" or "v2" or "v3";
}
}
// 注册约束
builder.Services.AddRouting(options =>
{
options.ConstraintMap.Add("apiVersion", typeof(ApiVersionConstraint));
});
// 使用
app.MapGet("/api/{version:apiVersion}/users", (string version) => $"API {version} Users");路由分组
MapGroup 模式
// .NET 7+ 路由分组
var api = app.MapGroup("/api");
api.MapGet("/users", () => "Users");
api.MapGet("/orders", () => "Orders");
// /api/users, /api/orders
// 嵌套分组
var v1 = app.MapGroup("/api/v1");
v1.MapGet("/users", () => "V1 Users");
var v2 = app.MapGroup("/api/v2");
v2.MapGet("/users", () => "V2 Users");
// 分组添加过滤器
var admin = app.MapGroup("/admin")
.RequireAuthorization("AdminPolicy")
.AddEndpointFilter(async (context, next) =>
{
// 管理员日志
return await next(context);
});
admin.MapGet("/dashboard", () => "Admin Dashboard");
admin.MapGet("/settings", () => "Admin Settings");
// 分组添加元数据
var tenant = app.MapGroup("/t/{tenantId}")
.AddEndpointFilter<TenantFilter>();
tenant.MapGet("/data", (string tenantId) => $"Data for {tenantId}");LinkGenerator
URL 生成器
// LinkGenerator — 根据路由名称和值生成 URL
public class EmailService
{
private readonly LinkGenerator _linkGenerator;
public EmailService(LinkGenerator linkGenerator)
{
_linkGenerator = linkGenerator;
}
public string GetConfirmationLink(int userId, string token)
{
// 按路由名称生成
return _linkGenerator.GetUriByName(
"ConfirmEmail",
values: new { userId, token })!;
}
}
// 注册路由名称
app.MapGet("/confirm/{userId}/{token}", (int userId, string token) =>
{
return $"Confirming {userId} with {token}";
}).WithName("ConfirmEmail");
// 按路由值生成
var url = _linkGenerator.GetPathByAction("Get", "Users", new { id = 123 });
// /api/users/123
// 在 Razor 中生成
// <a asp-action="Get" asp-controller="Users" asp-route-id="@Model.Id">View</a>性能优化
路由缓存
// 路由匹配优化策略:
// 1. 树形路由表 — 按路径段分层,快速排除不匹配的路由
// 2. 缓存终结点 — 匹配结果按 URL 缓存
// 3. 避免复杂正则约束
// 路由顺序控制
app.MapGet("/api/users/special", () => "Special"); // 先注册优先匹配
app.MapGet("/api/users/{id}", (string id) => $"User {id}");
// 或者使用 Order
app.MapGet("/api/users/{id}", (string id) => $"User {id}").WithOrder(2);
app.MapGet("/api/users/special", () => "Special").WithOrder(1); // 优先匹配
// 大量路由的性能建议:
// 1. 减少路由数量
// 2. 使用属性路由而非约定路由
// 3. 避免通配符路由
// 4. 使用 MapGroup 减少模板重复自定义约束与高级匹配
内置约束汇总
// ASP.NET Core 内置路由约束
// 路由模板中使用 {parameter:constraint}
//
// 类型约束:
// {id:int} — 整数
// {id:long} — 长整数
// {id:bool} — 布尔值
// {id:float} — 浮点数
// {id:double} — 双精度
// {id:decimal} — 十进制
// {id:guid} — GUID
// {id:datetime} — 日期时间
//
// 字符串约束:
// {name:alpha} — 仅字母
// {name:alpha(digit)} — 仅字母和数字
// {id:length(min,max)} — 长度范围
// {id:minlength(3)} — 最小长度
// {id:maxlength(10)} — 最大长度
// {id:min(1)} — 最小值
// {id:max(100)} — 最大值
// {id:range(1,100)} — 值范围
//
// 正则约束:
// {id:regex(^\\d{3}-\\d{4}$)} — 正则表达式
//
// 必选/可选:
// {id:required} — 必须提供(默认)
// {id?} — 可选参数
//
// 组合约束(用逗号分隔):
// {id:int:min(1):max(1000)} — 整数且在 1-1000 范围内
// 使用示例
app.MapGet("/api/products/{id:int:min(1)}", (int id) => $"Product {id}");
app.MapGet("/api/users/{name:alpha}", (string name) => $"User {name}");
app.MapGet("/api/orders/{code:regex(^ORD-\\d{6}$)}", (string code) => $"Order {code}");
app.MapGet("/api/files/{filename:length(1,100)}", (string filename) => $"File {filename}");自定义 IRouteConstraint
// 自定义约束:只允许偶数 ID
public class EvenIdConstraint : IRouteConstraint
{
public bool Match(
HttpContext? httpContext,
IRouter? route,
string routeKey,
RouteValueDictionary values,
RouteDirection routeDirection)
{
if (values.TryGetValue(routeKey, out var value) && value != null)
{
if (int.TryParse(value.ToString(), out var id))
{
return id % 2 == 0;
}
}
return false;
}
}
// 注册自定义约束
builder.Services.Configure<RouteOptions>(options =>
{
options.ConstraintMap.Add("even", typeof(EvenIdConstraint));
});
// 使用
app.MapGet("/api/items/{id:even}", (int id) => $"Even item {id}");
// /api/items/2 → 匹配
// /api/items/3 → 404
// 自定义约束:Slug 格式
public class SlugConstraint : IRouteConstraint
{
private static readonly Regex SlugRegex = new(
@"^[a-z0-9]+(?:-[a-z0-9]+)*$", RegexOptions.Compiled);
public bool Match(
HttpContext? httpContext, IRouter? route,
string routeKey, RouteValueDictionary values,
RouteDirection routeDirection)
{
if (values.TryGetValue(routeKey, out var value) && value != null)
{
return SlugRegex.IsMatch(value.ToString()!);
}
return false;
}
}
// 注册
builder.Services.Configure<RouteOptions>(options =>
{
options.ConstraintMap.Add("slug", typeof(SlugConstraint));
});
app.MapGet("/blog/{slug:slug}", (string slug) => $"Blog post: {slug}");参数转换器(Parameter Transformer)
// 参数转换器用于 URL 生成时格式化参数
// 和 URL 匹配时的反格式化
// 内置转换器:
// - SlugifyParameterTransformer: "SomeName" → "some-name"
// - LowercaseQueryStringsParameterTransformer: 查询字符串小写
// - UriLowerInvariantParameterTransformer: URI 小写
// 自定义转换器:将 PascalCase 转为 kebab-case
public class KebabCaseParameterTransformer : IOutboundParameterTransformer
{
public string? TransformOutbound(object? value)
{
if (value is null) return null;
return Regex.Replace(
value.ToString()!,
"([a-z])([A-Z])",
"$1-$2").ToLowerInvariant();
}
}
// 全局应用参数转换器
builder.Services.AddRouting(options =>
{
options.ConstraintMap["slug"] = typeof(SlugConstraint);
options.LowercaseUrls = true; // URL 小写
options.LowercaseQueryStrings = true; // 查询字符串小写
options.AppendTrailingSlash = false; // 不追加尾部斜杠
});
// 在 Controller 上应用
[Route("api/[controller]", Name = "products")]
[ApiController]
public class ProductsController : ControllerBase
{
[HttpGet("{categoryName}")]
public IActionResult GetByCategory(string categoryName)
{
return Ok(new { category = categoryName });
}
}
// 生成 URL 时自动转换
var url = linkGenerator.GetPathByName(
"products",
new { categoryName = "Electronics Deals" });
// → /api/products/electronics-deals路由分组与元数据
MapGroup 深入使用
// MapGroup 减少重复前缀和共享中间件
var apiGroup = app.MapGroup("/api/v1")
.WithTags("API v1")
.WithOpenApi();
// 认证分组
var authenticated = apiGroup.MapGroup("")
.RequireAuthorization();
// 角色分组
var adminOnly = authenticated.MapGroup("")
.RequireAuthorization("AdminPolicy")
.WithTags("Admin");
// 路由定义
apiGroup.MapGet("/health", () => "OK");
authenticated.MapGet("/profile", (ClaimsPrincipal user) =>
$"Hello, {user.Identity?.Name}");
adminOnly.MapDelete("/users/{id}", (int id) =>
$"Deleted user {id}");
// 速率限制分组
var rateLimited = apiGroup.MapGroup("")
.RequireRateLimiting("fixed");
rateLimited.MapPost("/orders", (Order order) => Results.Created());
// MapGroup 嵌套
var orders = apiGroup.MapGroup("/orders")
.WithTags("Orders")
.RequireAuthorization();
orders.MapGet("/", () => "Order list");
orders.MapGet("/{id:int}", (int id) => $"Order {id}");
var orderItems = orders.MapGroup("/{orderId:int}/items")
.WithTags("Order Items");
orderItems.MapGet("/", (int orderId) => $"Items for order {orderId}");
orderItems.MapPost("/", (int orderId, OrderItem item) => Results.Created());终结点元数据
// 自定义终结点元数据
[AttributeUsage(AttributeTargets.Method)]
public class ApiVersionAttribute : Attribute
{
public string Version { get; }
public bool Deprecated { get; set; }
public ApiVersionAttribute(string version)
{
Version = version;
}
}
[AttributeUsage(AttributeTargets.Method)]
public class ResponseCacheAttribute : Attribute
{
public int MaxAge { get; set; }
public bool MustRevalidate { get; set; }
}
// 在终结点上添加元数据
app.MapGet("/api/products/{id}", (int id) => $"Product {id}")
.WithMetadata(new ApiVersionAttribute("2.0") { Deprecated = false })
.WithMetadata(new ResponseCacheAttribute { MaxAge = 300, MustRevalidate = true });
// 读取终结点元数据(在中间件中)
app.Use(async (context, next) =>
{
var endpoint = context.GetEndpoint();
if (endpoint != null)
{
var version = endpoint.Metadata.GetMetadata<ApiVersionAttribute>();
var cache = endpoint.Metadata.GetMetadata<ResponseCacheAttribute>();
if (version != null)
{
context.Response.Headers["X-Api-Version"] = version.Version;
if (version.Deprecated)
{
context.Response.Headers["Warning"] = "299 - Deprecated API";
}
}
if (cache != null)
{
context.Response.Headers["Cache-Control"] =
$"max-age={cache.MaxAge}, must-revalidate";
}
}
await next();
});路由调试与诊断
路由匹配诊断
// 路由匹配日志中间件(排查路由问题)
app.Use(async (context, next) =>
{
var endpoint = context.GetEndpoint();
var logger = context.RequestServices.GetRequiredService<ILogger<Program>>();
if (endpoint != null)
{
logger.LogDebug(
"路由匹配: {Method} {Path} → {DisplayName}",
context.Request.Method,
context.Request.Path,
endpoint.DisplayName);
// 打印终结点所有元数据
foreach (var metadata in endpoint.Metadata)
{
logger.LogDebug(" 元数据: {Type}", metadata.GetType().Name);
}
}
else
{
logger.LogWarning(
"路由未匹配: {Method} {Path}",
context.Request.Method,
context.Request.Path);
}
await next();
});
// 启用详细的路由日志
builder.Logging.AddFilter("Microsoft.AspNetCore.Routing", LogLevel.Debug);动态路由注册
// 动态添加/删除路由(运行时)
public class DynamicRouteService
{
private readonly EndpointDataSource _dataSource;
public DynamicRouteService(EndpointDataSource dataSource)
{
_dataSource = dataSource;
}
public void ListAllEndpoints()
{
foreach (var endpoint in _dataSource.Endpoints)
{
var routeEndpoint = endpoint as RouteEndpoint;
if (routeEndpoint != null)
{
Console.WriteLine(
"路由: {Pattern} → {DisplayName} Order={Order}",
routeEndpoint.RoutePattern.RawText,
routeEndpoint.DisplayName,
routeEndpoint.Order);
}
}
}
}
// 使用动态终结点数据源
builder.Services.AddSingleton<DynamicRouteService>();优点
缺点
总结
终结点路由使用双中间件:EndpointRoutingMiddleware 匹配路由并设置终结点,EndpointMiddleware 执行匹配到的终结点。路由匹配基于模板模式匹配和约束检查。MapGroup 实现路由分组和公共元数据。自定义约束实现 IRouteConstraint 接口。LinkGenerator 根据路由名称或 Action/Controller 生成 URL。性能优化:路由按注册顺序匹配(先注册优先)、减少路由数量、避免复杂正则约束、使用 WithOrder 控制优先级。
关键知识点
- 先分清这个主题位于请求链路、后台任务链路还是基础设施链路。
- 服务端主题通常不只关心功能正确,还关心稳定性、性能和可观测性。
- 任何框架能力都要结合配置、生命周期、异常传播和外部依赖一起看。
- 框架能力的真正重点是它在请求链路中的位置和对上下游的影响。
项目落地视角
- 画清请求进入、业务执行、外部调用、日志记录和错误返回的完整路径。
- 为关键链路补齐超时、重试、熔断、追踪和结构化日志。
- 把配置与敏感信息分离,并明确不同环境的差异来源。
- 画清执行顺序、入参来源、失败返回和日志记录点。
常见误区
- 只会堆中间件或组件,不知道它们在链路中的执行顺序。
- 忽略生命周期和线程池、连接池等运行时资源约束。
- 没有监控和测试就对性能或可靠性下结论。
- 知道 API 名称,却不知道它应该放在请求链路的哪个位置。
进阶路线
- 继续向运行时行为、可观测性、发布治理和微服务协同深入。
- 把主题和数据库、缓存、消息队列、认证授权联动起来理解。
- 沉淀团队级模板,包括统一异常处理、配置约定和基础设施封装。
- 继续补齐协议选型、网关治理、端点可观测性和契约演进策略。
适用场景
- 当你准备把《终结点路由源码解析》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合 API 服务、后台任务、实时通信、认证授权和微服务协作场景。
- 当需求开始涉及稳定性、性能、可观测性和发布流程时,这类主题会成为基础设施能力。
落地建议
- 先定义请求链路与失败路径,再决定中间件、过滤器、服务边界和依赖方式。
- 为关键链路补日志、指标、追踪、超时与重试策略。
- 环境配置与敏感信息分离,避免把生产参数写死在代码或镜像里。
排错清单
- 先确认问题发生在路由、模型绑定、中间件、业务层还是基础设施层。
- 检查 DI 生命周期、配置来源、序列化规则和认证上下文。
- 查看线程池、连接池、缓存命中率和外部依赖超时。
复盘问题
- 如果把《终结点路由源码解析》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《终结点路由源码解析》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《终结点路由源码解析》最大的收益和代价分别是什么?
