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
User、Items、Request、Response 都属于当前这次请求。请求结束以后不要再拿 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------很多人把 UseRouting 和 MapControllers 当成一回事,其实前者是每个请求在选目标,后者是启动时把 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 才能拿到 ActionArguments 和 ModelState;更早的 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 保存当前请求上下文。 |