为何C#中Singleton对象可依赖Transient却不能依赖Scoped对象?
问题重现
类定义
public Dependent(Parent parent) { Parent = parent; }
正常运行的DI配置
当Parent设置为Transient生命周期时,代码可正常执行:
public override void Configure(IFunctionsHostBuilder builder) { builder.Services.AddTransient<Parent>(); builder.Services.AddSingleton<Dependent>(); }
报错场景及错误信息
将Parent改为Scoped生命周期后,触发以下错误:
public override void Configure(IFunctionsHostBuilder builder) { builder.Services.AddScoped<Parent>(); builder.Services.AddSingleton<Dependent>(); }
错误日志:
[2023-09-28T11:14:54.195Z] Executed 'Function1' (Failed, Id=19772818-9385-4382-8c53-116589abb2fa, Duration=1ms)
[2023-09-28T11:14:54.199Z] Microsoft.Azure.WebJobs.Script.WebHost: Dependency FunctionApp1.Parent {ReturnDefault} as parameter "parent" reuse CurrentScopeReuse {Lifespan=100} lifespan shorter than its parent's: singleton FunctionApp1.Dependent {ReturnDefault} #150
[2023-09-28T11:14:54.204Z] from scoped container with {no name}
核心原因解析
Transient实例的特性:Transient对象每次被请求时都会生成全新实例。当Singleton的
Dependent初始化时,容器会创建一个专属的Parent实例绑定到它身上——这个Parent的生命周期会和Dependent完全同步,直到Singleton被销毁,不存在生命周期不匹配的风险,因此容器允许这种配置。Scoped实例的特性:Scoped对象的生命周期严格绑定到当前作用域(在Azure Functions中,通常对应单个函数执行的生命周期)。当Singleton依赖Scoped对象时:
- Singleton在应用启动(或首次请求)时初始化,会从某个临时作用域中获取
Parent实例。 - 当该作用域结束(比如函数执行完成),容器会自动回收这个Scoped的
Parent实例。 - 后续Singleton再使用这个已被回收的
Parent,会引发空引用、状态异常等问题。
- Singleton在应用启动(或首次请求)时初始化,会从某个临时作用域中获取
Azure Functions的DI容器会主动检测这种短生命周期服务被长生命周期服务持有的风险,直接抛出错误阻止潜在的资源泄漏或逻辑异常。
解决思路
如果需要让Singleton使用Scoped服务,可采用以下两种方案:
- 注入
IServiceScopeFactory,在需要使用Parent时动态创建作用域并获取实例:public class Dependent { private readonly IServiceScopeFactory _scopeFactory; public Dependent(IServiceScopeFactory scopeFactory) { _scopeFactory = scopeFactory; } public void DoWork() { using var scope = _scopeFactory.CreateScope(); var parent = scope.ServiceProvider.GetRequiredService<Parent>(); // 执行业务操作 } } - 将
Dependent的生命周期改为Scoped,和Parent保持一致,确保每次函数执行都会创建配套的Dependent和Parent实例,符合Scoped的生命周期规则。
内容的提问来源于stack exchange,提问作者RKS

