In this installment of my Hacking LINQ series we’ll take a look at providing an IEqualityComparer
for use in a LINQ join
clause.
The Problem
Many of the Standard Query Operators require comparing sequence elements and the default query providers are kind enough to give us overloads that accept a suitable comparer. Among these operators, Join
and GroupJoin
have perhaps the most useful query syntax:
var res = from s in States join a in AreaCodes on s.Abbr equals a.StateAbbr select new { s.Name, a.AreaCode };
While a bit more verbose, I find the intent much easier to read then the method equivalent:
var res = States.Join(AreaCodes, s => s.Abbr, a => a.StateAbbr, (s, a) => new { s.Name, a.AreaCode });
Or maybe I’ve just spent too much time in SQL. Either way, I thought it would be useful to support joins by a comparer.
The Goal
We will use another extension method to specify how the join should be performed:
var res = from s in States join a in AreaCodes.WithComparer(StringComparer.OrdinalIgnoreCase) on s.Abbr equals a.StateAbbr select new { s.Name, a.AreaCode };
We can also support the same syntax for group joins:
var res = from s in States join a in AreaCodes.WithComparer(StringComparer.OrdinalIgnoreCase) on s.Abbr equals a.StateAbbr into j select new { s.Name, Count = j.Count() };
The Hack
As with most LINQ hacks, we’re going to use the result of WithComparer
to call a specialized version of Join
or GroupJoin
, in this case by providing a replacement for the join’s inner sequence:
var res = States.Join(AreaCodes.WithComparer(StringComparer.OrdinalIgnoreCase), s => s.Abbr, a => a.StateAbbr, (s, a) => new { s.Name, a.AreaCode });
Eventually leading to this method call:
var res = States.Join(AreaCodes, s => s.Abbr, a => a.StateAbbr, (s, a) => new { s.Name, a.AreaCode }, StringComparer.OrdinalIgnoreCase);
Since we need both the inner collection we’re extending and the comparer, we can guess our extension method will be implemented something like this:
public static JoinComparerProvider<T, TKey> WithComparer<T, TKey>( this IEnumerable<T> inner, IEqualityComparer<TKey> comparer) { return new JoinComparerProvider<T, TKey>(inner, comparer); }
With a trivial provider implementation:
public sealed class JoinComparerProvider<T, TKey> { internal JoinComparerProvider(IEnumerable<T> inner, IEqualityComparer<TKey> comparer) { Inner = inner; Comparer = comparer; } public IEqualityComparer<TKey> Comparer { get; private set; } public IEnumerable<T> Inner { get; private set; } }
The final piece is our Join
overload:
public static IEnumerable<TResult> Join<TOuter, TInner, TKey, TResult>( this IEnumerable<TOuter> outer, JoinComparerProvider<TInner, TKey> inner, Func<TOuter, TKey> outerKeySelector, Func<TInner, TKey> innerKeySelector, Func<TOuter, TInner, TResult> resultSelector) { return outer.Join(inner.Inner, outerKeySelector, innerKeySelector, resultSelector, inner.Comparer); }
Implementations of GroupJoin
and their IQueryable
counterparts are similarly trivial.