Search code examples
c#dependency-injectionsingletonbackgroundworkerdbcontext

Accessing scoped DbContext in singelton hosted service


I can't simply inject the DB context so I have to rely on injecting IServiceScopeFactory and obtain it from the registered services like this.

AppDbContext Context()
{
  using IServiceScope scope = Services.CreateScope();
  return scope.ServiceProvider.GetRequiredService<AppDbContext>();
}

It doesn't work when I invoke it elsewhere like this.

async Task Seed(int count)
{
  AppDbContext context = Context();
  ...
  await context.AddAsync(new Thing { ... });
  await context.SaveChangesAsync();
}

According to the error message, the service is disposed. And, indeed, since I'm setting it in the scope of using statement, as the method ends, it should be expected. I see two ways to deal with it.

  1. Request the service in the same method that talks to the DB.
  2. Omit disposal of the context keeping it alive in the singelton.

I dislike both of them. The first one leads to code redundancy. The other one may block the otherwise scoped DB access.

What would be a better option to deal with it?

Initially, I tried to create options and create the DB access internally in my class. However, that stopped working when I started to create migrations, as dotnet ef migrations add Init requires the service to be registered in Program, hence forcing me to obtain it using the method above.

DbContextOptions<AppDbContext> ContextOptions()
{
  DbContextOptionsBuilder<AppDbContext> builder = new();
  builder.UseInMemoryDatabase("memento");
  //builder.UseSqlServer(a => { });
  return builder.Options;
}

Solution

  • The normal solution is to create a scope around the work that needs it. Since your service is periodic, I recommend a scope for each time-based invocation. That's what I do for my scheduled services.

    After injecting an IServiceProvider, you can use that to create a scope and then create whatever "processor" type you need (which, in turn, would inject a DbContext):

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
      while (!stoppingToken.IsCancellationRequested)
      {
        using IServiceScope scope = _serviceProvider.CreateScope();
        AppDbContext context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
      }
    }
    

    Scopes are quite fast to create; the normal pattern for ASP.NET is to create a new scope for every request.