C# LINQ Extension Library

Words
433
Reading
2 min
Listen
Play
9y

I have a Git Hub and associated Nuget for a set of LINQ extensions that you might find helpful in your day to day life with LINQ.

Extras for IEnumberable

  • ToEnumerable = Wrap a value in an IEnumerable
  • Concat = Add a value to the end of an IEnumberable
  • MinOrElse = Get the Min of an IEnumberable or return a default when empty
  • MaxOrElse = Get the Max of an IEnumberable or return a default when empty
  • MinBy = Get the element with the Min value identified by a selector
  • MinByOrElse = Get the element with the Min value identified by a selector or run element factory when empty
  • MaxBy = Get the element with the Max value identified by a selector
  • MaxByOrElse = Get the element with the Max value identified by a selector or run element factory when empty
  • DistinctBy - Get the distinct elements using a selected key
  • Each = Foreach but lambda based

Extras for IEnumerator and IEnumerator<T>

Functionality

  • ToArray = Read the IEnumerator and buffer into an array
  • ToEnumerable = Convert the IEnumerator into and IEnumberable
  • Select = Run a selector over the IEnumerator and return as an IEnumberable

All functions are lazy, as per the rest of LINQ, where applicable. This is done via use of "yield return".

Code breakdown

First is something that takes any type of value and wraps it in an enumerable. Might not appear to helpful but you will see a handy building block.

public static IEnumerable<T> ToEnumerable<T>(this T value)
{
    yield return value;
}

Next a version of concat that appends a single value onto the end of a list, which makes use of the above.

public static IEnumerable<T> Concat<T>(
    this IEnumerable<T> head, T tail)
    => head.Concat(tail.ToEnumerable());

LINQ Min() and Max() have always been a pain, you really have to be sure the list is not empty or it throws. These solve that and default when you have an empty list. They are buffered and hence force evaluation.

public static T MinOrElse<T>(
    this IEnumerable<T> source, T orElseValue)
{
    var buffer = source.ToArray();
    return buffer.Length > 0 ? buffer.Min() : orElseValue;
}

public static T MaxOrElse<T>(
    this IEnumerable<T> source, T orElseValue)
{
    var buffer = source.ToArray();
    return buffer.Length > 0 ? buffer.Max() : orElseValue;
}

public static T MinBy<T, TKey>(
    this IEnumerable<T> source, Func<T, TKey> selector)
    => source.CompareBy(selector, x => x < 0);

public static T MinByOrElse<T, TKey>(
    this IEnumerable<T> source, 
    Func<T, TKey> selector, 
    Func<T> orElseFactory)
{
    var buffer = source.ToArray();
    return buffer.Length > 0 ? buffer.MinBy(selector) : orElseFactory();
}

public static T MaxBy<T, TKey>(
    this IEnumerable<T> source, Func<T, TKey> selector)
    => source.CompareBy(selector, x => x > 0);

public static T MaxByOrElse<T, TKey>(
    this IEnumerable<T> source, 
    Func<T, TKey> selector, 
    Func<T> orElseFactory)
{
    var buffer = source.ToArray();
    return buffer.Length > 0 
        ? buffer.MaxBy(selector) 
        : orElseFactory();
}

The following is the work horse for MinBy and MaxBy which pick an element by a value within it. It walks the list locating the applicable element and returns it.

private static T Identity<T>(T value) => value;


private static T CompareBy<T, TKey>(
    this IEnumerable<T> source, 
    Func<T, TKey> selector, 
    Func<int, bool> isBetter)
{
    var comparer = Comparer<TKey>.Default;

    using (var enumerator = source.GetEnumerator())
    {
        if (!enumerator.MoveNext())
        {
            throw new InvalidOperationException("Sequence has no elements");
        }

        var best = enumerator.Current;
        var bestKey = selector(best);

        while (enumerator.MoveNext())
        {
            var candidate = enumerator.Current;
            var candidateKey = selector(candidate);

            if (!isBetter(comparer.Compare(candidateKey, bestKey)))
            {
                continue;
            }

            best = candidate;
            bestKey = candidateKey;
        }

        return best;
    }
}

I always thought allowing distinct using a mapper to select the element to distinct by was handy. Hence DistinctBy.

public static IEnumerable<T> DistinctBy<T, TKey>(
    this IEnumerable<T> source, Func<T, TKey> keySelector)
    => source.GroupBy(keySelector).Select(x => x.First());

ForEach is the same as found on arrays but without the need to convert to an array first. I use this a lot.

public static void ForEach<T>(
    this IEnumerable<T> source, Action<T> action)
{
    foreach (var item in source)
    {
        action(item);
    }
}

The Enumerator set provides a ways to lift an Enumerator to IEnumerable to allow their use within LINQ. They should all be obvious.

public static T[] ToArray<T>(this IEnumerator source)
    => source.ToEnumerable<T>().ToArray();

public static T[] ToArray<T>(this IEnumerator<T> source)
    => source.ToEnumerable().ToArray();

public static IEnumerable<T> ToEnumerable<T>(this IEnumerator source)
    => source.Select<T, T>(Identity);

public static IEnumerable<T> ToEnumerable<T>(this IEnumerator<T> source)
    => source.Select(Identity);

public static IEnumerable<TResult> Select<T, TResult>(
    this IEnumerator source, Func<T, TResult> selector)
{
    while (source.MoveNext())
    {
        yield return selector((T)source.Current);
    }
}

public static IEnumerable<TResult> Select<T, TResult>(
    this IEnumerator<T> source, Func<T, TResult> selector)
{
    while (source.MoveNext())
    {
        yield return selector(source.Current);
    }
}

If you have ideas for extra functions to add let me know and I will add them if they make sense :)

Hope you enjoy the library and happy coding

Woz

C# LINQ Extension Library | Ecency