Passing DbContext through dependency injection to BackgroundService throwing exception

0

I'm trying to pass EF Core Dbcontext to my BackgroundService through dependency injection but somehow cannot get the hang of it.

Can someone tell me what I'm doing wrong and how to do it right/better/easier/another way (not cumbersome if possible)?

I'm using services.AddDbContext<MyDbContext> to add the DbContext in HostBuilder, then trying to receive it in the constructor of my BackgroundService.

I added the DbContext constructor receiving the DbContextOptions<MyDbContext> options and passing it to the base constructor of DbContext in my context, which is the way described in the documentation

Here's the exception I keep getting when starting the application:

System.AggregateException
  HResult=0x80131500
  Message=Some services are not able to be constructed (Error while validating the service descriptor 'ServiceType: Microsoft.Extensions.Hosting.IHostedService Lifetime: Singleton ImplementationType: XXXX.SenderService': Cannot consume scoped service 'XXXX.Entities.MyDbContext' from singleton 'Microsoft.Extensions.Hosting.IHostedService'.)
  Source=Microsoft.Extensions.DependencyInjection
  StackTrace:
   at Microsoft.Extensions.DependencyInjection.ServiceProvider..ctor(IEnumerable`1 serviceDescriptors, ServiceProviderOptions options)
   at Microsoft.Extensions.DependencyInjection.ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(IServiceCollection services, ServiceProviderOptions options)
   at Microsoft.Extensions.DependencyInjection.DefaultServiceProviderFactory.CreateServiceProvider(IServiceCollection containerBuilder)
   at Microsoft.Extensions.Hosting.Internal.ServiceFactoryAdapter`1.CreateServiceProvider(Object containerBuilder)
   at Microsoft.Extensions.Hosting.HostBuilder.CreateServiceProvider()
   at Microsoft.Extensions.Hosting.HostBuilder.Build()
   at XXXX.Program.Main(String[] args) in C:\WORK\REPOS\CRR\Main\XXXX\Program.cs:line 16

  This exception was originally thrown at this call stack:
    [External Code]

Inner Exception 1:
InvalidOperationException: Error while validating the service descriptor 'ServiceType: Microsoft.Extensions.Hosting.IHostedService Lifetime: Singleton ImplementationType: XXXX.SenderService': Cannot consume scoped service 'XXXX.Entities.MyDbContext' from singleton 'Microsoft.Extensions.Hosting.IHostedService'.

Inner Exception 2:
InvalidOperationException: Cannot consume scoped service 'XXXX.Entities.MyDbContext' from singleton 'Microsoft.Extensions.Hosting.IHostedService'.

Here's the Program.cs:

public static class Program
{
   public static void Main(string[] args)
   {
      CreateHostBuilder(args).ConfigureLogging(config => config.ClearProviders())
                             .Build()
                             .Run();
   }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureServices((builderContext, services) =>
                services.AddDbContext<MyDbContext>(options =>
                            options.UseSqlServer(builderContext.Configuration
                                                               .GetConnectionString(nameof(MyDbContext)))
                        ).AddHostedService<SenderService>())
            .UseWindowsService();
}

SenderService.cs:

public class SenderService : BackgroundService
{
    public SenderService(IConfiguration configuration, MyDbContext context)
    {
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
    }
}

and MyDbContext:

public class MyDbContext : DbContext
{
    public MyDbContext(DbContextOptions<MyDbContext> options) : base(options) { }
    public DbSet<XXXX> XXXX { get; set; }
}
dependency-injection
entity-framework-core
backgroundworker
.net-core-3.1
asked on Stack Overflow Sep 30, 2020 by mishan • edited Sep 30, 2020 by mishan

1 Answer

1

In the end, I figured it out!

Problem description

What I'm basically doing is using ConfigureServices to AddScoped object, which is what happens when AddDbContext<MyDbContext> is called with only the delegate action filling out the DbContextOptionsBuilder that is then passed to my MyDbcontext constructor.

The dog is buried in adding the context as scoped object, while Service needs it as singleton to accept it in its constructor.

How to remedy that

Well, I can update the piece of code inside the delegate in ConfigureServices to treat the DbContext as a singleton:

public static IHostBuilder CreateHostBuilder(string[] args) =>
Host
.CreateDefaultBuilder(args)
.ConfigureServices((builderContext, services) =>
{   
    //this adds the dbContext, needs action that accepts DbContextOptionsBuilder
    services
    .AddDbContext<MyDbContext>(
    optionsAction: (optionsBuilder) => 
        {
            //prepare connection string from IConfiguration existing in HostBuilderContext
            //that was passed to ConfigureServices
            var connectionString = builderContext.Configuration
                                                 .GetConnectionString(nameof(MyDbContext));
            //and tell the optionsBuilder to use the SqlServer with the connectionstring
            //which is then passed as parameter to MyDbContext constructor
            optionsBuilder.UseSqlServer(connectionString);
        },
        //!!!IMPORTANT!!! - also specify the lifetime of the context, default is Scoped
        contextLifetime: ServiceLifetime.Singleton,
        optionsLifetime: ServiceLifetime.Singleton)
    
    .AddHostedService<SenderService>()
})
.UseWindowsService();

Or, better, DON'T PASS CONTEXT AS SINGLETON TO THE SERVICE, CREATE IT WHEN NEEDED!

Which I can do for example by passing just the optionsBuilder like this:

// ---------in Program.cs------------

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureServices((builderContext, services) =>
            services.AddSingleton((_) => {
                            //again, get connection string from configuration
                            var connectionString = builderContext.Configuration
                                                                 .GetConnectionString(nameof(MyDbContext));
                            //and return DbContextOptions to be saved as singleton
                            //to be passed to my service as constructor parameter
                            return new DbContextOptionsBuilder<MyDbContext>().UseSqlServer(connectionString).Options;
                        })
                    .AddHostedService<SenderService>())
        .UseWindowsService();

// ---------in SenderService.cs------------
public class SenderService : BackgroundService
{
    private readonly DbContextOptions<MyDbcontext> contextOptions;
    public SenderService(IConfiguration configuration, DbContextOptions<MyDbContext> dbContextOptions)
    {
         contextOptions = dbContextOptions;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        ....
        using(var context = new MyDbContext(contextOptions))
        {
           ....
        }
        ....
    }
}

answered on Stack Overflow Sep 30, 2020 by mishan

User contributions licensed under CC BY-SA 3.0