[Из песочницы] Outer Join в LINQ

?v=1

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 Join(this IEnumerable outer, IEnumerable inner, Func outerKeySelector, Func innerKeySelector, Func resultSelector) {...}
}


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

Реализовав два следующих класса:

public class JoinedEnumerable : IEnumerable
{
	public readonly IEnumerable Source;
	public bool IsOuter;

	public JoinedEnumerable(IEnumerable source) { Source = source; }

	IEnumerator IEnumerable.GetEnumerator() { return Source.GetEnumerator(); }
	IEnumerator IEnumerable.GetEnumerator() { return Source.GetEnumerator(); }
}

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

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

	public static JoinedEnumerable Wrap(IEnumerable source, bool isOuter)
	{
		JoinedEnumerable joinedSource 
			= source as JoinedEnumerable ?? 
				new JoinedEnumerable(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 Join(this JoinedEnumerable outer, IEnumerable inner, Func outerKeySelector, Func innerKeySelector, Func resultSelector, IEqualityComparer 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) && ((JoinedEnumerable)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 LeftOuterJoin(this IEnumerable outer, IEnumerable inner, Func outerKeySelector, Func innerKeySelector, Func resultSelector, IEqualityComparer 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 RightOuterJoin(this IEnumerable outer, IEnumerable inner, Func outerKeySelector, Func innerKeySelector, Func resultSelector, IEqualityComparer 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 FullOuterJoin(this IEnumerable outer, IEnumerable inner, Func outerKeySelector, Func innerKeySelector, Func resultSelector, IEqualityComparer 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};

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

Спасибо за внимание.
Да пребудет с вами СИЛА!

© Habrahabr.ru