The prescribed way to do a left join using LINQ is to to a GroupJoin followed by a SelectMany. I prefer to abstract this logic into a LeftJoin extension method to make the code much more readable.
Here is the LeftJoin implementation that I use. The two major benefits of this implementation are:
It specifically works with IQueryables instead of materialized lists, which means the query can be translated into SQL.
The final parameter is an Expression<Func<TOuter,TInner,TResult>> instead of an Expression<Func<JoinResult<TOuter,Tinner>,TResult>> (or something similar). This means the LeftJoin method is used identically to the Join method:
var results = DbContext.Categories
.LeftJoin(
DbContext.Products,
c => c.Id,
p => p.CategoryId,
(c, p) => new { Category = c, ProductName = p == null ? "(No Products)" : p.ProductName })
.ToList();
var results = DbContext.Categories
.Join(
DbContext.Products,
c => c.Id,
p => p.CategoryId,
(c, p) => new { Category = c, ProductName = p.ProductName })
.ToList();
Here's how:
IQueryable Extensions:
public static class QueryableExtensions
{
public static IQueryable<TResult> LeftJoin<TOuter, TInner, TKey, TResult>(this IQueryable<TOuter> outer, IEnumerable<TInner> inner, Expression<Func<TOuter, TKey>> outerKeySelector, Expression<Func<TInner, TKey>> innerKeySelector, Expression<Func<TOuter, TInner, TResult>> resultSelector)
{
var query = outer
.GroupJoin(inner, outerKeySelector, innerKeySelector, (o, i) => new { o, i })
.SelectMany(o => o.i.DefaultIfEmpty(), (x, i) => new { x.o, i });
return ApplySelector(query, x => x.o, x => x.i, resultSelector);
}
private static IQueryable<TResult> ApplySelector<TSource, TOuter, TInner, TResult>(
IQueryable<TSource> source,
Expression<Func<TSource, TOuter>> outerProperty,
Expression<Func<TSource, TInner>> innerProperty,
Expression<Func<TOuter, TInner, TResult>> resultSelector)
{
var p = Expression.Parameter(typeof(TSource), $"param_{Guid.NewGuid()}".Replace("-", string.Empty));
Expression body = resultSelector?.Body
.ReplaceParameter(resultSelector.Parameters[0], outerProperty.Body.ReplaceParameter(outerProperty.Parameters[0], p))
.ReplaceParameter(resultSelector.Parameters[1], innerProperty.Body.ReplaceParameter(innerProperty.Parameters[0], p));
var selector = Expression.Lambda<Func<TSource, TResult>>(body, p);
return source.Select(selector);
}
}
Expression Extensions:
public static class ExpressionExtensions
{
public static Expression ReplaceParameter(this Expression source, ParameterExpression toReplace, Expression newExpression)
=> new ReplaceParameterExpressionVisitor(toReplace, newExpression).Visit(source);
}
Expression Visitors:
public class ReplaceParameterExpressionVisitor : ExpressionVisitor
{
public ReplaceParameterExpressionVisitor(ParameterExpression toReplace, Expression replacement)
{
this.ToReplace = toReplace;
this.Replacement = replacement;
}
public ParameterExpression ToReplace { get; }
public Expression Replacement { get; }
protected override Expression VisitParameter(ParameterExpression node)
=> (node == ToReplace) ? Replacement : base.VisitParameter(node);
}