BlackWaspTM

This web site uses cookies. By using the site you accept the cookie policy.This message is for compliance with the UK ICO law.

LINQ
.NET 3.5+

A LINQ Operator to Select Multiple Values from Sequences

Language-Integrated Query (LINQ) includes several projection operators but none that allow multiple items to be created from each item in a sequence. This article describes a custom operator that allows several selections to be made per item.

Projecting Multiple Items

LINQ includes several standard query operators that permit you to perform projection of sequences, transforming the items within a sequence by applying a function. The Select operator is possibly the most commonly used. This allows you to transform each item in a collection to produce a new sequence of the same length. SelectMany allows you to flatten a hierarchical collection. For example, you may have a sequence of objects where each item contains a List. Flattening this hierarchy allows you to create a sequence containing all of the combined List items.

If you have a sequence of objects where each item contains more than one item that you wish to extract, and those items are not held in a collection, LINQ does not provide a suitable operator. Let's consider an example. You may have a class that holds two values of the same type. In the sample code below, we have such a class. This represents a couple and holds the names of two people. If you had a sequence of Couple objects, you might want to project this into a sequence of names, with double the number of items in the results as were in the source collection.

public class Couple
{
    public string Person1 { get; set; }
    public string Person2 { get; set; }
}

A naive way of obtaining the desired results would be to project the Couples sequence twice and concatenate the results. You can see this working correctly with a Couples array in the following example code:

Couple[] couples = new Couple[]
{
    new Couple { Person1 = "Bob", Person2 = "Sue" },
    new Couple { Person1 = "Mel", Person2 = "Jim" },
    new Couple { Person1 = "Tim", Person2 = "Jan" }
};

IEnumerable<string> peopleMultiPass =
couples.Select(c => c.Person1).Concat(couples.Select(c => c.Person2));

/* RESULTS

Bob
Mel
Time
Sue
Jim
Jan

*/

Unfortunately, this approach is not suitable for all sequences so should not be encapsulated in a new extension method. In the above example the results are as desired because arrays can be enumerated several times. If the data source can only be read once, the operation would likely fail or return only the results from the Person1 properties.

SelectMulti

To solve the problem we need to create a new extension method that can perform multiple projections whilst only processing the source collection once. We'll create such a method in this section of the article. The method, named "SelectMulti", will be an extension method of IEnumerable<T>, so that it can work with most sequence types. It will accept an array of selector functions, each of which will generate new items for the resultant sequence. By using the params keyword for the selector array, we'll allow any number of selectors to be provided as a simple, comma-separated list of Func delegates.

The declarations for the class to contain the method, and for the method itself, are as follows:

public static class SelectMultiExtensions
{
    public static IEnumerable<TResult> SelectMulti<TSource, TResult>(
        this IEnumerable<TSource> source, params Func<TSource, TResult>[] selectors)
    {
    }
}

Validation

To follow the implied pattern of LINQ's standard query operators, we should validate the values provided to the parameters immediately and we should use deferred execution when generating the results. To do both, we'll split the method into two. The main method will perform the validation and then call the implementation method. The implementation method will perform the projection and return results using the yield keyword. If we were to combine both parts in the main method the validation would be deferred until the first result was requested, which could lead to exceptions being thrown unexpectedly.

We'll perform four validation checks. Firstly, the source and the array of functions must not be null. Next we'll check that the array is not empty. This check is not strictly necessary and could be removed if you wanted to be able to supply a zero-length array and generate a zero-length sequence. The final check verifies that none of the individual selectors are null.

To add the validation and the call to the implementation method, add the following code to the SelectMulti method.

if (source == null) throw new ArgumentNullException("source");
if (selectors == null) throw new ArgumentNullException("selectors");
if (selectors.Length == 0)
    throw new ArgumentException("At least one selector is required");

foreach (Func<TSource, TResult> selector in selectors)
{
    if (selector == null)
        throw new ArgumentException("Null selectors are not permitted");
}

return SelectMultiImpl(source, selectors);
2 September 2012