A GroupJoin is a Left-Outer-Join which you ungroup using SelectMany
Example: Schools with one or more Students
- School A has Students 1, 2, 7
- School B has Students 3, 5,
- School C has no Students
- School D has Students 4, 6,
A GroupJoin will just give the above: Schools with zero or more Students
Apparently you want the following result.
School.Id | Student.Id Student.SchoolId
A | 1 A
B | 3 B
A | 2 A
C | (null)
B | 5 B
D | 4 D
D | 6 D
IMHO, I'd prefer the first query (schools with their students), but hey, it's your design.
public static IEnumerable<TResult> LeftOuterJoin<TSource, TInner, TKey, TResult>(
this IEnumerable<TSource> outerCollection,
IEnumerable<TInner> innerCollection,
Func<TSource, TKey> outerKeySelector,
Func<TInner, TKey> innerkeySelector,
Func<TSource, TInner, TResult> resultSelector)
{
return outerCollection.GroupJoin(innnerCollection,
outerElement => outerKeySelector(outerElement),
innerElement => innerKeySelector(innerElement),
(outerElement, innerElementsOfThisOuterElement) => new
{
OuterElement = outerElement,
InnerElements = innerElementsOfThisOuterElement,
})
// result: a sequence of outerElements, each with their zero or more innerElements
// use SelectMany to ungroup:
.SelectMany(group => group.InnerElements, // = parameter collectionSelector
// parameter resultSelector:
(group, innerElement) => resultSelector(group.OuterElement, innerElement));
}
In words:
From every element from innerCollection, calculate an innerKey, using innerKeySelector.
From every element from outerCollection find all innerElements that belong to this outerElement, by calculating the outerKey, using outerKeySelector and fetching all innerElements that have innerKey equal to the outerKey.
The result is a group: a group with OuterElements, each with their zero or more InnerElements.
Ungroup this: from every group, take the group.OuterElement and each of its InnerElements. For every combination (outerElement, innerElement) use ResultSelector to create a TResult
If you use this only once, I wouldn't make a special method for this. Just write GroupJoin, followed by SelectMany:
var result = ...
.GroupJoin(...)
.SelectMany(...)
If you use this quite often, and you plan to create a separate method, consider to make the method more efficient:
public static IEnumerable<TResult> LeftOuterJoin<TSource, TInner, TKey, TResult>(
this IEnumerable<TSource> outerCollection,
IEnumerable<TInner> innerCollection,
Func<TSource, TKey> outerKeySelector,
Func<TInner, TKey> innerkeySelector,
Func<TSource, TInner, TResult> resultSelector)
{
var innerLookup = innerCollection.ToLookup(innerKeySelector);
foreach (var outerElement in outerCollection)
{
var outerKey = outerKeySelector(outerElement);
var innerElementsOfThisOuterElement = innerLookup[outerKey];
// might be empty!
// for every inner, return combinatino of (outer, inner)
// if inner empty: return (outer, null)
var innerEnumerator = innerElementsOfThisOuterElement.GetEnumerator();
if (innerEnumerator.MoveNext())
{
// there is at least one inner
yield return resultSelector(outerElement, innerEnumerator.Current);
// do the rest:
while enumerator.MoveNext())
yield return resultSelector(outerElement, innerEnumerator.Current);
}
else
{
// no inner
yield return resultSelector(outerElement, null);
}
}
}