Efcore: Ability to register async ConnectionString provider

Created on 27 Aug 2018  路  8Comments  路  Source: dotnet/efcore

I'm using EF Core 2.1 in project with fairly large codebase. We are using standard AddDbContext<T> method to register DbContext in DI:

C# services.AddDbContext<MyDbContext>((sp, options) => { var connectionString = ... //Code that retrieves connectionString options.UseNpgsql(connectionString); });
In the application code we use constructor injection to obtain DbContext. Currently connection string is static and passed to each microservice using environment variables, however we have several new requirements that will require connection string to be retrieved in runtime from external source.
For example DB credentials need to be retrieved from secure store e.g. HashiCorp Vault. Vault can rotate PostgreSQL credentials with some interval so we can't retrieve them once per app lifetime. Because of that our code to register DbContext in snippet above would need to perform async HTTP request. Currently it can't since this is not Task returning Func, it's Action<IServiceProvider, DbContextOptionsBuilder>.

Is there any other way to register async connection string provider either as async lambda or some dedicated interface implementation?

I know that it is possible for us to create our own DbContextFactory with async Create method but as I mentioned we have pretty large codebase that relies heavily on ready to use DbContext being constructor injected so that would be large refactoring. Also that would seam like not the idiomatic approach compared to what was proposed in most of EF Core usage docs.

closed-question customer-reported

All 8 comments

@KrzysztofBranicki That code, or the equivalent in OnConfiguring, is not something we're planning to make async. This means that the connection string would have to be resolved outside if it needs to be async. However, it would be possible to still use constructor injection, but there would need to be an additional async call to set the connection string. Something like:
```C#
public class BloggingContext : DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.UseSqlServer(ConnectionString);

public string ConnectionString { get; set; }

}

public class MyFakeController
{
private readonly BloggingContext _context;

public MyFakeController(BloggingContext context)
{
    _context = context;
}

public async Task DoSomethingAsync()
{
    _context.ConnectionString = await GetConnectionStringAsync();

    _context.Add(new Blog());
    await _context.SaveChangesAsync();
}

}
```
I don't know that this is a better pattern than using a factory like you suggest, but maybe it will give you some ideas.

@glennc Pinging you FYI since this is essentially an "async configuration" question related to microservices.

@ajcvickers thanks for your response,

The suggested workaround, contradicts the idea of DI.

What if I my BloggingContext implements IRepository<T> etc. and is provided as an agnostic service contract? The configuration of the context should not be the responsibility of the consumer.

This is an essential requirement in multi-tenant apps, or similar scenarios that deal with dynamic database creation, where the connection string is to be retrieved asynchronously from storage.

Another alternative is execute the async code synchronously in the OnConfiguring method - this is unacceptable either.

One option is to register a factory in DI, which would expose an async method to create a new context. That factory would get injected with the DbContextOptions, instantiate the context, and set it with the asynchronously-fetched connection string (just like @ajcvickers proposed above, but as part of the factory implementation instead of in user code). Then user code would be injected with the factory, and call a CreateDbContextAsync on it to get the context. The downside is that the context wouldn't be scoped, and disposing it would be the user's responsibility.

(BTW EF Core 5.0 is introducing IDbContextFactory which pretty much is that concept, but does not (yet) expose an async API. We could consider this for 6.0 (/cc @ajcvickers)).

The downside is that the context wouldn't be scoped

exactly.

I ended up with synchronously calling the connectionStringProvider.GetConnectionStringAsync() in the OnConfiguring until this issue is fixed or a better alternative is proposed. I am using .NET 5. An async OnConfiguring is very compelling.

@weitzhandler note that doing an actual async operation every time a DbContext needs to be instantiated can be potentially very bad for perf. The general recommendation is to cache whatever is being asynchronously fetched, e.g. have some singleton hold your connection string (or whatever), and refresh that via some timer, after some interval. If that's possible, it's far better perf-wise than to do it on every single instantiation. Out of curiosity, could you provide some background on whether that would be possible in your scenario, and if not, why?

Although the actual connection string is determined per scope, the connection string provider is registered as singleton and is responsible for caching the retrieved connection strings for reusage. But the actual call of GetConnectionString is async, because it might have to retrieve it from storage.

In that case, doesn't it make sense for your connection string provider to occasionally refresh storage "out of band", i.e. not as part of the GetConnectionString call? This could be timer-based, or a refresh triggered occasionally when GetConnectionString is called (but without GetConnectionString blocking/awaiting on it)? Any reason to not design things this way?

The reason I'm personally against adding an async OnConfiguring (or similar) is precisely that it would encourage a perf anti-pattern with users. I understand that in your specific case the async call caches and therefore perf-wise things are probably OK, but in the general case an async OnConfiguring would probably cause lots of users to perform real I/O and considerably slow down their application.

Note that if you're absolutely sure that GetConnectionString will only rarely actually be async (because it almost all cases it will be cached), then there's probably no real harm in blocking synchronously over the async call - although that's definitely not recommended in the general case. It also encodes the caching assumption - which is an internal detail - at the calling site, and if that ever changes you're in trouble.

Thanks for your explanations.

Was this page helpful?
0 / 5 - 0 ratings