返回

.NET 8 Worker Service 中 "Cannot consume scoped service from singleton" 问题解决

windows

在 .NET 8 的 Worker Service 项目中,你可能会碰到 System.AggregateException 异常,并看到 "Cannot consume scoped service ... from singleton" 的错误信息。这通常是因为你在单例服务(比如 Hosted Service)里尝试使用了作用域服务(例如 DbContext 或 Repository)。

服务生命周期不匹配是问题的根源

.NET Core 的依赖注入系统主要有三种服务生命周期:

  • Singleton: 在整个应用程序的生命周期中,只会有一个实例。
  • Scoped: 每次 HTTP 请求或者 Worker Service 执行的时候,都会创建一个新的实例。
  • Transient: 每次需要的时候,都会创建一个全新的实例。

如果你想在单例服务中使用作用域服务,就会出现生命周期不匹配的问题。这是因为单例服务在程序启动时就创建了,而作用域服务是在每次请求或工作单元开始时才创建的。所以,单例服务没法拿到正确的、与当前请求或工作单元关联的作用域服务实例。

分析你的代码示例

在你的代码里,TTRReportService 是一个 Hosted Service,它被注册成单例服务。而 TTRReportRepository 依赖于 SMPdbContext,它被注册成了作用域服务。当 TTRReportService 创建的时候,它会尝试注入 TTRReportRepository,但是 TTRReportRepository 需要一个作用域内的 SMPdbContext 实例,而此时还没有创建作用域,所以就会抛出异常。

解决方案:在作用域内使用作用域服务

解决这个问题的核心,就是要确保作用域服务只在作用域内使用。下面是一些常用的解决方法:

1. 在 ExecuteAsync 方法中创建作用域:

public class TTRReportService : BackgroundService
{
    private readonly IServiceProvider _serviceProvider;

    public TTRReportService(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            using (var scope = _serviceProvider.CreateScope())
            {
                var ttrRepository = scope.ServiceProvider.GetRequiredService<ITTRReportRepository>();
                ttrRepository.UserStatusUpdate(); 
            }

            await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken); 
        }
    }
}

在这个方法里,我们在 ExecuteAsync 方法的每次循环中都创建了一个新的作用域。在这个作用域内,我们获取 TTRReportRepository 的实例,然后调用 UserStatusUpdate 方法。这样,TTRReportRepository 就能正确地访问到与当前作用域关联的 SMPdbContext 实例了。

2. 使用工厂模式:

你可以创建一个工厂类,专门负责创建 TTRReportRepository 的实例。这个工厂类可以注册成单例服务,因为它本身不依赖于作用域服务。

public interface ITTRReportRepositoryFactory
{
    ITTRReportRepository Create();
}

public class TTRReportRepositoryFactory : ITTRReportRepositoryFactory
{
    private readonly IServiceScopeFactory _scopeFactory;

    public TTRReportRepositoryFactory(IServiceScopeFactory scopeFactory)
    {
        _scopeFactory = scopeFactory;
    }

    public ITTRReportRepository Create()
    {
        using var scope = _scopeFactory.CreateScope();
        return scope.ServiceProvider.GetRequiredService<ITTRReportRepository>();
    }
}

然后,在 TTRReportService 中注入 ITTRReportRepositoryFactory,并在需要使用 TTRReportRepository 的时候调用工厂方法创建实例。

public class TTRReportService : BackgroundService
{
    private readonly ITTRReportRepositoryFactory _repositoryFactory;

    public TTRReportService(ITTRReportRepositoryFactory repositoryFactory)
    {
        _repositoryFactory = repositoryFactory;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var ttrRepository = _repositoryFactory.Create();
            ttrRepository.UserStatusUpdate();

            await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
        }
    }
}

选择合适的方案

这两种方法都可以解决 "Cannot consume scoped service from singleton" 的问题。选择哪种方法取决于你的具体需求和代码结构。如果你的代码比较简单,直接在 ExecuteAsync 方法中创建作用域可能会更方便。如果你的代码比较复杂,或者需要在多个地方使用 TTRReportRepository,使用工厂模式可能会更合适。

常见问题解答

  1. 除了 DbContext,还有哪些服务通常是作用域的?
    答: 除了 DbContext,一些与请求或事务相关的服务也通常是作用域的,例如 Repository、服务层的一些类等。

  2. 如果我不处理这个异常,会有什么后果?
    答: 如果不处理这个异常,你的应用程序可能会在运行时崩溃,或者出现一些不可预知的行为,例如数据不一致等。

  3. 工厂模式和在 ExecuteAsync 中创建作用域,哪种方法性能更好?
    答: 两种方法的性能差异不大,因为创建作用域的开销相对较小。选择哪种方法主要取决于代码结构和个人偏好。

  4. 我可以在单例服务中注入 IServiceScopeFactory 吗?
    答: 可以,IServiceScopeFactory 本身就是一个单例服务,可以在任何地方注入。

  5. 除了这两种方法,还有其他解决方法吗?
    答: 是的,还可以使用一些第三方库,例如 Scrutor,来简化服务生命周期的管理。

希望以上内容能够帮助你理解和解决 "Cannot consume scoped service from singleton" 的问题。请记住,理解 .NET Core 中的服务生命周期,以及如何正确地处理不同生命周期服务之间的依赖关系,对于构建健壮的应用程序至关重要。