Trying to use parent property as parameter in chil

2019-06-22 13:56发布

问题:

I'm trying to dynamically construct an expression similar to the one below, where I can use the same comparison function, but where the values being compared can be passed in, since the value is passed from a property 'higher-up' in the query.

var people = People
    .Where(p => p.Cars
        .Any(c => c.Colour == p.FavouriteColour));

I believe I've constructed the query correctly, but the ExpressionExpander.VisitMethodCall(..) method throws the following exception when I try to use it:

"Unable to cast object of type 'System.Linq.Expressions.InstanceMethodCallExpressionN' to type 'System.Linq.Expressions.LambdaExpression'"

In real-world code, using Entity Framework and actual IQueryable<T>, I often get:

"Unable to cast object of type 'System.Linq.Expressions.MethodCallExpressionN' to type 'System.Linq.Expressions.LambdaExpression'" as well.

I've constructed a LinqPad-friendly example of my problem, as simple as I could make it.

void Main()
{
    var tuples = new List<Tuple<String, int>>() {
        new Tuple<String, int>("Hello", 4),
        new Tuple<String, int>("World", 2),
        new Tuple<String, int>("Cheese", 20)
    };

    var queryableTuples = tuples.AsQueryable();

    // For this example, I want to check which of these strings are longer than their accompanying number.
    // The expression I want to build needs to use one of the values of the item (the int) in order to construct the expression.
    // Basically just want to construct this:
    //      .Where (x => x.Item1.Length > x.Item2)

    var expressionToCheckTuple = BuildExpressionToCheckTuple();

    var result = queryableTuples
        .AsExpandable()
        .Where (t => expressionToCheckTuple.Invoke(t))
        .ToList();
}

public Expression<Func<string, bool>> BuildExpressionToCheckStringLength(int minLength) {

    return str => str.Length > minLength;

}

public Expression<Func<Tuple<string, int>, bool>> BuildExpressionToCheckTuple() {

    // I'm passed something (eg. Tuple) that contains:
    //  * a value that I need to construct the expression (eg. the 'min length')
    //  * the value that I will need to invoke the expression (eg. the string)

    return tuple => BuildExpressionToCheckStringLength(tuple.Item2 /* the length */).Invoke(tuple.Item1 /* string */);

}

If I'm doing something obviously wrong, I'd really appreciate a nudge in the right direction! Thanks.


Edit: I know that the following would work:

Expression<Func<Tuple<string, int>, bool>> expr = x => x.Item1.Length > x.Item2;

var result = queryableTuples
    .AsExpandable()
    .Where (t => expr.Invoke(t))
    .ToList();

However, I'm trying to separate the comparison from the location of the parameters, since the comparison could be complex and I would like to re-use it for many different queries (each with different locations for the two parameters). It is also intended that one of the parameters (in the example, the 'min length') would actually be calculated via another expression.


Edit: Sorry, I've just realised that some answers will work when attempted against my example code since my example is merely masquerading as an IQueryable<T> but is still a List<T> underneath. The reason I'm using LinqKit in the first place is because an actual IQueryable<T> from an EntityFramework DbContext will invoke Linq-to-SQL and so must be able to be parsed by Linq-to-SQL itself. LinqKit enables this by expanding everything to expressions.


Solution! Thanks to Jean's answer below, I think I've realised where I'm going wrong.

If a value has come from somewhere in the query (i.e. not a value that is known before-hand.) then you must build the reference/expression/variable to it into the expression.

In my original example, I was trying to pass the 'minLength' value taken from within the expression and pass it to a method. That method call could not be done before-hand, since it used a value from the expression, and it could not be done within the expression, since you can't build an expression within an expression.

So, how to get around this? I chose to write my expressions so that they can be invoked with the additional parameters. Though this has the downside that the parameters are no longer 'named' and I could end up with an Expression<Func<int, int, int, int, bool>> or something down the line.

// New signature.
public Expression<Func<string, int, bool>> BuildExpressionToCheckStringLength() {

    // Now takes two parameters.
    return (str, minLength) => str.Length > minLength;

}

public Expression<Func<Tuple<string, int>, bool>> BuildExpressionToCheckTuple() {

    // Construct the expression before-hand.
    var expression = BuildExpressionToCheckStringLength();

    // Invoke the expression using both values.     
    return tuple => expression.Invoke(tuple.Item1 /* string */, tuple.Item2 /* the length */);

}

回答1:

OK, so what you are trying to do (the transformation from a function that takes a single argument, that returns another function that takes a single argument f(x)(y) into a function that takes two arguments f(x, y)) is known as uncurrying. Look it up! :)

Now, the issue that you have in your code is that, in the expression returned by BuildExpressionToCheckTuple, there is a method call to BuildExpressionToCheckStringLength, which is not resolved. And you cannot resolve it because it takes an argument that is embedded in the tuple parameter.

The solution is, instead of using a method call, to use a lambda expression that will be equivalent to that method call.

That is:

public Expression<Func<int, Func<string, bool>>> ExpressionToCheckStringLengthBuilder() {
    return minLength =>
        str => str.Length > minLength;
}

public Expression<Func<Tuple<string, int>, bool>> BuildExpressionToCheckTuple() {
    // I'm passed something (eg. Tuple) that contains:
    //  * a value that I need to construct the expression (eg. the 'min length')
    //  * the value that I will need to invoke the expression (eg. the string)

    // Putting builder into a variable so that the resulting expression will be 
    // visible to tools that analyze the expression.
    var builder = ExpressionToCheckStringLengthBuilder();

    return tuple => builder.Invoke(tuple.Item2 /* the length */).Invoke(tuple.Item1 /* string */);
}


回答2:

So you are looking for something like this:

public static class Program
    {
        public class Person
        {
            public string FirstName { get; set; }
            public string LastName { get; set; }
        }

        public static IQueryable<T> WherePropertyEquals<T, TProperty>(
            this IQueryable<T> src, Expression<Func<T, TProperty>> property, TProperty value)
        {
            var result = src.Where(e => property.Invoke(e).Equals(value));
            return result;
        }

        public static IQueryable<T> WhereGreater<T, TProperty>(
            this IQueryable<T> src, Expression<Func<T, TProperty>> property, TProperty value)
            where TProperty : IComparable<TProperty>
        {
            var result = src.Where(e => property.Invoke(e).CompareTo(value) > 0);
            return result;
        }

        public static IQueryable<T> WhereGreater<T, TProperty>(
            this IQueryable<T> src, Expression<Func<T, TProperty>> left, Expression<Func<T, TProperty>> right)
            where TProperty : IComparable<TProperty>
        {
            var result = src.Where(e => left.Invoke(e).CompareTo(right.Invoke(e)) > 0);
            return result;
        }

        public static void Main()
        {
            var persons = new List<Person>()
                {
                    new Person
                        {
                            FirstName = "Jhon",
                            LastName = "Smith"
                        },
                    new Person
                        {
                            FirstName = "Chuck",
                            LastName = "Norris"
                        },
                    new Person
                        {
                            FirstName = "Ben",
                            LastName = "Jenkinson"
                        },
                    new Person
                        {
                            FirstName = "Barack",
                            LastName = "Obama"
                        }
                }
                .AsQueryable()
                .AsExpandable();

            var chuck = persons.WherePropertyEquals(p => p.FirstName, "Chuck").First();
            var ben = persons.WhereGreater(p => p.LastName.Length, 6).First();
            var barack = persons.WhereGreater(p => p.FirstName.Length, p => p.LastName.Length).First();
        }