asp.net core自定义中间件需严格遵循三原则:invokeasync签名必须为public async task invokeasync(httpcontext context);必须await _next(context)否则管道中断;注册顺序决定上下文数据有效性,如认证中间件须在日志中间件之前。

ASP.NET Core 自定义中间件不是“加个类就完事”,漏掉 await _next(context)、InvokeAsync 签名错、注册顺序颠倒,请求就会卡死、状态码错乱或静默失效——这三处任一出错,你根本收不到响应,连日志都看不到。
InvokeAsync 方法签名必须严格匹配框架要求
框架靠反射找入口方法,只认一种签名:public async Task InvokeAsync(HttpContext context)。任何偏差都会导致中间件被跳过,且不报错,只让你怀疑人生。
- 写成
Invoke(同步)→ 运行时报InvalidOperationException: No public Invoke or InvokeAsync method found - 返回
void或Task<iactionresult></iactionresult>→ 编译通过但实际不执行 - 参数名改成
ctx,或多加一个ILogger→ 找不到匹配方法,直接抛异常 - 没加
async修饰符 → 即使内部没await,也必须声明为async Task,否则 DI 解析失败
await _next(context) 漏掉就等于删掉整个后续管道
中间件不会自动调用下一个。漏掉这句,UseRouting、UseEndpoints、甚至 app.Run 全部失效,浏览器转圈,控制台无输出,你以为是网络问题,其实是管道断了。
- 错误写法:
_next(context);(没await)→ 返回未完成的Task,管道中断 - 更隐蔽的错误:在
try块里await _next(context),但catch里没return或重新抛出 → 响应未写入就结束 - 调试技巧:在
await _next(context)前后加Console.WriteLine("before/after"),只看到 “before” 就基本确定漏调用了
注册位置和顺序决定你能拿到什么数据
中间件顺序即执行顺序,它不是“全局生效”,而是从上到下进入、从下到上退出。注册错一位,context.User、context.Request.RouteValues、context.Response.StatusCode 全是错的。
- 把日志中间件放在
app.UseAuthentication()之前 →context.User.Identity.IsAuthenticated永远是false - 把权限校验中间件注册在
app.UseAuthorization()之后 → 授权已执行完毕,你的校验是马后炮 - 想读取
Response.StatusCode?必须在await _next(context)之后;想修改它?必须在之前,否则抛InvalidOperationException: Headers are read-only - 正确顺序示例(简化):
app.UseRouting(); app.UseAuthentication(); app.UseMiddleware<requestloggingmiddleware>(); app.UseEndpoints(...);</requestloggingmiddleware>
依赖注入该放构造函数还是 InvokeAsync 参数
中间件类实例默认是单例生命周期,但大多数业务服务(如 IDbContextFactory<appdbcontext></appdbcontext>、IUserService)是作用域(Scoped)级别——跨请求复用会引发并发异常或 DbContext 已释放。
- 单例(Singleton)服务(如
IConfiguration、ILogger<t></t>)可放构造函数 - 作用域(Scoped)服务必须放
InvokeAsync参数里:public async Task InvokeAsync(HttpContext context, IUserService service) - 错误写法:
public MyMiddleware(RequestDelegate next, IUserService service)→IUserService被提前解析,复用出错 - 别在
Use匿名委托里手动new中间件 → 不走 DI,Dispose不会被调用,资源泄漏
最常被忽略的其实是上下文生命周期:读取请求体前必须调用 context.Request.EnableBuffering() 并重置 Position,否则模型绑定会读空;Map 分支里的 next 不指向主管道,而是当前分支末端——这些细节不处理,日志能打,但内容永远为空。










