深入理解 ASP.NET Core 中的 UseRouting 与 UseEndpoints

一、从请求管道说起

ASP.NET Core 的核心是一条中间件管道(Middleware Pipeline)。每一个 HTTP 请求都会沿着这条管道依次经过各个中间件,最终产生响应。
HTTP 请求
异常处理

UseExceptionHandler
静态文件

UseStaticFiles
🔵 UseRouting

路由匹配
授权

UseAuthorization
🟣 UseEndpoints

端点执行
HTTP 响应

UseRoutingUseEndpoints 是这条管道中最关键的两个环节,但很多开发者对它们的分工并不清晰------它们看起来"都跟路由有关",实际上职责截然不同。


二、路由系统的两阶段设计

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 它做了什么

csharp 复制代码
app.UseRouting();

调用这一行后,框架会在管道中注入 EndpointRoutingMiddleware,其核心行为:

  1. 解析 URL:提取路径、查询字符串、HTTP Method
  2. 遍历路由表 :与所有已注册的 Endpoint 进行匹配(由 EndpointDataSource 提供)
  3. 写入结果 :将匹配到的 Endpoint 对象存入 HttpContext

HttpContext EndpointDataSource EndpointRoutingMiddleware HTTP 请求 HttpContext EndpointDataSource EndpointRoutingMiddleware HTTP 请求 进入 UseRouting 获取所有已注册 Endpoint 返回路由表 按优先级匹配路由 SetEndpoint(matchedEndpoint) 调用 next(),继续管道

3.2 匹配结果放在哪里

匹配结果通过 IEndpointFeature 接口存储在 HttpContext.Features 中,可以随时读取:

csharp 复制代码
// 在任意后续中间件中读取当前匹配的 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 版本控制

路由约束内置类型包括:intlongboolguiddatetimealpharegex()minlength()range() 等。

3.4 路由优先级

当多个路由可以匹配同一请求时,框架按以下原则选择最佳匹配:
所有候选路由
字面量段优先于参数段
有约束的参数优先于无约束的参数
非通配符优先于通配符
HTTP Method 精确匹配优先
选出唯一最优路由

写入 HttpContext


四、UseEndpoints 详解

4.1 它做了什么

csharp 复制代码
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

调试用描述名称

RouteEndpointEndpoint 的子类,额外携带 RoutePatternOrder 信息。

4.3 常见的注册方式

csharp 复制代码
app.UseEndpoints(endpoints =>
{
    // MVC Controller
    endpoints.MapControllers();

    // 带属性路由的 Controller
    endpoints.MapControllerRoute(
        name: "default",
        pattern: "{controller=Home}/{action=Index}/{id?}");

    // Razor Pages
    endpoints.MapRazorPages();

    // SignalR Hub
    endpoints.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 引入了 WebApplicationUseRoutingUseEndpoints 的使用方式发生了变化:

csharp 复制代码
// .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() 等方法时,框架会自动 在合适的位置插入等价的 UseRoutingUseEndpoints 逻辑。

但在以下情况下,仍需显式调用:

csharp 复制代码
// 当你需要在 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

csharp 复制代码
// 自定义约束:只允许特定国家代码
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/Handler UseEndpoints UseAuthorization UseRouting Kestrel 客户端 Controller/Handler UseEndpoints UseAuthorization UseRouting Kestrel 客户端 解析 URL 匹配路由表 选中 Endpoint 读取 [Authorize] 元数据 验证用户身份与权限 读取 HttpContext 中的 Endpoint 执行 RequestDelegate GET /api/products/42 传入 HttpContext next(),Endpoint 已写入 Context next(),授权通过 调用 ProductsController.Get(42) 返回 JSON 响应


九、常见错误与最佳实践

❌ 错误示例

csharp 复制代码
// 错误一:UseAuthorization 放在 UseRouting 之前
app.UseAuthorization();  // ❌ 此时 Endpoint 未知,无法读取授权元数据
app.UseRouting();

// 错误二:UseEndpoints 放在 UseRouting 之前
app.UseEndpoints(e => e.MapControllers()); // ❌ 路由还未匹配
app.UseRouting();

✅ 正确顺序

csharp 复制代码
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 网关、权限系统时,做出更优雅、更高效的设计决策。

相关推荐
fliter6 小时前
Rust 中的递归迭代器:一次让编译器教你理解 impl Trait 与生命周期的旅程
后端
考虑考虑6 小时前
JDK26支持Http3属性
java·后端·java ee
Cache技术分享6 小时前
415. Java 文件操作基础 - 精准读取压缩诗集:从二进制文件中高效提取指定十四行诗
前端·后端
XovH6 小时前
Django 从 0 到 1 打造完整电商平台:收货地址管理
后端
Postkarte不想说话6 小时前
Jupyter Lab安装
后端
fliter6 小时前
在 Async Rust 中实现请求合并(Request Coalescing)
后端
王立志_LEO6 小时前
Gunicorn 启动django服务
后端
fliter6 小时前
一个让我调试一周的 Rust match 陷阱
后端