一、从请求管道说起
ASP.NET Core 的核心是一条中间件管道(Middleware Pipeline)。每一个 HTTP 请求都会沿着这条管道依次经过各个 中间件 ,最终产生响应。
HTTP 请求
异常处理
UseExceptionHandler
静态文件
UseStaticFiles
🔵 UseRouting
路由匹配
授权
UseAuthorization
🟣 UseEndpoints
端点执行
HTTP 响应
UseRouting 和 UseEndpoints 是这条管道中最关键的两个环节,但很多开发者对它们的分工并不清晰——它们看起来"都跟路由有关",实际上职责截然不同。
二、路由系统的两阶段设计
ASP.NET Core 3.0 引入了端点路由(Endpoint Routing),将路由解析拆分为两个独立阶段:
⚡ 中间地带(两者之间的中间件)
UseAuthorization / UseCors
已知目标 Endpoint,可读取其元数据
(授权策略、CORS 策略等)
🟣 阶段二:UseEndpoints(端点执行)
从 HttpContext 读取已选中的 Endpoint
执行 Endpoint 的 RequestDelegate
Controller Action / RazorPage / Lambda...
🔵 阶段一:UseRouting(路由匹配)
解析请求 URL 和 HTTP Method
遍历路由表,找到最匹配的 Endpoint
将 Endpoint 写入 HttpContext.Features
这种两阶段设计的核心价值是:让中间件在请求被执行之前,就能知道"这个请求要去哪里",从而做出更精准的决策。
三、UseRouting 详解
3.1 它做了什么
app.UseRouting();
调用这一行后,框架会在管道中注入 EndpointRoutingMiddleware,其核心行为:
- 解析 URL:提取路径、查询字符串、HTTP Method
- 遍历路由表:与所有已注册的 Endpoint 进行匹配(由
EndpointDataSource提供) - 写入结果:将匹配到的
Endpoint对象存入HttpContext
HttpContextEndpointDataSourceEndpointRoutingMiddlewareHTTP 请求HttpContextEndpointDataSourceEndpointRoutingMiddlewareHTTP 请求进入 UseRouting获取所有已注册 Endpoint返回路由表按优先级匹配路由SetEndpoint(matchedEndpoint)调用 next(),继续管道
3.2 匹配结果放在哪里
匹配结果通过 IEndpointFeature 接口存储在 HttpContext.Features 中,可以随时读取:
// 在任意后续中间件中读取当前匹配的 Endpoint
var endpoint = context.GetEndpoint();if (endpoint != null)
{Console.WriteLine($"匹配到端点:{endpoint.DisplayName}");// 读取元数据(如授权策略、路由名称等)var authAttr = endpoint.Metadata.GetMetadata<AuthorizeAttribute>();var routeName = endpoint.Metadata.GetMetadata<RouteNameMetadata>();
}
3.3 路由模板匹配规则
UseRouting 支持多种路由模板语法:
| 模板示例 | 说明 |
|---|---|
/products/{id} |
基础参数捕获 |
/products/{id:int} |
带类型约束的参数 |
/products/{id:int:min(1)} |
多重约束 |
/files/{**path} |
通配符(贪婪匹配剩余所有路径段) |
/api/v{version:apiVersion} |
结合 API 版本控制 |
路由约束内置类型包括:int、long、bool、guid、datetime、alpha、regex()、minlength()、range() 等。
3.4 路由优先级
当多个路由可以匹配同一请求时,框架按以下原则选择最佳匹配:
所有候选路由
字面量段优先于参数段
有约束的参数优先于无约束的参数
非通配符优先于通配符
HTTP Method 精确匹配优先
选出唯一最优路由
写入 HttpContext
四、UseEndpoints 详解
4.1 它做了什么
app.UseEndpoints(endpoints =>
{endpoints.MapControllers();endpoints.MapRazorPages();endpoints.MapGet("/health", () => "OK");
});
UseEndpoints 做两件事:
① 注册阶段(应用启动时):将 Lambda、Controller、RazorPage 等封装为 Endpoint 对象,添加到 EndpointDataSource,供 UseRouting 查询。
② 执行阶段(请求到来时):从 HttpContext 读取 UseRouting 已经选好的 Endpoint,执行其 RequestDelegate。
4.2 Endpoint 的内部结构
每个 Endpoint 对象包含三个核心部分:
Endpoint 对象
RequestDelegate
实际执行逻辑
(Action / Lambda)
MetadataCollection
元数据集合
(授权策略 / CORS / 路由名称...)
DisplayName
调试用描述名称
RouteEndpoint 是 Endpoint 的子类,额外携带 RoutePattern 和 Order 信息 。
4.3 常见的 注册 方式
app.UseEndpoints(endpoints =>
{// MVC Controllerendpoints.MapControllers();// 带属性路由的 Controllerendpoints.MapControllerRoute(name: "default",pattern: "{controller=Home}/{action=Index}/{id?}");// Razor Pagesendpoints.MapRazorPages();// SignalR Hubendpoints.MapHub<ChatHub>("/chathub");// gRPC 服务endpoints.MapGrpcService<GreeterService>();// 最小 API(Minimal API)endpoints.MapGet("/api/ping", () => "pong");endpoints.MapPost("/api/echo", async (HttpRequest req) =>{var body = await new StreamReader(req.Body).ReadToEndAsync();return Results.Ok(body);});// 带元数据的端点endpoints.MapGet("/admin/data", AdminHandler).RequireAuthorization("AdminPolicy") // 附加授权策略.RequireCors("AllowAll") // 附加 CORS 策略.WithName("AdminData") // 设置路由名称.WithDisplayName("管理数据接口");
});
五、两者之间的"黄金地带"
这是整个端点路由设计中最精妙的地方。
中间地带
UseAuthentication
验证身份
UseAuthorization
检查权限策略
UseCors
检查 CORS 策略
UseRateLimiter
读取限流元数据
UseRouting
✅ Endpoint 已确定
⚡ 这里的中间件可以读取 Endpoint 元数据!
C
UseEndpoints
执行 Endpoint
为什么 UseAuthorization 必须在 UseRouting 之后?
因为授权中间件需要先知道"当前请求要访问哪个 Endpoint",才能去读取该 Endpoint 上附加的 [Authorize] 或 RequireAuthorization() 元数据,进而判断当前用户是否有权限。
如果把 UseAuthorization 放在 UseRouting 之前,HttpContext 中还没有 Endpoint 信息,授权中间件就无从检查——这是一个常见的配置错误。
六、.NET 6+ 的变化:WebApplication 与隐式调用
.NET 6 引入了 WebApplication,UseRouting 和 UseEndpoints 的使用方式发生了变化:
// .NET 6+ Minimal API 风格
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();var app = builder.Build();// ✅ 不再需要显式调用 UseRouting / UseEndpoints
// MapControllers 内部会自动触发
app.MapControllers();
app.MapGet("/health", () => Results.Ok("healthy"));app.Run();
当你调用 MapControllers()、MapGet() 等方法时,框架会自动在合适的位置插入等价的 UseRouting 和 UseEndpoints 逻辑。
但在以下情况下,仍需显式调用:
// 当你需要在 UseRouting 和 UseEndpoints 之间插入自定义中间件时
app.UseRouting();// 自定义中间件(此时可以读取 Endpoint 元数据)
app.Use(async (context, next) =>
{var endpoint = context.GetEndpoint();var myMeta = endpoint?.Metadata.GetMetadata<MyCustomMetadata>();// ... 基于元数据做决策await next();
});app.UseAuthorization();app.UseEndpoints(endpoints =>
{endpoints.MapControllers();
});
七、自定义路由约束
当内置约束不满足需求时,可以实现 IRouteConstraint:
// 自定义约束:只允许特定国家代码
public class CountryCodeConstraint : IRouteConstraint
{private static readonly HashSet<string> _validCodes =new(StringComparer.OrdinalIgnoreCase) { "CN", "US", "UK", "JP" };public bool Match(HttpContext? httpContext,IRouter? route,string routeKey,RouteValueDictionary values,RouteDirection routeDirection){if (values.TryGetValue(routeKey, out var value)){return _validCodes.Contains(value?.ToString() ?? "");}return false;}
}// 注册约束
builder.Services.AddRouting(options =>
{options.ConstraintMap.Add("countryCode", typeof(CountryCodeConstraint));
});// 使用约束
app.MapGet("/api/{country:countryCode}/products", (string country) =>Results.Ok($"Products for {country}"));
八、链路梳理:一次完整请求的生命周期
Controller/HandlerUseEndpointsUseAuthorizationUseRoutingKestrel客户端Controller/HandlerUseEndpointsUseAuthorizationUseRoutingKestrel客户端解析 URL匹配路由表选中 Endpoint读取 [Authorize] 元数据验证用户身份与权限读取 HttpContext 中的 Endpoint执行 RequestDelegateGET /api/products/42传入 HttpContextnext(),Endpoint 已写入 Contextnext(),授权通过调用 ProductsController.Get(42)返回 JSON 响应
九、常见错误与最佳实践
❌ 错误示例
// 错误一:UseAuthorization 放在 UseRouting 之前
app.UseAuthorization(); // ❌ 此时 Endpoint 未知,无法读取授权元数据
app.UseRouting();// 错误二:UseEndpoints 放在 UseRouting 之前
app.UseEndpoints(e => e.MapControllers()); // ❌ 路由还未匹配
app.UseRouting();
✅ 正确顺序
app.UseExceptionHandler("/error"); // 1. 最外层,捕获所有异常
app.UseHttpsRedirection(); // 2. HTTPS 重定向
app.UseStaticFiles(); // 3. 静态文件(短路,不进入路由)
app.UseCookiePolicy(); // 4. Cookie 策略app.UseRouting(); // 5. ✅ 路由匹配app.UseAuthentication(); // 6. ✅ 身份验证(需在 UseRouting 之后)
app.UseAuthorization(); // 7. ✅ 授权(读取 Endpoint 元数据)
app.UseCors(); // 8. ✅ CORS(读取 Endpoint 元数据)
app.UseRateLimiter(); // 9. ✅ 限流(读取 Endpoint 元数据)app.UseEndpoints(endpoints => // 10. ✅ 最后执行端点
{endpoints.MapControllers();endpoints.MapRazorPages();
});
最佳实践总结
| 实践 | 说明 |
|---|---|
| 遵守中间件顺序 | UseRouting → 策略中间件 → UseEndpoints |
| 善用元数据 | 用 RequireAuthorization()、RequireCors() 等在端点粒度配置策略 |
| 避免过度使用全局过滤器 | 端点级策略比全局中间件更精确、性能更好 |
| .NET 6+ 优先用 Minimal API | MapGet 等方法语义更清晰,配合 WithMetadata() 灵活扩展 |
| 理解"黄金地带" | 需要读 Endpoint 元数据的中间件,务必放在两者之间 |
十、总结
Endpoint 已选定
写入 HttpContext
授权 / CORS / 限流
通过检查
UseRouting
📍 我来决定
去哪个 Endpoint
中间层中间件
🔍 我来决定
要不要放行
UseEndpoints
⚡ 我来负责
真正执行
UseRouting 是侦察兵,负责找到目标;中间地带的中间件是守门人,负责把关;UseEndpoints 是执行者,负责干活。三者各司其职,共同构成 ASP.NET Core 高可扩展路由系统的基石。
理解这套机制,不仅能帮助你避免配置错误,更能让你在构建自定义中间件、API 网关、权限系统时,做出更优雅、更高效的设计决策。
