Hussein Salman
Hussein Salman

Reputation: 8226

Querying nested collection (parent / children)

I am trying to get all menus and children that satisfy those conditions using linq:

This is the Menu class:

public class Menu
{
    public string Name { get; set; }
    public string Link { get; set; }

    public List<Menu> Children { get; set; }

    public  Menu()
    {
        Children = new List<Menu>();
    }
}

Suppose we have this data structure:

        List<Menu> root = new List<Menu>();
        Menu parent_1 = new Menu() { Name = "Parent 1", Link = null };
        Menu parent_2 = new Menu() { Name = "Parent 2", Link = null };


        //children for parent 1
        Menu p1_child_1 = new Menu() { Name = "p1_child_1", Link = null };
        Menu p1_child_2 = new Menu() { Name = "p1_child_2", Link = null };
        //sub children of p1_child_2
        Menu p1_child_1_1 = new Menu() { Name = "p1_child_1_1", Link = "l1-1" };
        Menu p1_child_1_2 = new Menu() { Name = "p1_child_1_2", Link = null };

        p1_child_1.Children.AddRange(new List<Menu> { p1_child_1_1 , p1_child_1_2 });
        parent_1.Children.AddRange(new List<Menu> { p1_child_1, p1_child_2 });


        Menu p2_child_1 = new Menu() { Name = "p2_child_1", Link = null };
        Menu p2_child_2 = new Menu() { Name = "p2_child_2", Link = "l2-2" };

        Menu p2_child_1_1 = new Menu() { Name = "p2_child_1_1", Link = null };
        Menu p2_child_1_2 = new Menu() { Name = "p2_child_1_2", Link = null };

        p2_child_1.Children.AddRange(new List<Menu> { p2_child_1_1, p2_child_1_2 });


        parent_2.Children.AddRange(new List<Menu> { p2_child_1, p2_child_2 });

        root.Add(parent_1);
        root.Add(parent_2);

Result: The filtered list returned based on the conditions requested will be:

parent_1

parent_2

How to achieve that using Linq or alternative approach taking into consideration the menu could have up to many levels?

Trying the solution as proposed in the comments, i added the extension method

 public static IEnumerable<TResult> SelectHierarchy<TResult>(this IEnumerable<TResult> source, Func<TResult, IEnumerable<TResult>> collectionSelector, Func<TResult, bool> predicate)
    {
        if (source == null)
        {
            yield break;
        }
        foreach (var item in source)
        {
            if (predicate(item))
            {
                yield return item;
            }
            var childResults = SelectHierarchy(collectionSelector(item), collectionSelector, predicate);
            foreach (var childItem in childResults)
            {
                yield return childItem;
            }
        }

Then called the method:

var result = root.SelectHierarchy(n => n.Children, n => n.Children.Count > 0 || n.Link != null).ToList();

However this is not what i want, I expect two menus which carry the subMenus that satisfy my condition, but i am getting 6 menus which i guess are flattened.

enter image description here

Although, p2_child_1 was returned since children count > 0, however it shouldn't cause its menus has no links. ( I placed the predicate as above, since i don't have other option.

Upvotes: 2

Views: 2268

Answers (2)

Ivan Stoev
Ivan Stoev

Reputation: 205539

Assuming the depth is not so big to cause stack overflow, you can use a simple recursive method or recursive lambda as follows:

Func<List<Menu>, List<Menu>> filter = null;
filter = items =>
    (from item in items
     let children = filter(item.Children)
     where item.Link != null || children.Any()
     select new Menu { Name = item.Name, Link = item.Link, Children = children }
    ).ToList();
var filtered = filter(root);

The essential part is to process the children first (post order traversal) before filtering the parent.

Upvotes: 2

Enigmativity
Enigmativity

Reputation: 117019

This works for me:

public static class Ex
{
    public static List<Menu> CloneWhere(this List<Menu> source, Func<Menu, bool> predicate)
    {
        return
            source
                .Where(predicate)
                .Select(x => new Menu()
                {
                    Name = x.Name,
                    Link = x.Link,
                    Children = x.Children.CloneWhere(predicate)
                })
                .Where(predicate)
                .ToList();
    }
}

The sample data looks like this:

source

...then I can apply this:

var result = root.CloneWhere(m => m.Children.Any() || m.Link != null);

...and I get this result:

result

Upvotes: 2

Related Questions