C# LINQ SelectMany with Default
Asked Answered
M

4

5

I am looking for an elegant solution to aggregate a child collection in a collection into one large collection. My issue is when certain child collections could be null.

EG:

var aggregatedChildCollection = parentCollection.SelectMany(x=> x.ChildCollection);

This throws an exception should any of the child collection objects be null. Some alternatives are:

// option 1
var aggregatedChildCollection = parentCollection
    .Where(x=>x.ChildCollection != null)
    .SelectMany(x => x.ChildCollection);

// option 2
var aggregatedChildCollection = parentCollection
    .SelectMany(x => x.ChildCollection ?? new TypeOfChildCollection[0]);

Both would work but I am doing a certain operation on quite a few child collections on the parent, and it is becoming a bit unweilding.

What I would like is to create an extension method that checks if the collection is null and if so does what option 2 does - adds an empty array. But my understanding of Func is not to a point where I know how to code this extension method. I do know that the syntax I would like is like this:

var aggregatedChildCollection = parentCollection.SelectManyIgnoringNull(x => x.ChildCollection);

Is there a simple extension method that would accomplish this?

Missionary answered 5/9, 2016 at 23:55 Comment(0)
V
4

You can use a custom extension method:

public static IEnumerable<TResult> SelectManyIgnoringNull<TSource, TResult>(
    this IEnumerable<TSource> source, 
    Func<TSource, IEnumerable<TResult>> selector)
{
    return source.Select(selector)
        .Where(e => e != null)
        .SelectMany(e => e);
}

And use like this:

var aggregatedChildCollection = parentCollection
    .SelectManyIgnoringNull(x => x.ChildCollection);
Vivian answered 5/9, 2016 at 23:59 Comment(2)
A great solution - in my case I have combined it with Cory Nelson's answer that uses Enumerable.Empty<T>() instead of cutting out the null collections so that I will always have at the very least an empty collection.Missionary
That also works, but be mindful that Cory linked to code that has a licence attached, so you will need to provide attribution.Vivian
N
1

If ParentCollection is your own class you should also be able to a default constructor to the class such as:

public ParentCollection{
    public ParentCollection() {
        ChildCollection = new List<ChildCollection>();
    }
}

That should prevent the null ref exception and give you an empty list if it doesn't have anything in it. At least this works with EF models.

Nineteenth answered 6/9, 2016 at 0:6 Comment(1)
No, unfortunately I cannot guarantee that any of the child collections will be populated. It's not that the class is invalid if a child collection is null, I just want to group all the child collections into one collection and ignore any nulls by treating them as an empty collection.Missionary
H
1

Your "option 2" is what I would do, with a minor tweak: use Enumerable.Empty() instead of creating an empty array to reduce the amount of new objects you're creating.

I use a trivial extension method Touch() -- named after the *nix utility -- to keep the LINQ syntax flow and reduce typing:

public static IEnumerable<T> Touch<T>(this IEnumerable<T> items) =>
    items ?? Enumerable.Empty<T>();

And would use it as:

var aggregatedChildCollection = parentCollection
    .SelectMany(x => x.ChildCollection.Touch());
Holusbolus answered 6/9, 2016 at 0:19 Comment(0)
V
1

You can avoid the overhead from the LINQ extension by using the SelectMany Reference Source

public static IEnumerable<TResult> SelectManyNotNull<TSource, TResult>(
         this IEnumerable<TSource> source, Func<TSource, IEnumerable<TResult>> selector) 
{
    foreach (TSource element in source)
    {
        var subElements = selector(element);
        if (subElements != null)
            foreach (TResult subElement in subElements )
                yield return subElement;
    }
}
Vinyl answered 6/9, 2016 at 1:14 Comment(1)
Thanks for that - a good resource and interesting to see how they do it.Missionary

© 2022 - 2024 — McMap. All rights reserved.