Get service inside derived DbContext

Viewed 1360

I have the following derived DbContext:

public class PowerDbContext : DbContext
{
    readonly IDbAuthTokenService _authTokenService;
    readonly string _connectionString;
    readonly bool _useManagedIdentity;

    public MyDbContext(DbContextOptions<NyDbContext> options) : base(options)
    {
        _authTokenService = this.GetService<IOptionsSnapshot<AzureSqlAuthTokenService>>().Value;
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        var connection = new SqlConnection(_connectionString);

        connection.AccessToken = _authTokenService.GetTokenAsync().GetAwaiter().GetResult();
    }

in Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
    ...
    services.AddTransient<IDbAuthTokenService, AzureSqlAuthTokenService>();
    services.AddEntityFrameworkSqlServer();
    services.AddEntityFrameworkProxies();
    services.AddDbContextPool<MyDbContext>((serviceProvider, optionsBuilder) =>
    {
        optionsBuilder.UseLazyLoadingProxies();
        optionsBuilder.UseSqlServer(connectionString);
        optionsBuilder.UseInternalServiceProvider(serviceProvider);
    }, Configuration.GetAppSetting<int>("PoolSize"));
    ...
}

But I get the following error:

System.InvalidOperationException : Unable to resolve service for type 'Microsoft.Extensions.Options.IOptionsSnapshot<Myproject.Database.Services.AzureSqlAuthTokenService>'. This is often because no database provider has been configured for this DbContext. A provider can be configured by overriding the DbContext.OnConfiguring method or by using AddDbContext on the application service provider. If AddDbContext is used, then also ensure that your DbContext type accepts a DbContextOptions<TContext> object in its constructor and passes it to the base constructor for DbContext.

How do I get the DbContext to resolve the token service in OnConfiguring()?

UPDATE: To be clear, I can't use constructor injection, since AddDbContextPool() requires my derived DbContext to have a constructor with only one parameter (DbContextOptions)

1 Answers

For the injection itself a simple constructor injection should do:

public MyDbContext(DbContextOptions<NyDbContext> options, IOptionsSnapshot<AzureSqlAuthTokenService> authTokenService) : base(options)
...

However, it seems to me (at least from the code you provide) that you never configured IOptionsSnapshot<AzureSqlAuthTokenService> in the first place. Instead you configure IDbAuthTokenService service pointing to AzureSqlAuthTokenService implementation. I'm not 100% sure which way you want to go, but I see two possibilities:
1) Inject your IDbAuthTokenService service:

public MyDbContext(DbContextOptions<NyDbContext> options, IDbAuthTokenService authTokenService) : base(options)
...

2) Configure IOptionsSnapshot, then inject it (see docs):
Startup.cs:

public void ConfigureServices(IServiceCollection services)
...
services.Configure<MyAzureTokenOptions>(Configuration.GetSection("Proper:Config:Section"));
...

DbContext:

public MyDbContext(DbContextOptions<NyDbContext> options, IOptionsSnapshot<MyAzureTokenOptions> azureTokenOptions) : base(options)
...

EDIT:
I missed the fact that you are using DbContext pooling. Yes, you are correct - constructor injection won't work in this case. However the rest of the answer still applies: you register a IDbAuthTokenService service (by its AzureSqlAuthTokenService implementation), but attempt to retrieve IOptionsSnapshot<AzureSqlAuthTokenService> from service provider. In other words you're looking for a service (or options snapshot) that is not registered in the DI, which is why you get that particular error. Fix either the retrieval or the registration of your service (or options snapshot) as described above.

IMPORTANT:
If it's the service you want, you should know a bit how DbContext pooling works. The main point is that after serving a request instead of destroying DbContext instance, its state is reset and it is returned to the pool. That means that it is effectively singleton. Since you cannot inject a scoped (or transient) dependency into a singleton principal, your dependency must be singleton as well. That means:

public void ConfigureServices(IServiceCollection services)
...
services.AddSingleton<IDbAuthTokenService, AzureSqlAuthTokenService>();

If your AzureSqlAuthTokenService itself depends on other scoped dependencies, you either propogate the same principle down the dependency chain (if that is reasonable for said dependencies) or switch away from pooling and save yourself some headache.

Related