0

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?

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
ilya_i
  • 333
  • 5
  • 14
  • I do not understand from question why you have introduced `ApplicationDbContextMySQL` etc. classes? Why not `services.AddDbContextFactory` for all providers as it was done before introducing factories? – Svyatoslav Danyliv Apr 26 '23 at 08:42
  • 1
    @SvyatoslavDanyliv before introducing factories it was also using specific derived classes for each provider: services.AddDbContext(options => ...) – ilya_i Apr 26 '23 at 08:46
  • 1
    @ilya_i We understand what you have used with `AddDbContext`. But even there, derived db context classes looks redundant. And since now `AddDbContextFactory` has no similar overloads, you'd have to switch to a single db context anyway. – Ivan Stoev Apr 26 '23 at 09:13
  • @IvanStoev thanks for your reply. Could you elaborate please why it was redundant to use derived class in AddDbContext? Many thanks – ilya_i Apr 26 '23 at 09:18
  • @ilya_i Because the only difference is the options configuration. At least what you have shown. That's why options and db context classes are separate. Single context supports multiple database providers. Do you have something "database specific" in each derived `OnModelCreating`? – Ivan Stoev Apr 26 '23 at 09:52
  • @IvanStoev yes I should have included another derived class ApplicationDbContextMySQL55 which has database specific stuff in its OnModelCreating. Thanks for your answer – ilya_i Apr 26 '23 at 10:21

1 Answers1

1

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());

Svyatoslav Danyliv
  • 21,911
  • 3
  • 16
  • 32
  • unfortunately this has introduced new issues, like some objects don't get saved and the old caching issue is back. Could it be because of the Singleton factory, or Singleton is not the issue because dbContext objects created from singleton factory have their own lifetime and are managed with `using` declaration? Thank you – ilya_i Apr 26 '23 at 20:58
  • 1
    Well, in Blazor case, you cannot use `services.AddScoped` because it do not create scope during request. So, you have to create DbContext from factory and dispose manually (using). [Doocumentation](https://learn.microsoft.com/en-us/ef/core/dbcontext-configuration/#using-a-dbcontext-factory-eg-for-blazor) – Svyatoslav Danyliv Apr 27 '23 at 04:48
  • 1
    Other option that you can create and dispose scope manually. – Svyatoslav Danyliv Apr 27 '23 at 04:50
  • In case with the Blazor app would you recommend creating and disposing scope manually with IServiceScopeFactory or with IServiceProvider? – ilya_i Apr 27 '23 at 08:13
  • 1
    Check this [fiddle](https://dotnetfiddle.net/i7E3B8) with sample code how to use scope in your case. – Svyatoslav Danyliv Apr 27 '23 at 08:35
  • Thanks, I will try that. For this approach, what ServiceLifetime should be user for AddDbContext in Startup. Would it be scoped? – ilya_i Apr 27 '23 at 10:03
  • 1
    Yes, lifetime should be scoped. Do not change anything. – Svyatoslav Danyliv Apr 27 '23 at 10:10