0

I run into following exception when mapping any value object in any aggregate:

 ---> System.InvalidCastException: Unable to cast object of type 'Microsoft.EntityFrameworkCore.Metadata.Internal.ComplexType' to type 'Microsoft.EntityFrameworkCore.Metadata.IReadOnlyEntityType'.
   at Microsoft.EntityFrameworkCore.Metadata.Internal.ComplexType.<>c.<get_ConstructorBinding>b__42_0(ComplexType complexType)
   at Microsoft.EntityFrameworkCore.Internal.NonCapturingLazyInitializer.EnsureInitialized[TParam,TValue](TValue& target, TParam param, Action`1 valueFactory)
   at Microsoft.EntityFrameworkCore.Metadata.Internal.ComplexType.get_ConstructorBinding()
   at Microsoft.EntityFrameworkCore.Infrastructure.ModelValidator.<ValidateFieldMapping>g__Validate|24_0(ITypeBase typeBase)
   at Microsoft.EntityFrameworkCore.Infrastructure.ModelValidator.<ValidateFieldMapping>g__Validate|24_0(ITypeBase typeBase)
   at Microsoft.EntityFrameworkCore.Infrastructure.ModelValidator.ValidateFieldMapping(IModel model, IDiagnosticsLogger`1 logger)
   at Microsoft.EntityFrameworkCore.Infrastructure.ModelValidator.Validate(IModel model, IDiagnosticsLogger`1 logger)
   at Microsoft.EntityFrameworkCore.Infrastructure.RelationalModelValidator.Validate(IModel model, IDiagnosticsLogger`1 logger)
   at Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal.NpgsqlModelValidator.Validate(IModel model, IDiagnosticsLogger`1 logger)
   at Microsoft.EntityFrameworkCore.Infrastructure.ModelRuntimeInitializer.Initialize(IModel model, Boolean designTime, IDiagnosticsLogger`1 validationLogger)
   at Microsoft.EntityFrameworkCore.Internal.DbContextServices.CreateModel(Boolean designTime)
   at Microsoft.EntityFrameworkCore.Internal.DbContextServices.get_Model()
   at Microsoft.EntityFrameworkCore.Infrastructure.EntityFrameworkServicesBuilder.<>c.<TryAddCoreServices>b__8_4(IServiceProvider p)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.Resolve(ServiceCallSite callSite, ServiceProviderEngineScope scope)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.DynamicServiceProviderEngine.<>c__DisplayClass2_0.<RealizeService>b__0(ServiceProviderEngineScope scope)
   at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(ServiceIdentifier serviceIdentifier, ServiceProviderEngineScope serviceProviderEngineScope)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope.GetService(Type serviceType)
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType)
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider)
   at Microsoft.EntityFrameworkCore.DbContext.get_DbContextDependencies()
   at Microsoft.EntityFrameworkCore.DbContext.get_ContextServices()
   at Microsoft.EntityFrameworkCore.DbContext.get_InternalServiceProvider()
   at Microsoft.EntityFrameworkCore.DbContext.get_DbContextDependencies()
   at Microsoft.EntityFrameworkCore.DbContext.Set[TEntity]()
   at Venture.Server.Common.Persistence.EntityEvents.PublishedEntityEventReadRepository.GetConsumedAsync(CancellationToken cancellationToken) in C:\Users\N.Wolf\source\repos\venture-erp\src\Venture.Server.Common\Persistence\EntityEvents\PublishedEntityEventReadRepository.cs:line 25
   at Venture.Server.Common.Persistence.EntityEvents.Jobs.EntityEventCleanupJob.Execute(IJobExecutionContext context) in C:\Users\N.Wolf\source\repos\venture-erp\src\Venture.Server.Common\Persistence\EntityEvents\Jobs\EntityEventCleanupJob.cs:line 18
   at Quartz.Core.JobRunShell.Run(CancellationToken cancellationToken)

The 'department' aggregate and its configuration:

public class Department
{
    public Guid AggregateId { get; protected set; }
    public DateTime Created { get; protected set; }
    public DateTime LastChanged { get; protected set; }
    public Guid EditEmployeeId { get; protected set; }
    public required string Name { get; init; }
    public string? Comment { get; init; }
    public required Address Address { get; init; }

    public Guid DivisionId { get; init; }
    public required Division Division { get; init; }

    public List<Role> EmployeeRoles { get; init; } = [];
    public List<EmployeeDepartmentAssignment> EmployeeAssignments { get; init; } = [];
    public List<DepartmentPosition> Positions { get; init; } = [];

    protected override void ApplyEvent(DepartmentEvent @event)
    {
        throw new NotImplementedException();
    }
}
public record Address
{
    public required string Street { get; init; }
    public required string HouseNumber { get; init; }
    public required string City { get; init; }
    public required string PostalCode { get; init; }
    public required string State { get; init; }
    public required string Country { get; init; }
}
        builder.ToTable("access_management_departments");
        builder.HasKey(a => a.AggregateId);
        builder.Property(a => a.AggregateId).HasColumnName("id").IsRequired();
        builder.Property(a => a.Created).HasColumnName("created").IsRequired();
        builder.Property(a => a.LastChanged).HasColumnName("last_changed").IsRequired();
        builder.Property(a => a.EditEmployeeId).HasColumnName("edit_employee_id").IsRequired();
        builder.Property(d => d.Name).HasColumnName("name").HasMaxLength(70).IsRequired();
        builder.Property(d => d.Comment).HasColumnName("comment").HasMaxLength(200);
        builder.ComplexProperty(d => d.Address, p =>
        {
            p.Property(a => a.Street).HasColumnName("address_street").IsRequired();
            p.Property(a => a.HouseNumber).HasColumnName("address_house_number").IsRequired();
            p.Property(a => a.City).HasColumnName("address_city").IsRequired();
            p.Property(a => a.PostalCode).HasColumnName("address_postal_code").IsRequired();
            p.Property(a => a.State).HasColumnName("address_state").IsRequired();
            p.Property(a => a.Country).HasColumnName("address_country").IsRequired();
            p.IsRequired();
        });
        builder.Property(d => d.DivisionId).HasColumnName("division_id").IsRequired();

        builder.Ignore(d => d.Positions);
        builder.Ignore(d => d.EmployeeAssignments);
        builder.Ignore(d => d.EmployeeRoles);
        builder.Ignore(d => d.Division);

        //builder.HasMany(d => d.Positions).WithOne().HasForeignKey(d => d.DepartmentId).OnDelete(DeleteBehavior.Cascade);
        //builder.HasMany(d => d.EmployeeAssignments).WithOne(e => e.Department).HasForeignKey(a => a.DepartmentId).OnDelete(DeleteBehavior.Cascade);
        //builder.HasMany(d => d.EmployeeRoles).WithMany().UsingEntity<DepartmentRole>();
        //builder.HasOne(d => d.Division).WithMany().HasForeignKey(d => d.DivisionId);

I have tried out a bunch of things such as removing the required keyword or using a class instead of a record for the address and stuff like that. From my experience nothing I do here should cause the configuration to fail. However if I ignore the address property and comment out the ComplexProperty mapping no problems arise. The closest post I have found to this is this issue, but that seems to be part of the EF core backlog now.

I have also tried to decompile and debug the EF core code responsible for this according to the stacktrace but I fear my inseniority made me not come to any conclusions.

My question is: Are there any obvious blunders in my attempts or does this seem like a EF core bug?

1 Answer 1

0

Like often it was something silly.

I dont know exactly what it was but following extension method was used to register IEntityTypeConfiguration<TEntity>s from multiple assemblies:

    private static DbContextOptionsBuilder AddServerModel(this DbContextOptionsBuilder options, IEnumerable<Assembly> assemblies)
    {
        var model = new ModelBuilder();
        foreach (var assembly in assemblies)
            model.ApplyConfigurationsFromAssembly(assembly);

        return options.UseModel(model.FinalizeModel());
    }

Admittedly not very pretty but most importantly faulty. The ModelBuilder has some EF Core internal constructors with some configuration parameters configuring the model building process I assume. Well those configurations were now skipped since I instantiated one of my own.

I have now changed that approach to following code (screw new naming for a moment):

internal abstract class DbContextRegistrationMetadata
{
    internal static DbContextRegistrationMetadata<TDbContext> Create<TDbContext>()
        where TDbContext : DbContext
    {
        return new DbContextRegistrationMetadata<TDbContext>();
    }
}

public sealed class DbContextRegistrationMetadata<TDbContext>
    where TDbContext : DbContext
{
    internal DbContextRegistrationMetadata()
    {
    }

    private Assembly[]? _entityConfigurationAssemblies;

    public DbContextRegistrationMetadata<TDbContext> SearchInAssembliesForEntityConfigurations(params Assembly[] entityConfigurationAssemblies)
    {
        _entityConfigurationAssemblies = entityConfigurationAssemblies;
        return this;
    }

    internal Assembly[]? GetEntityConfigurationAssemblies() => _entityConfigurationAssemblies;
}

public static class DbContextRegistrationMetadataExtensions
{
    public static IServiceCollection AddDbContextRegistrationMetadata<TDbContext>(this IServiceCollection services, Action<DbContextRegistrationMetadata<TDbContext>> metadataAction)
        where TDbContext : DbContext
    {
        var metadata = DbContextRegistrationMetadata.Create<TDbContext>();
        metadataAction.Invoke(metadata);

        return services.AddSingleton(sp => metadata);
    }
}

And the DbContext now receives this like the DbContextOptions:

public class CommonDbContext(DbContextOptions<CommonDbContext> options, DbContextRegistrationMetadata<CommonDbContext> _registrationMetadata) : DbContext(options)
{
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        var entityConfigurationAssemblies = _registrationMetadata.GetEntityConfigurationAssemblies();
        if (entityConfigurationAssemblies == null)
            return;

        foreach (var assembly in entityConfigurationAssemblies)
            modelBuilder.ApplyConfigurationsFromAssembly(assembly);
    }
}

This allows me to configure multiple assemblies to be scanned for without any kind of reference of the assembly containing the DbContext implementation to the assemblies containing the entity configurations.

Silly, silly me

Not the answer you're looking for? Browse other questions tagged or ask your own question.