views:

96

answers:

4

Hi, I'm already familiar with Linq but have little understanding of extension methods I'm hoping someone can help me out.

So I have this hierarchical collection pseudo code ie:

class Product
  prop name
  prop type
  prop id
  prop List<Product> children

And I have a list of products List products.

Is there any way I can look for product in this collection by the id with a extension method ? In other words I need one item somewhere within the hierarchy.

Help would be most appreciated

cheers /sushiBite

+2  A: 

You can flatten your tree structure using this extension method:

static IEnumerable<Product> Flatten(this IEnumerable<Product> source)
{
    return source.Concat(source.SelectMany(p => p.Children.Flatten()));
}

Usage:

var product42 = products.Flatten().Single(p => p.Id == 42);

Note that this is probably not very fast. If you repeatedly need to find a product by id, create a dictionary:

var dict = products.Flatten().ToDictionary(p => p.Id);

var product42 = dict[42];
dtb
Nice, i like that Flatten method. If i'm not mistaken it'll iterate breadth-first (edit: i am mistaken, it's not breadthfirst, question is still relevant though). Does that mean that if the product is the first item in the list, and you use First instead of Single, that it won't Flatten the entire hierarchy? Will linq's delayed execution help out here?
Bubblewrap
This looks like a good solution, but it ignores the possibility that the list of children might be `null`.
Gabe
@Bubblewrap: You're right. If you use `First` then, thanks to delayed execution, `Flatten` will only flatten as much as is needed.
dtb
A: 

If you want to "sub-iterate" and find a child in a list of Products:

List<Product>
    Product
       Child
       Child
       Child
       Child
    Product
       Child
       Child *find this one
       Child

You can use the existing SelectMany extension method. SelectMany can be used to "flatten" a two-level hierarchy.

Here's a great explanation of SelectMany: http://team.interknowlogy.com/blogs/danhanan/archive/2008/10/10/use-linq-s-selectmany-method-to-quot-flatten-quot-collections.aspx

Your syntax would like like this:

List<Product> p = GetProducts(); //Get a list of products
var child = from c in p.SelectMany(p => p.Children).Where(c => c.Id == yourID);
Dave Swersky
Well, that's good, but the hierarchy and be x many levels deep for each productso Product A can have 3 children 5 grandchildren and 100 grand grandchildren and Product be can maybe only have 1 child and no grand child, there is no way for me to know.if I am understanding this correctly I would have to use SelectMany() for each level of the hierarchy ?
sushiBite
You can chain SelectMany together as far as necessary to get to the level you want.
Dave Swersky
+2  A: 

Here is a generic solution that will short-circuit traversal of the hierarchy once a match is found.

public static class MyExtensions
{
    public static T FirstOrDefaultFromMany<T>(
        this IEnumerable<T> source, Func<T, IEnumerable<T>> childrenSelector,
        Predicate<T> condition)
    {
        // return default if no items
        if(source == null || !source.Any()) return default(T);

        // return result if found and stop traversing hierarchy
        var attempt = source.FirstOrDefault(t => condition(t));
        if(!Equals(attempt,default(T))) return attempt;

        // recursively call this function on lower levels of the
        // hierarchy until a match is found or the hierarchy is exhausted
        return source.SelectMany(childrenSelector)
            .FirstOrDefaultFromMany(childrenSelector, condition);
    }
}

To use it in your case:

var matchingProduct = products.FirstOrDefaultFromMany(p => p.children, p => p.Id == 27);
Jay
Hi I made one minor change to this code and that works :)I changed thisif(Equals(attempt,default(T))) return attempt; toif(Equals(attempt != null) return attempt;It works like a charmThank you all for your help.
sushiBite
@sushiBite I think it should actually be `if(!Equals(attempt, default(T))) return attempt;` because the default value of `T` might not be `null` (if `T` is a value type).
Jay
ahh, yes thanks
sushiBite
A: 
static IEnumerable<Product> FindProductById(this IEnumerable<Product> source, int id) 
{
    return source.FirstOrDefault(product => product.Id = id) ?? source.SelectMany(product => product.Children).FindProductById(id);
}
Jimmy Hoffa