Skip to content

Cycle detection when detecting value converter types over foreign keys throws InvalidOperationException #38301

@zlepper

Description

@zlepper

Bug description

I have ended up with a rather complex/annoying set of relationships between 2 types (See the attached code).

I have tried reducing it further, but that doesn't seem possible without causing the issue to disappear.

If I explicitly set the ProviderClrType annotation on the WorkspaceId property, when the issue stops occuring.

If I don't use the multi-column (foreign)keys, then the issue does not occur.

The issue still occurs when specifying all the columns to be int rather than Guid. (Setting ProviderClrType still works to 'fix' the issue.)

The issue seems to occur on all versions between EFCore 8.0.0 up to and including EFCore version 11.0.0-preview.3.26207.106. (I only have dotnet 11 preview 3 available in my package manager, not preview 4 yet, so I have not tested preview 4).

The issue seems to occur no matter the database provider. I have tested "InMemory", "Sqlite" and "Postgres".

Your code

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

await using var context = new ReproContext();
await context.Database.EnsureDeletedAsync();
await context.Database.EnsureCreatedAsync();

public class ReproContext : DbContext
{
    public DbSet<ChatMessage> Messages => Set<ChatMessage>();
    public DbSet<ChatSession> Sessions => Set<ChatSession>();

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) =>
        optionsBuilder
            .UseInMemoryDatabase("Repro")
            .LogTo(Console.WriteLine, LogLevel.Information)
            .EnableSensitiveDataLogging();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<ChatSession>(session =>
        {
            // session.Property(s => s.WorkspaceId).HasAnnotation(CoreAnnotationNames.ProviderClrType, typeof(Guid));
            session.HasKey(s => new { s.WorkspaceId, s.Id });

            session
                .HasOne(s => s.ParentMessage)
                .WithMany(m => m.ChildChatSessions)
                .HasForeignKey(s => new { s.WorkspaceId, s.ParentMessageId })
                .HasPrincipalKey(m => new {m.WorkspaceId, m.Id})
                .IsRequired(false)
                .OnDelete(DeleteBehavior.Restrict);
            
            session
                .HasOne(s => s.ParentSession)
                .WithMany(s => s.ChildSessions)
                .HasForeignKey(s => new { s.WorkspaceId, s.ParentSessionId })
                .HasPrincipalKey(s => new {s.WorkspaceId, s.Id})
                .IsRequired(false)
                .OnDelete(DeleteBehavior.Restrict);
        });

        modelBuilder.Entity<ChatMessage>(message =>
        {
            // message.Property(s => s.WorkspaceId).HasAnnotation(CoreAnnotationNames.ProviderClrType, typeof(Guid));
            message.HasKey(m => new { m.WorkspaceId, m.Id });

            message
                .HasOne(m => m.Session)
                .WithMany(s => s.Messages)
                .HasForeignKey(m => new { m.WorkspaceId, m.SessionId })
                .HasPrincipalKey(s => new { s.WorkspaceId, s.Id })
                .OnDelete(DeleteBehavior.Restrict);

            message
                .HasOne(m => m.PreviousMessage)
                .WithMany(m => m.FollowingMessages)
                .HasForeignKey(m => new { m.WorkspaceId, m.PreviousMessageId })
                .HasPrincipalKey(m => new { m.WorkspaceId, m.Id })
                .OnDelete(DeleteBehavior.Restrict);
        });
    }
}

public class ChatMessage
{
    public Guid WorkspaceId { get; set; }
    public Guid Id { get; set; }

    public Guid SessionId { get; set; }
    public ChatSession Session { get; set; } = null!;

    public Guid? PreviousMessageId { get; set; }
    public ChatMessage? PreviousMessage { get; set; }
    
    public ICollection<ChatMessage> FollowingMessages { get; set; } = null!;

    public ICollection<ChatSession> ChildChatSessions { get; set; } = null!;
}

public class ChatSession
{
    public Guid WorkspaceId { get; set; }

    public Guid Id { get; set; }

    public Guid? ParentMessageId { get; set; }
    public ChatMessage? ParentMessage { get; set; }

    public Guid? ParentSessionId { get; set; }
    public ChatSession? ParentSession { get; set; }

    public ICollection<ChatSession> ChildSessions { get; set; } = null!;

    public ICollection<ChatMessage> Messages { get; set; } = [];
}

Stack traces

Unhandled exception. System.InvalidOperationException: A relationship cycle involving the property 'ChatMessage.WorkspaceId' was detected. This prevents Entity Framework from determining the correct configuration. Review the foreign keys defined on the property and the corresponding principal property and either remove one of them or specify 'ValueConverter' explicitly on one of the properties.
   at Microsoft.EntityFrameworkCore.Metadata.Internal.Property.GetConversion(Boolean throwOnValueConverterConflict, Boolean throwOnProviderClrTypeConflict)
   at Microsoft.EntityFrameworkCore.Metadata.Internal.Property.GetValueConverter()
   at Microsoft.EntityFrameworkCore.Storage.TypeMappingInfo..ctor(IReadOnlyList`1 principals, Nullable`1 fallbackUnicode, Nullable`1 fallbackSize, Nullable`1 fallbackPrecision, Nullable`1 fallbackScale)
   at Microsoft.EntityFrameworkCore.Storage.TypeMappingSource.FindMapping(IProperty property)
   at Microsoft.EntityFrameworkCore.Metadata.Conventions.ElementMappingConvention.<ProcessModelFinalizing>g__Validate|4_0(IConventionTypeBase typeBase)
   at Microsoft.EntityFrameworkCore.Metadata.Conventions.ElementMappingConvention.ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext`1 context)
   at Microsoft.EntityFrameworkCore.Metadata.Conventions.Internal.ConventionDispatcher.ImmediateConventionScope.OnModelFinalizing(IConventionModelBuilder modelBuilder)
   at Microsoft.EntityFrameworkCore.Metadata.Conventions.Internal.ConventionDispatcher.OnModelFinalizing(IConventionModelBuilder modelBuilder)
   at Microsoft.EntityFrameworkCore.Metadata.Internal.Model.FinalizeModel()
   at Microsoft.EntityFrameworkCore.Infrastructure.ModelRuntimeInitializer.Initialize(IModel model, Boolean designTime, IDiagnosticsLogger`1 validationLogger)
   at Microsoft.EntityFrameworkCore.Infrastructure.ModelSource.CreateModel(DbContext context, ModelCreationDependencies modelCreationDependencies, Boolean designTime)
   at Microsoft.EntityFrameworkCore.Infrastructure.ModelSource.GetModel(DbContext context, ModelCreationDependencies modelCreationDependencies, Boolean designTime)
   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.CallSiteRuntimeResolver.VisitFactory(FactoryCallSite factoryCallSite, 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.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.Microsoft.EntityFrameworkCore.Infrastructure.IInfrastructure<System.IServiceProvider>.get_Instance()
   at Microsoft.EntityFrameworkCore.Infrastructure.Internal.InfrastructureExtensions.GetService(IInfrastructure`1 accessor, Type serviceType)
   at Microsoft.EntityFrameworkCore.Infrastructure.Internal.InfrastructureExtensions.GetService[TService](IInfrastructure`1 accessor)
   at Microsoft.EntityFrameworkCore.Infrastructure.AccessorExtensions.GetService[TService](IInfrastructure`1 accessor)
   at Microsoft.EntityFrameworkCore.Infrastructure.DatabaseFacade.get_Dependencies()
   at Microsoft.EntityFrameworkCore.Infrastructure.DatabaseFacade.EnsureDeletedAsync(CancellationToken cancellationToken)
   at Program.<Main>$(String[] args) in /home/rasmus/projects/EfCoreReproduction/EfCoreReproduction/Program.cs:line 9
   at Program.<Main>$(String[] args) in /home/rasmus/projects/EfCoreReproduction/EfCoreReproduction/Program.cs:line 10
   at Program.<Main>(String[] args)

Verbose output


EF Core version

10.0.8

Database provider

Microsoft.EntityFrameworkCore.InMemory, Microsoft.EntityFrameworkCore.Sqlite, Npgsql.EntityFrameworkCore.PostgreSQL

Target framework

.net 10

Operating system

Nixos

IDE

Commandline

Metadata

Metadata

Assignees

Type

No type
No fields configured for issues without a type.

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions