单例类能否依赖非单例服务?单例ILoggerProvider依赖ApplicationDbContext疑问
AddDbContext 注册逻辑说明
默认调用IServiceCollection.AddDbContext<ApplicationDbContext>()且未显式指定生命周期参数时,ApplicationDbContext会被注册为Scoped生命周期,你推测的「每次请求创建新实例」是正确的,实例会在当前请求结束后由依赖注入容器自动释放。
单例 ILoggerProvider 依赖 Scoped DbContext 的问题
依赖注入容器有生命周期继承规则:长生命周期的服务只能依赖等长或更长生命周期的服务,短生命周期服务不能被长生命周期服务直接持有,否则会出现生命周期提升问题,具体会触发以下故障:
- 线程安全问题:DbContext 本身设计为非线程安全,单例服务持有的同一个 DbContext 实例会被多线程(多个并发请求)同时操作,极易出现数据错乱、未捕获的运行时异常。
- 状态污染:DbContext 自带实体追踪能力,长期不释放会堆积大量过时的实体状态缓存,会导致后续查询返回过期数据、保存数据时出现主键冲突、实体重复跟踪等错误。
- 资源泄漏:被单例持有的 DbContext 永远不会被容器自动释放,关联的数据库连接也会长期占用不归还连接池,运行时间越长占用的连接资源越多,严重时会触发数据库连接池耗尽,导致所有数据库操作超时。
兼容解决方案
不要在单例 ILoggerProvider 的构造函数中直接注入 DbContext,改为注入根IServiceProvider,每次需要写日志时手动创建作用域解析 DbContext 实例,用完后作用域会自动释放 DbContext 相关资源,示例实现如下:
public class CustomLoggerProvider : ILoggerProvider { private readonly IServiceProvider _rootServiceProvider; // 只注入根服务提供者,不直接注入DbContext public CustomLoggerProvider(IServiceProvider rootServiceProvider) { _rootServiceProvider = rootServiceProvider; } public ILogger CreateLogger(string categoryName) { return new CustomDbLogger(_rootServiceProvider); } public void Dispose() { } } public class CustomDbLogger : ILogger { private readonly IServiceProvider _rootServiceProvider; public CustomDbLogger(IServiceProvider rootServiceProvider) { _rootServiceProvider = rootServiceProvider; } public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter) { // 每次写日志时单独创建作用域,获取当前的DbContext实例 using var scope = _rootServiceProvider.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>(); // 执行日志落库逻辑 dbContext.Logs.Add(new LogEntry { Level = logLevel.ToString(), Message = formatter(state, exception), CreatedTime = DateTimeOffset.Now }); dbContext.SaveChanges(); } // 其余ILogger接口的默认实现可根据需求补充 public bool IsEnabled(LogLevel logLevel) => true; public IDisposable BeginScope<TState>(TState state) => default; }
内容的提问来源于stack exchange,提问作者Jonathan Wood
相关产品推荐
相关产品推荐

