知

模块二 · 请求与异步

第 3 / 11 节23 分钟阅读型

一次请求在 ASP.NET Core 里怎么走完

从 Kestrel 接收到中间件管道、路由、模型绑定、过滤器与返回结果。这一节按请求经过的顺序讲,每一站都能挂一个面试题。

读完这一节,你能做到

  1. 按顺序说出 ASP.NET Core 的请求处理链路。
  2. 说清中间件的注册顺序为什么重要。
  3. 区分 Singleton/Scoped/Transient 的生命周期与陷阱。
  4. 说清过滤器的执行顺序与适用场景。

一、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>();

四个必须知道的坑

  1. 不能注入 Scoped 服务:BackgroundService 是单例,直接注入 DbContext 会抛 "cannot consume scoped service"。必须用 IServiceScopeFactory 自己开作用域。
  2. 要响应停止信号:所有异步调用都传 stoppingToken,否则容器停止时会卡住直到超时。
  3. 异常会导致宿主退出:ExecuteAsync 里抛异常默认停掉整个应用,用 try/catch 包住循环体,记日志后继续。
  4. 多实例会重复执行:K8s 多副本部署时每个副本都会跑一遍,要么用分布式锁,要么交给专门的调度组件。

测试时别真的等一小时:把间隔抽成配置项,测试里注入极短间隔。

八、常见坑

  1. 中间件顺序错误,鉴权拿不到用户。
  2. Singleton 注入 Scoped,导致 DbContext 被共享。
  3. 在中间件构造器里注入 Scoped 服务,启动即报错。
  4. 特性路由与约定路由混用,URL 冲突。
  5. 忘记 [ApiController],模型校验不生效。

九、动手任务:自写 → AI 写 → 对比 → 取舍

四步不跳:① 自己写(限时,关掉补全)→ ② 让 AI 写同一题 → ③ 逐条比对 → ④ 说出保留谁的写法。 完整动作与记录模板见《AI 协作与代码评审》。

① 自己写

  1. 写一个记录耗时的中间件,放在管道最外层。
  2. 故意把 UseAuthorization 放到 UseRouting 之前,观察行为。
  3. 写一个 Singleton 捕获 Scoped 的例子,用 ScopeFactory 修好。

② 让 AI 写同一题

把第 2 题(自定义中间件)交给 AI,补一句 说明注册顺序对执行顺序的影响,以及异常处理器该放哪。

③ 对比要盯的点

  • 异常处理中间件它放在管道最前面了吗?放后面就兜不住前面的异常。
  • app.Use... 的顺序它讲清了吗?中间件是洋葱模型,注册顺序即执行顺序。
  • 它在中间件里注入了 Scoped 服务却存在单例里吗?那是 captive dependency。

④ 一句话取舍

它这套顺序下,认证失败会先走鉴权还是先走异常处理?

十、自测

  1. ASP.NET Core 的请求链路是什么?
  2. 中间件顺序为什么重要?
  3. 三种生命周期的差异与陷阱?
  4. 过滤器与中间件的区别?
  5. 模型绑定失败会怎样?
  6. 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 的三个原因

  1. 路由模板不匹配(方法/参数/前缀)。
  2. 中间件短路,根本没走到端点。
  3. 控制器未注册或程序集未加载。

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。

配套题目

★ 越多越该先背;点题目可看完整答案与深入版。

提示:读完可以顺手做几道对应题目,检验是否真的理解