Scoped服务必须在IServiceScope内解析,根容器无作用域上下文,直接解析会失败或退化;正确方式是通过CreateScope()创建作用域后获取服务。

Scoped 服务不能在根容器直接解析,否则会抛出 InvalidOperationException: Cannot resolve scoped service 或静默退化为 Transient 行为——这不是配置错误,而是作用域机制本身的硬性限制。
为什么从 Program.Services 解析 IShoppingCartService 会失败
根容器(Program.Services.BuildServiceProvider() 返回的 IServiceProvider)没有作用域上下文。Scoped 服务必须绑定到一个明确的 IServiceScope 实例,而根容器连“作用域”这个概念都不存在。
- 常见错误现象:
serviceProvider.GetService<IShoppingCartService>()返回null(.NET 6+ 默认行为),或直接抛出异常 - 更危险的是“看似成功”:某些旧版本或自定义容器可能 fallback 到新建实例,导致状态不一致、DbContext 并发写入崩溃
- 中间件构造函数里注入 Scoped 服务也会触发同样异常,因为中间件由根容器创建并长期存活
AddScoped 不等于“每个请求一个”,而是“每个 IServiceScope 一个”
ASP.NET Core 自动为每个 HTTP 请求创建一个 scope,并通过 HttpContext.RequestServices 暴露;但 scope 可以手动创建,也能嵌套。关键不是“请求”,而是“scope 实例”。
- Controller 构造函数注入的
IDbContext和你在 Action 里调用context.RequestServices.GetRequiredService<IDbContext>()拿到的是同一个对象 - 但如果你在 Controller 里写
using var scope = serviceProvider.CreateScope(); scope.ServiceProvider.GetRequiredService<IDbContext>(),那就得到一个全新的、与当前请求无关的 DbContext 实例 -
AddDbContext<T>()内部已调用AddScoped,显式再写services.AddScoped<MyDbContext>()会覆盖注册,丢失连接配置和 options builder 设置
什么时候该用 AddScoped 而不是 AddTransient
判断标准不是“要不要共享”,而是“共享是否安全且必要”。Shared ≠ Safe。
- 需要跨多个组件维持临时状态:如一次请求内的购物车、表单验证上下文、请求级缓存
- 封装了非线程安全资源:如
DbContext必须是 Scoped,否则并发访问会破坏内部状态 - 初始化开销较高,但又不能全局共享:比如某服务要加载用户权限树,一次请求内多次用到,但不同请求之间必须隔离
- 别为了“省点 GC 开销”就把轻量服务设为 Scoped——它会延长对象生命周期,反而更容易引发内存泄漏
HostedService 或后台任务里怎么用 Scoped 服务
不能直接从 IServiceProvider 解析,必须手动创建 scope,并确保释放。
- 正确写法:
using var scope = serviceProvider.CreateScope(); var service = scope.ServiceProvider.GetRequiredService<IMyScopedService>(); - 必须用
using或显式Dispose(),否则DbContext连接不释放、内存不回收 - 别在
while (true)循环外只建一次 scope——那相当于把 Scoped 降级成 Singleton;每次业务单元开始前都应新建 scope - 如果用
IHostedService,推荐注入IServiceScopeFactory,而不是IServiceProvider,更清晰也更符合设计意图
最易被忽略的一点:Scoped 的“边界”是代码里真实存在的 IServiceScope 对象,不是魔法标签。没 CreateScope() 就没有 Scoped,无论你注册时写了多少遍 AddScoped。


















