Попытка использовать родительское свойство в качестве параметра в выражении дочерней коллекции; LinqKit выдает Невозможно преобразовать MethodCallExpressionN в LambdaExpression

Я пытаюсь динамически построить выражение, подобное приведенному ниже, где я могу использовать ту же функцию сравнения, но где сравниваемые значения могут быть переданы, поскольку значение передается из свойства «выше» в запрос.

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

Я считаю, что я правильно построил запрос, но метод ExpressionExpander.VisitMethodCall(..) выдает следующее исключение, когда я пытаюсь его использовать:

«Невозможно привести объект типа 'System.Linq.Expressions.InstanceMethodCallExpressionN' к типу 'System.Linq.Expressions.LambdaExpression'»

В реальном коде, используя Entity Framework и фактический IQueryable<T>, я часто получаю:

«Не удалось преобразовать объект типа 'System.Linq.Expressions.MethodCallExpressionN' к типу 'System.Linq.Expressions.LambdaExpression'».

Я создал удобный для LinqPad пример моей проблемы, настолько простой, насколько я мог это сделать.

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 */);

}

Если я делаю что-то явно не так, я был бы очень признателен, если бы меня подтолкнули в правильном направлении! Спасибо.


Изменить: я знаю, что сработает следующее:

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

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

Однако я пытаюсь отделить сравнение от местоположения параметров, поскольку сравнение может быть сложным, и я хотел бы повторно использовать его для множества разных запросов (каждый с разными местоположениями для двух параметров). Также предполагается, что один из параметров (в примере «минимальная длина») будет фактически вычисляться через другое выражение.


Изменить: извините, я только что понял, что некоторые ответы будут работать при попытке против моего примера кода, поскольку мой пример просто маскируется под IQueryable<T>, но все еще List<T> внизу. Причина, по которой я использую LinqKit в первую очередь, заключается в том, что фактический IQueryable<T> из EntityFramework DbContext будет вызывать Linq-to-SQL и, следовательно, должен иметь возможность анализировать сам Linq-to-SQL. LinqKit обеспечивает это, расширяя все до выражений.


Решение! Благодаря ответу Джин ниже, я думаю, что понял, куда иду неправильный.

Если значение получено откуда-то в запросе (т. Е. не значение, которое известно заранее.), Тогда вы должны встроить ссылку / выражение / переменную на него в выражение.

В моем исходном примере я пытался передать значение minLength, взятое из выражения, и передать его методу. Этот вызов метода нельзя было выполнить заранее, поскольку он использовал значение из выражения, и это невозможно было сделать внутри выражения, поскольку вы не можете построить выражение внутри выражения.

Итак, как это обойти? Я решил написать свои выражения так, чтобы их можно было вызывать с дополнительными параметрами. Хотя у этого есть обратная сторона: параметры больше не имеют «именований», и я могу получить Expression<Func<int, int, int, int, bool>> или что-то в этом роде.

// 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 */);

}

person Ben Jenkinson    schedule 13.05.2014    source источник
comment
Почему вам нужно использовать AsExpandable() и Expression<Func<..., а не просто Func<...?   -  person sgmoore    schedule 13.05.2014
comment
AsExpandable() - это главная магия LinqKit, все должно быть выражением, чтобы оно могло быть скомпилированным непосредственно в SQL позже.   -  person Ben Jenkinson    schedule 13.05.2014
comment
Invoke Вы используете метод расширения? Из LinqKit?   -  person Jean Hominal    schedule 13.05.2014
comment
Да, Invoke - это вспомогательный метод, который расширяется до .Compile().Invoke(arg1, arg2..), что удовлетворяет компилятор. Во время выполнения AsExpandable() использует пользовательский ExpressionVisitor, называемый LinqKit.ExpressionExpander, чтобы удалить все вызовы Invoke и заменить их деревом выражений, а не скомпилированной функцией. (Я не совсем понимаю, как это работает, поэтому я здесь, но я думаю, что это примерно так.) Домашняя страница LinqKit находится здесь: albahari.com/nutshell/linqkit.aspx или вы можете просмотреть исходный код на Github: github.com/scottksmith95/LINQKit   -  person Ben Jenkinson    schedule 13.05.2014


Ответы (2)


Хорошо, поэтому то, что вы пытаетесь сделать (преобразование из функции, которая принимает один аргумент, которая возвращает другую функцию, которая принимает один аргумент f(x)(y), в функцию, которая принимает два аргумента f(x, y)), называется uncurrying. Поищи это! :)

Теперь проблема в вашем коде заключается в том, что в выражении, возвращаемом BuildExpressionToCheckTuple, есть вызов метода для BuildExpressionToCheckStringLength, который не разрешен. И вы не можете разрешить его, потому что он принимает аргумент, встроенный в параметр кортежа.

Решение состоит в том, чтобы вместо вызова метода использовать лямбда-выражение, которое будет эквивалентно вызову этого метода.

Это:

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 */);
}
person Jean Hominal    schedule 14.05.2014

Итак, вы ищете что-то вроде этого:

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();
        }
person Vitaliy Kalinin    schedule 13.05.2014
comment
К сожалению, я пытаюсь разделить выражение, содержащее сравнение, и выражение, указывающее, где находится сравниваемый параметр. Вариант использования состоит в том, что у меня есть несколько разных запросов, которые должны будут выполнить одну и ту же (запутанную) проверку, и я хотел бы вывести ее в выражение; каждый случай будет получать свои входные значения из разных мест, в зависимости от запрашиваемых элементов. - person Ben Jenkinson; 13.05.2014
comment
Я обновил свой ответ. Вы ищете что-то подобное? - person Vitaliy Kalinin; 13.05.2014
comment
Не совсем. Это похоже на то, с чего я начал. Если вы настроите свой класс Person так, чтобы он имел свойство ICollection<Person> под названием «Дети», затем попытайтесь найти элементы из корневого списка Person в зависимости от того, есть ли у них какие-либо дочерние элементы, соответствующие предикату? Вы не можете записать их как методы расширения для IQueryable<T>, потому что ваш запрос зависает от свойства ICollection<T>, а Linq-to-SQL вообще не может обрабатывать методы расширения. - person Ben Jenkinson; 13.05.2014
comment
Я только что попробовал ваш примерный код, и он отлично работает, запрашивая мой пример, потому что, хотя он маскируется под IQueryable<T>, на самом деле он все еще на самом деле List<T>, а Linq-To-SQL фактически не задействован. Я не думаю, что это сработает против EntityFramework DbContext. - person Ben Jenkinson; 13.05.2014
comment
Так что попробуйте против EF. Фактически, если второй общий параметр WhereGreater может быть исключен (что означает, что вы заранее будете знать значения, какие типы следует сравнивать), тогда реализация может быть упрощена, и есть хорошие шансы, что LinqKit построит дерево выражений, распознаваемое для EF. - person Vitaliy Kalinin; 13.05.2014
comment
Я скорректировал ваш, как я описал, и был прав, Linq-to-SQL не может обрабатывать методы расширения. Я просто пишу более подробный пример. Думаю, я сделал оригинал слишком простым. - person Ben Jenkinson; 13.05.2014
comment
LINQ to Entities не распознает метод X метода, и этот метод нельзя преобразовать в выражение хранилища. Это стандартная ошибка, когда вы делаете что-то, чего нет в SQL. Одна из основных функций LinqKit - предоставить способ обойти такое исключение. - person Ben Jenkinson; 13.05.2014