Shoe
Shoe

Reputation: 76298

How to consume a Scoped service from a Singleton?

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).

Upvotes: 146

Views: 73366

Answers (5)

Sarah Abdelwhab
Sarah Abdelwhab

Reputation: 11

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

Upvotes: 1

andrew pate
andrew pate

Reputation: 4307

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

Upvotes: 0

Martin Schneider
Martin Schneider

Reputation: 15398

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).

Upvotes: 10

Ganesh Todkar
Ganesh Todkar

Reputation: 537

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;

Upvotes: 3

Martin Ullrich
Martin Ullrich

Reputation: 100791

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

Upvotes: 273

Related Questions