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
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
.