1

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

Avrohom Yisroel
  • 8,555
  • 8
  • 50
  • 106

1 Answers1

2

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.

Ivan Stoev
  • 195,425
  • 15
  • 312
  • 343
  • Thanks for the reply, but although the code all compiles, whenever I try to inject a factory, I get an exception "_System.InvalidOperationException: A second operation was started on this context instance before a previous operation completed_", which is precisely why I wanted to use a factory in the first place. I have changed all my code to use the factory, but still get this exception. Any ideas? Thanks again. – Avrohom Yisroel Nov 27 '22 at 17:54
  • 1
    @Ivan has provided a cleaner solution to the original problem. Thanks, Ivan! Your latest issue "A second operation ..." is not going to be related to registration and normally happens if you are not 'awaiting' a call to the context somewhere, and a second operation is started before first is completed. Now you have a tidy registration thanks to Ivan, you'll need to check the calls you make that invoke methods on the DbContext. Sometimes you are not awaiting a direct call to the context, sometimes it might be that you are not awaiting a call to service class that is using the DbContext. – Neil W Nov 27 '22 at 18:02
  • @NeilW Thanks for the comment, but I always await all db calls, no exceptions. I tried this on a plain page, and added one single db call, and got the exception. The only way I can see this happening is if the same context is being used somewhere else, ie in another component. However, I thought the whole point of the factory was to avoid that – Avrohom Yisroel Nov 27 '22 at 19:04
  • @AvrohomYisroel SO rules are single question per post. You posted question regarding concrete problem and got it answered. If you have new issue, then create new post. But from what you say, we'd need to see how you use the factory, since the factory method `CreateDbContext` always returns different instance (similar to C# `new` operator), so you are the only one who can share it with some other code. Also note that when retrieving the db context instance from factory, you are responsible for disposing it. – Ivan Stoev Nov 27 '22 at 19:33
  • @IvanStoev Seems VS was being silly. I restarted it, and the exact same code works fine. Thanks again for the help. – Avrohom Yisroel Nov 27 '22 at 20:26
  • @IvanStoev I just discovered an issue with your solution. I just tried adding a new migration. If I have the Blazor project selected as the default project, then VS adds a migration for every table in the solution, as if this was the very first migration. That will cause no end of errors if I tried to apply it to the database, as all those tables already exist. (to be continued)... – Avrohom Yisroel Dec 01 '22 at 19:46
  • If I choose the class library project that holds my `AppDbContext` class to be the default project, then I get an error "_Unable to create an object of type 'AppDbContext'. For the different patterns supported at design time, see https://go.microsoft.com/fwlink/?linkid=851728_". I looked at that link, but don't see how it helps, as the `AppDbContext` is in a class library, so there isn't a `Program.cs`, nor an `appSettings.json`. I tried adding a parameterless constructor, but that didn't help. Any ideas? Thanks again. – Avrohom Yisroel Dec 01 '22 at 19:46
  • @AvrohomYisroel As I mentioned previously, my solution is for your original problem *"How do I configure options for a DbContext factory when my context extends another context?"*. What you are asking now is a different problem and needs new post (question). Here there is simply no space for answering new questions. Meanwhile, you can take a look at [Using multiple context types](https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/providers?tabs=dotnet-core-cli#using-multiple-context-types) topic - migration should be for `AppDbContext`, which should have parameterless ... – Ivan Stoev Dec 01 '22 at 20:48
  • constructor and `OnConfiguring` override, in which you would have something like `if (!optionsBuilder.IsConfigured) { optionsBuilder.UseSql(...); }` etc. Or use [From a design-time factory](https://learn.microsoft.com/en-us/ef/core/cli/dbcontext-creation?tabs=dotnet-core-cli#from-a-design-time-factory) approach for your `AppDbContext`. – Ivan Stoev Dec 01 '22 at 21:01
  • @IvanStoev Sorry for the delay in replying, rebuilt my PC and been a bit busy! Anyway, thanks for the tip about the design-time factory, worked a treat. – Avrohom Yisroel Dec 12 '22 at 15:17