一次请求的 DI 到底怎么跑:从 HttpContext.RequestServices 创建 Scope 到 ServiceProvider.GetService 的第一跳
本文想做的事情很具体把“每请求 scope”的创建和释放落到两处能在源码里指认、在调试器里复现的点上——RequestServicesFeature的 getter以及Response.RegisterForDisposeAsync。很多 ASP.NET Core 开发者都听过 scoped 服务“每请求一个实例”。但你真把它拆成几个可验证的问题scope 是不是请求一进管道就建好CreateScope到底在第几行发生释放又是哪一行触发这时往往就说不清了。结论先摆出来在默认实现下ASP.NET Core 不会在请求开始时强制创建 scopescope 是RequestServicesFeature延迟创建的——第一次有人访问HttpContext.RequestServices时才会调用CreateScope。同一处还会把释放逻辑挂到响应结束阶段自动Dispose。接下来只做三件事1把CreateScope的发生点在源码/调试器里定位出来2把Dispose的发生点定位出来3确认它们针对的是同一个 scope且成对发生。总览一次请求中的 DI “第一跳”分层Context → Feature → Scope → Provider适用场景/背景你已经知道 scoped/singleton/transient 的语义但对“per-request scope 什么时候创建、什么时候释放”缺少能落地的定位点通常会卡在两类推断上每个请求都能解析 scoped于是觉得“请求一开始就创建了 scope”。scoped 会在请求结束回收于是觉得“框架肯定 somewhere 显式 Dispose 了它”但找不到位置。核心结构本文只讨论 DI 的“第一跳”把请求里与 DI 相关的入口链路拆成 4 层范围严格止步于“拿到 scope 内部的 ServiceProvider”为止1HttpContext对外暴露RequestServices业务代码最常碰到的入口。2IServiceProvidersFeatureRequestServices的真正承载者HttpContext本质上是转发。3RequestServicesFeature默认实现负责“要不要创建 scope”“创建后怎么缓存”“释放怎么挂到请求结束”。4IServiceScope / ServiceProviderscope 创建后_scope.ServiceProvider就是后续解析入口再往下才会进入GetService(...)、call-site 等容器内部细节本文不展开。一句可检验公式本文的验收口径用调试器能直接验证的口径把结论闭环起来没访问HttpContext.RequestServices⇒ 不会创建 scope第一次访问HttpContext.RequestServices⇒CreateScope 缓存 provider 注册请求结束释放请求结束 ⇒ 自动Dispose释放 scope 及其内部资源常见误解对比“每请求都有 scope”经常被听成“请求一开始就创建 scope”。本文后面只盯两件事创建点到底在哪个 getter/分支里发生CreateScope释放点释放动作怎么绑定到响应结束创建点RequestServicesFeature.RequestServices getter 的延迟 CreateScope 条件适用场景/背景当代码第一次读取HttpContext.RequestServices中间件、过滤器、控制器甚至某些框架组件内部时DI 才需要一个“请求级”的解析入口。scope 就是在这里真正出现的。核心原理逐行看 RequestServices getter 的关键分支RequestServicesFeature的RequestServices属性 getter 里有一个很明确的“延迟创建”分支。1触发条件(!_requestServicesSet _scopeFactory ! null)拆开看就是两层意思_requestServicesSet false之前从未计算/设置过RequestServices也就是“第一次访问”的判据。_scopeFactory ! null当前上下文确实有创建 scope 的工厂没有它就谈不上请求 scope。2首次访问时的创建动作只会执行一次_context.Response.RegisterForDisposeAsync(this)_scope _scopeFactory.CreateScope()_requestServices _scope.ServiceProvider_requestServicesSet true这里顺序值得注意先注册释放再创建 scope。这样只要 scope 一创建出来释放路径就已经挂上了第 4 节会把这件事说清。3结果缓存返回不重复创建getter 最后return _requestServices!;。当_requestServicesSet变成 true后续再访问RequestServices就直接返回缓存的_requestServices不会再CreateScope。可观察变量调试器里直接看在断点处建议盯这几个字段_requestServicesSet是否已经完成首次初始化。_scopescope 是否已创建首次访问后应非 null。_requestServices是否已经指向_scope.ServiceProvider。边界/对比setter 会改变默认行为RequestServices还有 setterset { _requestServices value; _requestServicesSet true; }。意思很直白如果外部代码手动 set 过RequestServices_requestServicesSet会提前变成 true此后 getter 就不会再走默认的CreateScope分支。这种情况多见于测试或自定义 hosting/管线注入不是框架“换规则”而是你跳过了默认 feature 的 scope 创建路径。回扣创建点不在“请求刚进管道”的某个固定位置而是落在RequestServicesFeature.RequestServicesgetter 的首次访问分支里。挂载点DefaultHttpContext 如何把 RequestServices 转发到 RequestServicesFeature适用场景/背景搞清楚“scope 在 getter 里延迟创建”之后下一步通常是把断点打到对的文件。但现实里你访问的是HttpContext.RequestServicesRequestServicesFeature又是从哪来的、什么时候塞进 context 的核心原理DefaultHttpContext 的 FeatureReferences 延迟挂载DefaultHttpContext并不是构造时就把所有 feature 都 new 一遍它通过FeatureReferences做按需创建和缓存。跟RequestServices相关的关键点主要是三条1默认实现的“工厂函数”DefaultHttpContext内部有一个静态委托_newServiceProvidersFeature context new RequestServicesFeature(context, context.ServiceScopeFactory)这句话回答了“谁 new 了RequestServicesFeature”默认就是DefaultHttpContext在需要时自己创建。2延迟创建与缓存Fetch(...)ServiceProvidersFeature属性会走_features.Fetch(ref _features.Cache.ServiceProviders, this, _newServiceProvidersFeature)含义是cache 里没有就用_newServiceProvidersFeature创建并缓存有了就直接复用。3HttpContext.RequestServices只是转发DefaultHttpContext的RequestServices属性本身就是get ServiceProvidersFeature.RequestServices;set ServiceProvidersFeature.RequestServices value;重要结论两层延迟把“第一次访问RequestServices才创建 scope”拆开其实是两层延迟第一次访问HttpContext.RequestServices可能先触发RequestServicesFeature的创建feature 层延迟。第一次访问RequestServicesFeature.RequestServices才触发CreateScope()scope 层延迟。回扣调试时别只盯“scope 什么时候创建”feature 本身也可能是第一次访问时才出现。两层延迟叠一起是很多人断点打了却“打空”的原因。释放点RegisterForDisposeAsync 如何把 scope 绑定到请求结束自动 Dispose适用场景/背景scoped 服务之所以能在“请求结束自动释放”根子在它们属于某个 scopescope 释放时会把它创建/跟踪的可释放实例一起处理掉。所以问题可以收敛成一句话请求结束时到底是谁来Dispose这个 scope核心原理创建 scope 时就把释放挂到 ResponseRequestServicesFeature的 getter 在首次创建分支里第一句就是1注册释放_context.Response.RegisterForDisposeAsync(this)意思是把“当前对象RequestServicesFeature实例”交给 Response 的生命周期管理响应结束时框架会回调已注册对象的 dispose。这里注册的是this不是_scope。原因也很简单RequestServicesFeature持有_scope字段它最清楚应该怎么释放_scope同步/异步分派也能在释放后把内部状态清掉。2RequestServicesFeature实现IAsyncDisposable/IDisposable它同时实现了IAsyncDisposable与IDisposableDispose()本质上是对DisposeAsync()的同步封装。3DisposeAsync内部对_scope做分派并清理字段DisposeAsync()的逻辑大致是_scope是IAsyncDisposable走DisposeAsync()。否则_scope是IDisposable走Dispose()。最后把字段清掉_scope null; _requestServices null;调试时这点非常好用请求结束触发 dispose 后你能在同一个 feature 实例上看到_scope从非 null 变成 null。边界没访问 RequestServices ⇒ 根本不用释放如果整个请求过程中从未访问过RequestServices不会进入首次访问分支不会CreateScope()也不会RegisterForDisposeAsync(this)。这时谈“释放 scope”其实没有对象可谈。回释放点不是业务代码手动Dispose也不是某个“后台统一清理”它就绑在首次访问RequestServices的那次调用里注册到 Response响应结束时触发RequestServicesFeature.DisposeAsync/Dispose。可复现调试路径断点、观察点、与“第一跳”确认清单适用场景/背景想把结论从“我知道”变成“我看见”最省事的方法就是用一条最短的请求空的 Minimal API 或一个 MVC action 都行按顺序打断点看变量怎么变。下面是一套可以重复跑的断点/观察点清单按执行顺序放就够了。断点与观察点按执行顺序1断点 ADefaultHttpContext.RequestServicesgetter目标确认HttpContext.RequestServices就是转发。观察点进入 getter 后是否直接转到ServiceProvidersFeature.RequestServices。如果之前没触发过ServiceProvidersFeature这次访问可能会先命中Fetch(...)创建 feature调用栈里能看到。2断点 BRequestServicesFeature.RequestServicesgetter 的 if 分支内部目标确认“首次访问触发CreateScope”。观察点进入前_requestServicesSet false_scope null通常如此。执行后_requestServicesSet true_scope ! null_requestServices _scope.ServiceProvider。第二次命中 getter不再进入 if 分支验证缓存。3断点 C_context.Response.RegisterForDisposeAsync(this)目标确认释放绑定与创建发生在同一处。观察点this就是当前RequestServicesFeature实例后续 dispose 时也是同一个对象。Response 内部“待释放队列”的结构本文不展开你只需要确认注册确实发生了。4断点 DRequestServicesFeature.DisposeAsync/Dispose目标确认请求结束自动释放 scope。观察点_scope的运行时类型是不是IAsyncDisposable走 async 分支或只有IDisposable。执行完成后_scope null_requestServices null。“第一跳”到此为止下一步才是容器内部解析在断点 B 里拿到_scope.ServiceProvider后继续跟进去就是下一跳ServiceProvider.GetService(...)、call-site、缓存等容器内部实现。本文的边界很明确把 per-request scope 的创建/释放闭环跑通容器解析链路留到后续再说。误区校验清单用断点直接证伪/证实误区 1scope 在请求开始就创建。校验让请求尽量不触达RequestServices或尽量晚触达看断点 B 什么时候第一次命中。误区 2scope 需要手动 Dispose。校验不写任何释放代码看断点 D 是否仍会在请求结束触发。回扣验收标准就一条在调试器里看到同一个 scope 的CreateScope和Dispose/DisposeAsync成对出现。把“每请求 scope”落到源码上其实就是两件事创建默认情况下不是请求一开始就创建而是首次访问HttpContext.RequestServices时经由RequestServicesFeature.RequestServicesgetter 进入CreateScope()。释放同一处会RegisterForDisposeAsync(this)从而在响应结束触发RequestServicesFeature.DisposeAsync/Dispose释放_scope并清空引用。如果你想把它变成可复述的结论按第 5 节断点清单跑一遍就够了。下一篇再从_scope.ServiceProvider往下跟进到ServiceProvider.GetService(...)的解析链路。

相关新闻

标记“”不是此版本中的有效语句分隔符

标记“”不是此版本中的有效语句分隔符

问题原因 PowerShell 版本过低‌:Windows 自带的 PowerShell 不支持&&运算符,该运算符从 PowerShell 7.0 才开始支持。 使用 $PSVersionTable 变量 $PSVersionTable 是一个包含PowerShell版本和其他系统信息的哈希表。你可以直接查看这个变量来…

2026/6/27 5:49:31阅读更多 →
必得8元 免费领取 教程!最新可用!

必得8元 免费领取 教程!最新可用!

如果你想薅一下羊毛,想喝一下免费的奶茶,真的可以试一下这个口令:千问新用户专属860982就能领8元支付宝无门槛立减券,外卖、打车、网购都能用。这个福利有效期是领到之后14天,每个手机号和设备只能领一次~

2026/6/27 5:49:31阅读更多 →
海外APP定制开发|多语言支付上架全流程

海外APP定制开发|多语言支付上架全流程

海外APP定制开发|多语言支付上架全流程想做海外APP、海外软件系统、跨境业务平台、多语言应用等,但不知道功能怎么规划、预算怎么评估、后期怎么上线? 我们提供: APP定制开发|小程序开发|系统开发&#xff…

2026/6/27 5:49:31阅读更多 →
DOPE-PEG-CY3 荧光磷脂不同 PEG 分子量荧光亮度与抗团聚性能差异说明

DOPE-PEG-CY3 荧光磷脂不同 PEG 分子量荧光亮度与抗团聚性能差异说明

一、材料基础结构DOPE-PEG-CY3 为不饱和油酰磷脂荧光标记脂质,三段结构:DOPE 疏水脂质段:带有不饱和脂肪酸双烷基链,兼具优良膜嵌入能力,可高效融合细胞膜磷脂双层结构。PEG 亲水间隔链:包裹于纳米载体表层…

2026/6/27 7:09:39阅读更多 →
从Chatbot到数字员工:2026年AI Agent落地的5个真相

从Chatbot到数字员工:2026年AI Agent落地的5个真相

79%的企业已经部署了AI Agent,但40%的项目面临被叫停。在这场从"会说话"到"会干活"的跃迁中,到底什么在推动变革,什么又在拖后腿?一、一个数字背后的转折点 2026年第二季度,一组数据让整个行业重新…

2026/6/27 7:09:39阅读更多 →
[SWPUCTF 2021新生赛]nc签到

[SWPUCTF 2021新生赛]nc签到

1. 工具:Windows 版 netcat(nc-win32)工具存放路径:D:\cxdownload\netcat-win32-1.11\netcat-1.11\ 作用:建立 TCP 网络连接,获取靶机提供的交互式 Linux 虚拟终端。2. 前置知识铺垫nc ip 端口:…

2026/6/27 7:09:39阅读更多 →
Photoshop Mac 使用教程Photoshop Mac 2026下载安装教程

Photoshop Mac 使用教程Photoshop Mac 2026下载安装教程

文章目录Photoshop Mac 2026 安装包获取Photoshop Mac 2026 安装教程(全流程详解)Photoshop Mac版必学的10个基础操作,新手入门不再难Adobe Photoshop(简称PS)是Adobe公司出品的专业图像处理工具,覆盖平面设…

2026/6/27 7:09:39阅读更多 →
从代码到赛场:亚马逊云科技用黑客松打开了体育科技的想象空间

从代码到赛场:亚马逊云科技用黑客松打开了体育科技的想象空间

6月23日至24日,上海世博中心,一场特殊的“选秀”正在上演。60名开发者齐聚亚马逊云科技中国峰会现场,用代码和算法,在24小时内预测2026年NBA选秀第一轮的最终结果。6 月 24 日中午,随着 2026 年 NBA 选秀大会首轮结果全…

2026/6/27 7:09:39阅读更多 →
硅基流动公有云 MaaS 助力科研实验室一站式实现模型弹性调用与精细化治理

硅基流动公有云 MaaS 助力科研实验室一站式实现模型弹性调用与精细化治理

当一个科研团队的工作台上同时摊开 DeepSeek、Kimi、Qwen、GLM 等多个开源大模型,真正的难题往往不是“选哪一个模型”,而是一些更现实的问题:今天要跑几百万条数据,明天可能只跑几万条;这个课题组要追最新权重&#x…

2026/6/27 7:04:39阅读更多 →
【人工智能】一文搞定到底什么是智能体

【人工智能】一文搞定到底什么是智能体

【人工智能】一文搞定到底什么是智能体 一文搞定到底什么是智能体【人工智能】一文搞定到底什么是智能体一. LM,WorkFlow,Agent分别有什么么不同二. Agent的思考过程是怎样的三. Agent的五个核心部分1)LLM2)Prompt3)Me…

2026/6/26 11:03:22阅读更多 →
嵌入式GUI控件实战:ROTARY、SCROLLBAR、SLIDER原理与应用

嵌入式GUI控件实战:ROTARY、SCROLLBAR、SLIDER原理与应用

1. 嵌入式GUI控件:从原理到实战的深度解析在嵌入式系统开发中,图形用户界面(GUI)的设计与实现往往是项目从“能用”到“好用”的关键一跃。不同于资源充沛的PC或移动平台,嵌入式设备的GUI需要在有限的CPU性能、内存空间…

2026/6/27 5:46:02阅读更多 →
Google AI Studio 300美元额度的真相与实战指南

Google AI Studio 300美元额度的真相与实战指南

1. 这300美金不是“送钱”,而是Google埋下的第一道技术门槛 你看到标题里那个醒目的“$300美金”时,第一反应可能是:又一个免费额度?领完就完事?我亲手试过——这300美金根本不是红包,而是一张入场券&…

2026/6/26 9:29:01阅读更多 →
10分钟AI语音克隆与实时变声:Retrieval-based-Voice-Conversion-WebUI完整指南

10分钟AI语音克隆与实时变声:Retrieval-based-Voice-Conversion-WebUI完整指南

10分钟AI语音克隆与实时变声&#xff1a;Retrieval-based-Voice-Conversion-WebUI完整指南 【免费下载链接】Retrieval-based-Voice-Conversion-WebUI Easily train a good VC model with voice data < 10 mins! 项目地址: https://gitcode.com/GitHub_Trending/re/Retrie…

2026/6/27 0:04:03阅读更多 →
Layerdivider:3分钟AI智能分层,彻底告别手动抠图时代

Layerdivider:3分钟AI智能分层,彻底告别手动抠图时代

Layerdivider&#xff1a;3分钟AI智能分层&#xff0c;彻底告别手动抠图时代 【免费下载链接】layerdivider A tool to divide a single illustration into a layered structure. 项目地址: https://gitcode.com/gh_mirrors/la/layerdivider 还在为复杂的图像分层工作烦…

2026/6/27 0:04:03阅读更多 →
Tomcat中X-Frame-Options配置实战:防御点击劫持的四种方法与最佳实践

Tomcat中X-Frame-Options配置实战:防御点击劫持的四种方法与最佳实践

1. 项目概述&#xff1a;为什么X-Frame-Options是Web安全的“防盗门”&#xff1f;最近在排查一个老项目的安全审计报告时&#xff0c;又被提到了“点击劫持”风险&#xff0c;矛头直指缺失的X-Frame-Options响应头。这已经不是第一次了&#xff0c;很多开发团队&#xff0c;尤…

2026/6/27 0:04:03阅读更多 →