1

I have a solution that contains (amongst others) a Blazor server-side project, three minimal API projects, a MAUI project (that uses the minimal APIs), a Models project (contains the models for the solution) and a data access project. That last project contains my AppDbContext class...

public class AppDbContext : IdentityDbContext<User> {
  public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) {}
  // DbSets go here...
}

This is registered as a service in the Blazor and minimal API projects. As an example, in the Blazor project, Program.cs contains the following...

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

Note that I am in the process of switching from plain injection (using a property with the [Inject] attribute) to using factory injection, which is why I have that last line above. This way I can convert my code in bits, and it all still works.

This all works fine.

I now have a requirement to do some extra logging on every database update. I added the following to AppDbContext...

public new Task<int> SaveChangesAsync(string userId = "", CancellationToken cancellationToken = default) {
  // Do logging with userId
  return await base.SaveChangesAsync(cancellationToken);
}

Passing in the user's ID in the three minimal API projects aren't a problem, as I use JWT, and can easily get the Id from the token, and pass that in to SaveChangesAsync (or "" for anonymous API endpoints). That all works fine.

However, to get the logged-in user's Id in the Blazor project, I seem to need to use AuthenticationStateProvider. I thought that adding the following class to the Blazor project would work...

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

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

  public new Task<int> SaveChangesAsync(CancellationToken cancellationToken = default) {
    // Use the AuthenticationStateProvider to get the user Id and log as before
    return await base.SaveChangesAsync(cancellationToken);
  }
}

I modified the code in Program.cs to use BlazorAppDbContext instead of AppDbContext and tried to run, but got the following exception when it hit WebApplication app = builder.Build();...

System.AggregateException: 'Some services are not able to be constructed

(Error while validating the service descriptor 'ServiceType: MyProject.Admin.Helpers.BlazorAppDbContext Lifetime: Transient ImplementationType: MyProject.Admin.Helpers.BlazorAppDbContext': Unable to resolve service for type 'Microsoft.EntityFrameworkCore.DbContextOptions`1[MyProject.DataAccess.AppDbContext]' while attempting to activate 'MyProject.Admin.Helpers.BlazorAppDbContext'.)

(Error while validating the service descriptor 'ServiceType: Microsoft.AspNetCore.Identity.ISecurityStampValidator Lifetime: Scoped ImplementationType: Microsoft.AspNetCore.Identity.SecurityStampValidator1[MyProject.Models.User]': Unable to resolve service for type 'Microsoft.EntityFrameworkCore.DbContextOptions1[MyProject.DataAccess.AppDbContext]' while attempting to activate 'MyProject.Admin.Helpers.BlazorAppDbContext'.)

(Error while validating the service descriptor 'ServiceType: Microsoft.AspNetCore.Identity.ITwoFactorSecurityStampValidator Lifetime: Scoped ImplementationType: Microsoft.AspNetCore.Identity.TwoFactorSecurityStampValidator1[MyProject.Models.User]': Unable to resolve service for type 'Microsoft.EntityFrameworkCore.DbContextOptions1[MyProject.DataAccess.AppDbContext]' while attempting to activate 'MyProject.Admin.Helpers.BlazorAppDbContext'.)

This was followed by loads of almost identical exceptions, but for other types in the solution.

I've done a lot of searching around, but all of the answers to this sort of exception seem to be based around people not registering the services correctly. Given that this was working fine with AppDbContext, and I replaced all of the instances of that in Program.cs with BlazorAppDbContext (which extends AppDbContext), I'm really not sure what I'm doing wrong.

Following this answer, I tried adding the following line...

builder.Services.AddHttpContextAccessor();

...but it didn't help. I'm not surprised, as I use IHttpContextAccessor in a few places in the project already, so if the DI container wasn't able to resolve this, I would have found out some time ago.

I tried changing the constructor as follows...

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

...but that gave a compiler error, as base takes DbContextOptions<AppDbContext>, not DbContextOptions<BlazorAppDbContext>.

Anyone able to help?

Avrohom Yisroel
  • 8,555
  • 8
  • 50
  • 106
  • Could it be related to this constructor -> public BlazorAppDbContext(DbContextOptions options,..... pointing to ? Try putting instead. – Scott Mildenberger Nov 23 '22 at 21:34
  • @ScottMildenberger Forgot to add that to the end of my question. I've updated it, please have another look and see what you think. Thanks for the reply. – Avrohom Yisroel Nov 23 '22 at 22:27

1 Answers1

1

You could build the options manually by using a factory method when registering the BlazorAppDbContext:

builder.Services.AddTransient<BlazorAppDbContext>(sp =>
{
    DbContextOptionsBuilder<AppDbContext> optionsBuilder = new();
    optionsBuilder.UseSqlServer(
        builder.Configuration.GetConnectionString("DefaultConnection"));
    optionsBuilder.EnableSensitiveDataLogging();
    optionsBuilder.EnableDetailedErrors();

    return new BlazorAppDbContext(
        optionsBuilder.Options,
        sp.GetService<AuthenticationStateProvider>());
});

UPDATE

When using:

builder.Services.AddDbContext<MyContextType>(options => ...);

then the resulting DbContextOptions will be:

DbContextOptions<MyContextType>

In other words you cannot use AddDbContext to provide a DbContextOptions with a type argument of anything other that the type argument used for the AddDbContext method.

This is why you would REPLACE the AddDbContext with AddTransient and the factory method if you want to satisfy the constructor:

public BlazorAppDbContext(DbContextOptions<AppDbContext> options);
Neil W
  • 7,670
  • 3
  • 28
  • 41
  • Thanks for the reply. Is this instead of the call to `AddDbContext`, or in addition? Also, I get a "cannot resolve symbol" compiler error on all three of the methods called on `options`. Any ideas? Thanks again. – Avrohom Yisroel Nov 23 '22 at 22:53
  • If you'd only ever want to inject a BlazorAppDbContext into other classes and not AppDbContext, then you'd replace it. See my edit. – Neil W Nov 23 '22 at 23:06
  • And my bad. That class should be DbContextOptionsBuilder, not DbContextOptions. Have edited. – Neil W Nov 23 '22 at 23:08
  • Thanks, definitely getting closer. However, when I run the app, I get "_InvalidOperationException: Unable to resolve service for type 'Microsoft.EntityFrameworkCore.DbContextOptions`1[MyProject.DataAccess.AppDbContext]' while attempting to activate 'MyProject.Admin.Helpers.BlazorAppDbContext'._" – Avrohom Yisroel Nov 23 '22 at 23:10
  • Ah, just realised that the home page of the app injects the context with `BlazorAppDbContext ctx = await ContextFactory.CreateDbContextAsync();` - what do I need to change the get the factory version registered? Thanks again. – Avrohom Yisroel Nov 23 '22 at 23:12
  • Are you sure you're not registering BlazorAppDbContext somewhere else? That error would indicate that the DI container is trying to get the constructor params for BlazorAppDbContext automatically for you instead of using the factory method (sp => ...). And you should only have one constructor on BlazorAppDbContext. – Neil W Nov 23 '22 at 23:14
  • I presume ContextFactory is an IDbContextFactory injected into home page? You can now just inject BlazorAppDbContext instead and use it directly. – Neil W Nov 23 '22 at 23:19
  • I'm in the process of switching from plain injection for the context to using a factory, so I have both ways registered in `Program.cs`. Please see my updated question, where I added that line of code. Your code enabled plain injection to work, but now any components that try to use the factory to get a context throw the exception I showed. I'm trying to work out how to register the factory along the lines you showed. Can you help? Thanks again. – Avrohom Yisroel Nov 23 '22 at 23:20
  • Hmm, according to [this SO answer](https://stackoverflow.com/questions/65022729/use-both-adddbcontextfactory-and-adddbcontext-extension-methods-in-the-same/66996960#66996960), once I've set up the options once (which I do in your code), the factory should pick them up, so I'm not sure why I'm getting the exception. – Avrohom Yisroel Nov 23 '22 at 23:22
  • Are you able to help with the context factory? I can't seem to work out how to set it up, and when using plain injection, I keep hitting "_A second operation was started on this context instance before a previous operation completed_" exception, which was solved by using the factory – Avrohom Yisroel Nov 24 '22 at 17:46
  • Hi @AvrohomYisroel. This issue has gotten a little complicated and am now a little lost. It seems that your issue regarding using options for DbContextOptions in the constructor of BlazorAppDbContextOptions is resolved and the issue has now changed to be about the use of a registered DbContext and a registered DbContextFactory. I would recommend preparing a new question and stripping it down to the minimal scenario that creates the problem and then include the relevant registrations from the Program file and the classes/code that try to consume those services. – Neil W Nov 24 '22 at 18:17
  • Thanks again for all you help. I opened [a new question](https://stackoverflow.com/questions/74591589/how-do-i-configure-options-for-a-dbcontext-factory-when-my-context-extends-anoth) that (hopefully) clarifies what I'm asking. Please can you take a look and see if you can help. Thanks again. – Avrohom Yisroel Nov 27 '22 at 16:05