3

In accordance with my model, I want to extract the data as a tree list at once.

public class FolderInResearch : EntityBase
{
    public FolderInResearch()
    {
        SubFolders = new List<FolderInResearch>();
    }
    public string Name { get; set; }      
    public Guid? ParentFolderId { get; set; }
    [ForeignKey("ParentFolderId")]
    public ICollection<FolderInResearch> SubFolders { get; set; }        
}
Harun
  • 51
  • 1
  • 6
  • Check out this post on recursive loading using EF https://patrickdesjardins.com/blog/how-to-load-hierarchical-structure-with-recursive-with-entity-framework-5 – Jon Sep 10 '19 at 15:21
  • I read the article but it came a bit confused. I couldn't fully understand. – Harun Sep 10 '19 at 16:01
  • Loading the whole tree is quite easy with the following trick https://stackoverflow.com/questions/46160780/map-category-parent-id-self-referencing-table-structure-to-ef-core-entity/46161259#46161259. Loading recursively just part of the tree is... well, hard and inefficient. – Ivan Stoev Sep 10 '19 at 17:22
  • You could refer to:https://learn.microsoft.com/en-us/ef/core/querying/related-data – Rena Sep 11 '19 at 06:34

1 Answers1

6

This is how to configure and use the tree structure in EF Core:

Entity class:

public class Folder
{
    public Guid Id { get; set; }
    public string Name { get; set; }      
    public Folder Parent { get; set; }
    public Guid? ParentId { get; set; }
    public ICollection<Folder> SubFolders { get; } = new List<Folder>();
}

DB schema configuration:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Folder>(entity =>
    {
        entity.HasKey(x => x.Id);
        entity.Property(x => x.Name);
        entity.HasOne(x => x.Parent)
            .WithMany(x => x.SubFolders)
            .HasForeignKey(x => x.ParentId)
            .IsRequired(false)
            .OnDelete(DeleteBehavior.Restrict);
    });

    // ...
}

This is how to load data from DB as a tree:

{
    List<Folder> all = _dbContext.Folders.Include(x => x.Parent).ToList();

    TreeExtensions.ITree<Folder> virtualRootNode = all.ToTree((parent, child) => child.ParentId == parent.Id);

    List<TreeExtensions.ITree<Folder>> rootLevelFoldersWithSubTree = virtualRootNode.Children.ToList();

    List<TreeExtensions.ITree<Folder>> flattenedListOfFolderNodes = virtualRootNode.Children.Flatten(node => node.Children).ToList();

    // Each Folder entity can be retrieved via node.Data property:
    TreeExtensions.ITree<Folder> folderNode = flattenedListOfFolderNodes.First(node => node.Data.Name == "MyFolder");

    Folder folder = folderNode.Data;
    int level = folderNode.Level;
    bool isLeaf = folderNode.IsLeaf;
    bool isRoot = folderNode.IsRoot;
    ICollection<TreeExtensions.ITree<Folder>> children = folderNode.Children;
    TreeExtensions.ITree<Folder> parent = folderNode.Parent;
    List<Folder> parents = GetParents(folderNode);
}

Sample method to get all parents from the tree for node:

private static List<T> GetParents<T>(TreeExtensions.ITree<T> node, List<T> parentNodes = null) where T : class
{
    while (true)
    {
        parentNodes ??= new List<T>();

        if (node?.Parent?.Data == null) return parentNodes;

        parentNodes.Add(node.Parent.Data);

        node = node.Parent;
    }
}

Tree operation extension methods and helper interface for wrapping entities into the tree nodes:

public static class TreeExtensions
{
    /// <summary> Generic interface for tree node structure </summary>
    /// <typeparam name="T"></typeparam>
    public interface ITree<T>
    {
        T Data { get; }
        ITree<T> Parent { get; }
        ICollection<ITree<T>> Children { get; }
        bool IsRoot { get; }
        bool IsLeaf { get; }
        int Level { get; }
    }

    /// <summary> Flatten tree to plain list of nodes </summary>
    public static IEnumerable<TNode> Flatten<TNode>(this IEnumerable<TNode> nodes, Func<TNode, IEnumerable<TNode>> childrenSelector)
    {
        if (nodes == null) throw new ArgumentNullException(nameof(nodes));

        return nodes.SelectMany(c => childrenSelector(c).Flatten(childrenSelector)).Concat(nodes);
    }

    /// <summary> Converts given list to tree. </summary>
    /// <typeparam name="T">Custom data type to associate with tree node.</typeparam>
    /// <param name="items">The collection items.</param>
    /// <param name="parentSelector">Expression to select parent.</param>
    public static ITree<T> ToTree<T>(this IList<T> items, Func<T, T, bool> parentSelector)
    {
        if (items == null) throw new ArgumentNullException(nameof(items));

        var lookup = items.ToLookup(item => items.FirstOrDefault(parent => parentSelector(parent, item)),
            child => child);

        return Tree<T>.FromLookup(lookup);
    }

    /// <summary> Internal implementation of <see cref="ITree{T}" /></summary>
    /// <typeparam name="T">Custom data type to associate with tree node.</typeparam>
    internal class Tree<T> : ITree<T>
    {
        public T Data { get; }

        public ITree<T> Parent { get; private set; }

        public ICollection<ITree<T>> Children { get; }

        public bool IsRoot => Parent == null;

        public bool IsLeaf => Children.Count == 0;

        public int Level => IsRoot ? 0 : Parent.Level + 1;

        private Tree(T data)
        {
            Children = new LinkedList<ITree<T>>();
            Data = data;
        }

        public static Tree<T> FromLookup(ILookup<T, T> lookup)
        {
            var rootData = lookup.Count == 1 ? lookup.First().Key : default(T);
            var root = new Tree<T>(rootData);
            root.LoadChildren(lookup);
            return root;
        }

        private void LoadChildren(ILookup<T, T> lookup)
        {
            foreach (var data in lookup[Data])
            {
                var child = new Tree<T>(data) {Parent = this};
                Children.Add(child);
                child.LoadChildren(lookup);
            }
        }
    }
}
Dmitry Pavlov
  • 30,789
  • 8
  • 97
  • 121
  • 2
    I have decided to make a blog post from my answer [Tree Structure in EF Core: How to configure a self-referencing table and use it.](https://medium.com/@dmitry.pavlov/tree-structure-in-ef-core-how-to-configure-a-self-referencing-table-and-use-it-53effad60bf) – Dmitry Pavlov Aug 25 '20 at 11:33
  • thanks a lot for this awesome implementation, but there's a tricky issue, which is the "Circular Reference" each subFolder has a parent,.... etc ! is there anyway to stop this ? Thanks in advance – Ahmed Abdelhak Jul 05 '21 at 08:24
  • Can't this be done simpler by reading a node and all it's children by using something like an ltree path in postgresql, then iterate over all the nodes, adding each node to a dictionary keyed by their ids. Assuming a properly sorted ltree query, child nodes follow parent nodes. As nodes are read, get the parent from the dictionary by its ID (parentNodeId), and add the child to the parent's child collection. No need for really inefficient `FirstOrDefault` selectors. – Richard Collette Aug 10 '22 at 20:49
  • This is in memory tree-ing :) not on DB side. We are getting just list in reccords from DB here and then apply the rule how to build the tree using parent ID. So `TreeExtensions` I demo here are for converting any list of items to tree-like structure if there is a child-parent condition – Dmitry Pavlov Aug 10 '22 at 22:25