模块二 · 请求与异步
一次请求在 ASP.NET Core 里怎么走完
从 Kestrel 接收到中间件管道、路由、模型绑定、过滤器与返回结果。这一节按请求经过的顺序讲,每一站都能挂一个面试题。
读完这一节,你能做到
- 按顺序说出 ASP.NET Core 的请求处理链路。
- 说清中间件的注册顺序为什么重要。
- 区分 Singleton/Scoped/Transient 的生命周期与陷阱。
- 说清过滤器的执行顺序与适用场景。
一、Kestrel 与托管模型
请求先到 Kestrel(跨平台 Web 服务器),再由 Host 交给应用管道。IIS 部署时通常是"IIS → ANCM → Kestrel"的反向代理模式。
三个关键点:
- 每个请求在管道里依次经过中间件,形成"洋葱模型"。
- 中间件可以短路(不调用 next),也可以包装后续处理。
- 管道是单例构建的,中间件实例通常只创建一次,不能依赖 Scoped 服务。
二、中间件管道
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(e => e.MapControllers());
顺序为什么重要:
UseRouting要在UseAuthorization之前,否则拿不到端点元数据。UseAuthentication要在UseAuthorization之前,否则没有用户身份。- 异常处理中间件要放最外层,才能捕获后面的异常。
三种写法:Use(可调用 next)、Run(终止管道)、Map(按路径分支)。
三、依赖注入的生命周期
| 生命周期 | 创建时机 | 适用 |
|---|---|---|
| Singleton | 首次请求,进程内唯一 | 无状态服务、配置 |
| Scoped | 每个请求一个 | DbContext、请求级状态 |
| Transient | 每次解析都新建 | 轻量无状态工具 |
经典陷阱:Singleton 捕获 Scoped。比如单例服务在构造器里注入 DbContext,会导致 DbContext 被长期持有,出现并发与数据串扰。修法是用 IServiceScopeFactory 在需要时创建作用域。
四、路由与模型绑定
路由有两种:约定路由({controller}/{action})与特性路由([Route]/[HttpGet])。新项目推荐特性路由,URL 与代码在一起更清晰。
模型绑定来源:
| 来源 | 特性 | 说明 |
|---|---|---|
| 路由 | [FromRoute] | URL 模板 |
| 查询 | [FromQuery] | query string |
| 表单 | [FromForm] | 表单/文件 |
| 请求体 | [FromBody] | JSON,默认 System.Text.Json |
绑定失败会进入 ModelState,用 [ApiController] 时会自动返回 400,不需要手写判断。
五、过滤器
授权 → 资源 → 模型绑定 → 动作 → 异常 → 结果
| 过滤器 | 作用 |
|---|---|
| Authorization | 鉴权,最早执行 |
| Resource | 包裹后续整个管道,可做缓存 |
| Action | 动作前后 |
| Exception | 统一异常处理 |
| Result | 结果序列化前后 |
与中间件的区别:中间件面向"请求",过滤器面向"MVC 动作",能拿到 ActionContext 与模型状态。
六、配置与 Options
配置来源按顺序叠加:appsettings.json → 环境变量 → 命令行。后面的覆盖前面的。
Options 模式有三种:IOptions<T>(单例)、IOptionsSnapshot<T>(Scoped,配置变更可感知)、IOptionsMonitor<T>(单例,变更通知)。需要热更新时用 Monitor。
七、后台任务:IHostedService 与 BackgroundService
定时清理、队列消费、缓存预热这类任务不属于任何一次请求,需要托管在应用生命周期里。
| 方式 | 适用 | 写法 |
|---|---|---|
IHostedService | 需要精确控制启动/停止顺序 | 自己实现 StartAsync / StopAsync |
BackgroundService | 长驻循环任务(推荐) | 继承并重写 ExecuteAsync |
| 第三方调度库 | 需要 cron 表达式、任务持久化 | 如 Quartz.NET、Hangfire |
最小实现
public class CleanupService : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
public CleanupService(IServiceScopeFactory scopeFactory) => _scopeFactory = scopeFactory;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
// 后台任务是单例,不能直接注入 Scoped 服务,必须自己开作用域
using var scope = _scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await CleanupAsync(db, stoppingToken);
await Task.Delay(TimeSpan.FromHours(1), stoppingToken);
}
}
}
// 注册
builder.Services.AddHostedService<CleanupService>();
四个必须知道的坑
- 不能注入 Scoped 服务:
BackgroundService是单例,直接注入DbContext会抛 "cannot consume scoped service"。必须用IServiceScopeFactory自己开作用域。 - 要响应停止信号:所有异步调用都传
stoppingToken,否则容器停止时会卡住直到超时。 - 异常会导致宿主退出:
ExecuteAsync里抛异常默认停掉整个应用,用 try/catch 包住循环体,记日志后继续。 - 多实例会重复执行:K8s 多副本部署时每个副本都会跑一遍,要么用分布式锁,要么交给专门的调度组件。
测试时别真的等一小时:把间隔抽成配置项,测试里注入极短间隔。
八、常见坑
- 中间件顺序错误,鉴权拿不到用户。
- Singleton 注入 Scoped,导致 DbContext 被共享。
- 在中间件构造器里注入 Scoped 服务,启动即报错。
- 特性路由与约定路由混用,URL 冲突。
- 忘记
[ApiController],模型校验不生效。
九、动手任务:自写 → AI 写 → 对比 → 取舍
四步不跳:① 自己写(限时,关掉补全)→ ② 让 AI 写同一题 → ③ 逐条比对 → ④ 说出保留谁的写法。 完整动作与记录模板见《AI 协作与代码评审》。
① 自己写
- 写一个记录耗时的中间件,放在管道最外层。
- 故意把 UseAuthorization 放到 UseRouting 之前,观察行为。
- 写一个 Singleton 捕获 Scoped 的例子,用 ScopeFactory 修好。
② 让 AI 写同一题
把第 2 题(自定义中间件)交给 AI,补一句 说明注册顺序对执行顺序的影响,以及异常处理器该放哪。
③ 对比要盯的点
- 异常处理中间件它放在管道最前面了吗?放后面就兜不住前面的异常。
app.Use...的顺序它讲清了吗?中间件是洋葱模型,注册顺序即执行顺序。- 它在中间件里注入了 Scoped 服务却存在单例里吗?那是 captive dependency。
④ 一句话取舍
它这套顺序下,认证失败会先走鉴权还是先走异常处理?
十、自测
- ASP.NET Core 的请求链路是什么?
- 中间件顺序为什么重要?
- 三种生命周期的差异与陷阱?
- 过滤器与中间件的区别?
- 模型绑定失败会怎样?
- BackgroundService 为什么要用 IServiceScopeFactory?四个常见坑是什么?
参考答案要点:Kestrel→管道→路由→过滤器→动作;短路与依赖;Singleton 捕获 Scoped;粒度不同;ModelState/400;后台任务是单例不能注入 Scoped,坑包括作用域、停止信号、异常导致宿主退出、多实例重复执行。
附录:高频追问
问:中间件能拿到 Scoped 服务吗? 构造器里不能(中间件是单例),可以在 InvokeAsync 的参数里注入。
问:Use 与 Run 的区别? Use 可以调用 next 继续管道,Run 终止管道不再往后走。
问:为什么 DbContext 是 Scoped? 它内部有变更跟踪与连接状态,跨请求共享会串数据。
问:[ApiController] 做了什么? 自动模型校验、绑定来源推断、问题详情返回、要求特性路由。
问:过滤器能替代中间件吗? 不能。过滤器只覆盖 MVC 动作,静态文件、健康检查等不经过过滤器。
问:如何做全局异常处理? 中间件捕获 + ProblemDetails,或 ExceptionFilter;推荐中间件,覆盖面更广。
问:Options 怎么热更新? 用 IOptionsMonitor<T> 并监听 OnChange。
问:路由冲突怎么排查? 打开端点列表日志,检查模板是否有重复或歧义。
问:如何限制请求体大小? Kestrel 的 MaxRequestBodySize 或 [RequestSizeLimit]。
问:健康检查怎么接? AddHealthChecks + MapHealthChecks,暴露给探针。
附:中间件短路与异常
中间件不调用 next 就短路,后面的管道不会执行。常见用途:鉴权失败直接返回 401、限流直接返回 429。
异常处理要放在最外层:如果异常发生在响应已经开始写入之后,就无法再改状态码,只能断开连接。所以“先算完再写响应”是好习惯。
app.Use(async (ctx, next) =>
{
try { await next(); }
catch (Exception ex)
{
if (!ctx.Response.HasStarted)
{
ctx.Response.StatusCode = 500;
await ctx.Response.WriteAsJsonAsync(new { error = "internal" });
}
}
});
中间件与过滤器的分工
中间件面向整个请求管道,适合跨域、压缩、鉴权、日志;过滤器面向 MVC 动作,适合模型校验、动作前后、结果包装。
判断标准:这段逻辑是否需要知道“哪个 Controller/Action”。不需要就用中间件,需要就用过滤器。
补充:请求体与响应流
[FromBody]默认只能读一次,读完要自己缓存。- 大文件上传用流式处理,不要整个读进内存。
- 响应开始写入后不能再改状态码或头。
- 压缩(ResponseCompression)要放在合适位置,避免压缩已压缩的内容。
补充:管道调试
- 在中间件里打日志,确认执行顺序与短路点。
- 用
app.UseRouting()后的端点信息排查 404。 - 用
DeveloperExceptionPage只在开发环境,生产用统一异常处理。 - 打印所有注册的服务,确认 DI 没有重复注册。
一个最小可用的中间件顺序
异常处理 → HTTPS 重定向 → 静态文件 → 路由
→ 认证 → 授权 → 端点
顺序错了,轻则鉴权失效,重则异常无法捕获。先保证顺序正确,再谈性能。
常见 404 的三个原因
- 路由模板不匹配(方法/参数/前缀)。
- 中间件短路,根本没走到端点。
- 控制器未注册或程序集未加载。
404 先查路由模板,再查中间件短路,最后查注册;按顺序排查,别猜。先模板,后短路,再注册。先查模板。
补遗:Minimal API 与模型校验
Minimal API 用 app.MapGet/MapPost 直接注册端点,适合小服务与快速原型;复杂项目仍推荐 Controller(过滤器、绑定、约定更完整)。
模型校验:DataAnnotations([Required]、[Range])配合 [ApiController] 自动返回 400;复杂规则用 FluentValidation。校验失败统一返回 ProblemDetails(RFC 7807),前端好处理。
补遗:限流与输出缓存
.NET 7+ 内置限流中间件:AddRateLimiter + 固定窗口/滑动窗口/令牌桶/并发限制;被拒返回 429 或 503,客户端要能重试。
输出缓存(AddOutputCache/UseOutputCache)按 URL 或策略缓存响应;按用户区分的接口要自定义缓存键,避免串数据。分布式部署时用 Redis 作为缓存存储。
补遗:HTTP 与 TCP 基础
HTTP/1.1 长连接但有队头阻塞;HTTP/2 多路复用与头部压缩;HTTP/3 基于 QUIC。
HTTPS 用 TLS 握手协商密钥,再对称加密;证书链必须可信。
TCP 三次握手/四次挥手;TIME_WAIT 过多多为短连接,用 HttpClientFactory 连接池复用。HttpClient 不要频繁 new,会耗尽 socket。
配套题目
★ 越多越该先背;点题目可看完整答案与深入版。
- ★★★★★ ASP.NET Core 的中间件管道是怎么工作的?顺序为什么重要?
- ★★★★★ ASP.NET Core 的依赖注入有哪几种生命周期?会踩什么坑?
- ★★★☆☆ 一次 ASP.NET Core 请求的完整链路是怎样的?
- ★★★☆☆ 中间件和过滤器有什么区别?
提示:读完可以顺手做几道对应题目,检验是否真的理解