asp.net core自定义中间件必须满足三要素:invokeasync签名严格为public task invokeasync(httpcontext context)、不可遗漏await _next(context)、注册顺序决定执行时机;构造函数仅能注入singleton/transient服务,scoped服务须在invokeasync参数中声明。

自定义中间件不是“写个类就能跑”,关键在 InvokeAsync 是否被框架识别、await _next(context) 是否漏掉、以及注册顺序是否破坏管道——任一出错,请求就卡死、状态码错乱或静默失效。
InvokeAsync 方法签名必须严格匹配
ASP.NET Core 只认一种签名:public Task InvokeAsync(HttpContext context)。哪怕多一个参数、少一个 async、返回 void 或 Task<iactionresult></iactionresult>,中间件都会被忽略,且不报错。
- 错误示例:
public async Task InvokeAsync(HttpContext context, ILogger logger)—— 构造函数注入服务可以,但InvokeAsync参数只能是HttpContext - 正确写法:服务通过构造函数注入,业务逻辑用
await _next(context)前后包裹 - 别写
Invoke(同步)方法,.NET 6+ 默认只扫描InvokeAsync,否则中间件不执行
await _next(context) 漏掉会导致请求卡死
中间件必须显式调用下一个组件,否则请求停在当前中间件,后续所有中间件(包括 MVC、静态文件)都不会执行,浏览器一直转圈,控制台也无日志。
- 常见错误:忘记
await,写成_next(context);—— 这会立即返回未完成的Task,管道中断 - 更隐蔽的错误:在
try块里await _next(context),但catch里没return或没重新抛出,导致响应未写入就结束 - 调试技巧:在
await _next(context)前后加Console.WriteLine("before/after"),若只看到 “before” 就基本确定漏调用了
UseMiddleware() 注册位置决定执行时机
中间件顺序即执行顺序,它不是“全局生效”,而是从上到下依次进入、从下到上依次退出。比如你把日志中间件放在 app.UseRouting() 之后,那它就收不到路由前的 404;放在 app.UseAuthentication() 之前,就拿不到用户身份。
- 典型陷阱:把权限校验中间件注册在
app.UseAuthorization()之后 —— 此时授权已执行完毕,你的校验变成“马后炮” - 调试建议:用
app.Use(async (ctx, next) => { Console.WriteLine($"[{ctx.Request.Path}] entering"); await next(); Console.WriteLine($"[{ctx.Request.Path}] exiting"); })插在关键位置,观察执行流 - 注意:内联中间件(
app.Use(...))和类中间件(app.UseMiddleware<t>()</t>)混用时,顺序仍按代码书写顺序,不按类型区分
构造函数注入与作用域服务要小心生命周期
中间件实例在应用启动时创建一次(单例),所以构造函数里不能注入 Scoped 服务(如 DbContext),否则会跨请求污染或抛出异常。
- 正确做法:构造函数只注入
Singleton或Transient服务;需要Scoped服务时,在InvokeAsync方法参数里声明,框架会自动解析当前请求的作用域 - 错误示例:
public MyMiddleware(MyDbContext db) { ... }—— 启动时报Cannot resolve scoped service from root provider - 正确示例:
public async Task InvokeAsync(HttpContext ctx, MyDbContext db)—— 框架保障每次调用都给新实例
最易被忽略的是:中间件类本身没有 [ServiceLifetime] 属性,它的生命周期完全由注册方式决定;UseMiddleware<t>()</t> 总是单例,哪怕你在 Program.cs 里把它注册为 Scoped 也没用。











