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

The entity type is part of a relationship cycle involving its primary key #23289

Closed
MoRooz opened this issue Nov 12, 2020 · 3 comments
Closed
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

@MoRooz
Copy link

MoRooz commented Nov 12, 2020

After upgrade to EF Core 5 I got this error
'The entity type 'xxx' is part of a relationship cycle involving its primary key.'

simplified sample composition

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

public class Pet
{
 public int Id {get; set;}
 public  Animal Animal {get; set;}
}

public class Cat
{
 public int Id {get; set;}
 public  Animal Animal {get; set;}
 public  Cat Cat {get; set;}
}

model configs

Animal
entity.HasKey(r => r.Id);

Pet
entity.HasKey(r => r.Id);
entity.Property(p => p.Id).ValueGeneratedNever();

entity.HasOne(b => b.Animal).WithOne().HasForeignKey<Pet>(r => r.Id).OnDelete(DeleteBehavior.Restrict);

Cat
entity.HasKey(r => r.Id);
entity.Property(p => p.Id).ValueGeneratedNever();

entity.HasOne(b => b.Animal).WithOne().HasForeignKey<Cat>(r => r.Id);
entity.HasOne(b => b.Pet).WithOne().HasForeignKey<Cat>(r => r.Id);

the error occurs in EFCore/Infrastructure/ModelValidator.cs
protected virtual void ValidateNoCycles

while (unvalidatedEntityTypes.Count > 0)
            {
                var rootEntityType = unvalidatedEntityTypes.First();
                reachableTypes.Clear();
                reachableTypes.Add(rootEntityType);
                typesToValidate.Enqueue(rootEntityType);

                while (typesToValidate.Count > 0)
                {
                    var entityType = typesToValidate.Dequeue();
                    var primaryKey = entityType.FindPrimaryKey();
                    if (primaryKey == null)
                    {
                        continue;
                    }

                    foreach (var foreignKey in entityType.GetForeignKeys())
                    {
                        var principalType = foreignKey.PrincipalEntityType;
                        if (!foreignKey.PrincipalKey.IsPrimaryKey()
                            || !unvalidatedEntityTypes.Contains(principalType)
                            || foreignKey.PrincipalEntityType.IsAssignableFrom(entityType)
                            || !PropertyListComparer.Instance.Equals(foreignKey.Properties, primaryKey.Properties))
                        {
                            continue;
                        }

                        if (!reachableTypes.Add(principalType))
                        {
                            throw new InvalidOperationException(CoreStrings.IdentifyingRelationshipCycle(
                                rootEntityType.DisplayName(),
                                primaryKey.Properties.Format()));
                        }

                        typesToValidate.Enqueue(principalType);
                    }
                }

                foreach (var entityType in reachableTypes)
                {
                    unvalidatedEntityTypes.Remove(entityType);
                }
            }

I did not understand the purpose of
!PropertyListComparer.Instance.Equals(foreignKey.Properties, primaryKey.Properties)

on validation cat, reachableTypes contains { cat, pet , animal}
on next loop it will try to add animal because of pet => animal and this occurs => !reachableTypes.Add(principalType)

so to summarize
I can get from cat to pet and from cat to animal
also from pet to animal

just cant find the relationship cycle.

EF Core version: EF Core 5.0.0

@ajcvickers
Copy link
Contributor

@MoRooz I am not able to reproduce this--see my code below. Please attach a small, runnable project or post a small, runnable code listing that reproduces what you are seeing so that we can investigate.

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

public class Pet
{
    public int Id {get; set;}
    public  Animal Animal {get; set;}
}

public class Cat
{
    public int Id {get; set;}
    public  Animal Animal {get; set;}
    public  Pet Pet {get; set;}
}

public class SomeDbContext : DbContext
{
    private static ILoggerFactory ContextLoggerFactory
        => LoggerFactory.Create(b => b.AddConsole().SetMinimumLevel(LogLevel.Information));

    public DbSet<Animal> Animals { get; set; }
    public DbSet<Pet> Pets { get; set; }
    public DbSet<Cat> Cats { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder
            //.UseSqlite("Data Source=test.db")
            .UseSqlServer(Your.ConnectionString)
            .EnableSensitiveDataLogging()
            .UseLoggerFactory(ContextLoggerFactory);

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Animal>(b =>
        {
            b.HasKey(r => r.Id);
        });
            
        modelBuilder.Entity<Pet>(b =>
        {
            b.HasKey(r => r.Id);
            b.Property(p => p.Id).ValueGeneratedNever();
            b.HasOne(b => b.Animal).WithOne().HasForeignKey<Pet>(r => r.Id).OnDelete(DeleteBehavior.Restrict);
        });

        modelBuilder.Entity<Cat>(b =>
        {
            b.HasKey(r => r.Id);
            b.Property(p => p.Id).ValueGeneratedNever();

            b.HasOne(b => b.Animal).WithOne().HasForeignKey<Cat>(r => r.Id);
            b.HasOne(b => b.Pet).WithOne().HasForeignKey<Cat>(r => r.Id);
        });
    }
}

public class Program
{
    public static void Main()
    {
        using (var context = new SomeDbContext())
        {
            context.Database.EnsureDeleted();
            context.Database.EnsureCreated();
        }
    }
}

@MoRooz
Copy link
Author

MoRooz commented Nov 12, 2020

the trick is to rename Animal to something alphabetically after cat

public class Program
    {
        public class XAnimal
        {
            public int Id { get; set; }
        }

        public class Pet
        {
            public int Id { get; set; }
            public XAnimal XAnimal { get; set; }
        }

        public class Cat
        {
            public int Id { get; set; }
            public XAnimal XAnimal { get; set; }
            public Pet Pet { get; set; }
        }

        public class SomeDbContext : DbContext
        {
            public DbSet<XAnimal> XXAnimals { get; set; }
            public DbSet<Pet> Pets { get; set; }
            public DbSet<Cat> Cats { get; set; }

            protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
                => optionsBuilder
                    //.UseSqlite("Data Source=test.db")
                    .UseSqlServer("Data Source=localhost; Initial Catalog=test;")
                    .EnableSensitiveDataLogging();

            protected override void OnModelCreating(ModelBuilder modelBuilder)
            {
                modelBuilder.Entity<XAnimal>(b =>
                {
                    b.HasKey(r => r.Id);
                });

                modelBuilder.Entity<Pet>(b =>
                {
                    b.HasKey(r => r.Id);
                    b.Property(p => p.Id).ValueGeneratedNever();
                    b.HasOne(b => b.XAnimal).WithOne().HasForeignKey<Pet>(r => r.Id).OnDelete(DeleteBehavior.Restrict);
                });

                modelBuilder.Entity<Cat>(b =>
                {
                    b.HasKey(r => r.Id);
                    b.Property(p => p.Id).ValueGeneratedNever();

                    b.HasOne(b => b.XAnimal).WithOne().HasForeignKey<Cat>(r => r.Id);
                    b.HasOne(b => b.Pet).WithOne().HasForeignKey<Cat>(r => r.Id);
                });
            }
        }

        public static void Main()
        {
            using (var context = new SomeDbContext())
            {
                context.Database.EnsureDeleted();
                context.Database.EnsureCreated();
            }
        }
    }

@ajcvickers
Copy link
Contributor

@AndriySvyryd Validation fails on 5.0, but not 3.1. Have not investigated further.

@ajcvickers ajcvickers added this to the 5.0.x milestone Nov 13, 2020
@ajcvickers ajcvickers modified the milestones: 5.0.x, 5.0.1 Nov 13, 2020
@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 Nov 13, 2020
@AndriySvyryd AndriySvyryd removed their assignment Nov 13, 2020
AndriySvyryd added a commit that referenced this issue Nov 13, 2020
AndriySvyryd added a commit that referenced this issue Nov 17, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
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
Projects
None yet
Development

No branches or pull requests

3 participants