middleware和actionfilter本质不同:前者运行于全http管道,可操作原始请求响应;后者仅作用于mvc action阶段,能获取action参数、控制器名等上下文信息。

Middleware 和 ActionFilter 根本不是一回事
别被“过滤器”这个词带偏——Middleware 在整个 HTTP 请求管道里运行,ActionFilter 只在 MVC 的 Controller/Action 执行阶段起作用。前者能拿到原始 Request.Body、改 Response.StatusCode、甚至终止请求;后者连 HttpContext.Request.Path 都可能为空(比如模型绑定失败前),更别说读 Body 了。
常见错误现象:在 ActionFilter 里调 context.HttpContext.Request.ReadFormAsync() 报 System.InvalidOperationException: Form value count limit 1024 exceeded,其实是 Body 已被前面的中间件或模型绑定耗尽了,不是配置问题。
- 要统一记录请求头、耗时、IP、路由匹配结果 → 用
Middleware - 要校验某个 Action 的参数是否满足业务规则(比如订单号不能为 null 且必须存在)→ 用
ActionFilter - 要拦截所有未授权访问并跳转登录页 →
IAuthorizationFilter(比IActionFilter更早执行)
Middleware 必须注册在 UseRouting() 之后、UseEndpoints() 之前
顺序错了就等于没写。比如把自定义日志中间件放在 app.UseEndpoints() 后面,它根本不会被执行;放在 app.UseRouting() 前面,则 context.GetRouteData() 是 null,context.Request.Path 也拿不到真实路由值。
正确位置示例(.NET 6+ Program.cs):
var app = builder.Build();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting(); // ← 必须在这之后
app.UseMyLoggingMiddleware(); // ← 自定义中间件放这里
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); // ← 必须在这之前
- 想只处理
/api/下的请求?别在中间件里写if (context.Request.Path.StartsWithSegments("/api")),改用app.MapWhen(ctx => ctx.Request.Path.StartsWithSegments("/api"), branch => branch.UseMyApiMiddleware()) - 中间件里调用
await next()是必须的,不写就断链;写return或直接throw也会中断后续流程 -
UseExceptionHandler是中间件,但它只能捕获管道中未处理的异常;IExceptionFilter是 ActionFilter,只管 Controller 里的异常,两者不重叠也不替代
ActionFilterAttribute 构造函数注入有硬限制
继承 ActionFilterAttribute 写过滤器最常用,但它的 DI 注入不是无条件的:类必须是 public,且要有可访问的无参构造函数。否则运行时报 InvalidOperationException: No constructor for type xxx,哪怕你已经在 Program.cs 里注册了服务也没用。
如果真需要构造函数注入(比如要传 IConfiguration 或 ILogger),必须显式提供无参构造函数,并在内部延迟解析:
public class MyActionFilter : ActionFilterAttribute
{
private readonly ILogger _logger;
public MyActionFilter() { } // ← 必须有
public MyActionFilter(ILogger logger) // ← 这个会被忽略,除非你手动注册为服务
{
_logger = logger;
}
public override void OnActionExecuting(ActionExecutingContext context)
{
// 正确做法:从当前请求作用域取服务
var logger = context.HttpContext.RequestServices.GetService<ilogger>();
logger?.LogInformation("Before action");
}
}</ilogger>
- 全局注册过滤器(
options.Filters.Add<myactionfilter>()</myactionfilter>)时,它生命周期是 singleton,但HttpContext是 scoped,所以不能在构造函数里直接注入HttpContext或任何 scoped 服务 - 想让过滤器支持属性语法(如
[MyActionFilter(EnableLog = true)]),就得把参数做成 public 属性,并加[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)] - 多个过滤器混用时,Order 值只在同一类型内生效:
IAuthorizationFilter总比IActionFilter先跑,别指望靠 Order 调整跨类型顺序
Middleware 读 Request.Body 前必须 EnableBuffering
这是生产环境最容易翻车的点。Request.Body 默认是只读流,且只能读一次。你在中间件里调 ReadAsStringAsync(),后面 MVC 模型绑定就读不到数据,context.ActionArguments 全是 null,ModelState.IsValid 直接 false。
解决方法分三步:
- 在
Program.cs最早位置(app.UseRouting()前)加缓冲启用中间件:app.Use((ctx, next) => { ctx.Request.EnableBuffering(); return next(); }); - 在你自己的中间件里读完 Body 后,立刻重置位置:
context.Request.Body.Position = 0; - 别在中间件里用
ReadAsStringAsync()处理大 JSON,改用JsonSerializer.DeserializeAsync<t>(context.Request.Body)</t>,避免字符串拷贝和内存暴涨
注意:开启缓冲会增加内存压力,尤其高并发时。如果只是校验 Token 或签名,建议只读前 N 字节(比如 context.Request.Body.Read(buffer, 0, 1024)),而不是全量缓冲。











