I like sehe's answer, but it does not use deferred execution (the input sequences are eagerly enumerated by the calls to ToLookup). So after looking at the .NET sources for LINQ-to-objects, I came up with this:
public static class LinqExtensions
{
public static IEnumerable<TResult> FullOuterJoin<TLeft, TRight, TKey, TResult>(
this IEnumerable<TLeft> left,
IEnumerable<TRight> right,
Func<TLeft, TKey> leftKeySelector,
Func<TRight, TKey> rightKeySelector,
Func<TLeft, TRight, TKey, TResult> resultSelector,
IEqualityComparer<TKey> comparator = null,
TLeft defaultLeft = default(TLeft),
TRight defaultRight = default(TRight))
{
if (left == null) throw new ArgumentNullException("left");
if (right == null) throw new ArgumentNullException("right");
if (leftKeySelector == null) throw new ArgumentNullException("leftKeySelector");
if (rightKeySelector == null) throw new ArgumentNullException("rightKeySelector");
if (resultSelector == null) throw new ArgumentNullException("resultSelector");
comparator = comparator ?? EqualityComparer<TKey>.Default;
return FullOuterJoinIterator(left, right, leftKeySelector, rightKeySelector, resultSelector, comparator, defaultLeft, defaultRight);
}
internal static IEnumerable<TResult> FullOuterJoinIterator<TLeft, TRight, TKey, TResult>(
this IEnumerable<TLeft> left,
IEnumerable<TRight> right,
Func<TLeft, TKey> leftKeySelector,
Func<TRight, TKey> rightKeySelector,
Func<TLeft, TRight, TKey, TResult> resultSelector,
IEqualityComparer<TKey> comparator,
TLeft defaultLeft,
TRight defaultRight)
{
var leftLookup = left.ToLookup(leftKeySelector, comparator);
var rightLookup = right.ToLookup(rightKeySelector, comparator);
var keys = leftLookup.Select(g => g.Key).Union(rightLookup.Select(g => g.Key), comparator);
foreach (var key in keys)
foreach (var leftValue in leftLookup[key].DefaultIfEmpty(defaultLeft))
foreach (var rightValue in rightLookup[key].DefaultIfEmpty(defaultRight))
yield return resultSelector(leftValue, rightValue, key);
}
}
This implementation has the following important properties:
These properties are important, because they are what someone new to FullOuterJoin but experienced with LINQ will expect.