How to consume a Scoped service from a Singleton?
Asked Answered
W

5

145

How should I inject (using .NET Core's built-in dependency injection library, MS.DI) a DbContext instance into a Singleton? In my specific case the singleton is an 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).

Woollyheaded answered 21/1, 2018 at 15:50 Comment(1)
Maybe inject a context factory instead? e.g. https://mcmap.net/q/145590/-how-to-implement-idbcontextfactory-for-use-with-entity-framework-data-migrationsSherrisherrie
A
268

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>();
            …
        }
    }
    …
}
Agna answered 21/1, 2018 at 16:19 Comment(14)
Thanks. What's the difference between injecting IServiceScopeFactory and injecting an IServiceProvider directly?Woollyheaded
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.Agna
If I spawn a new thread (that will outlive the lifetime of DoWork) within the using clause and use some of the services that I've fetched in it will it be ok? Or does the scope define when a service gets disposed?Woollyheaded
The scope will define when things are disposed. be aware that dbcontexts may be reused by another component when using pooling. I only have hosted services code that uses async/await.. like await Task.Delay(…) to do long running scheduled work.. that way I can span a using over it easily.Agna
@MartinUllrich after creating the scope like you mentioned above , I am calling/triggering some services which calls some methods that use the dbcontext. If I have dbcontext added at startup as singleton, will all the scopes create their own instance of dbcontext each or share the same singleton? I am totally confused, I was thinking in order to access the dbcontext from ServiceProvider (like above snippet) you must have added it at Startup, was it singleton or scoped, if it is scoped, how did you achieve that?Gavrila
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.Agna
working only with dbcontext , what about if i need to add serviceHiccup
@hosamhemaily any singleton or transient service can be required directly from the constructor. For any scoped services (like EF does by default or when you call AddScoped() on the service collection) you should follow a similar approach creating a new scope inside of the hosted service. If you need to instantiate new instances of a transient service, you can also inject IServiceProvider directlyAgna
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?Pintail
@Pintail 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 issuesAgna
@MartinUllrich is it possible to expand your example to show how this would work when your DbContext is abstracted away in a repository class?Kaki
@MartinUllrich It would be better/easier/safer to depend on IDbContextFactory<TContext> instead of the container / service locator. What do you think? (Maybe that wasn't possible when you wrote this answer... I see it was added in v5)Lynettalynette
@Lynettalynette IDbContextFactory is deprecated again and meant for design time purposes. If you resolve the context (or repository as asked before), you'll get the expected result from how EF sets up the injection - e.g. in pooled setups this will take care of using a DbContext from the pool and returning it properly when the scope is disposed at the end of the using block.Agna
I have setup clean arch for a project and faced similar error where mediatR was unable to create dbcontext in a hosted service timer of .net8 webapp. Finally got to know that to resolve the DI with mediatR the environment should not be 'Development' as dbcontext is registered as scoped and hostedservice code executes in singleton scope. github issueCanvass
O
6

For the specific case of consuming a DbContext from a singleton service

Since .NET 5 you can register an IDbContextFactory<TContext> for your DbContext and inject it to the singleton services.

Use AddDbContextFactory<TContext> to register a factory:

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

Note: Since .NET 6 you can remove AddDbContext() when using AddDbContextFactory() as the latter also registers the context type itself as a scoped service.

In the singleton service inject IDbContextFactory<TContext> and use CreateDbContext() to create an instance of your DbContext where needed:

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

See also
MS Docs: Using a DbContext factory (e.g. for Blazor)

Remarks
Do not confuse with:
- IDbContextFactory<TContext> from Entity Framework (4.3.1, 5.0.0, 6.2.0).
- IDbContextFactory<TContext> from Entity Framework Core (1.0, 1.1, 2.0, 2.1, 2.2).

Other answered 23/5, 2023 at 17:54 Comment(0)
C
3

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;
Calore answered 23/12, 2020 at 14:16 Comment(2)
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 properlyAgna
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.Agna
I
0

Here is a solution that works for me when I needed to create a singleton readonly cache which used a scoped lifetime entity framework DB context:

// 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 is from Microsoft.Extensions.DependencyInjection.Abstractions

Importance answered 2/11, 2023 at 17:15 Comment(0)
G
0

It's better to create a scope whenever you need, to use dbContext with the lifetime configuration it is set up with you can try this (for .net 8):

  • in Program.cs
 builder.Services.AddDbContextFactory<ApplicationDbContext>(options =>
 options.UseSqlServer("name=ConnectionStrings:DevConnection"));
     builder.Services.AddSingleton<IHostedService, HostedService>();
  • in HostedService.cs:

     private readonly IServiceScopeFactory scopeFactory;    
           public HostedService(IServiceScopeFactory scopeFactory)
           {
               this.scopeFactory = scopeFactory;
           }
           public List<Category> GetCategories()
           {
               List<Category> categories = new List<Category>();
               using (var scope = scopeFactory.CreateScope())
               {
                   var _dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
                   categories = _dbContext.Category.ToList();
               }
               return categories;
           }
    
Giselle answered 17/3 at 5:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.