Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Inconsistent behavior related to relationship cycle #32422

Closed
dzsibi opened this issue Nov 27, 2023 · 11 comments · Fixed by #32573 or #32598
Closed

Inconsistent behavior related to relationship cycle #32422

dzsibi opened this issue Nov 27, 2023 · 11 comments · Fixed by #32573 or #32598
Labels
area-model-building closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. customer-reported regression Servicing-approved type-bug
Milestone

Comments

@dzsibi
Copy link

dzsibi commented Nov 27, 2023

Hello,

Take the following DbContext:

using Microsoft.EntityFrameworkCore;

namespace CircularKeys;

public class CircularContext : DbContext
{
    public record ChildEntity(string ParentId, string Id);

    public record ParentEntity(string Id, string? FavoriteChildId);

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseNpgsql("Host=localhost;Database=postgres;Username=postgres;Password=postgres");
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        var parent = modelBuilder.Entity<ParentEntity>();
        parent.HasKey(e => e.Id);
        parent.HasOne<ChildEntity>()
            .WithOne()
            .HasForeignKey<ParentEntity>(x => new { x.Id, x.FavoriteChildId });

        var child = modelBuilder.Entity<ChildEntity>();
        child.HasKey(e => new { e.ParentId, e.Id });
        child.HasOne<ParentEntity>()
            .WithMany()
            .HasForeignKey(e => e.ParentId);
    }
}

When I want to add a migration using dotnet ef migrations add Init --verbose, it works fine. Let's add another entity:

using Microsoft.EntityFrameworkCore;

namespace CircularKeys;

public class CircularContext : DbContext
{
    public record ChildEntity(string ParentId, string Id);

    public record ParentEntity(string Id, string? FavoriteChildId);
    
    public record AnotherEntity(string ParentId, string Id);

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseNpgsql("Host=localhost;Database=postgres;Username=postgres;Password=postgres");
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        var parent = modelBuilder.Entity<ParentEntity>();
        parent.HasKey(e => e.Id);
        parent.HasOne<ChildEntity>()
            .WithOne()
            .HasForeignKey<ParentEntity>(x => new { x.Id, x.FavoriteChildId });

        var child = modelBuilder.Entity<ChildEntity>();
        child.HasKey(e => new { e.ParentId, e.Id });
        child.HasOne<ParentEntity>()
            .WithMany()
            .HasForeignKey(e => e.ParentId);
        
        var another = modelBuilder.Entity<AnotherEntity>();
        another.HasKey(e => new { e.ParentId, e.Id });
        another.HasOne<ParentEntity>()
            .WithMany()
            .HasForeignKey(e => e.ParentId);
    }
}

A new migration can no longer be added, with the following error message:

Microsoft.EntityFrameworkCore.Design.OperationException: Unable to create a 'DbContext' of type ''. The exception 'A relationship cycle involving the property 'AnotherEntity.ParentId' 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.' was thrown while attempting to create an instance. For the different patterns supported at design time, see https://go.microsoft.com/fwlink/?linkid=851728
 ---> System.InvalidOperationException: A relationship cycle involving the property 'AnotherEntity.ParentId' 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.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.RelationalTypeMappingInfo..ctor(IReadOnlyList`1 principals, String storeTypeName, String storeTypeNameBase, Nullable`1 fallbackUnicode, Nullable`1 fallbackFixedLength, Nullable`1 fallbackSize, Nullable`1 fallbackPrecision, Nullable`1 fallbackScale)
   at Microsoft.EntityFrameworkCore.Storage.RelationalTypeMappingSource.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.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.Design.Internal.DbContextOperations.CreateContext(String contextType)
   --- End of inner exception stack trace ---
   at Microsoft.EntityFrameworkCore.Design.Internal.DbContextOperations.CreateContext(String contextType)
   at Microsoft.EntityFrameworkCore.Design.Internal.MigrationsOperations.AddMigration(String name, String outputDir, String contextType, String namespace)
   at Microsoft.EntityFrameworkCore.Design.OperationExecutor.AddMigrationImpl(String name, String outputDir, String contextType, String namespace)
   at Microsoft.EntityFrameworkCore.Design.OperationExecutor.AddMigration.<>c__DisplayClass0_0.<.ctor>b__0()
   at Microsoft.EntityFrameworkCore.Design.OperationExecutor.OperationBase.<>c__DisplayClass3_0`1.<Execute>b__0()
   at Microsoft.EntityFrameworkCore.Design.OperationExecutor.OperationBase.Execute(Action action)
Unable to create a 'DbContext' of type ''. The exception 'A relationship cycle involving the property 'AnotherEntity.ParentId' 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.' was thrown while attempting to create an instance. For the different patterns supported at design time, see https://go.microsoft.com/fwlink/?linkid=851728

The new entity did not introduce any new relationship cycles, yet EF was able to handle the previous one just fine. So I started digging. The exception is thrown here:

https://github.com/dotnet/efcore/blob/45673126512a0fe99ef73bf5c1e5701012fd9c26/src/EFCore/Metadata/Internal/Property.cs#L812C22-L812C22

There is this condition:

if (principalProperty == this || principalProperty == property)

In the first case, on the second cycle, this condition is triggered with principalProperty == this, and the function returns. In the second case, with the additional entity, this does not happen, and an exception is thrown. I am not sure why this behaves like this. If the goal was to detect cycles and throw, it fails to throw on the trivial cycle in the first example. If the goal was to break cycles, it throws anyways if there is a "leaf", as in a related entity that is not included in the cycle.

So let's try following the recommendation in the exception, and add value converters to the properties involved:

using Microsoft.EntityFrameworkCore;

namespace CircularKeys;

public class CircularContext : DbContext
{
    public record ChildEntity(string ParentId, string Id);

    public record ParentEntity(string Id, string? FavoriteChildId);
    
    public record AnotherEntity(string ParentId, string Id);
    

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseNpgsql("Host=localhost;Database=postgres;Username=postgres;Password=postgres");
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        var parent = modelBuilder.Entity<ParentEntity>();
        parent.HasKey(e => e.Id);
        parent.HasOne<ChildEntity>()
            .WithOne()
            .HasForeignKey<ParentEntity>(x => new { x.Id, x.FavoriteChildId });
        parent.Property(e => e.Id).HasConversion(e => e, e => e);
        parent.Property(e => e.FavoriteChildId).HasConversion(e => e, e => e);

        var child = modelBuilder.Entity<ChildEntity>();
        child.HasKey(e => new { e.ParentId, e.Id });
        child.HasOne<ParentEntity>()
            .WithMany()
            .HasForeignKey(e => e.ParentId);
        child.Property(e => e.Id).HasConversion(e => e, e => e);
        child.Property(e => e.ParentId).HasConversion(e => e, e => e);
        
        var another = modelBuilder.Entity<AnotherEntity>();
        another.HasKey(e => new { e.ParentId, e.Id });
        another.HasOne<ParentEntity>()
            .WithMany()
            .HasForeignKey(e => e.ParentId);
    }
}

This works, and the migration is generated as expected. We are not out of the woods yet, however, as dotnet ef database update --verbose will fail with the following error:

System.InvalidOperationException: A relationship cycle involving the property 'CircularKeys.CircularContext+AnotherEntity (Dictionary<string, object>).ParentId' 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.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.RelationalTypeMappingInfo..ctor(IReadOnlyList`1 principals, String storeTypeName, String storeTypeNameBase, Nullable`1 fallbackUnicode, Nullable`1 fallbackFixedLength, Nullable`1 fallbackSize, Nullable`1 fallbackPrecision, Nullable`1 fallbackScale)
   at Microsoft.EntityFrameworkCore.Storage.RelationalTypeMappingSource.FindMapping(IProperty property)
   at Microsoft.EntityFrameworkCore.Metadata.Internal.Property.<>c.<get_TypeMapping>b__91_0(IProperty property)
   at Microsoft.EntityFrameworkCore.Internal.NonCapturingLazyInitializer.EnsureInitialized[TParam,TValue](TValue& target, TParam param, Func`2 valueFactory)
   at Microsoft.EntityFrameworkCore.Metadata.Internal.Property.get_TypeMapping()
   at Microsoft.EntityFrameworkCore.Metadata.Internal.Property.Microsoft.EntityFrameworkCore.Metadata.IReadOnlyProperty.FindTypeMapping()
   at Microsoft.EntityFrameworkCore.RelationalPropertyExtensions.FindRelationalTypeMapping(IReadOnlyProperty property)
   at Microsoft.EntityFrameworkCore.RelationalPropertyExtensions.GetColumnType(IReadOnlyProperty property)
   at Microsoft.EntityFrameworkCore.RelationalPropertyExtensions.GetColumnType(IProperty property)
   at Microsoft.EntityFrameworkCore.Metadata.Internal.RelationalModel.AddDefaultMappings(RelationalModel databaseModel, IEntityType entityType, IRelationalTypeMappingSource relationalTypeMappingSource)
   at Microsoft.EntityFrameworkCore.Metadata.Internal.RelationalModel.Create(IModel model, IRelationalAnnotationProvider relationalAnnotationProvider, IRelationalTypeMappingSource relationalTypeMappingSource, Boolean designTime)
   at Microsoft.EntityFrameworkCore.Metadata.Internal.RelationalModel.Add(IModel model, IRelationalAnnotationProvider relationalAnnotationProvider, IRelationalTypeMappingSource relationalTypeMappingSource, Boolean designTime)
   at Microsoft.EntityFrameworkCore.Infrastructure.RelationalModelRuntimeInitializer.InitializeModel(IModel model, Boolean designTime, Boolean prevalidation)
   at Microsoft.EntityFrameworkCore.Infrastructure.ModelRuntimeInitializer.Initialize(IModel model, Boolean designTime, IDiagnosticsLogger`1 validationLogger)
   at Microsoft.EntityFrameworkCore.Migrations.Internal.Migrator.FinalizeModel(IModel model)
   at Microsoft.EntityFrameworkCore.Migrations.Internal.Migrator.GenerateUpSql(Migration migration, MigrationsSqlGenerationOptions options)
   at Microsoft.EntityFrameworkCore.Migrations.Internal.Migrator.<>c__DisplayClass16_2.<GetMigrationCommandLists>b__2()
   at Microsoft.EntityFrameworkCore.Migrations.Internal.Migrator.Migrate(String targetMigration)
   at Npgsql.EntityFrameworkCore.PostgreSQL.Migrations.Internal.NpgsqlMigrator.Migrate(String targetMigration)
   at Microsoft.EntityFrameworkCore.Design.Internal.MigrationsOperations.UpdateDatabase(String targetMigration, String connectionString, String contextType)
   at Microsoft.EntityFrameworkCore.Design.OperationExecutor.UpdateDatabaseImpl(String targetMigration, String connectionString, String contextType)
   at Microsoft.EntityFrameworkCore.Design.OperationExecutor.UpdateDatabase.<>c__DisplayClass0_0.<.ctor>b__0()
   at Microsoft.EntityFrameworkCore.Design.OperationExecutor.OperationBase.Execute(Action action)
A relationship cycle involving the property 'CircularKeys.CircularContext+AnotherEntity (Dictionary<string, object>).ParentId' 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.

Looks familiar, but the stack is very different. It turns out that the generated migration conveniently forgot about the value converters:

modelBuilder.Entity("CircularKeys.CircularContext+ParentEntity", b =>
    {
        b.Property<string>("Id")
            .HasColumnType("text");

        b.Property<string>("FavoriteChildId")
            .HasColumnType("text");

        b.HasKey("Id");

        b.HasIndex("Id", "FavoriteChildId")
            .IsUnique();

        b.ToTable("ParentEntity");
    });

And when this is loaded, the annotation is once again missing. We could work around this by overriding the implementation of AnnotationCodeGenerator:

using Microsoft.EntityFrameworkCore.Design;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.Extensions.DependencyInjection;
using Npgsql.EntityFrameworkCore.PostgreSQL.Design.Internal;

namespace CircularKeys;

public class DesignTimeServices : IDesignTimeServices
{
    public void ConfigureDesignTimeServices(IServiceCollection serviceCollection)
    {
        serviceCollection.AddSingleton<IAnnotationCodeGenerator, CircularAnnotationCodeGenerator>();
    }
}

public class CircularAnnotationCodeGenerator : NpgsqlAnnotationCodeGenerator
{
    public CircularAnnotationCodeGenerator(AnnotationCodeGeneratorDependencies dependencies) : base(dependencies) { }

    public override IReadOnlyList<MethodCallCodeFragment> GenerateFluentApiCalls(IProperty property, IDictionary<string, IAnnotation> annotations)
    {
        var ret = base.GenerateFluentApiCalls(property, annotations);

        if ((property.DeclaringType.ClrType == typeof(CircularContext.ParentEntity) &&
             property.PropertyInfo?.Name is nameof(CircularContext.ParentEntity.Id) or nameof(CircularContext.ParentEntity.FavoriteChildId)))
        {
            var valueConverterFragment = new MethodCallCodeFragment(
                nameof(PropertyBuilder.HasAnnotation), CoreAnnotationNames.ValueConverter, null);
            var providerClrTypeFragment = new MethodCallCodeFragment(
                nameof(PropertyBuilder.HasAnnotation), CoreAnnotationNames.ProviderClrType, null);
            return ret.Concat(new[] { valueConverterFragment, providerClrTypeFragment }).ToList();
        }

        return ret;
    }
}

This will include a null-valued annotation in the generated code for ValueConverter and ProviderClrType, and the migration will run:

modelBuilder.Entity("CircularKeys.CircularContext+ParentEntity", b =>
    {
        b.Property<string>("Id")
            .HasColumnType("text")
            .HasAnnotation("ValueConverter", null)
            .HasAnnotation("ProviderClrType", null);

        b.Property<string>("FavoriteChildId")
            .HasColumnType("text")
            .HasAnnotation("ValueConverter", null)
            .HasAnnotation("ProviderClrType", null);

        b.HasKey("Id");

        b.HasIndex("Id", "FavoriteChildId")
            .IsUnique();

        b.ToTable("ParentEntity");
    });

Once again, I don't know whether this is intentional or not. The recommended workaround does not survive code generation, and overriding methods in AnnotationCodeGenerator seems hacky at best. To me, this whole thing smells like a bug somewhere, but I am not familiar enough with the codebase to pinpoint exactly where.

Provider and version information

EF Core version: 8.0
Database provider: Npgsql.EntityFrameworkCore.PostgreSQL
Target framework: .NET 8.0
Operating system: Ubuntu 22.04
IDE: JetBrains Rider 2023.2.3

@TXP-AlbertPucciani
Copy link

I've found this happening to several of our code bases, this worked in EF6 and EF7. It's trivial to reproduce having an entity like this:

class Entity {
  public int Id { get; set; }

  public Entity Parent { get; set; }

  public int? ParentId { get; set; }

  public ICollection<Entity> Children { get; set; } = new HashSet<Entity>();
}

@ajcvickers
Copy link
Member

/cc @AndriySvyryd

@bbascarevic
Copy link

bbascarevic commented Dec 5, 2023

Same case here. The issue is preventing us from upgrading to .Net 8.

@bbascarevic
Copy link

I just tried debugging this in EF 7. It turns out the only reason it works is because Property.GetValueConverter() isn't throwing but is silently giving up and returning null after 10_000 tries.

@AndriySvyryd
Copy link
Member

The first part was a deliberate breaking change, the second part (when generating the next migration) wasn't

@RenesansJG
Copy link

@AndriySvyryd It seems that the current algorithm breaks the loop when we get back to the property we've originally started with or the next property would be the same as the current one. Wouldn't it be better to maintain a set of the properties we've already traversed and break when we get to a property already in the set?

Here you can see exactly what I mean:
RenesansJG@e531d9d

I tried it out and it seemed to solve the problem presented in the original post. I know the extra allocation wouldn't do any good for performance but it can be optimized so that the allocation only gets done if it's actually needed.

@AndriySvyryd
Copy link
Member

Wouldn't it be better to maintain a set of the properties we've already traversed and break when we get to a property already in the set?

It would be more accurate, but would have a measurable negative perf impact. We can improve the cycle breaking logic without tracking more than 1 property.

AndriySvyryd added a commit that referenced this issue Dec 9, 2023
…hen not starting on one.

Additionally, detect multiple relationship chains that end with incompatible conversions.

Fixes #32422
@AndriySvyryd AndriySvyryd added the closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. label Dec 9, 2023
AndriySvyryd added a commit that referenced this issue Dec 9, 2023
…hen not starting on one.

Additionally, detect multiple relationship chains that end with incompatible conversions.

Fixes #32422
AndriySvyryd added a commit that referenced this issue Dec 9, 2023
…hen not starting on one.

Additionally, detect multiple relationship chains that end with incompatible conversions.

Fixes #32422
AndriySvyryd added a commit that referenced this issue Dec 11, 2023
…hen not starting on one. (#32573)

Additionally, detect multiple relationship chains that end with incompatible conversions.

Fixes #32422
AndriySvyryd added a commit that referenced this issue Dec 12, 2023
…tor.

Don't allocate Queue unless necessary.
Fix an issue with configuration of an FK to the base type.

Part of #32422
@AndriySvyryd AndriySvyryd reopened this Dec 12, 2023
AndriySvyryd added a commit that referenced this issue Dec 12, 2023
…tor.

Don't allocate Queue unless necessary.
Fix an issue with configuration of an FK to the base type.

Part of #32422
AndriySvyryd added a commit that referenced this issue Dec 12, 2023
…tor.

Don't allocate Queue unless necessary.
Fix an issue with configuration of an FK to the base type.

Part of #32422
AndriySvyryd added a commit that referenced this issue Dec 12, 2023
…tor. (#32593)

Don't allocate Queue unless necessary.
Fix an issue with configuration of an FK to the base type.

Part of #32422
AndriySvyryd added a commit that referenced this issue Dec 12, 2023
…hen not starting on one.

Additionally, detect multiple relationship chains that end with incompatible conversions.

Fixes #32422
@AndriySvyryd AndriySvyryd removed their assignment Dec 12, 2023
@ajcvickers ajcvickers removed this from the 8.0.x milestone Dec 12, 2023
@ajcvickers ajcvickers added this to the 8.0.2 milestone Dec 12, 2023
@Peter-B-
Copy link

This issue also surfaces during scaffolding.

I try to scaffold an Oracle database created with EF (not-core) 6. Scaffolding works with Microsoft.EntityFrameworkCore.Relational 7.0.14 and Oracle.EntityFrameworkCore 7.21.12, but fails with Microsoft.EntityFrameworkCore.Relational 8.0.0 and Oracle.EntityFrameworkCore 8.21.121.

This is quite inconvenient, because there is not much I can do about the existing database structure (which works fine, by the way). So this new behavior de facto breaks scaffolding for my database.

When I create a DbContext with EF 7 and then upgrade to EF 8 I get a runtime error, but at least I can now try to adapt the structure to get a consistent EF 8 model.

@ErikEJ
Copy link
Contributor

ErikEJ commented Dec 20, 2023

@Peter-B- I think this will be fixed in 8.0.2 - and how is it "failing"?

@Peter-B-
Copy link

Hi @ErikEJ,

When I run dotnet ef DbContext scaffold ... it fails with this error message:

System.InvalidOperationException: A relationship cycle involving the property 'PcbType (Dictionary<string, object>).DutCoordinateSetId' 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.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.RelationalTypeMappingInfo..ctor(IReadOnlyList`1 principals, String storeTypeName, String storeTypeNameBase, Nullable`1 fallbackUnicode, Nullable`1 fallbackFixedLength, Nullable`1 fallbackSize, Nullable`1 fallbackPrecision, Nullable`1 fallbackScale)
   at Microsoft.EntityFrameworkCore.Storage.RelationalTypeMappingSource.FindMapping(IProperty property)
   at Microsoft.EntityFrameworkCore.Metadata.Internal.Property.<>c.<get_TypeMapping>b__91_0(IProperty property)
   at Microsoft.EntityFrameworkCore.Internal.NonCapturingLazyInitializer.EnsureInitialized[TParam,TValue](TValue& target, TParam param, Func`2 valueFactory)
   at Microsoft.EntityFrameworkCore.Metadata.Internal.Property.get_TypeMapping()
   at Microsoft.EntityFrameworkCore.Metadata.Internal.Property.Microsoft.EntityFrameworkCore.Metadata.IReadOnlyProperty.FindTypeMapping()
   at Microsoft.EntityFrameworkCore.RelationalPropertyExtensions.FindRelationalTypeMapping(IReadOnlyProperty property)
   at Microsoft.EntityFrameworkCore.RelationalPropertyExtensions.GetColumnType(IReadOnlyProperty property)
   at Microsoft.EntityFrameworkCore.RelationalPropertyExtensions.GetColumnType(IProperty property)
   at Microsoft.EntityFrameworkCore.Metadata.Internal.RelationalModel.AddDefaultMappings(RelationalModel databaseModel, IEntityType entityType, IRelationalTypeMappingSource relationalTypeMappingSource)
   at Microsoft.EntityFrameworkCore.Metadata.Internal.RelationalModel.Create(IModel model, IRelationalAnnotationProvider relationalAnnotationProvider, IRelationalTypeMappingSource relationalTypeMappingSource, Boolean designTime)
   at Microsoft.EntityFrameworkCore.Metadata.Internal.RelationalModel.Add(IModel model, IRelationalAnnotationProvider relationalAnnotationProvider, IRelationalTypeMappingSource relationalTypeMappingSource, Boolean designTime)
   at Microsoft.EntityFrameworkCore.Infrastructure.RelationalModelRuntimeInitializer.InitializeModel(IModel model, Boolean designTime, Boolean prevalidation)
   at Microsoft.EntityFrameworkCore.Infrastructure.ModelRuntimeInitializer.Initialize(IModel model, Boolean designTime, IDiagnosticsLogger`1 validationLogger)
   at Microsoft.EntityFrameworkCore.Scaffolding.Internal.RelationalScaffoldingModelFactory.Create(DatabaseModel databaseModel, ModelReverseEngineerOptions options)
   at Microsoft.EntityFrameworkCore.Scaffolding.Internal.ReverseEngineerScaffolder.ScaffoldModel(String connectionString, DatabaseModelFactoryOptions databaseOptions, ModelReverseEngineerOptions modelOptions, ModelCodeGenerationOptions codeOptions)
   at Microsoft.EntityFrameworkCore.Design.Internal.DatabaseOperations.ScaffoldContext(String provider, String connectionString, String outputDir, String outputContextDir, String dbContextClassName, IEnumerable`1 schemas, IEnumerable`1 tables, String modelNamespace, String contextNamespace, Boolean useDataAnnotations, Boolean overwriteFiles, Boolean useDatabaseNames, Boolean suppressOnConfiguring, Boolean noPluralize)
   at Microsoft.EntityFrameworkCore.Design.OperationExecutor.ScaffoldContextImpl(String provider, String connectionString, String outputDir, String outputDbContextDir, String dbContextClassName, IEnumerable`1 schemaFilters, IEnumerable`1 tableFilters, String modelNamespace, String contextNamespace, Boolean useDataAnnotations, Boolean overwriteFiles, Boolean useDatabaseNames, Boolean suppressOnConfiguring, Boolean noPluralize)
   at Microsoft.EntityFrameworkCore.Design.OperationExecutor.ScaffoldContext.<>c__DisplayClass0_0.<.ctor>b__0()
   at Microsoft.EntityFrameworkCore.Design.OperationExecutor.OperationBase.<>c__DisplayClass3_0`1.<Execute>b__0()
   at Microsoft.EntityFrameworkCore.Design.OperationExecutor.OperationBase.Execute(Action action)

No models or db context are created.

I saw that there are PRs for 8.0.2. I just wanted to bring to your attention, that this also affects scaffolding.

Thanks for your prompt reply and the fast fix
Peter

@nwoolls

This comment was marked as resolved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment