lenny
lenny

Reputation: 784

Many-to-Many relationship in EF Core with dependent and principal entity both being the same type

Imagine this class for a data model used with Entity Framework:

public class CriteriaCatalogue : BaseDataModel
{
    public string Name { get; set; }

    public List<CriterionToCriteriaCatalogueLink> CriteriaLinks { get; set; }

    public virtual ICollection<CriteriaCatalogueToCriteriaCatalogueLink> CatalogueChildren { get; set; }
    public virtual ICollection<CriteriaCatalogueToCriteriaCatalogueLink> CatalogueParents { get; set; }
    // a bunch of additional properties with custom getters and setters to more easily access and create the child/parent `CriteriaCatalogue`s
}

The property CriteriaLinks gets filled perfectly, but my Parent and Children lists linking to the same type don't work at all, they get filled with the same object (so when looking at Owner1, both the children will be Owner1 and the parents will be Owner1)

So while the following is valid code, and will work as expected as long as you don't let EF touch it, EF will remove one of the connections:

CriteriaCatalogue owned = new CriteriaCatalogue { Name = "Owned" };
CriteriaCatalogue owner1 = new CriteriaCatalogue { Name = "Owner1", CriteriaCatalogues = new List<CriteriaCatalogue> { owned } };
CriteriaCatalogue owner2 = new CriteriaCatalogue { Name = "Owner2", CriteriaCatalogues = new List<CriteriaCatalogue> { owned } };

If you save all three objects to a database through EF, this is the result: owned doesn't have any children or parents. owner1 has owner1 as child and owner1 as parent. owner2 has owner2 as child and owner2 as parent.

How do I configure EF correctly to allow that owned can belong to several other entities?

Edit 2: I got it to work. All I needed to do was switch the CatalogueChildren and CatalogueParents references in the OnModelCreating method:

        modelBuilder.Entity<CriteriaCatalogueToCriteriaCatalogueLink>(cctcc =>
        {
            cctcc.HasKey(cctcc => new { cctcc.ChildId, cctcc.ParentId });

            cctcc.HasOne(cctcc => cctcc.Child)
                .WithMany(children => children.CatalogueParents)
                .HasForeignKey(cctcc => cctcc.ChildId)
                .OnDelete(DeleteBehavior.ClientSetNull);

            cctcc.HasOne(cctcc => cctcc.Parent)
                .WithMany(parents => parents.CatalogueChildren)
                .HasForeignKey(cctcc => cctcc.ParentId);
        });

Edit: Here's my full implementation according to Sergey's answer:

Linking class:

public class CriteriaCatalogueToCriteriaCatalogueLink
{
    public Guid ParentId { get; set; }
    public virtual CriteriaCatalogue Parent { get; set; }
    public Guid ChildId { get; set; }
    public virtual CriteriaCatalogue Child { get; set; }
}

DbContext.OnModelCreating snippet:

        modelBuilder.Entity<CriteriaCatalogue>(criterionCatalogue =>
        {
            criterionCatalogue.HasMany(cc => cc.CriteriaLinks);
            criterionCatalogue.HasMany(cc => cc.CatalogueChildren);
            criterionCatalogue.HasMany(cc => cc.CatalogueParents);
        });

        modelBuilder.Entity<CriterionToCriteriaCatalogueLink>(ctcc => 
        { 
            ctcc.HasKey(ctcc => new { ctcc.CriteriaCatalogueId, ctcc.CriterionId });

            ctcc.HasOne(ctcc => ctcc.Criterion)
                .WithMany(criterion => criterion.CriteriaCatalogueLinks)
                .HasForeignKey(ctcc => ctcc.CriterionId);

            ctcc.HasOne(ctcc => ctcc.CriteriaCatalogue)
                .WithMany(criteriaCatalogue => criteriaCatalogue.CriteriaLinks)
                .HasForeignKey(ctcc => ctcc.CriteriaCatalogueId);
        });

        modelBuilder.Entity<CriteriaCatalogueToCriteriaCatalogueLink>(cctcc =>
        {
            cctcc.HasKey(cctcc => new { cctcc.ChildId, cctcc.ParentId });

            cctcc.HasOne(cctcc => cctcc.Child)
                .WithMany(children => children.CatalogueChildren)
                .HasForeignKey(cctcc => cctcc.ChildId)
                .OnDelete(DeleteBehavior.ClientSetNull);

            cctcc.HasOne(cctcc => cctcc.Parent)
                .WithMany(parents => parents.CatalogueParents)
                .HasForeignKey(cctcc => cctcc.ParentId);
        });

As you can see, the implementation for connecting Criterions is a bit different but essentially the same. I tried the exact same for connecting CriteriaCatalogues but it doesn't work. I think EF gave me an error regarding circle references when I tried that. Most importantly, it's still not possible for one CriteriaCatalogue to have serveral parents.

Upvotes: 1

Views: 548

Answers (1)

Serge
Serge

Reputation: 43860

You can try this code:

Create a new class

public class MyClassMyClass
{
    public int ParentId { get; set; }
    public int ChildId { get; set; }

    public virtual MyClass Parent { get; set; }

    public virtual MyClass Child { get; set; }

}

and add this code to dbcontext:

modelBuilder.Entity<MyClassMyClass>(entity =>
            {
              
                entity.HasOne(d => d.Child)
                    .WithMany(p => p.MyClassMyClassChildren)
                    .HasForeignKey(d => d.ChildId)
                    .OnDelete(DeleteBehavior.ClientSetNull);

                entity.HasOne(d => d.Parent)
                    .WithMany(p => p.MyClassMyClassParents)
                    .HasForeignKey(d => d.ParentId);
            });

Fix your class:

public partial class MyClass
    {
       
        public int Id { get; set; }

        public virtual ICollection<MyClassMyClass> MyClassMyClassChildren { get; set; }
        public virtual ICollection<MyClassMyClass> MyClassMyClassParents { get; set; }
    }

I tested it in VS2019 using this code:

MyClass objectA = new MyClass();
var objectB = new MyClassMyClass { Parent = new MyClass(), Child = objectA };
var objectC = new MyClassMyClass { Parent = new MyClass(), Child = objectA };


_context.MyClassMyClasses.AddRange(new MyClassMyClass[] { objectB, objectC });
 _context.SaveChanges();

Everything was working perfect.

Upvotes: 2

Related Questions