Outer Join в LINQ

LINQ — как много было придумано в C# лишь для того чтобы мы могли наслаждаться прелестями Language Integrated Query. А именно:
  • Generics
  • Extension Methods
  • Lamda expressions
  • Expression trees
  • Anonumus types
  • Object initializers
  • Type inferring

И это все чтобы мы могли написать нечто вроде такого:
	var query = 
		from itemA in listA
		join itemB in listB
			on itemA.Key equals itemB.Key
		select new {itemA, itemB};
	

Нельзя не согласиться — впечталяет.

И среди всего этого синтаксического сахара была ложка дегдя которая мне не давала нормально выспаться :)
Это тотальное отсутствие поддержки OUTER JOIN. Но как оказалось деготь с легкостью превращается… превращается… превращается…

… в еще один «синтаксический сахар».

Тем, кто пытался найти решение для LEFT OUTER JOIN в интернете, наверняка знакомо подобное решение:
	var query = 
		from itemA in listA
		join itemB in listB
			on itemA.Key equals itemB.Key into outer
		from itemO in outer.DefaultIfEmpty()
		select new {itemA, itemO};
	

Подобная конструкция явно на порядок запутывает понимание и усложняет и без того простую конструкцию. А это лишь замена INNER JOIN на LEFT OUTER JOIN. Чтобы не продолжать шокировать, пример с FULL OUTER JOIN приводить не буду.

Казалось бы как было бы просто если бы могли написать вот так:
	var query = 
		from itemA in listA
		left join itemB in listB
			on itemA.Key equals itemB.Key
		select new {itemA, itemB};
	

или так
	var query = 
		from itemA in listA
		full join itemB in listB
			on itemA.Key equals itemB.Key
		select new {itemA, itemB};
	

Ан нет. Авторы C# нам такого удовольствия не предоставили. Ну не беда. Все же они позволят нам это сделать самостоятельно, хоть и не таким красивым способом.

Начнем с того, что если кто то вам скажет, что LINQ и интерфейс System.Collections.Generic.IEnumerable имеют что то общее и не могут существовать по отдельности можете смело рассмеятся в лицо…

Конструкция
	var query = 
		from itemA in listA
		join itemB in listB
			on itemA.Key equals itemB.Key
		select new {itemA, itemB};

просто напросто транслируется компилятором в следующую последовательность символов:
	var query = listA.Join(listB, itemA => itemA.Key, itemB => itemB.Key, (itemA, itemB) => new {itemA, itemB});

и абсолютно не важно какого типа переменные listA и listB. Предположим что listA переменная типа TypeA, а пермеменная itemB типа TypeB. Так вот, если TypeA и TypeB содеражат свойство или поле с именем Key, TypeA содержит метод Join() с 4мя аргументами. Этот LINQ запрос свободно откомпилируется.

При использовании в LINQ переменных которые реализуют стандартный интерфейс IEnumerable используется метод расширения
public class System.Linq.Enumerable
{
		public static IEnumerable<TResult> Join<TOuter, TInner, TKey, TResult>(this IEnumerable<TOuter> outer, IEnumerable<TInner> inner, Func<TOuter, TKey> outerKeySelector, Func<TInner, TKey> innerKeySelector, Func<TOuter, TInner, TResult> resultSelector) {...}
}

Собственно этот метод и производит хорошо известный нам INNER JOIN. И вот теперь начинается уличная магия. Чтобы реализовать нам LEFT / RIGHT / FULL OUTER JOIN (или JOIN какой вашей душе будет угоден) необходимо подменить вызов стандартного метода на реализованый нами. Чтобы это сделать
надо переменную listA преобразовать каким то образом в тип который мы можем контролировать.

Реализовав два следующих класса:
public class JoinedEnumerable<T> : IEnumerable<T>
{
	public readonly IEnumerable<T> Source;
	public bool IsOuter;

	public JoinedEnumerable(IEnumerable<T> source) { Source = source; }

	IEnumerator<T> IEnumerable<T>.GetEnumerator() { return Source.GetEnumerator(); }
	IEnumerator IEnumerable.GetEnumerator() { return Source.GetEnumerator(); }
}

public static class JoinedEnumerable
{
	public static JoinedEnumerable<TElement> Inner<TElement>(this IEnumerable<TElement> source)
	{
		return Wrap(source, false);
	}

	public static JoinedEnumerable<TElement> Outer<TElement>(this IEnumerable<TElement> source)
	{
		return Wrap(source, true);
	}

	public static JoinedEnumerable<TElement> Wrap(IEnumerable<TElement> source, bool isOuter)
	{
		JoinedEnumerable<TElement> joinedSource 
			= source as JoinedEnumerable<TElement> ?? 
				new JoinedEnumerable<TElement>(source);
		joinedSource.IsOuter = isOuter;
		return joinedSource;
	}
}

мы запросто пишем следующий LINQ запрос
	var query = 
		from itemA in listA.Outer()
		join itemB in listB
			on itemA.Key equals itemB.Key
		select new {itemA, itemB};


и теперь реализовав метод расширения Join для класса JoinedEnumerable нужным нам образом получаем все что нам нужно.

А вот собственно и методы расширения:
public static IEnumerable<TResult> Join<TOuter, TInner, TKey, TResult>(this JoinedEnumerable<TOuter> outer, IEnumerable<TInner> inner, Func<TOuter, TKey> outerKeySelector, Func<TInner, TKey> innerKeySelector, Func<TOuter, TInner, TResult> resultSelector, IEqualityComparer<TKey> comparer = null)
{
	if (outer == null) throw new ArgumentNullException("outer");
	if (inner == null) throw new ArgumentNullException("inner");
	if (outerKeySelector == null) throw new ArgumentNullException("outerKeySelector");
	if (innerKeySelector == null) throw new ArgumentNullException("innerKeySelector");
	if (resultSelector == null) throw new ArgumentNullException("resultSelector");

	bool leftOuter = outer.IsOuter;
	bool rightOuter = (inner is JoinedEnumerable<TInner>) && ((JoinedEnumerable<TInner>)inner).IsOuter;

	if (leftOuter && rightOuter)
		return FullOuterJoin(outer, inner, outerKeySelector, innerKeySelector, resultSelector, comparer);

	if (leftOuter)
		return LeftOuterJoin(outer, inner, outerKeySelector, innerKeySelector, resultSelector, comparer);

	if (rightOuter)
		return RightOuterJoin(outer, inner, outerKeySelector, innerKeySelector, resultSelector, comparer);

	return Enumerable.Join(outer, inner, outerKeySelector, innerKeySelector, resultSelector, comparer);
}

public static IEnumerable<TResult> LeftOuterJoin<TOuter, TInner, TKey, TResult>(this IEnumerable<TOuter> outer, IEnumerable<TInner> inner, Func<TOuter, TKey> outerKeySelector, Func<TInner, TKey> innerKeySelector, Func<TOuter, TInner, TResult> resultSelector, IEqualityComparer<TKey> comparer = null)
{
	var innerLookup = inner.ToLookup(innerKeySelector, comparer);

	foreach (var outerItem in outer)
		foreach (var innerItem in innerLookup[outerKeySelector(outerItem)].DefaultIfEmpty())
			yield return resultSelector(outerItem, innerItem);
}

public static IEnumerable<TResult> RightOuterJoin<TOuter, TInner, TKey, TResult>(this IEnumerable<TOuter> outer, IEnumerable<TInner> inner, Func<TOuter, TKey> outerKeySelector, Func<TInner, TKey> innerKeySelector, Func<TOuter, TInner, TResult> resultSelector, IEqualityComparer<TKey> comparer = null)
{
	var outerLookup = outer.ToLookup(outerKeySelector, comparer);

	foreach (var innerItem in inner)
		foreach (var outerItem in outerLookup[innerKeySelector(innerItem)].DefaultIfEmpty())
			yield return resultSelector(outerItem, innerItem);
}

public static IEnumerable<TResult> FullOuterJoin<TOuter, TInner, TKey, TResult>(this IEnumerable<TOuter> outer, IEnumerable<TInner> inner, Func<TOuter, TKey> outerKeySelector, Func<TInner, TKey> innerKeySelector, Func<TOuter, TInner, TResult> resultSelector, IEqualityComparer<TKey> comparer = null)
{
	var outerLookup = outer.ToLookup(outerKeySelector, comparer);
	var innerLookup = inner.ToLookup(innerKeySelector, comparer);

	foreach (var innerGrouping in innerLookup)
		if (!outerLookup.Contains(innerGrouping.Key))
			foreach (TInner innerItem in innerGrouping)
				yield return resultSelector(default(TOuter), innerItem);

	foreach (var outerGrouping in outerLookup)
		foreach (var innerItem in innerLookup[outerGrouping.Key].DefaultIfEmpty())
			foreach (var outerItem in outerGrouping)
				yield return resultSelector(outerItem, innerItem);
}


Вуаля…

Красивый LEFT OUTER JOIN:
	var query = 
		from itemA in listA.Outer()
		join itemB in listB
			on itemA.Key equals itemB.Key
		select new {itemA, itemB};


Красивый RIGHT OUTER JOIN:
	var query = 
		from itemA in listA.Inner()
		join itemB in listB.Outer()
			on itemA.Key equals itemB.Key
		select new {itemA, itemB};


Красивый FULL OUTER JOIN:
	var query = 
		from itemA in listA.Outer()
		join itemB in listB.Outer()
			on itemA.Key equals itemB.Key
		select new {itemA, itemB};


Теперь при желании вы можете использовать и свой подход — так как поле для фантазии здесь громаднейшее. У меня в загашнике есть еще несколько инетерсных решений для реализации вкусностей. Будет время обязательно поделюсь ими.

Спасибо за внимание.
Да прибудет с вами СИЛА!
Tags:
linq, .net, c#, outer, join

You can't comment this post because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author's username will be hidden by an alias.