如何从Singleton中使用Scoped服务?

140
我应该如何使用.NET Core内置的依赖注入库MS.DI将一个DbContext实例注入到单例(Singleton)中?在我的具体情况中,单例是一个IHostedService?
我已经尝试了什么: 我目前让我的IHostedService类在构造函数中接受一个MainContext(从DbContext派生而来)实例。 当我运行应用程序时,出现以下错误: “无法从单例‘Microsoft.Extensions.Hosting.IHostedService’消费作用域服务‘Microsoft.EntityFrameworkCore.DbContextOptions’。” 因此,我尝试通过指定以下方式使DbContextOptions短暂:
services.AddDbContext<MainContext>(options =>
    options.UseSqlite("Data Source=development.db"),
    ServiceLifetime.Transient);

在我的Startup类中。

但是错误仍然存在,尽管根据这个已解决的Github问题,传递的DbContextOptions应该具有在AddDbContext调用中指定的相同生命周期。

我不能将数据库上下文设置为单例,否则对它的并发调用将导致并发异常(因为不能保证数据库上下文是线程安全的)。


1
也许可以注入一个上下文工厂?例如:https://dev59.com/ymgu5IYBdhLWcg3wWVuF#11748370 - DavidG
4个回答

265

在托管服务中使用服务的好方法是在需要时创建一个作用域。这样可以按照它们设置的生命周期配置来使用服务/数据库上下文等。如果不创建作用域,理论上可能会导致创建单例DbContext并且重复使用上下文(EF Core 2.0使用DbContext池)。

要做到这一点,请注入IServiceScopeFactory并在需要时使用它创建一个作用域。然后从此范围解析任何您需要的依赖项。这还允许您将自定义服务注册为作用域依赖项,以便从托管服务中移出逻辑,仅使用托管服务触发某些工作(例如定期触发任务-这将定期创建作用域,在此作用域中创建任务服务,该服务还注入了一个db上下文)。

public class MyHostedService : IHostedService
{
    private readonly IServiceScopeFactory scopeFactory;

    public MyHostedService(IServiceScopeFactory scopeFactory)
    {
        this.scopeFactory = scopeFactory;
    }

    public void DoWork()
    {
        using (var scope = scopeFactory.CreateScope())
        {
            var dbContext = scope.ServiceProvider.GetRequiredService<MyDbContext>();
            …
        }
    }
    …
}

9
谢谢。注入 IServiceScopeFactory 和直接注入 IServiceProvider 有什么区别? - Shoe
9
IServiceProvider只提供根服务提供程序。虽然它还实现了创建范围的接口,但您可以使用它,但一般规则是尽可能请求较少的服务。 - Martin Ullrich
13
EF提供的默认AddDbContext()方法仅将其注册为作用域范围内的。在作用域结束时,EF将清理实例。在Web应用程序中,您不希望有单例数据库上下文,否则所有组件都会干扰其他组件的事务。所有使用DB上下文实例(通过构造函数注入)的服务也需要作为作用域范围。 - Martin Ullrich
2
@Peter 这应该会给你相同的实例。但是我建议在这里使用作用域生命周期(如果不可能则使用瞬态),因为EF Core / DbContext 不是线程安全的,如果从多个后台服务或控制器中使用它,可能会导致问题。 - Martin Ullrich
2
@MartinUllrich,依赖于IDbContextFactory<TContext>比依赖于容器/服务定位器更好、更容易、更安全。您认为呢?(也许在您撰写此答案时还不可能...我看到它是v5中添加的 - lonix
显示剩余8条评论

3

对于从单例服务中使用DbContext的特定情况

自 .NET 5 起,您可以为 DbContext 注册一个 IDbContextFactory<TContext> 并将其注入到单例服务中。

使用 AddDbContextFactory<TContext> 来注册工厂:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContextFactory<MyDbContext>(
        options.UseSqlite("Data Source=development.db"));
}

注意:自 .NET 6 开始,当使用 AddDbContextFactory() 时,您可以删除 AddDbContext(),因为后者也会将上下文类型本身注册为作用域服务。

在单例服务中注入 IDbContextFactory<TContext> 并使用 CreateDbContext() 在需要的地方创建您的 DbContext 实例:

public class MySingletonService : BackgroundService, IHostedService
{
    private readonly IDbContextFactory<MyDbContext> _contextFactory;

    public MySingletonService(IDbContextFactory<MyDbContext> contextFactory)
    {
        _contextFactory = contextFactory;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        using (MyDbContext dbContext = _contextFactory.CreateDbContext())
        {
            await dbContext.MyData.ToListAsync(stoppingToken);
        }
    }
}

另请参阅
MS Docs:使用 DbContext 工厂(例如用于 Blazor)

备注
不要与以下内容混淆:
- {{link2:IDbContextFactory<TContext>}} 来自 Entity Framework(4.3.1、5.0.0、6.2.0)。
- {{link3:IDbContextFactory<TContext>}} 来自 Entity Framework Core(1.0、1.1、2.0、2.1、2.2)。


3

你可以在构造函数中按如下方式添加创建作用域:

 public ServiceBusQueueListner(ILogger<ServiceBusQueueListner> logger, IServiceProvider serviceProvider, IConfiguration configuration)
        {
            _logger = logger;
            _reportProcessor = serviceProvider.CreateScope().ServiceProvider.GetRequiredService<IReportProcessor>();
            _configuration = configuration;
        }

请添加

using Microsoft.Extensions.DependencyInjection;

3
请注意,这将创建一个范围并永远不会正确处理它。虽然默认实现 AFAIK 不会在服务被垃圾回收时处理它,但这可能是一个问题。这也意味着任何涉及的服务,如果也是 IDisposable 并在范围中注册,将无法正确处理。 - Martin Ullrich
1
一种解决方法是将服务范围作为字段捕获,并通过在托管服务本身上实现IDisposable来处理它,以处理创建的范围。 - Martin Ullrich

0
这是一个对我来说非常有效的解决方案,当我需要创建一个单例只读缓存,并且使用了一个作用域生命周期的Entity Framework数据库上下文时。
// Allows singleton lifetime classes to obtain DB contexts which has the same
// lifetime as the MyDbContextFactory
MyDbContextFactory : IMyDbContextFactory
{
    private readonly IServiceScope? _scope;

    public MyDbContextFactory(IServiceScopeFactory serviceScopeFactory)
    {
        _scope = serviceScopeFactory.CreateScope();
    }

    public IMyDbContext Create()
    {
        if (_scope == null)
        {
            throw new NullReferenceException("Failed creating scope");

        }

        return _scope.ServiceProvider.GetRequiredService<IMyDbContext>();
    }
}

IServiceScopeFactory来自Microsoft.Extensions.DependencyInjection.Abstractions。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接