ASP.NET Core 中间件执行机制详解:Use、Run、Map 与请求管道原理

中间件

中间件是 ASP.NET Core 的核心组件,例如响应缓存、用户身份验证、CORS、Swagger等重要的功能都是由 ASP.NET 内置的中间件提供的

注:这一篇主要讲解中间件的一些简单概念,下一篇文章讲解自定义中间件。

中间件的三个概念 Map、Use、Run

处理顺序

Map 用来定义一个管道可以处理哪些请求, Use 和 Run 用来定义管道, “一个管道” 由 “若干个Use” 和 “一个Run” 组成,
每个 Use 引入一个中间件,而 Run 用来执行最终的核心应用逻辑。

上代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
app.Map("/test", async appbuilder => { 
appbuilder.Use(async (context, next) => {
context.Response.ContentType = "text/html;charset=utf-8";
await context.Response.WriteAsync("1 Start<br/>");
await next.Invoke();
await context.Response.WriteAsync("1 End<br/>");
});
appbuilder.Use(async (context, next) => {
await context.Response.WriteAsync("2 Start<br/>");
await next.Invoke();
await context.Response.WriteAsync("2 End<br/>");
});
appbuilder.Run(async context =>
{
await context.Response.WriteAsync("3 Run Start<br/>");
await context.Response.WriteAsync("3 Run End<br/>");
});
});

猜一下访问 /test 后的输出内容是什么(如图)?

屏幕截图 2026-02-20 114139

这个顺序是不是和大家想的有点不一样。

为什么不是这个顺序

1
2
3
4
5
6
1 Start
1 End
2 Start
2 End
3 Run Start
3 Run End

因为中间件是分为前、后逻辑,而前后逻辑是由 next 分开的。
awit next.Invoke(); 把请求转到下一个中间件,即不会执行该语句后面的代码(后逻辑),
直接把请求转到下一个 “use” ;
直到遇到 “run” , “ run “ 负责业务规则且不再将请求向后传递,当 “run” 执行结束后,
响应会按照请求的相反顺序执行中间件中的后逻辑。

注:如果我们在一个中间件中使用 context.Response.WriteAsync 等方式向客户端发送响应,我们就尽量不再执行 next.Invoke 把请求转到其他中间件,
因为其他中间件中有可能对 Response 中进行了修改,比如修改响应状态码、报文头、向报文中写入其他数据,这样就会造成报文体被损坏的问题。

所以正常的请求处理路径是(如图):

屏幕截图 2026-02-20 105320

中间件执行顺序是:先进后出(栈结构)

执行顺序

我们现在把第一个和第二个中间件交换一下顺序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
app.Map("/test", async appbuilder => {

appbuilder.Use(async (context, next) => {
context.Response.ContentType = "text/html;charset=utf-8";
await context.Response.WriteAsync("2 Start<br/>");
await next.Invoke();
await context.Response.WriteAsync("2 End<br/>");
});

appbuilder.Use(async (context, next) => {
await context.Response.WriteAsync("1 Start<br/>");
await next.Invoke();
await context.Response.WriteAsync("1 End<br/>");
});

appbuilder.Run(async context =>
{
await context.Response.WriteAsync("3 Run Start<br/>");
await context.Response.WriteAsync("3 Run End<br/>");
});
});

运行截图:

屏幕截图 2026-02-20 142428

从图中我们可以看出中间件的注册顺序就是执行顺序。
然后再回到我们的代码,两个中间件除了交换了一下顺序,是否还有其他修改。
修改在这里

context.Response.ContentType = “text/html;charset=utf-8”;
回看 “处理顺序” 实验的代码部分。 这句代码在第一个中间件里面,现在中间件交换顺序后,还是在第一个中间件里面。

设置 效果 浏览器行为
text/html; charset=utf-8 告诉浏览器:”这是 HTML,用 UTF-8 编码” 正确渲染 HTML 标签,中文不乱码
不设置(默认) 浏览器猜测内容类型 可能显示纯文本、乱码,或不渲染

为什么把context.Response.ContentType = “text/html;charset=utf-8”;放在第一个中间件。
在第一个中间件中使用 WriteAsync 方法写入body 会触发客户端响应,导致 header 被锁定

当第一次向 Response.Body 写入数据时:

    1. 服务器会发送响应头
    1. 此时 Response.HasStarted = true
    1. 之后再修改 Header 会抛异常

此时因为 header 已经被锁定,就无法在第二个中间件中再设置 header

如果想在第二个中间件中使用context.Response.ContentType = “text/html;charset=utf-8”; 那么需要注意不要在该中间件之前中间件不要触发客户端响应。

所以平时就尽量先设头,后写体。