Hacking LINQ Expressions: Join With Comparer

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.

About Keith Dahlby

I'm a .NET developer, Git enthusiast and language geek from Cedar Rapids, IA. I work as a software guru at J&P Cycles and studied Human-Computer Interaction at Iowa State University.
This entry was posted in Hacking LINQ, LINQ. Bookmark the permalink. Follow any comments here with the RSS feed for this post.