Post

Injecting an EF DbContext instance into an IHostedService

Injecting an EF DbContext instance into an IHostedService

Problem

As we all know, there are hardly any APIs that do not use a database to persist their data. ASP.NET makes it easy to connect applications to databases via DbContext, part of Entity Framework.

While working on one of my personal projects, I have encountered the following error when trying to inject DbContext into a hosted background service:

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

This makes sense since at the time I was using AddDbContext() to register my context, which by default registers a context as scoped. IHostedServices are singletons, hence the conflict.

Solution

To fix the issue, I decided to use AddDbContextFactory() registration instead:

1
2
3
4
5
6
builder.Services.AddDbContextFactory<MonitorDbContext>(options =>
{
    options.UseLazyLoadingProxies();
    options.UseNpgsql(builder.Configuration.GetConnectionString("ChangeMonitor"));
    options.UseSnakeCaseNamingConvention();
});

This method registers a singleton IDbContextFactory and a scoped DbContext which in turn allows us to produce instances of the second on demand via CreateDbContext():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
...

private readonly IDbContextFactory<MonitorDbContext> _contextFactory;

public MonitorJobsRegistrationService(
  ...
  IDbContextFactory<MonitorDbContext> contextFactory)
{
  ...
  _contextFactory = contextFactory;
}

public async Task StartAsync(CancellationToken cancellationToken)
{
  await InitiateScheduler(cancellationToken);

  using (var context = _contextFactory.CreateDbContext())
  {
    var targetEntities = await context.Targets.ToListAsync(cancellationToken);

    if (targetEntities.Count > 0)
    {
      _logger.LogInformation("Existing targets found, count: {TargetCount}. Registering jobs...",
          targetEntities.Count);

      await _jobService.ScheduleAsync(targetEntities.Select(entity => entity.ToTarget()),
          cancellationToken);
    }
  }
}

...

The 2 types registered in WebApplicationBuilder.Services:

Desktop View The 2 types registered in WebApplicationBuilder.Services

The other nice thing about AddDbContextFactory() is that you can still inject scoped DbContext instances directly without using the factory:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
...

private readonly MonitorDbContext _context;
private readonly IMonitorJobService _jobService;

public ResourceService(
  MonitorDbContext context,
  IMonitorJobService jobService)
{
  _context = context;
  _jobService = jobService;
}

...

That’s pretty neat. It makes me wonder whether it’s a good idea to just adopt it as a common practice and always use AddDbContextFactory() by default when first wiring up Entity Framework in new projects.

Useful URLs

This post is licensed under CC BY 4.0 by the author.