Question

Use both AddDbContextFactory() and AddDbContext() extension methods in the same project

I'm trying to use the new DbContextFactory pattern discussed in the DbContext configuration section of the EF Core docs.

I've got the DbContextFactory up and running successfully in my Blazor app, but I want to retain the option to inject instances of DbContext directly in order to keep my existing code working.

However, when I try to do that, I'm getting an error along the lines of:

System.AggregateException: Some services are not able to be constructed (Error while validating the service descriptor 'ServiceType: Microsoft.EntityFrameworkCore.IDbContextFactory1[MyContext] Lifetime: Singleton ImplementationType: Microsoft.EntityFrameworkCore.Internal.DbContextFactory1[MyContext]': Cannot consume scoped service 'Microsoft.EntityFrameworkCore.DbContextOptions1[MyContext]' from singleton 'Microsoft.EntityFrameworkCore.IDbContextFactory1[MyContext]'.) ---> System.InvalidOperationException: Error while validating the service descriptor 'ServiceType: Microsoft.EntityFrameworkCore.IDbContextFactory1[MyContext] Lifetime: Singleton ImplementationType: Microsoft.EntityFrameworkCore.Internal.DbContextFactory1[MyContext]': Cannot consume scoped service 'Microsoft.EntityFrameworkCore.DbContextOptions1[MyContext]' from singleton 'Microsoft.EntityFrameworkCore.IDbContextFactory1[MyContext]'. ---> System.InvalidOperationException: Cannot consume scoped service 'Microsoft.EntityFrameworkCore.DbContextOptions1[MyContext]' from singleton 'Microsoft.EntityFrameworkCore.IDbContextFactory1[MyContext]'.

I also managed to get this error at one point while experimenting:

Cannot resolve scoped service 'Microsoft.EntityFrameworkCore.DbContextOptions`1[MyContext]' from root provider.

Is it theoretically possible to use both AddDbContext and AddDbContextFactory together?

 46  38367  46
1 Jan 1970

Solution

 66

It is, it's all about understanding the lifetimes of the various elements in play and getting those set correctly.

By default the DbContextFactory created by the AddDbContextFactory() extension method has a Singleton lifespan. If you use the AddDbContext() extension method with it's default settings it will create a DbContextOptions with a Scoped lifespan (see the source-code here), and as a Singleton can't use something with a shorter Scoped lifespan, an error is thrown.

To get round this, we need to change the lifespan of the DbContextOptions to also be 'Singleton'. This can be done using by explicitly setting the scope of the DbContextOptions parameter of AddDbContext()

services.AddDbContext<FusionContext>(options =>
    options.UseSqlServer(YourSqlConnection),
    optionsLifetime: ServiceLifetime.Singleton);

There's a really good discussion of this on the EF core GitHub repository starting here. It's also well worth having a look at the source-code for DbContextFactory here.

Alternatively, you can also change the lifetime of the DbContextFactory by setting the ServiceLifetime parameter in the constructor:

services.AddDbContextFactory<FusionContext>(options => 
    options.UseSqlServer(YourSqlConnection), 
    ServiceLifetime.Scoped);

The options should be configured exactly as you would for a normal DbContext as those are the options that will be set on the DbContext the factory creates.

2020-11-26

Solution

 16

Important point:

Both AddDbContextFactory and AddDbContext internally register the DbContextOptions<T> inside a shared private method AddCoreServices using TryAdd. (source)

Which effectively means whichever one is in your code first is the one that gets used.

So you can actually do this for a cleaner setup:

services.AddDbContext<RRStoreContext>(options => {

   // apply options

});

services.AddDbContextFactory<RRStoreContext>(lifetime: ServiceLifetime.Scoped);

I'm using the following to prove to myself it really does function like that:

services.AddDbContextFactory<RRStoreContext>(options =>
{
   throw new Exception("Oops!");  // this should never be reached

}, ServiceLifetime.Scoped);    

Unfortunately I have some query interceptors that aren't thread safe (which is the whole reason I wanted to make multiple instances with a factory), so I think I'll need to make my own context factory because I have separate initialization for Context vs. ContextFactory.


Edit: I ended u making my own context factory to be able to create new options for every new context that was created. The only reason was to allow for non-thread safe interceptors, but if you need that or something similar then this should work.

Influenced by: DbContextFactory

public class SmartRRStoreContextFactory : IDbContextFactory<RRStoreContext>
{
    private readonly IServiceProvider _serviceProvider;

    public SmartRRStoreContextFactory(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public virtual RRStoreContext CreateDbContext()
    {
        // need a new options object for each 'factory generated' context
        // because of thread safety isuess with Interceptors
        var options = (DbContextOptions<RRStoreContext>) _serviceProvider.GetService(typeof(DbContextOptions<RRStoreContext>));
        return new RRStoreContext(options);
    }
}

Note: I only have one context that needs this so I'm hardcoding the new context in my CreateDbContext method. The alternative would be to use reflection - something like this DbContextFactorySource.

Then in my Startup.cs I have:

services.AddDbContext<RRStoreContext>(options => 
{
    var connection = CONNECTION_STRING;

    options.UseSqlServer(connection, sqlOptions =>
    {
        sqlOptions.EnableRetryOnFailure();
    });

    // this is not thread safe
    options.AddInterceptors(new RRSaveChangesInterceptor());

}, optionsLifetime: ServiceLifetime.Transient);

// add context factory, this uses the same options builder that was just defined
// but with a custom factory to force new options every time
services.AddDbContextFactory<RRStoreContext, SmartRRStoreContextFactory>();  

And I'll end with a warning. If you're using a factory (CreateDbContext) in additional to a 'normal' injected DbContext make extra sure not to mix entities. If for instance you call SaveChanges on the wrong context then your entities won't get saved.

2021-04-08