中间件核心在于执行时机、依赖注入方式和是否短路;use直接嵌入管道无生命周期管理,usemiddleware通过di解析实例并支持作用域服务注入;invokeasync必须await next(context)且不可遗漏,否则中断后续管道。

中间件不是“写个类扔进去就完事”,核心在于执行时机、依赖注入方式和是否短路——这三点没理清,90% 的自定义中间件会在上线后突然不生效或导致整个管道中断。
Use 和 UseMiddleware 的本质区别
Use 接收的是一个 RequestDelegate 委托(即 Func<httpcontext requestdelegate task></httpcontext>),它直接嵌入管道,没有生命周期管理;UseMiddleware<t>()</t> 则要求 T 是一个类,且必须有公开的 InvokeAsync(HttpContext, ...) 方法,框架会通过 DI 容器解析并缓存该类型实例。
-
Use适合调试、临时逻辑(比如打印请求头)、或完全无状态的处理,但无法构造注入服务(除非手动从app.ApplicationServices拿) -
UseMiddleware<t>()</t>才是生产环境标准写法:支持方法参数注入(如IConfiguration、ILogger<t></t>)、可单元测试、能复用 - 常见错误:把
InvokeAsync写成Invoke(同步方法),或返回void而非Task,会导致编译失败或运行时跳过
InvokeAsync 里忘了 await next(context) 就等于删掉了整个后续管道
这是最隐蔽也最致命的坑。中间件执行分“请求阶段”和“响应阶段”,await next(context) 是分水岭:它之后的代码属于响应阶段;如果漏掉这句,后续所有中间件(包括 UseRouting、UseEndpoints)全被跳过,请求直接卡死或返回空响应。
- 正确写法:
await next(context);必须显式await,不能只写next(context) - 短路场景(如鉴权失败)要主动写
return或抛异常,避免误执行await next(context) - 如果中间件需要读取请求体(如日志记录原始 JSON),必须先调用
context.Request.EnableBuffering(),否则后续模型绑定会读不到内容
依赖服务该放构造函数还是 InvokeAsync 参数
答案很明确:**作用域服务(Scoped)必须放 InvokeAsync 参数里,单例(Singleton)可放构造函数**。因为中间件类实例是单例生命周期,而 HttpContext 和大多数业务服务(如 IDbContextFactory<appdbcontext></appdbcontext>、IUserRepository)都是作用域级别。
- 错例:
public MyMiddleware(RequestDelegate next, IUserService service)→IUserService会被提前解析,跨请求复用,引发并发问题或 DbContext 已释放异常 - 对例:
public async Task InvokeAsync(HttpContext context, IUserService service)→ 框架在每次请求时动态解析,安全可靠 - 构造函数只适合放
RequestDelegate、ILogger<t></t>(默认单例)、IOptions<t></t>等无状态依赖
中间件顺序不对,功能就等于没写
ASP.NET Core 管道是严格顺序执行的,前一个中间件决定后一个能看到什么。比如 UseAuthentication() 必须在 UseRouting() 之后、UseEndpoints() 之前,否则 User.Identity 始终为 null。
- 静态文件必须在身份验证前:
UseStaticFiles()放UseAuthentication()后面 → 未登录用户也能访问/favicon.ico导致 401 - 想让中间件看到路由信息(如
context.GetEndpoint()),它必须注册在UseRouting()之后、UseEndpoints()之前 - 调试建议:在中间件开头加
Console.WriteLine($"[{nameof(MyMiddleware)}] {context.Request.Path}"),启动后看输出顺序是否符合预期
真正难的不是写中间件,而是理解它在哪一刻介入、能拿到什么、又把什么交给了下一个环节。多数线上问题都出在“以为自己写了,其实根本没执行到”。











