Search code examples
c#entity-framework-coremany-to-manyef-fluent-apitable-per-hierarchy

Many-to-many with TPH inheritance in EF Core


I am having trouble with setting up a many-to-many join table with EF core if one side of the join table is a derived class in table-per-hierarchy set up.

Here's the set up:

class Chore
{
    Guid Id;
}

class LaundryChore : Chore
{
    // PROBLEMATIC
    List<Clothing> ManyClothing;   
}

class FoldingChore : Chore
{
    Clothing SingleClothing;   
}

class Clothing
{
    Guid Id;
    
    // PROBLEMATIC
    List<Chore> Chores;
}

I have the TPH set up with discriminator and that all works fine. IF the ManyClothing field was on the Chore class then I can just do:

builder.Entity<Clothing>().HasMany(clothing => clothing.Chores)
    .WithMany(chore => chore.ManyClothing);

and this works as expected.

But since ManyClothing field was on the LaundryChore class, I would get DNE error with above.

I tried switching the direction:

builder.Entity<LaundryChore>().HasMany(chore => clothing.ManyClothing)
    .WithMany(clothing => clothing.Chores);

and I get a casting error:

Cannot implicitly convert type 'System.Collections.Generic.List' to 'System.Collections.Generic.IEnumerable'

If I change to:

class Clothing
{
    Guid Id;

    List<LaundryChore> Chores;
}

Then the error I get is:

The filter expression ... cannot be specified for entity type 'LaundryChore'. A filter may only be applied to the root entity type 'Chore'

Any guidance would be appreciated - thanks!


Solution

  • A navigation properties can only participate in a single relationship.

    It isn't a limitation type, but a limitation by relation. For example, if you have only the Chore and Clothing classes :

    public class Chore
    {
        public int Id { get; set; }
        public List<Clothing> ManyClothingToLaundry { get; set; }
        public Clothing SingleClothingToFolding { get; set; }
    }
    
    public class Clothing
    {
        public int Id { get; set; }
        public List<Chore> Chores { get; set; }
    }
    

    Now, you want add a Chore to a Clothing :

    clothing.Chores.Add(chore);
    

    Do this add a chore to laundry or to folding? EF Core can't know in this case.

    In your specific case, EF Core could detect the relationship from the type, but this functionality is not implemented.

    If Clothing has two distinct relationship, then Clothing need two navigation properties :

    public class Clothing
    {
        public int Id { get; set; }
        public List<Chore> FoldingChores { get; set; }
        public List<Chore> LaundryChores { get; set; }
        public IEnumerable<Chore> Chores => FoldingChores.Union(LaundryChores);
    }
    
    ...
    
    protected override void OnModelCreating(ModelBuilder builder)
    {
        ...
        builder.Entity<Clothing>().Ignore(c => c.Chores);
        builder.Entity<Clothing>()
            .HasMany<LaundryChore>(nameof(Clothing.LaundryChores))
            .WithMany(chore => chore.ManyClothing);
        builder.Entity<Clothing>()
            .HasMany<FoldingChore>(nameof(Clothing.FoldingChores))
            .WithOne(chore => chore.SingleClothing);
    }
    

    Clothing.FoldingChores is a collection of the base entity Chore for the example, but it can be directly a collection of the finale type FoldingChore. Idem with Clothing.LaundryChores :

    public class Clothing
    {
        public int Id { get; set; }
        public List<FoldingChore> FoldingChores { get; set; }
        public List<LaundryChore> LaundryChores { get; set; }
        public IEnumerable<Chore> Chores => FoldingChores.Cast<Chore>().Union(LaundryChores.Cast<Chore>());
    }