13

Database Table:

enter image description here

I tried this approach to map the category table to EF core:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Category>(entity =>
    {
        entity
            .HasMany(e => e.Children)
            .WithOne(e => e.Parent) 
            .HasForeignKey(e => e.ParentId);
    });
}

Entity:

[Table("Category"]
public class Category : EntityBase
{
    [DataType(DataType.Text), MaxLength(50)]
    public string Name { get; set; }

    public int? ParentId { get; set; }

    public int? Order { get; set; }

    [ForeignKey("ParentId")]
    public virtual Category Parent { get; set; }

    public virtual ICollection<Category> Children { get; set; }
}

Then in the repository:

public override IEnumerable<Category> GetAll()
{ 
    IEnumerable<Category> categories = Table.Where(x => x.Parent == null).Include(x => x.Children).ThenInclude(x=> x.Children);
    return categories;
}

This worked but anything after 3 levels was not returned no matter how many times you call Include() or ThenInclude().

I ended up writing the code myself to populate the child categories with a recursive function:

public override IEnumerable<Category> GetAll()
{
    IEnumerable<Category> categories = Table.Where(x => x.Parent == null).ToList();
    categories = Traverse(categories);
    return categories;
}

private IEnumerable<Category> Traverse(IEnumerable<Category> categories)
{
    foreach(var category in categories)
    {
        var subCategories = Table.Where(x => x.ParentId == category.Id).ToList();
        category.Children = subCategories;
        category.Children = Traverse(category.Children).ToList();
    }
    return categories;
}

Does anyone know a better way to write a stored procedure to get the table hierarchy and map to the Category entity I have provided in the example?

Vivendi
  • 20,047
  • 25
  • 121
  • 196
lschmid
  • 183
  • 1
  • 1
  • 11

1 Answers1

25

EF (and LINQ in general) has issues loading tree like data due to lack of recursive expression/CTE support.

But in case you want to load the whole tree (as opposed to filtered tree branch), there is a simple Include based solution. All you need is a single Include and then the EF navigation property fixup will do the work for you. And when you need to get only the root nodes as in your sample, the trick is to apply the filter after the query has been materialized (and navigation properties being fixed) by switching to LINQ to Objects context (using AsEnumerable() as usual).

So the following should produce the desired result with single SQL query:

public override IEnumerable<Category> GetAll()
{ 
    return Table
       .AsEnumerable()
       .Where(x => x.ParentId == null)
       .ToList();
}
Ivan Stoev
  • 195,425
  • 15
  • 312
  • 343
  • 2
    Thanks for your help, your solution worked perfectly. – lschmid Sep 11 '17 at 19:39
  • 1
    DV-er is probably someone who thinks you should create a recursive method firing umpteen queries. But Ivan, shouldn't this even work without the `Include`? – Gert Arnold Sep 11 '17 at 19:59
  • 1
    Hi @Gert, Good point, actually it does! Now I recall we have discussed this behavior for EF6. The only difference is that leaf nodes gets `null` children w/o `Include` and empty collection with. Of course if the collection members are always initialized, there is no difference. – Ivan Stoev Sep 11 '17 at 20:17
  • Testing this now in my code-i think I've probably lost 4-5 hours googling this only to find it mentioned in a comment on another answer.. :/ – TheTalentedDrAl Mar 27 '18 at 06:21
  • 2
    OMG. you king :) – Erçin Dedeoğlu Jan 08 '20 at 10:38
  • 1
    thanks a lot mate, I was losing my mind haha, cheers idk .. EF Core breaking changes are frustrating .. i see the benefits but some lacking features from EF6 are crazy. – Nexus Apr 11 '20 at 13:53
  • This code loads the entire table into memory and filters on server side. – pantonis May 13 '20 at 08:54
  • 1
    OMG. You are real king. Thank you. – rcanpahali Aug 15 '20 at 09:33
  • I'm confused, OP asks how to get all children, yet there is no code for this here, only a brief mention without code to clarify, the only code in this answer, is to do something he didn't ask (get only root nodes) – PandaWood Jun 09 '22 at 00:06
  • @PandaWood The code which does what OP asks is `Table.AsEnumerable()` (or directly `ToList()` etc.), i.e. enumerating the query which returns the **whole** table (**all** records). As I wrote in the answer, *"then the EF navigation property fixup will do the work for you"*, so at the end you have your model parent/children loaded and linked together in memory. – Ivan Stoev Jun 09 '22 at 03:09