Search code examples
entity-framework-coreasp.net-identityblazor-server-side.net-5dbcontext

How to use dbFactory with my dbContext base class instead of its derived classes


The application is .NET 5.0 with Blazor server side, Entity Framework Core and AspNetCore.Identity.

Originally its code for setting up DbContext and IdentityContext used to look like this:

// ConfigureServices
if (databaseType == "MySQL")
{
    services.AddDbContext<ApplicationDbContext, ApplicationDbContextMySQL>(options =>
    {
        options.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString));
    }, ServiceLifetime.Transient);
}
else if (databaseType == "SQLServer")
{
    services.AddDbContext<ApplicationDbContext, ApplicationDbContextSQLServer>(options =>
    {
        options.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString));
    }, ServiceLifetime.Transient);
}
else if (databaseType == "PostgreSQL")
...

// Identity service configuration
services.AddIdentity<User, UserRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultTokenProviders()
    .AddDefaultUI();

// ApplicationDbContext
public class ApplicationDbContext : IdentityContext
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
    {
    }

    protected ApplicationDbContext(DbContextOptions options) : base(options)
    {
    }
}

// ApplicationDbContextMysql

public class ApplicationDbContextMySQL : ApplicationDbContext
{
    public ApplicationDbContextMySQL(DbContextOptions<ApplicationDbContextMySQL> options) : base(options)
    {
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
    }
}

//Identity class
public class IdentityContext : IdentityDbContext<User, UserRole, long>
{
    public IdentityContext(DbContextOptions<IdentityContext> options) : base(options)
    {
    }

    protected IdentityContext(DbContextOptions options) : base(options)
    {
    }

It worked fine for the most part, but there was a caching issue with Entity Framework due to app using Blazor. Everything I read on that issue said that Blazor apps should use AddDbContextFactory to be able properly maintain DbContext lifetime, which indeed fixed that issue.

So I switched to using AddDbContextFactory instead of using AddDbContext:

if (databaseType == "MySQL")
{
    services.AddDbContextFactory<ApplicationDbContextMySQL>(options =>
    {
        options.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString));
    });
}
...
...

This introduced error on startup:

Unable to resolve service for type 'ModuleLibrary.Data.ModuleLibraryDbContext' while attempting to activate 'ModuleLibrary.Utils.UserInfoClaimsTransformation'.

According to these two questions, one needs to register dbContext service on top of the existing AddDbContextFactory for the ASP.Identity

Using Identity with AddDbContextFactory in Blazor ,

Identity stores with DB Context Factory

So I have changed the above code section to be

if (databaseType == "MySQL")
{
    services.AddDbContextFactory<ApplicationDbContextMySQL>(options =>
    {
        options.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString));
    });
    services.AddScoped<ApplicationDbContext>(p => p.GetRequiredService<IDbContextFactory<ApplicationDbContextMySQL>>().CreateDbContext());
}

This worked fine, but since different dbContext classes are registered depending on the db provider (ApplicationDbContextMySQL, ApplicationDbContextSQLServer) I'm forced to have it hardcoded to a specific class when I need to use dbContext in the app - eg ApplicationDbContextMySQL. I am not able to use my dbContext base class ApplicationDbContext in the application code.

public UserCacheService(IMemoryCache cache, IServiceProvider serviceProvider, IDbContextFactory<ApplicationDbContextMySQL> dbFactory)
{
    ...
}

When I try to use the base class ApplicationDbContext, I get this error:

Unable to resolve service for type 'MyApplication.EntityFrameworkCore.IDbContextFactory`1[MyApplication.Data.ApplicationDbContext

What would be the correct way to register dbFactory with my generic ApplicationDbContext class so that it can be used instead of its derived classes like ApplicationDbContextMySQL, ApplicationDbContextSQLServer etc?


Solution

  • You can introduce helper class for such case:

    public class DbContextFactoryHelper<TBaseContext, TCurrent> : IDbContextFactory<TBaseContext>
        where TBaseContext: DbContext
        where TCurrent: TBaseContext
    {
        private readonly IDbContextFactory<TCurrent> _factory;
    
        public DbContextFactoryHelper(IDbContextFactory<TCurrent> factory)
        {
            _factory = factory;
        }
    
        public TBaseContext CreateDbContext()
        {
            return _factory.CreateDbContext();
        }
    }
    

    And register as singleton for concrete ApplicationDbContext implementation. Singleton because it is default IDbContextFactory lifetime.

    if (databaseType == "MySQL")
    {
        services.AddDbContextFactory<ApplicationDbContextMySQL>(options =>
        {
            options.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString));
        });
    
        // register for each provider accordingly
        services.AddSingleton<IDbContextFactory<ApplicationDbContext>, DbContextFactoryHelper<ApplicationDbContext, ApplicationDbContextMySql>>();
    }
    else // other providers
    {
    }
    
    // common for all providers
    services.AddScoped<ApplicationDbContext>(p => p.GetRequiredService<IDbContextFactory<ApplicationDbContext>>().CreateDbContext());