隔两个月不看 Program.cs,我就又会搞混 UseRun 的区别。后来画了张洋葱图,把 MapUseWhen 和短路返回的位置标上去,才算真正记住。这里记的是那张图和三种「看起来能跑、其实会吞掉响应」的写法,前两种我都亲手写过。

一、Use 和 Run

Use 注册的是「可以往下传」的中间件,Run 注册的是终点,后面写什么都不会执行。真正容易错的不是这个定义,而是 Use 里那句 await next() 的位置:

app.Use(async (context, next) =>
{
    // 这里的代码在请求进来时执行
    await next();
    // 这里的代码在响应出去时执行
});

写在 await next() 前面的是「进」,后面的是「出」。我第一次写日志中间件的时候把计时结束的 Stopwatch 放在了前面,测出来的耗时全是 0 毫秒,还以为是缓存命中太快。

二、我画的那张图

请求从外向内穿过,响应从内向外回来。我们项目里 Program.cs 一共有 14 个 Use,全记住没意义,我只需要记住这五层的相对位置:

1. UseExceptionHandler / UseStatusCodePages 2. UseHttpsRedirection / UseStaticFiles 3. UseRouting 4. UseAuthentication → UseAuthorization 5. MapControllers(终点)
图:我按自己的项目画的中间件分层,请求由外向内、响应由内向外。第 1 层必须在最外,否则内层抛的异常没人接。

这张图唯一想说明的事是:异常处理必须在最外层。我把它画在中间那层过一次,结果内层抛的异常全变成了 500 且日志里什么都没有,查了整整一个下午。

三、三种会吞掉响应的写法

第一种,Use 里忘了 await next()。接口返回 200,响应体是空的,浏览器白屏,状态码完全正常,日志里也没有任何错误。这种最难查,因为从监控上看一切正常。

第二种,app.Run(...) 写在了中间。它之后注册的所有中间件都不执行,包括我最外层以为会兜住异常的那一层。写完之后忘了为什么加,是很常见的。

第三种我踩得最疼:把 UseAuthorization 写在了 UseRouting 前面。鉴权不报错,接口照常返回数据——因为它根本没生效。这个顺序问题在 .NET 6 之后编译器不会提醒,只有自己对着文档数一遍。

顺手一记

UseWhen 是「条件性地走一段,走完还回到主管道」,Map 是「按路径分叉,分叉里通常自己结束」。我把 MapUseWhen 用过一次,分叉后面主管道里的响应头设置全部没生效。

四、记错过的两个点

一个是 UseStatusCodePages 的位置:它只对「响应已经生成但状态码是 4xx/5xx」的情况生效,如果异常在最外层之前抛出,它同样接不到。另一个是 UseStaticFiles 放在 UseAuthentication 之后,会让静态资源也走一遍鉴权逻辑,我们登录页的 CSS 有两天加载不出来,原因就是这个。

这些方法我记不住的时候还有一个笨办法:临时在每层中间件里加一行日志,打上时间戳和中间件名,跑一次请求就能看到完整的进出顺序。不算优雅,但救过我三次。

(2026-07 补一句:上面第 4 层我把认证和授权画在一层里,严格说它们是两个中间件,只是我从来没把它们分开排过,如果分开放,顺序仍然是认证在前。)