以 LINQ to Entities 安全方式将 lambda Expression<Func<TOuter, TInner, TResult>> 修改为 Expression<Func<a', TResult>

Modifying lambda Expression<Func<TOuter, TInner, TResult>> to Expression<Func<a', TResult> in an LINQ to Entities safe way

底部是左连接实现的示例。它适用于普通列表和数组,但不适用于 LINQ to entities,因为我使用 Expression.Invoke().

我想要实现的是 modifying/wrapping resultSelector 输入接受匿名 class 'a 的单个实例(由 leftJoin 可查询)而不是两个单独的参数。

检查 resultSelector 看来我想创建它的一个修改版本,它有一个 'a 类型的参数和从属性 Outer 和 [=20 中提取的两个参数=] 在 'a.

我该如何着手进行此修改?如何更改 Arguments?

[TestClass]
public class LeftJoinTests
{
    public class Outer
    {
        public int Key { get; }
        public int? ForeignKey { get; }

        public Outer(int key, int? foreignKey)
        {
            Key = key;
            ForeignKey = foreignKey;
        }
    }

    public class Inner
    {
        public int Key { get; }
        public string Data { get; }

        public Inner(int key, string data)
        {
            Key = key;
            Data = data;
        }
    }

    [TestMethod]
    public void LeftJoinTest()
    {
        var outers = new []
        { 
            new Outer(1, 1),
            new Outer(2, 2),
            new Outer(3, 3),
            new Outer(4, null),
        };

        var inners = new []
        { 
            new Inner(5, "5"),
            new Inner(2, "2"),
            new Inner(1, "1")
        };

        var leftJoin = LeftJoin(outers.AsQueryable(), inners.AsQueryable(), o => o.ForeignKey, i => i.Key, (oooo, iiii) => new { Outer = oooo, Inner = iiii }).ToArray();

        Assert.AreEqual(4, leftJoin.Length);

        Assert.AreSame(outers[0], leftJoin[0].Outer);
        Assert.AreSame(outers[1], leftJoin[1].Outer);
        Assert.AreSame(outers[2], leftJoin[2].Outer);
        Assert.AreSame(outers[3], leftJoin[3].Outer);

        Assert.AreSame(inners[2], leftJoin[0].Inner);
        Assert.AreSame(inners[1], leftJoin[1].Inner);
        Assert.IsNull(leftJoin[2].Inner);
        Assert.IsNull(leftJoin[3].Inner);
    }

    public IQueryable<TResult> LeftJoin<TOuter, TInner, TKey, TResult>(
        IQueryable<TOuter> outer,
        IQueryable<TInner> inner,
        Expression<Func<TOuter, TKey>> outerKeySelector,
        Expression<Func<TInner, TKey>> innerKeySelector,
        Expression<Func<TOuter, TInner, TResult>> resultSelector)
    {
        var leftJoin = outer.GroupJoin(
            inner,
            outerKeySelector,
            innerKeySelector,
            (o, i) => new 
            {
                Outer = o,
                Inners = i
            }).SelectMany(
                oi => oi.Inners.DefaultIfEmpty(),
                (oi, i) => new 
                {
                    oi.Outer,
                    Inner = i
                }
            );

        // Break the anonymous type of the left join into the two parameters needed for the resultSelector
        var anonymousType = leftJoin.GetType().GetGenericArguments()[0];
        var parameter = Expression.Parameter(anonymousType, "oi");

        var outerProperty = Expression.Property(parameter, "Outer");
        var outerLambda = Expression.Lambda(outerProperty, parameter);

        var innerProperty = Expression.Property(parameter, "Inner");
        var innerLambda = Expression.Lambda(innerProperty, parameter);

        var wrapper = Expression.Lambda(Expression.Invoke(resultSelector, new Expression[] {
            Expression.Invoke(outerLambda, parameter),
            Expression.Invoke(innerLambda, parameter)
        }), parameter);

        Expression<Func<TAnonymous, TResult>> Cast<TAnonymous>(Expression expression, IQueryable<TAnonymous> queryable)
        {
            return expression as Expression<Func<TAnonymous, TResult>>;
        }

        var typeSafeWrapper = Cast(wrapper, leftJoin);

        return leftJoin.Select(typeSafeWrapper);
    }
}

使用 ExpressionVisitor,您可以在仅替换参数时替换 Expression.Invoke,实际上用新表达式替换出现的参数。

这是 Replace 方法 - 它用另一个表达式替换(参考)相等表达式。

public static class ExpressionExt {
    /// <summary>
    /// Replaces an Expression (reference Equals) with another Expression
    /// </summary>
    /// <param name="orig">The original Expression.</param>
    /// <param name="from">The from Expression.</param>
    /// <param name="to">The to Expression.</param>
    /// <returns>Expression with all occurrences of from replaced with to</returns>
    public static Expression Replace(this Expression orig, Expression from, Expression to) => new ReplaceVisitor(from, to).Visit(orig);    
}

/// <summary>
/// ExpressionVisitor to replace an Expression (that is Equals) with another Expression.
/// </summary>
public class ReplaceVisitor : ExpressionVisitor {
    readonly Expression from;
    readonly Expression to;

    public ReplaceVisitor(Expression from, Expression to) {
        this.from = from;
        this.to = to;
    }

    public override Expression Visit(Expression node) => node == from ? to : base.Visit(node);
}

有了这个,您可以用新的所需表达式替换正文中的参数来替换 Expression.Invoke

public IQueryable<TResult> LeftJoin<TOuter, TInner, TKey, TResult>(
    IQueryable<TOuter> outer,
    IQueryable<TInner> inner,
    Expression<Func<TOuter, TKey>> outerKeySelector,
    Expression<Func<TInner, TKey>> innerKeySelector,
    Expression<Func<TOuter, TInner, TResult>> resultSelector) {
    var leftJoin = outer.GroupJoin(
        inner,
        outerKeySelector,
        innerKeySelector,
        (o, i) => new {
            Outer = o,
            Inners = i
        }).SelectMany(
            oi => oi.Inners.DefaultIfEmpty(),
            (oi, i) => new {
                oi.Outer,
                Inner = i
            }
        );

    // Break the anonymous type of the left join into the two parameters needed for the resultSelector
    var anonymousType = leftJoin.GetType().GetGenericArguments()[0];
    var parameter = Expression.Parameter(anonymousType, "oi");
    // oi.Outer
    var outerProperty = Expression.Property(parameter, "Outer");
    // oi.Inner
    var innerProperty = Expression.Property(parameter, "Inner");

    // resultSelector = (o,i) => expr(o,i)
    // o
    var resultOuterParm = resultSelector.Parameters[0];
    // i
    var resultInnerParm = resultSelector.Parameters[1];
    // expr(o,i) --> expr(oi.Outer, oi.Inner)
    var newBody = resultSelector.Body.Replace(resultOuterParm, outerProperty).Replace(resultInnerParm, innerProperty);

    // oi => expr(oi.Outer, oi.Inner)
    Expression<Func<TAnonymous, TResult>> typeSafeLambda<TAnonymous>(IQueryable<TAnonymous> _) =>
        Expression.Lambda<Func<TAnonymous, TResult>>(newBody, parameter);

    var wrapper = typeSafeLambda(leftJoin);

    return leftJoin.Select(wrapper);
}