Home > Mobile >  System.AggregateException: "Some services are not able to be constructed" when extending m
System.AggregateException: "Some services are not able to be constructed" when extending m

Time:11-28

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?

CodePudding user response:

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);
  • Related