ChatGPT解决这个技术问题 Extra ChatGPT

How should I inject a DbContext instance into an IHostedService?

Question

How should I inject (using the standard dependency injection) a DbContext instance into a IHostedService?

What have I tried

I currently have my IHostedService class take a MainContext (deriving from DbContext) instance in the constructor.

When I run the application I get:

Cannot consume scoped service 'Microsoft.EntityFrameworkCore.DbContextOptions' from singleton 'Microsoft.Extensions.Hosting.IHostedService'.

So I tried to make the DbContextOptions transient by specifying:

services.AddDbContext<MainContext>(options => 
                options.UseSqlite("Data Source=development.db"), ServiceLifetime.Transient);

in my Startup class.

But the error remains the same, even though, according to this solved Github issue the DbContextOptions passed should have the same lifetime specified in the AddDbContext call.

I can't make the database context a singleton otherwise concurrent calls to it would yield concurrency exceptions (due to the fact that the database context is not guaranteed to be thread safe).

Maybe inject a context factory instead? e.g. stackoverflow.com/a/11748370/1663001

M
Martin Ullrich

A good way to use services inside of hosted services is to create a scope when needed. This allows to use services / db contexts etc. with the lifetime configuration they are set up with. Not creating a scope could in theory lead to creating singleton DbContexts and improper context reusing (EF Core 2.0 with DbContext pools).

To do this, inject an IServiceScopeFactory and use it to create a scope when needed. Then resolve any dependencies you need from this scope. This also allows you to register custom services as scoped dependencies should you want to move logic out of the hosted service and use the hosted service only to trigger some work (e.g. regularly trigger a task - this would regularly create scopes, create the task service in this scope which also gets a db context injected).

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>();
            …
        }
    }
    …
}

Thanks. What's the difference between injecting IServiceScopeFactory and injecting an IServiceProvider directly?
IServiceProvider will give you only the root service provider. while it also implements the interface to create a scope, you could use it. but the general rule is to request as little as necessary.
The default AddDbContext() methods provided by EF register it as scoped only. At the end of the scope, EF will do cleanup for instance. You don't want to have a singleton db context in web apps or all your components would mess with other component's transactions. All services that use db context instances (via constructor injection) need to be scoped as well.
Hi @MartinUllrich, that's really useful, thanks! I've used the suggested pattern in my BackgroundService and everything runs OK. But I'm still confused: my DBContext is wrapped in a repository and I add both the repository and the DBContext to the service collection as singletons in Program.cs' CreateHostBuilder... is my BackgroundService getting it's "own" singleton repository/DBContext via the injected IServiceScopeFactory?
@Peter that should give you the same instance. However I recommend that you use scoped lifetime (or transient if that's not possible) here since EF Core / DbContext is not thread safe and using it from multiple background services or the controllers will likely cause issues
G
Ganesh Todkar

You can add create Scope in constructor as below:

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

Do add

using Microsoft.Extensions.DependencyInjection;

Do note that this will create a scope and never properly dispose it. While the default implementation AFAIK doesn't dispose of the service if it is ever garbage collected, this might be a problem. This also means that any involved services that are also IDisposable and registered in the scope won't be disposed properly
A way to fix it is to also capture the service scope as a field and dispose of it by implementing IDisposable on the hosted service itself to dispose of the created scope.

关注公众号,不定期副业成功案例分享
Follow WeChat

Success story sharing

Want to stay one step ahead of the latest teleworks?

Subscribe Now