
在 ASP.NET Core Minimal API 中,中间件(Middleware) 与 Endpoint 过滤器(Endpoint Filters) 是处理 HTTP 请求管道(Pipeline)的两大核心利器。
很多初学者容易混淆它们:
context 参数,这两个 context 到底有什么本质区别?读完这篇文章,你将彻底掌握 Minimal API 中间件与过滤器的每一个细节、Context 参数核心差异与最佳实践。
在深入代码之前,我们先搞清楚二者的界限与分工:
客户端请求 ---> [中间件 1] ---> [中间件 2] ---> [路由匹配] ---> [过滤器 1] ---> [过滤器 2] ---> [Endpoint Handler]
|
客户端响应 <--- [中间件 1] <--- [中间件 2] <------------------- [过滤器 1] <--- [过滤器 2] <--------+维度 | 中间件 (Middleware) | Endpoint 过滤器 (Endpoint Filter) |
|---|---|---|
作用层级 | 全局管道(在路由匹配之前或之后运行)。 | Endpoint / 路由组层级(路由匹配成功后运行)。 |
感知能力 | 无法感知具体匹配到了哪个 Controller/Action 的元数据或强类型参数。 | 精准感知已被框架解析好的路由参数(Arguments)与 EndpointMetadata。 |
核心 Context | HttpContext(底层 HTTP 协议上下文)。 | EndpointFilterInvocationContext(高级调用上下文)。 |
典型场景 | 全局异常捕获、CORS、身份认证、静态文件响应、全局日志。 | 特定接口的强类型参数校验(Validation)、业务鉴权、响应格式统一包装。 |
HttpContext vs InvocationContext 参数异同同样是接收 context 参数,中间件与过滤器的上下文设计维度截然不同:
对比维度 | 中间件的 HttpContext | 过滤器的 EndpointFilterInvocationContext |
|---|---|---|
本质定位 | 原始 HTTP 协议的封装。 | 目标 API Handler 执行过程的上下文封装。 |
参数获取方式 | 原始字符串/流。只能拿到 Request.Query["id"] 或读取 Request.Body 字节流(需自己反序列化)。 | 强类型 C# 对象。可以直接通过 GetArgument<T>(index) 拿到框架已反序列化好的 DTO 或参数。 |
对 Route 的感知 | 早期阶段无法感知(发生在路由匹配前)。后期阶段虽能拿到 Endpoint 终节点,但操作繁琐。 | 原生天然感知。直接绑定在特定 Endpoint 上,清楚知道即将执行哪个方法、哪些参数。 |
返回值处理 | 直接写 Response 流,通过设置 StatusCode 或 WriteAsync 做出响应。 | 拦截并返回 IResult(如 Results.BadRequest()),对 Minimal API 原生类型更友好。 |
id假设有一个接口 POST /orders/{id},如果 id <= 0,需要拦截并返回 400 Bad Request。
app.Use(async (HttpContext context, RequestDelegate next) =>
{
// 中间件拿到的是原始 Path 字符串,需要自己写正则或手动拆分字符串解析 id!
var path = context.Request.Path.Value; // "/orders/0"
if (path != null && path.StartsWith("/orders/"))
{
var idSegment = path.Split('/').LastOrDefault();
if (int.TryParse(idSegment, out int id) && id <= 0)
{
// 中间件无法使用 Results.BadRequest(),必须手写 Response 流
context.Response.StatusCode = StatusCodes.Status400BadRequest;
context.Response.ContentType = "application/json";
await context.Response.WriteAsJsonAsync(new { Error = "ID 必须大于 0" });
return; // 短路,不调用 next
}
}
await next(context);
}); app.MapPost("/orders/{id:int}", (int id) => Results.Ok($"Order {id}"))
.AddEndpointFilter(async (EndpointFilterInvocationContext context, EndpointFilterDelegate next) =>
{
// 1. 直接从 context.Arguments 中强类型提取 id,框架已经帮你解析并转成了 int!
var id = context.GetArgument<int>(0);
if (id <= 0)
{
// 2. 直接返回 Minimal API 熟悉的 Results 对象打断请求
return Results.BadRequest(new { Error = "ID 必须大于 0" });
}
return await next(context);
});核心结论:中间件看的是“HTTP 请求流”,过滤器看的是“C# 方法与强类型参数”。需要处理底层网络协议/全局事件用中间件;需要校验、处理业务参数用过滤器。
中间件在 Minimal API 中主要通过 app.Use(...) 来定义。我们重点拆解最核心的 app.Use(Func<HttpContext, RequestDelegate, Task>)。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// 自定义中间件
app.Use(async (HttpContext context, RequestDelegate next) =>
{
// 【1. 请求前置处理】
Console.WriteLine($"[Middleware IN] Path: {context.Request.Path}");
// 【2. 调用管道中的下一个中间件】
await next(context);
// 【3. 响应后置处理】
Console.WriteLine($"[Middleware OUT] Status: {context.Response.StatusCode}");
});
app.MapGet("/hello", () => "Hello World!");
app.Run();HttpContext context(HTTP 上下文对象)它是整个请求生命周期中最核心的对象,包含了与当前 HTTP 请求相关的所有数据:
context.Request:请求信息。
context.Request.Path(请求路径,如 /api/orders)context.Request.Headers(请求头,可读取 Authorization 等)context.Request.Query(查询参数,如 ?id=123)context.Request.Body(请求体 Stream,读取时注意 Seek/Position 指针)context.Response:响应信息。
context.Response.StatusCode(状态码,如 200, 401, 500)context.Response.Headers(设置响应头,如 X-Process-Time)context.Response.WriteAsync(...)(直接向客户端写入响应内容)context.RequestServices:当前请求的作用域 DI 容器(IServiceProvider)。如果需要在中间件中解析 Scoped 服务(如 DbContext),必须通过 context.RequestServices.GetRequiredService<T>() 获取!context.Items:一个字典(IDictionary<object, object?>),用于在当前请求的各个中间件/过滤器之间传递临时自定义数据。RequestDelegate next(下一个管道节点的委托)next 是一个异步委托,签名等同于 Func<HttpContext, Task>。
await next(context);:将请求控制权交给管道中的下一个中间件。await next(context),请求就会在此中断,后续的中间件和 Endpoint Handler 均不会执行,而是直接原路返回(例如未授权时直接返回 401)。直接在 Program.cs 写 app.Use 会导致代码臃肿。推荐通过类和扩展方法进行标准封装。
// 1. 定义中间件类
public class RequestTimingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestTimingMiddleware> _logger;
// 构造函数传入 next 委托和 Singleton/Transient 服务
public RequestTimingMiddleware(RequestDelegate next, ILogger<RequestTimingMiddleware> logger)
{
_next = next;
_logger = logger;
}
// 必须包含名为 InvokeAsync 或 Invoke 的方法,第一个参数必须是 HttpContext
public async Task InvokeAsync(HttpContext context)
{
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
// 执行下一个中间件
await _next(context);
stopwatch.Stop();
_logger.LogInformation("请求 {Path} 耗时: {Elapsed} ms", context.Request.Path, stopwatch.ElapsedMilliseconds);
}
}
// 2. 编写扩展方法
public static class MiddlewareExtensions
{
public static IApplicationBuilder UseRequestTiming(this IApplicationBuilder app)
{
return app.UseMiddleware<RequestTimingMiddleware>();
}
}
// 3. 在 Program.cs 中使用
app.UseRequestTiming();Minimal API 引入了原生的 Endpoint Filter,它专门作用于特定 Endpoint 或路由组(Route Group),可以轻松拿到已被解析好的强类型参数。
通过 .AddEndpointFilter(...) 为 Endpoint 绑定过滤器。
我们在一个创建用户的接口上添加过滤器:
app.MapPost("/users", (UserDto user) => Results.Created($"/users/{user.Name}", user))
.AddEndpointFilter(async (EndpointFilterInvocationContext context, EndpointFilterDelegate next) =>
{
// 【1. 前置逻辑】
Console.WriteLine("过滤器前置校验开启...");
// 【2. 执行下一个过滤器或真正 Handler】
var result = await next(context);
// 【3. 后置逻辑】
Console.WriteLine("过滤器后置处理完成...");
return result;
});EndpointFilterInvocationContext context(过滤器调用上下文)这是过滤器的灵魂所在,提供了获取强类型参数的直接途径:
context.Arguments(核心参数列表):
IList<object?>。它包含了框架已经帮我们解析好的 Handler 参数(比如从 Body 反序列化出来的 UserDto,或者从 Route 拿到的 id)。context.HttpContext:当前的 HttpContext 对象,方便获取 Header、URL、DI 容器等。EndpointFilterDelegate next(下一个过滤器委托)ValueTask<object?> EndpointFilterDelegate(EndpointFilterInvocationContext context)。var result = await next(context); 会触发后续的过滤器;如果没有后续过滤器,则触发执行具体的 Endpoint Handler。ValueTask<object?>next(context),从而打断执行链:
if (string.IsNullOrEmpty(dto.Name)) { // 直接返回 BadHttpRequest,打断后续逻辑 return Results.BadRequest(new { Error = "Name 不能为空" }); }await next(context) 之后修改返回结果(例如包装统一的 API Response)。IEndpointFilter 接口(强类型模块化)在大型项目中,强烈建议将过滤器拆分为独立的类,实现 IEndpointFilter 接口。
假设我们结合 FluentValidation 写一个通用的参数校验过滤器:
using FluentValidation;
public class ValidationFilter<T> : IEndpointFilter where T : class
{
public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
{
// 1. 在参数列表中寻找类型为 T 的待校验对象
var argToValidate = context.Arguments.OfType<T>().FirstOrDefault();
if (argToValidate is not null)
{
// 2. 从 HttpContext 的 DI 容器中拿到对应的 Validator
var validator = context.HttpContext.RequestServices.GetService<IValidator<T>>();
if (validator is not null)
{
var validationResult = await validator.ValidateAsync(argToValidate);
if (!validationResult.IsValid)
{
// 校验失败,直接拦截并返回 400
var errors = validationResult.ToDictionary();
return Results.ValidationProblem(errors);
}
}
}
// 3. 校验通过,放行
return await next(context);
}
} // 单个 Endpoint 链式调用
app.MapPost("/products", (ProductDto product) => Results.Ok(product))
.AddEndpointFilter<ValidationFilter<ProductDto>>();如果有几十个 API 都需要应用相同的过滤器(比如 /api/v1/admin/* 下的所有接口都需要管理员鉴权过滤器),一个个写 .AddEndpointFilter 显然不可取。
Minimal API 提供了 Route Group(路由组),可以一次性给一组 Endpoint 挂载过滤器!
var app = WebApplication.CreateBuilder(args).Build();
// 1. 创建路由组,并统一定义前缀与过滤器
var adminGroup = app.MapGroup("/api/admin")
.RequireAuthorization("AdminPolicy") // 授权策略
.AddEndpointFilter<AdminAuditLogFilter>(); // 统一挂载审计日志过滤器
// 2. 在组内注册具体的 Endpoint,它们会自动继承路由组的所有过滤器!
adminGroup.MapGet("/users", () => "Admin: All Users");
adminGroup.MapDelete("/users/{id}", (int id) => $"Deleted User {id}");
app.Run();我们把中间件、过滤器、路由组融会贯通,写一段包含完整异常捕获、审计日志和参数校验的现代 Minimal API 应用:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// ---------------- 1. 全局中间件阶段 ----------------
// 1.1 全局未捕获异常处理中间件
app.Use(async (context, next) =>
{
try
{
await next(context);
}
catch (Exception ex)
{
context.Response.StatusCode = 500;
await context.Response.WriteAsJsonAsync(new { Error = "服务器内部错误", Detail = ex.Message });
}
});
// ---------------- 2. 路由组与 Endpoint 阶段 ----------------
var orderGroup = app.MapGroup("/orders")
.AddEndpointFilter(async (context, next) =>
{
// 组级过滤器:给所有订单接口加上统一耗时日志
var sw = System.Diagnostics.Stopwatch.StartNew();
var result = await next(context);
sw.Stop();
Console.WriteLine($"[Orders API] 耗时 {sw.ElapsedMilliseconds}ms");
return result;
});
// 结合单个 Endpoint 专属过滤器
orderGroup.MapPost("/", (CreateOrderDto order) => Results.Ok(new { OrderId = 1001, order.Amount }))
.AddEndpointFilter(async (context, next) =>
{
// Endpoint 专属过滤器:校验订单金额
var order = context.GetArgument<CreateOrderDto>(0);
if (order.Amount <= 0)
{
return Results.BadRequest(new { Error = "订单金额必须大于 0" });
}
return await next(context);
});
app.Run();
// DTO 定义
public record CreateOrderDto(string ProductId, decimal Amount);context.Request.Body:
Request.Body 是一个不可重复读取的 Stream(默认 Position = 0)。如果你在中间件读取了它却没有将指针重置,后续 Endpoint Handler 在反序列化时就会报错。如需读取,请先调用 context.Request.EnableBuffering() 并将 Position 重置为 0。Scoped 服务(如 DbContext)。必须在 InvokeAsync(HttpContext context) 方法体内部通过 context.RequestServices 解析。Scoped 或 Transient 服务。原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。