如何理解 ASP.NET Core 请求生命周期

ASP.NET Core 的很多知识点------DI、Middleware、Filter、Routing、Controller、DbContext------单独看都不难。

真正容易混的是:它们到底是在请求的哪个阶段发生的?

如果把一次 HTTP 请求完整走一遍,这些知识点就会自然串起来。

text 复制代码
GET /api/orders/1
Authorization: Bearer xxx

记住这条主线:

text 复制代码
Kestrel → HttpContext → Request Scope → Middleware → Routing → Endpoint
→ MVC(Controller / Binding / Filter / Action)→ Result → Response → Dispose Scope

请求进来:Kestrel 和 HttpContext

Kestrel 监听端口,解析 HTTP,然后为这一次 请求准备好 HttpContext。它不是 Controller 创建的;后面 Middleware、MVC、Filter 都围着它转。

text 复制代码
Request A → HttpContext A
Request B → HttpContext B

UserItemsRequestResponse 都属于当前这次请求。请求结束以后不要再拿 HttpContext 做后台逻辑;需要的话在请求里把数据拷出来。

框架最终调用的入口,可以抽象成:

csharp 复制代码
public delegate Task RequestDelegate(HttpContext context);

应用启动时,Middleware 被 Build一个 大的 RequestDelegate;Kestrel 每收到一个请求,大致就是 await application(httpContext)。后面说的 Middleware、Endpoint,本质上都是在包装或调用 RequestDelegate


进入管道:Middleware 在干什么

HttpContext 就绪后,请求进入 Middleware 管道。常见顺序可以理解为:

text 复制代码
ExceptionHandler → StaticFiles → Routing → Authentication → Authorization → EndpointMiddleware

Middleware 在 HTTP 层,所有进应用的请求都会经过(除非前面就短路返回)。静态文件可能在这里直接返回;全局异常、HTTPS、CORS 也适合放这一层。

Middleware 和 MVC 里的 Filter 容易混:Middleware 在管道前端,管的是 HTTP 层,所有请求都会经过;Filter 在 MVC 内部,只有进了 Controller 才会跑,而且那时 Action 参数已经绑好了。日志、异常、CORS 放 Middleware;校验某个 Action 的入参、改返回值,放 Filter。


Routing:只选人,不执行

请求 GET /api/orders/1 走到 UseRouting() 时,框架在启动时已登记好的 Endpoint 表里做匹配,例如:

text 复制代码
GET /api/orders/{id} → OrdersController.Get(int id)

UseRouting 不是 开始执行 Controller------很多人把 UseRoutingMapControllers 当成一回事,其实前者是每个请求在选目标,后者是启动时把 Action 登记进 Endpoint 表。Routing 这一步只做一件事:把匹配到的 Endpoint 挂到 HttpContext 上。

Endpoint 不是 Controller 类本身,而是路由、策略、执行方式打包在一起的一份描述:

text 复制代码
RoutePattern   --- 怎么匹配 URL
Metadata       --- Authorize、Cors 等策略
RequestDelegate --- 匹配成功后由谁、怎么执行

Routing 真正匹配的是 Endpoint,而不是 Controller。自 Endpoint Routing 起,框架的模型就是这样;很多人仍习惯说"路由找到了 Controller",其实中间还隔着 Endpoint 这一层。

[Authorize] 会变成 Metadata。所以 UseAuthentication 写在 Routing 后面:先知道访问的是哪个 Endpoint,再认用户、再按 Endpoint 上的策略做授权(读 HttpContext.GetEndpoint()?.Metadata)。


执行 Endpoint:进入 MVC

Routing 之后,EndpointMiddleware 调用该 Endpoint 的 RequestDelegate。对 Controller 来说,这个委托会跳进 MVC,由 ControllerActionInvoker 接手。

text 复制代码
UseRouting:选中目标
EndpointMiddleware:执行目标

MapControllers() 是在启动时把 Action 注册成 Endpoint,不是每个请求再扫描一遍 Controller。


MVC 里:创建对象、绑参数、Filter、Action

进入 MVC 后,顺序大致是:

text 复制代码
创建 Controller(DI 从当前 Request Scope 解析依赖)
→ Model Binding(route/query/body → Action 参数、ModelState)
→ Filter Pipeline
→ 执行 Action
→ 得到 IActionResult

MVC 会通过当前请求的 HttpContext.RequestServices 创建 Controller,并解析构造函数依赖。如果依赖的是 Scoped 服务(例如 DbContext),整个请求期间共享同一实例,请求结束随 Scope 统一释放。

对本例,Get(int id) 绑定后 id = 1。此时 Action Filter 才能拿到 ActionArgumentsModelState;更早的 Middleware 还没有这些信息。

Action 里若 await _dbContext.Orders.FindAsync(id):I/O 未完成时线程可以去干别的,但请求归属不变 ------HttpContext 和 Request Scope 仍属于这次请求(AsyncLocal 等机制)。await 之后可能换了一条线程继续,Scoped 也不会串到别的请求;请求边界跟的是 HttpContext,不是某一条线程。


return Ok(order) 之后:还没写完响应

csharp 复制代码
return Ok(order);

只是得到一个 OkObjectResult(状态码 + 要输出的对象)。真正写入 Response.Body 在 Result 执行阶段:

text 复制代码
ObjectResultExecutor → 内容协商 → OutputFormatter(常见是 System.Text.Json)→ Response.Body

所以 Action 只负责决定返回什么;真正把 JSON 写进 Response.Body,是后面的 Formatter 干的。很多人以为 return Ok(order) 那一刻响应已经发出去了,其实还没有。

IActionResult 设计成接口,Action 返回的是"结果对象",真正执行结果的是不同的 ResultExecutor------因此 MVC 可以同一套流程支持 JSON、View、File、Redirect 等不同响应形式。Result Filter 包在这一段前后。


请求结束:管道返回与释放

Result 写完后,控制流沿 Middleware 的 await next() 往回 走(例如记耗时的中间件在 next 之后才打日志)。响应发出后,Request Scope 释放,Scoped 服务 Dispose,HttpContext 不再可用,线程回线程池。

text 复制代码
请求边界 = HttpContext + Request Scope
不是某一条线程

把整个顺序再过一遍

text 复制代码
GET /api/orders/1
    ↓
Kestrel,创建 HttpContext,准备 Request Scope(RequestServices)
    ↓
Middleware(异常、静态文件、...)
    ↓
UseRouting → 匹配 Endpoint(Pattern + Metadata + RequestDelegate)
    ↓
Authentication → User 写入 HttpContext
    ↓
Authorization → 看 Endpoint Metadata + User
    ↓
EndpointMiddleware → RequestDelegate → MVC
    ↓
创建 Controller,解析 Scoped 依赖(如 DbContext)
    ↓
Model Binding:id=1
    ↓
Filter → Action(可 await 查库)
    ↓
return Ok(order) → ResultExecutor / Formatter → JSON
    ↓
Middleware 往回,发 Response
    ↓
Dispose Scope

一张表记住整个生命周期

概念 出现在哪一步 要点
RequestDelegate 贯穿全程 Middleware 和 Endpoint 最终都是委托调用
HttpContext 请求一进来的载体 不属于线程,属于这次请求
RequestServices / Scoped 管道运行期间 DbContext 随请求创建和释放
Middleware HttpContext 之后、MVC 之前 管 HTTP,不管 Action 参数
Endpoint Routing 匹配结果 不是 Controller,是 Pattern + Metadata + 委托
Filter MVC 内部、Action 前后 能拿绑定后的参数
IActionResult / Formatter Action 返回之后 Ok() 不等于已写 JSON
async/await Action 或 Service 内 不绑线程,绑请求上下文

生命周期里最容易搞混的几个问题

问题 一句话回答
HttpContext 谁创建? Kestrel 接收请求后,由框架创建。
Controller 谁创建? MVC 通过 DI 创建。
DbContext 什么时候创建? 第一次解析 Scoped 服务时。
Routing 干什么? 匹配 Endpoint,不执行 Action。
EndpointMiddleware 干什么? 执行 Endpoint。
Filter 为什么能拿到 ActionArguments? Model Binding 已完成。
return Ok() 是否已经写响应? 没有,只返回 IActionResult。
Scoped 为什么不会串? Scope 属于请求,不属于线程。
HttpContext 为什么 await 后还能拿到? AsyncLocal 保存当前请求上下文。

官方文档

相关推荐
时代的狂2 小时前
ASP.NET Core 认证与授权(三):请求认证与权限校验
asp.net·.net core·web api·认证与授权
她说彩礼65万1 天前
Asp.net core 返回类型
数据库·后端·asp.net
她说彩礼65万2 天前
Asp.net core返回类型总结
后端·asp.net
她说彩礼65万2 天前
Asp.net core ContenResult
后端·asp.net
步步为营DotNet4 天前
借助 Microsoft.Extensions.AI 与 ASP.NET Core 10 实现智能 Web 应用故障预测
人工智能·microsoft·asp.net
品尚公益团队4 天前
C#在asp.net网页开发中的带cookie登录实现
前端·c#·asp.net
time展天15 天前
Dddify:给 ASP.NET Core 项目一套轻量、清晰、可落地的 DDD 基础设施
数据库·后端·asp.net
拉长的苗条5 天前
细说 ASP.NET Cache 及其高级用法
后端·asp.net
醉熏的斑马5 天前
ASP.Net MVC3 】使用Moq让单元测试变得更简单
单元测试·log4j·asp.net