2
votes

I have two expressions:

Expression<Func<T1, T2>> 
Expression<Func<T2, bool>> 

And I want to combine these and get a new expression of type Expression<Func<T1, bool>> so that it can be used in Entity Framework LINQ.

I combine them with help of Expression.Invoke(), but it doesn't work.

//Extensinon method
public static Expression<Func<T1, bool>> Compose<T1, T2>(this Expression<Func<T1, T2>> convertExpr, Expression<Func<T2, bool>> predicate) 
    => Expression.Lambda<Func<T1, bool>>(Expression.Invoke(predicate, convertExpr.Body), convertExpr.Parameters.First());
...
Expression<Func<One, Two>> convert;
Expression<Func<Two, bool>> predicate;
Expression<Func<One, bool>> filter=convert.Compose(predicate);

// Works fine
List<One> lst;
lst.AsQueryable().Where(filter);

DbSet<One> src;
// In next line throws runtime NotSupportedException: The LINQ expression node type 'Invoke' is not supported in LINQ to Entities
src.Where(filter);

UPDATE

For example:

public class Book
{ 
  public int Id {get; protected set;}
  public string Name {get; protected set;}
  public virtual Genre {get; protected set;}
}

public class Genre
{
  public int Id {get; protected set;}
  public string Name {get; protected set;}
}
...
Expression<Func<Book, Genre>> convert = b=>b.Genre;
Expression<Func<Genre, bool>> predicate = g=>g.Name=="fantasy";
// Need: b=>b.Genre=="fantasy"
Expression<Func<Genre, bool>> filter=convert.Compose(predicate);

// Works fine
List<Book> lst;
lst.AsQueryable().Where(filter);

DbSet<Book> books;
// In next line throws runtime NotSupportedException: The LINQ expression node type 'Invoke' is not supported in LINQ to Entities
books.Where(filter);
1
Do you get an error or a wrong result? Please describe your output.zypro
I get runtime error: System.NotSupportedException: The LINQ expression node type 'Invoke' is not supported in LINQ to EntitiesVik Kony
My comment on a similar question: I ran into a similar problem in my Full Outer Join code - it builds expression trees for a full outer join, worked fine in LINQ to SQL, but not with EF. I built an ExpressionVisitor that essentially is a limited version of LINQKit's Invoke/Expand called Apply that in-place expands a LambdaExpression which you can see here. Note that this specific Apply has special consideration for null propagation when null is an argument (statically handles null.member -> null as if it were null?.member).NetMage

1 Answers

2
votes

Using the common ExpressionVisitor for replacing one Expression with another, my standard Compose function (in the more common mathematical order, I think), substitutes the Body of one LambdaExpression for the parameter in another:

public static class ExpressionExt {
    // Compose: (y => f(y)).Compose(x => g(x)) -> x => f(g(x))
    /// <summary>
    /// Composes two LambdaExpression into a new LambdaExpression
    /// </summary>
    /// <param name="Tpg">Type of parameter to gFn, and type of parameter to result lambda.</param>
    /// <param name="Tpf">Type of result of gFn and type of parameter to fFn.</param>
    /// <param name="TRes">Type of result of fFn and type of result of result lambda.</param>
    /// <param name="fFn">The outer LambdaExpression.</param>
    /// <param name="gFn">The inner LambdaExpression.</param>
    /// <returns>LambdaExpression representing outer composed with inner</returns>
    public static Expression<Func<Tpg, TRes>> Compose<Tpg, Tpf, TRes>(this Expression<Func<Tpf, TRes>> fFn, Expression<Func<Tpg, Tpf>> gFn) =>
        Expression.Lambda<Func<Tpg, TRes>>(fFn.Body.Replace(fFn.Parameters[0], gFn.Body), gFn.Parameters[0]);

    /// <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, your example is straightforward:

Expression<Func<One, Two>> convert = p1 => new Two(p1);
Expression<Func<Two, bool>> predicate = p2 => p2 == new Two();
Expression<Func<One, bool>> filter = predicate.Compose(convert);

However, especially with EF expressions, it may be preferable to use my replacement for Invoke, which handles null propogation, in cases where your arguments may be null. It also uses the same Replace ExpressionVisitor as above:

public static class ExpressionExt2 {   
    public static Expression PropagateNull(this Expression orig) => new NullVisitor().Visit(orig);

    // Apply: (x => f).Apply(args)
    /// <summary>
    /// Substitutes an array of Expression args for the parameters of a lambda, returning a new Expression
    /// </summary>
    /// <param name="e">The original LambdaExpression to "call".</param>
    /// <param name="args">The Expression[] of values to substitute for the parameters of e.</param>
    /// <returns>Expression representing e.Body with args substituted in</returns>
    public static Expression Apply(this LambdaExpression e, params Expression[] args) {
        var b = e.Body;

        foreach (var pa in e.Parameters.Zip(args, (p, a) => (p, a)))
            b = b.Replace(pa.p, pa.a);

        return b.PropagateNull();
    }
}

/// <summary>
/// ExpressionVisitor to replace a null.member Expression with a null
/// </summary>
public class NullVisitor : System.Linq.Expressions.ExpressionVisitor {
    public override Expression Visit(Expression node) {
        if (node is MemberExpression nme && nme.Expression is ConstantExpression nce && nce.Value == null)
            return Expression.Constant(null, nce.Type.GetMember(nme.Member.Name).Single().GetMemberType());
        else
            return base.Visit(node);
    }
}

public static class MeberInfoExt {
    public static Type GetMemberType(this MemberInfo member) {
        switch (member) {
            case FieldInfo mfi:
                return mfi.FieldType;
            case PropertyInfo mpi:
                return mpi.PropertyType;
            case EventInfo mei:
                return mei.EventHandlerType;
            default:
                throw new ArgumentException("MemberInfo must be if type FieldInfo, PropertyInfo or EventInfo", nameof(member));
        }
    }    
}

Given Apply, your Compose is simply:

public static Expression<Func<T1, bool>> Compose<T1, T2>(this Expression<Func<T1, T2>> convertExpr, Expression<Func<T2, bool>> predicate)
    => Expression.Lambda<Func<T1, bool>>(predicate.Apply(convertExpr.Body), convertExpr.Parameters.First());