有没有一种方法可以简化 ef core 中多个字段的许多连接?

Is there a way to simplify many joins over multiple fields in ef core?

我有很多非常相似的函数,可以像这样连接多个字段:

public IQueryable<TableJoinResult<Ta,Tb>> JoinFittingRows(IQueryable<Ta> theQueryable, IQueryable<Tb> theOtherQueryable)
    {
        return theQueryable
            .Join(theOtherQueryable, t => new
            {
                f1 = t.f1,
                f3 = t.f3,
                f4 = t.f4,
                f6 = t.f6,
                f7 = t.f7,
                f8 = t.f8,
                f9 = t.f9,
                f10 = t.f10,
               
            }, p => new
            {
                f1 = p.f1,
                f3 = p.f3,
                f4 = p.f4,
                f6 = p.f6,
                f7 = p.f7,
                f8 = p.f8,
                f9 = p.f9,
                f10 = p.f10,
            }, (t, p) => new TableJoinResult<Ta,Tb> {ta= t, tb= p});
    }

它们看起来都很相似,但必须相同的字段会发生变化。例如,我不需要 f2 相等。现在我有这个代码片段的几十个版本,其中的字段每次都不同。有没有办法简化或概括这个?例如取一个参数来获取必须相等的字段?

public IQueryable<TableJoinResult<Ta,Tb>> JoinFittingRows(IQueryable<Ta> theQueryable, IQueryable<Tb> theOtherQueryable, SomeType fieldsThatShouldBeEqual)

这是一个棘手的部分,因为这种语法需要某种通用的可变元组。所以我选择了最简单的方法来创建连接,它基于 SelectMany 函数。

用法简单

var joined = query.JoinFittingRows(otherQuery, t => t.f1, t => t.f2, t => t.f5...);

实现

public static class QueryableExtensions
{
    public class JoinResult<TOuter, TInner>
    {
        public TOuter Outer { get; set; } = default!;
        public TInner Inner { get; set; } = default!;
    }

    public static IQueryable<JoinResult<TOuter, TInner>> JoinFittingRows<TOuter, TInner>(this IQueryable<TOuter> outer, IQueryable<TInner> inner, params Expression<Func<TOuter, object>>[] properties)
    {
        Expression<Func<TOuter, TInner, JoinResult<TOuter, TInner>>> resultPattern = (o, i) => new JoinResult<TOuter, TInner> {Outer = o, Inner = i};

        var outerParam = resultPattern.Parameters[0];
        var innerParam = resultPattern.Parameters[1];

        Expression predicate = null;

        foreach (var property in properties)
        {
            var outerPropExpr = (MemberExpression)ExpressionReplacer.GetBody(property, outerParam).Unwrap();
            var innerProp = typeof(TInner).GetProperty(outerPropExpr.Member.Name);
            if (innerProp == null)
                throw new InvalidOperationException(
                    $"Property '{outerPropExpr.Member.Name}' not found in class '{typeof(TInner).Name}'");

            var innerPropExpr = (Expression)Expression.MakeMemberAccess(innerParam, innerProp);
            if (innerPropExpr.Type != outerPropExpr.Type)
                innerPropExpr = Expression.Convert(innerPropExpr, outerPropExpr.Type);

            var equality = Expression.Equal(outerPropExpr, innerPropExpr);
            if (predicate == null)
                predicate = equality;
            else
                predicate = Expression.AndAlso(predicate, equality);
        }

        var predicateLambda = Expression.Lambda(predicate, innerParam);

        var detailBody = Expression.Convert(
            Expression.Call(typeof(Queryable), "Where", new[] {typeof(TInner)},
                inner.Expression,
                predicateLambda),
            typeof(IEnumerable<TInner>));

        var joinCall = Expression.Call(typeof(Queryable), "SelectMany", new []{typeof(TOuter), typeof(TInner), typeof(JoinResult<TOuter, TInner>) },
            outer.Expression,
            Expression.Lambda(detailBody, outerParam),
            resultPattern);

        return outer.Provider.CreateQuery<JoinResult<TOuter, TInner>>(joinCall);
    }


    public static Expression? Unwrap(this Expression? ex)
    {
        if (ex == null)
            return null;

        switch (ex.NodeType)
        {
            case ExpressionType.Quote:
            case ExpressionType.ConvertChecked:
            case ExpressionType.Convert:
                return ((UnaryExpression)ex).Operand.Unwrap();
        }

        return ex;
    }

    class ExpressionReplacer : ExpressionVisitor
    {
        readonly IDictionary<Expression, Expression> _replaceMap;

        public ExpressionReplacer(IDictionary<Expression, Expression> replaceMap)
        {
            _replaceMap = replaceMap ?? throw new ArgumentNullException(nameof(replaceMap));
        }

        public override Expression Visit(Expression exp)
        {
            if (exp != null && _replaceMap.TryGetValue(exp, out var replacement))
                return replacement;
            return base.Visit(exp);
        }

        public static Expression Replace(Expression expr, Expression toReplace, Expression toExpr)
        {
            return new ExpressionReplacer(new Dictionary<Expression, Expression> { { toReplace, toExpr } }).Visit(expr);
        }

        public static Expression Replace(Expression expr, IDictionary<Expression, Expression> replaceMap)
        {
            return new ExpressionReplacer(replaceMap).Visit(expr);
        }

        public static Expression GetBody(LambdaExpression lambda, params Expression[] toReplace)
        {
            if (lambda.Parameters.Count != toReplace.Length)
                throw new InvalidOperationException();

            return new ExpressionReplacer(Enumerable.Zip(lambda.Parameters, toReplace, (f, s) => Tuple.Create(f, s))
                .ToDictionary(e => (Expression)e.Item1, e => e.Item2)).Visit(lambda.Body);
        }
    }
}

通过添加 DefaultIfEmptyWhere 的调用,可以轻松地将此实现扩展到 LEFT JOIN