Home > Software engineering >  How do I configure options for a DbContext factory when my context extends another context?
How do I configure options for a DbContext factory when my context extends another context?

Time:11-28

This is a follow-on from a question I asked the other day. I have an AppDbContext, that extends the DbContext class, and is used in most projects in my solution. I want to add a BlazorAppDbContext class (that extends AppDbContext) to my Blazor server-side project, and add in some Blazor-specific code. My problem was working out how to configure the options to be passed in.

Kudos to Neil W, who walked me through this. I ended up with the following in Program.cs...

DbContextOptionsBuilder<AppDbContext> b = new();
b.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"));
b.EnableSensitiveDataLogging();
b.EnableDetailedErrors();
builder.Services.AddTransient(sp =>
  new BlazorAppDbContext(b.Options, sp.GetService<AuthenticationStateProvider>()));

That works fine for when I'm doing plain injection, ie decorating a property with the [Inject] attribute. However, it doesn't help me when I want to use a factory to create the context. See new BlazorAppDbContext(b.Options, sp.GetService(), sp.GetService()));" rel="nofollow noreferrer">this Microsoft article for why I want to do this).

The code for injecting a regular AppDbContext factory looked like this...

builder.Services.AddDbContextFactory<AppDbContext>(lifetime: ServiceLifetime.Scoped);

However, substituting BlazorAppDbContext instead suffers from the same problem that motivated my previous question, namely that I get "System.AggregateException: 'Some services are not able to be constructed" when it tries to create the service.

I thought of trying to mimic the code Neil W showed me, but couldn't work out what to create. When we inject a factory, we ask for an object of type IDbContextFactory<MyDbContext>, so I tried that...

DbContextOptionsBuilder<AppDbContext> b = new();
b.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"));
b.EnableSensitiveDataLogging();
b.EnableDetailedErrors();
builder.Services.AddTransient(sp => new BlazorAppDbContext(b.Options, sp.GetService<AuthenticationStateProvider>(), sp.GetService<IHttpContextAccessor>()));

builder.Services
  .AddTransient<IDbContextFactory<BlazorAppDbContext>>(sp 
    => new DbContextFactory<BlazorAppDbContext>(sp, 
           b.Options,
           new DbContextFactorySource<BlazorAppDbContext>()));

...where b is the DbContextOptionsBuilder that Neil W mentioned, extracted into a separate variable so I can use it in both cases.

However, this gave me a compiler error... "Argument 2: cannot convert from 'Microsoft.EntityFrameworkCore.DbContextOptions<AppDbContext>' to 'Microsoft.EntityFrameworkCore.DbContextOptions<BlazorAppDbContext>'".

Anyone any idea how I do this? Thanks

CodePudding user response:

Actually the answer to your previous question led you in the wrong direction.

Take a look at the DbContext class (or one of the IdentityDbContext classes) constructors. There is no requirement for TContext constructor having generic DbContextOptions<TContext> parameter - the non generic DbContextOptions is enough. The generic one is provided just for type safety (there is runtime check inside base constructor if options.ContextType == GetType()).

So, the proper solution is to (1) have your derived context public constructor receive DbContextOptions<TDerivedContext>, while (2) the public constructor of the based context still receive DbContextOptions<TBaseContext>, but also (3) add protected constructor receiving non generic DbContextOptions in the base class and call it from the other two, e.g.

public class AppDbContext : IdentityDbContext<User>
{
    // (2)
    public AppDbContext(DbContextOptions<AppDbContext> options)
        : this(options) { }

    // (3)
    protected AppDbContext(DbContextOptions options)
        : base(options) { }

    // ...
}

public class BlazorAppDbContext : AppDbContext
{
    private readonly AuthenticationStateProvider? _authenticationStateProvider;

    // (1)
    public BlazorAppDbContext(
        DbContextOptions<BlazorAppDbContext> options,
        AuthenticationStateProvider? authenticationStateProvider)
    : base(options) => _authenticationStateProvider = authenticationStateProvider;

    // ...
}

This solves the compilation error while keeping the type safety.

Now you can use the usual AddDbContext / AddDbContextFactory calls, e.g.

builder.Services.AddDbContextFactory<BlazorAppDbContext>(options =>
{
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"));
    options.EnableSensitiveDataLogging();
    options.EnableDetailedErrors();
}, lifetime: ServiceLifetime.Scoped);

Note that registering IDbContextFactory<TContext> also registers TContext for you, so there is no need having both AddDbContextFactory and AddDbContext.

  • Related