一次 HTTP 请求里的 DI 全链路:从 RequestServicesFeature.CreateScope 到 ServiceProviderEngineScope.GetService 的真实
将“每请求 Scope”拆开来看其实就两件事第一次用到RequestServices才创建 scope响应结束时自动释放。中间再补上对象分层你就能把整条链路用断点锁定。ASP.NET Core 的“每请求 Scope”有一个严格的规则它不会在请求一进来就生成而是等到第一次读取HttpContext.RequestServices时RequestServicesFeature才会延迟调用CreateScope()。释放也不依赖你手动using而是通过Response.RegisterForDisposeAsync(...)挂到响应结束时自动触发。下面我只追踪两处源码入口将结论落实到具体可下断点的位置RequestServicesFeature.csRequestServicesgetter 决定什么时候创建DisposeAsync()决定什么时候释放。ServiceProviderEngineScope.csGetService(...)决定 scope 作为“解析入口”时最终把调用转发到哪里。最后你会得到两样东西一条能复述的调用路径以及一份够用的最短断点清单——从HttpContext.Features一路走到ServiceProviderEngineScope.GetService。总览一次请求内 DI 的层级结构与职责边界Request → Feature → Scope → RootProvider适用场景/诞生背景场景你已经知道 Singleton/Scoped/Transient 的概念但通常会卡在三个点Scoped 服务到底什么时候“开始存在”请求结束时由谁来负责 DisposeRequestServices.GetService(...)真正的解析发生在什么位置目标把“每请求 Scope”拆成能定位的4 层对象和1 个触发条件每一步都能用断点验证。核心原理1层级框架定义式HttpContext.Features请求态的扩展点容器和 DI 相关的入口是IServiceProvidersFeature。RequestServicesFeature实现IServiceProvidersFeature主要负责两件事延迟创建 request scopelazy init。把 scope 的释放注册到响应结束automatic disposal。IServiceScope默认实现ServiceProviderEngineScopescope 的“状态载体”包括缓存、可释放对象追踪同时它本身也充当“当前 scope 的IServiceProvider”。RootServiceProvider默认容器的解析引擎入口。真正的 CallSite 构建、缓存命中、实例创建等逻辑都在这里本文只停在“调用会回到 RootProvider”这一层。2触发条件公式化表达Scope 创建时机条件首次读取HttpContext.RequestServices并且_requestServicesSet false且_scopeFactory ! null。结果调用CreateScope()生成 request scope然后把scope.ServiceProvider暴露为RequestServices。缺陷/对比很多人会按直觉以为“请求进来就创建 scope”但默认实现不是这么做的它是 lazy。直接能推到一个可验证的结论如果整个请求里从没访问RequestServices就不会从这条入口创建 request scope。至于有没有别的路径触发解析那是另一回事但至少这里不会。回扣下一节从RequestServicesFeature.RequestServices的 getter 开始把“创建”和“释放”都固定到同一段代码、同一组字段上。入口RequestServicesFeature 如何在第一次访问 RequestServices 时 CreateScope 并注册请求结束释放适用场景/诞生背景场景调试时经常会问Scoped 服务到底什么时候“出现”为什么不用写using也会在请求结束释放对齐方式别从某个业务服务的构造函数往回倒直接从第一次读取HttpContext.RequestServices的那一行进源码。核心原理对源码逐行对齐1延迟创建lazy init触发条件!_requestServicesSet _scopeFactory ! null。执行顺序是_context.Response.RegisterForDisposeAsync(this)_scope _scopeFactory.CreateScope()_requestServices _scope.ServiceProvider_requestServicesSet true结论很直接request scope 不会在 middleware 管线的“起点”自动生成而是在RequestServicesgetter 真正被用到时才创建。2释放绑定automatic disposalRequestServicesFeature自己实现了IDisposable/IAsyncDisposable。更关键的是它在创建 scope 之前就先做了RegisterForDisposeAsync(this)等于把“释放这件事”先挂到响应结束的回调上。DisposeAsync()里主要是对_scope做类型分派_scope是IAsyncDisposable就走DisposeAsync()。否则如果是IDisposable就走Dispose()。最后把_scope和_requestServices置空避免 feature 继续持有一个已经释放过的 scope。缺陷/对比和你手动写using var scope provider.CreateScope()放一起看会更清楚手动 scope创建和释放都在你代码里显式发生。request scope创建靠首次访问触发释放靠Response生命周期回调。边界也很明确只有真的触发了 getter 里的创建分支才会有 scope也才谈得上释放_scope。回扣到这一步其实就能把两件事钉死创建发生在RequestServicesgetter 的 if 分支。释放发生在响应结束触发的DisposeAsync/Dispose。下一步继续追CreateScope()产生的_scope.ServiceProvider到底是什么对象它的GetService又会把解析带到哪里。中继Scope 不是解析引擎——ServiceProviderEngineScope 的职责是“状态容器 转发入口”适用场景/诞生背景场景看到_requestServices _scope.ServiceProvider很容易误会是不是“解析就在 scope 里完成”先把分工说明白在默认容器里scope 更像请求态上下文它不承载解析算法本体。核心原理1Scope 的接口形态定义ServiceProviderEngineScope同时实现IServiceScope定义 scope 的生命周期边界。IServiceProvider能直接GetService(...)所以它可以被当作RequestServices暴露出去。IServiceScopeFactory允许从当前 scope 再创建子 scope。IAsyncDisposable支持异步释放。直观含义它既像一个“请求内的 IServiceProvider 外壳”也负责 scope 的释放与子 scope 的创建。2Scope 的核心状态读字段即可验证ResolvedServicesDictionaryServiceCacheKey, object?。用来缓存解析结果尤其是 scoped 生命周期那部分“每 scope 复用”。_disposablesListobject??。记录需要跟着 scope 一起释放的实例IDisposable或IAsyncDisposable。Sync以ResolvedServices作为锁对象。用于保护 scope 状态root 与非 root 的保护范围略有差异源码注释里有交代。缺陷/对比如果把“scope解析器”当成前提会走偏解析算法选构造函数、处理 IEnumerable、闭包缓存等并不在 scope 里。scope 更像“解析上下文”给 RootProvider 提供缓存位置和释放边界。总结下一节把断点下在ServiceProviderEngineScope.GetService用一行代码确认它怎么把请求转发给 RootProvider同时观察 scope 参数是怎么参与解析语境的。落点ServiceProviderEngineScope.GetService 如何把解析委托给 RootProvider以及这意味着什么适用场景/诞生背景场景你想在断点里看清楚“Scoped 解析到底走到哪一层”并能区分 RootScope 和 request scope 的行为差异。方法不用猜直接看ServiceProviderEngineScope.GetService(Type)。核心原理1GetService 的真实动作ServiceProviderEngineScope.GetService(Type serviceType)的核心就是这一句RootProvider.GetService(ServiceIdentifier.FromServiceType(serviceType), this)这里的this当前 scope被当成参数传进去意思是“在这个 scope 的语境下解析”。2语义解释可检验RootProvider解析引擎入口决定怎么构建、怎么缓存、怎么创建实例。scope(this)解析上下文决定缓存写到哪里、可释放对象挂到哪里。所以结论可以讲得很清楚同一个服务类型换个 scope 可能就是另一个实例scoped 的语义。解析算法不会在每个 scope 里复制一份它集中在 RootProvider。缺陷/对比默认容器并不是“每个 scope 一套独立解析器”。scope 更像 RootProvider 的一个参数它改变的是缓存与释放边界不是解析算法本身。总结这时链路其实已经闭合RequestServicesFeature暴露出来的RequestServices本质上就是一个 scope实现了IServiceProvider。而 scope 的GetService会回到 RootProvider。要继续追 CallSite、缓存命中、实例创建就从RootProvider.GetService(...)往下走。断点清单把“创建—解析—释放”做成一次可复现的调试脚本适用场景 / 诞生背景场景你想照着源码复现结论需要的是最短路径断点位置 预期现象 观察项。调试脚本断点位置 预期现象 观察项1断点 ARequestServicesFeature.RequestServicesgetter预期现象第一次访问会进 if 分支之后再访问直接 return_requestServices。观察项_requestServicesSetfalse → true。_scope从null变为非空。_requestServices被赋值为_scope.ServiceProvider。2断点 B_context.Response.RegisterForDisposeAsync(this)预期现象注册发生在CreateScope()之前这能确认“先把释放挂上再创建 scope”。观察项该调用执行完返回即可如果还想验证“响应结束如何触发回调”要继续追HttpResponse内部的注册与执行点本文不展开调用栈证据。3断点 CServiceProviderEngineScope.GetService(Type serviceType)预期现象只要从RequestServices发起解析比如构造函数注入背后的解析通常就会命中这里。命中后会马上转发到RootProvider.GetService(..., this)。观察项_disposed是否已释放用来排除“请求结束后仍在解析”的异常路径。IsRootScope区分 root scope 和 request scope。RootProviderRootServiceProvider的引用。入参serviceType当前正在解析的服务类型。4断点 DRequestServicesFeature.DisposeAsync()预期现象请求结束时触发对_scope做 IDisposable/IAsyncDisposable 分派释放最后字段置空。观察项_scope的实际运行时类型。走的是Dispose()还是DisposeAsync()。_scope、_requestServices在末尾被置为null。缺陷 / 对比如果请求期间从未访问HttpContext.RequestServices断点 A 不会命中创建分支。断点 D 仍可能被调用feature 被 dispose但_scope为空时不会发生 scope 释放动作这就是“没创建就没释放”的边界。总结把 A创建→ C解析转发→ D释放连起来就足够在本机断点里还原“每请求 scope”到底是怎么回事。想再往里追就从 C 里看到的RootProvider.GetService(...)继续向下走进入 CallSite、缓存命中、捕获可释放对象等细节。最后总结成一句话就够了ASP.NET Core 的“每请求 Scope”不是玄学第一次访问RequestServices才会CreateScope响应结束会触发Dispose解析入口统一交给 RootServiceProviderscope 主要提供“缓存位置 释放边界”。行动建议最短闭环按上面的断点 A → C → D 跑一遍真实请求把三处调用栈截图留档后面你要继续追RootProvider.GetService(...)、CallSite 构建和 scoped 缓存命中这三张图就是最稳的基线。代码示例在控制器中验证断点 A、C、D下面是一个简单的 ASP.NET Core 控制器示例演示如何触发断点清单中的关键点usingMicrosoft.AspNetCore.Mvc;usingMicrosoft.Extensions.DependencyInjection;namespaceDebugDi.Controllers{[ApiController][Route(api/[controller])]publicclassDebugScopeController:ControllerBase{privatereadonlyIServiceProvider_serviceProvider;privatereadonlyILoggerDebugScopeController_logger;publicDebugScopeController(IServiceProviderserviceProvider,ILoggerDebugScopeControllerlogger){_serviceProviderserviceProvider;_loggerlogger;}[HttpGet(test-scope)]publicIActionResultTestRequestScope(){// 断点 A 触发点 // 第一次访问 HttpContext.RequestServices 会触发 scope 创建// 在 RequestServicesFeature.RequestServices getter 设置断点varrequestServicesHttpContext.RequestServices;_logger.LogInformation(第一次访问 RequestServicesscope 应已创建);// 断点 C 触发点 // 通过 RequestServices 解析服务会触发 ServiceProviderEngineScope.GetService// 在 ServiceProviderEngineScope.GetService(Type) 设置断点varscopedServicerequestServices.GetServiceIMyScopedService();_logger.LogInformation(解析 Scoped 服务应命中断点 C);// 再次访问不会创建新 scope断点 A 不会进 if 分支varsameRequestServicesHttpContext.RequestServices;_logger.LogInformation(再次访问 RequestServices应直接返回已有实例);// 断点 D 触发点 // 响应结束时自动触发无需手动代码// 在 RequestServicesFeature.DisposeAsync() 设置断点// 观察请求结束后 scope 如何被释放returnOk(new{Message断点验证完成,RequestServicesCreatedrequestServices!null,ScopedServiceResolvedscopedService!null,SameInstanceReferenceEquals(requestServices,sameRequestServices)});}}// 示例 Scoped 服务publicinterfaceIMyScopedService{}publicclassMyScopedService:IMyScopedService,IDisposable{privatereadonlyGuid_idGuid.NewGuid();publicMyScopedService(){Console.WriteLine($MyScopedService 实例创建:{_id});}publicvoidDispose(){Console.WriteLine($MyScopedService 实例释放:{_id});}}}调试步骤与预期输出断点 A 验证在RequestServicesFeature.RequestServicesgetter 设置断点首次调用/api/debugscope/test-scope时断点会命中 if 分支观察调试器_requestServicesSet从false变为true_scope从null变为非空断点 C 验证在ServiceProviderEngineScope.GetService(Type)设置断点当执行requestServices.GetServiceIMyScopedService()时断点命中观察调试器_disposed应为falseIsRootScope应为falseserviceType参数应为IMyScopedService断点 D 验证在RequestServicesFeature.DisposeAsync()设置断点请求结束后响应完成断点自动命中观察调试器_scope的实际类型释放方法调用Dispose()或DisposeAsync()最后字段被置为null预期调试器输出截图描述断点 A 截图调用栈显示从控制器到HttpContext.RequestServicesgetter 的路径局部变量窗口显示_requestServicesSet: false → true的变化_scope从null变为ServiceProviderEngineScope实例断点 C 截图调用栈显示解析链控制器 →GetServiceT→ServiceProviderEngineScope.GetService监视窗口显示this的RootProvider引用和IsRootScope: false参数窗口显示serviceType: IMyScopedService断点 D 截图调用栈显示释放路径HttpResponse完成 →RequestServicesFeature.DisposeAsync局部变量窗口显示_scope被释放前后的状态输出窗口显示MyScopedService实例的 Dispose 调用注册服务配置在Program.cs中添加builder.Services.AddScopedIMyScopedService,MyScopedService();这个示例提供了一个可运行的验证环境让你能够实际观察「创建—解析—释放」的完整生命周期。