0

At the bottom is an example of a left join implementation. It will work for normal lists and arrays but it will not work with LINQ to entities since I use Expression.Invoke().

What I'm trying to achieve is modifying/wrapping the resultSelector input to accept a single instance of the anonymous class 'a (used by the leftJoin queryable) instead of two separate parameters.

Inspecting resultSelector it seems I will want to create a modified version of it that has one parameter of type 'a and two arguments extracted from the properties Outer and Inner on 'a.

How do I go about doing this modification? How do I change Arguments?

enter image description here

[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);
    }
}
Linus
  • 3,254
  • 4
  • 22
  • 36
  • 1
    So I created an `ExpressionVisitor` to replace `Invoke` for my Left Outer Join implementation to get it working with EF that does parameter substitution in a lambda using method `Apply`. It is in [this answer](https://stackoverflow.com/a/49418695/2557128). Note that my answer also has special handling of `null` expressions so they aren't substituted but instead propagated as well for when you have `null.Member` references. – NetMage Dec 04 '19 at 20:47
  • Looks like a great post. Seems like the answer to my question will be found in understanding how your ReplaceVisitor and NullVisitor works. Not had time to take a deeper look yet. – Linus Dec 06 '19 at 09:35
  • I added an implementation using `ReplaceVisitor`. – NetMage Dec 06 '19 at 18:51

1 Answers1

1

Using an ExpressionVisitor, you can replace Expression.Invoke when just substituting parameters, with actually replacing occurrences of the paramaters with the new expressions.

Here is the Replace method - it replaces a (reference) equal expression with another expression.

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);
}

With this available, you can replace the Expression.Invoke with replacing the parameters in the body with the new desired expressions:

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);
}
NetMage
  • 26,163
  • 3
  • 34
  • 55
  • Nicely commented example. I had actually tried rebuilding the expressions using visitors but didn't succeed. The fact that I didn't realize that parameters are also expressions (of type ParameterExpression) was what preventing me from connecting the dots. Thanks! – Linus Dec 06 '19 at 22:36
  • @Linus np. I like to comment my `Expression` handling code with the corresponding lambda patterns to show how I'm emulating the compiler. – NetMage Dec 06 '19 at 23:14