这是我使用的LeftJoin实现。注意,resultSelector表达式接受2个参数:来自连接两端的一个实例。在我看到的大多数其他实现中,结果选择器只接受一个参数,这是一个具有左/右或外部/内部属性的“连接模型”。我更喜欢这个实现,因为它具有与内置Join方法相同的方法签名。它也适用于IQueryables和EF。
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();
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);
}
}
public static class ExpressionExtensions
{
public static Expression ReplaceParameter(this Expression source, ParameterExpression toReplace, Expression newExpression)
=> new ReplaceParameterExpressionVisitor(toReplace, newExpression).Visit(source);
}
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);
}