Die Standard-Extensions für IEnumerable
/IQueryable
enthalten ja bekanntlich kein Äquivalent für ein left outer join. Dieses kann jedoch leicht mit einem GroupJoin
und DefaultIfEmpty
nachgebildet werden(vgl. Join Operators):
public static class EnumerableExtensions
{
public static IEnumerable<TResult> LeftOuterJoin<TOuter, TInner, TKey, TResult>(this IEnumerable<TOuter> outer, IQueryable<TInner> inner, Func<TOuter, TKey> outerKeySelector, Func<TInner, TKey> innerKeySelector, Func<TOuter, TInner, TResult> resultSelector)
{
return outer
.GroupJoin(inner, outerKeySelector, innerKeySelector, (outerItem, innerGroup) => innerGroup.DefaultIfEmpty().Select(innerGroupItem => resultSelector(outerItem, innerGroupItem)))
.SelectMany(value => value);
}
}
Das knifflige ist nun aber, das ganze als Erweiterungsmethode für IQueryable
bereitzustellen. Damit es z.B. der LINQ2SQL-Provider auch korrekt verarbeiten kann, muss nämlich komplett mit Expressions gearbeitet werden:
public static class QueryableExtensions
{
public static IQueryable<TResult> LeftOuterJoin<TOuter, TInner, TKey, TResult>(this IQueryable<TOuter> outer, IQueryable<TInner> inner, Expression<Func<TOuter, TKey>> outerKeySelector, Expression<Func<TInner, TKey>> innerKeySelector, Expression<Func<TOuter, TInner, TResult>> resultSelector)
{
ParameterExpression outerItemParameterExpression = resultSelector.Parameters[0]; // Parameter(typeof(TOuter), "outerItem");
ParameterExpression innerGroupParameterExpression = Expression.Parameter(typeof(IEnumerable<TInner>), "innerGroup");
ParameterExpression innerGroupItemParameterExpression = resultSelector.Parameters[1]; // Parameter(typeof(TInner), "innerGroupItem");
MethodInfo enumerableSelectMethodInfo = typeof(Enumerable).GetGenericMethod("Select", typeof(IEnumerable<>), typeof(Func<,>)).MakeGenericMethod(typeof(TInner), typeof(TResult));
MethodInfo enumerableDefaultIfEmptyMethodInfo = typeof(Enumerable).GetGenericMethod("DefaultIfEmpty", typeof(IEnumerable<>)).MakeGenericMethod(typeof(TInner));
var callResultSelectorExpression = Expression.Invoke(resultSelector, outerItemParameterExpression, innerGroupItemParameterExpression);
var callDefaultIfEmtptyExpression = Expression.Call(null, enumerableDefaultIfEmptyMethodInfo, innerGroupParameterExpression);
var callSelectExpression = Expression.Call(null, enumerableSelectMethodInfo, callDefaultIfEmtptyExpression, Expression.Lambda<Func<TInner, TResult>>(callResultSelectorExpression, innerGroupItemParameterExpression));
var selectManyExpression = Expression.Lambda<Func<TOuter, IEnumerable<TInner>, IEnumerable<TResult>>>(callSelectExpression, outerItemParameterExpression, innerGroupParameterExpression);
return outer
.GroupJoin(inner, outerKeySelector, innerKeySelector, selectManyExpression)
.SelectMany(value => value);
}
}
public static class TypeExtensions
{
public static MethodInfo GetGenericMethod(this Type type, string name, params Type[] parameterTypes)
{
return type.GetMethods()
.Where(method => method.Name == name)
.Where(method => parameterTypes.SequenceEqual(method.GetParameters().Select(p =>(p.ParameterType.IsGenericType) ? p.ParameterType.GetGenericTypeDefinition() : p.ParameterType)))
.SingleOrDefault();
}
}
Schlagwörter: Left Outer Join, Extension Method, LINQ, IEnumerable, IQueryable, Expressions
(Der Anhang enthält zusätzlich noch XML-Kommentare)