尝试在子集合表达式中使用父属性作为参数;LinqKit 抛出“无法将 MethodCallExpressionN 转换为 LambdaExpression”的错误。

5

我正在尝试动态构建一个类似下面的表达式,其中我可以使用相同的比较函数,但要比较的值可以传递,因为该值是从查询中的“高级”属性传递的。

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

然而,我试图将比较与参数的位置分开,因为比较可能很复杂,我想重用它来执行许多不同的查询(每个查询具有不同的两个参数位置)。还打算通过另一个表达式计算其中一个参数(在示例中为“最小长度”)。
编辑:抱歉,我刚意识到一些答案只能针对我的示例代码进行尝试,因为我的示例只是伪装成了一个>,但实际上是一个List 。我之所以首先使用LinqKit,是因为来自EntityFramework DbContext的实际>会调用Linq-to-SQL,因此必须能够被Linq-to-SQL本身解析。 LinqKit通过将所有内容扩展为表达式来实现这一点。
解决方案!感谢下面Jean的答案,我认为我已经意识到自己的错误所在。
如果值来自查询中的某个地方(即不是预先知道的值),则必须将对其的引用/表达式/变量构建到表达式中。
在我的原始示例中,我试图传递从表达式中取出的'minLength'值并将其传递给方法。由于它使用表达式中的值,因此无法在之前进行方法调用,并且由于您无法在表达式中构建表达式,因此无法在表达式中进行方法调用。
那么,如何解决这个问题?我选择编写我的表达式,以便可以使用附加参数调用它们。虽然这样做的缺点是参数不再是“命名的”,并且我最终可能会得到一个Expression >之类的东西。
// 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 */);

}

为什么你需要使用 AsExpandable()Expression<Func<... 而不是只用 Func<... - sgmoore
AsExpandable()LinqKit的主要神奇功能,所有内容都需要成为表达式,这样后续就可以直接编译为SQL。 - Ben Jenkinson
你使用的 Invoke 是一个扩展方法吗?来自 LinqKit? - Jean Hominal
是的,Invoke 是一个辅助方法,它扩展到 .Compile().Invoke(arg1, arg2..),以满足编译器。在运行时,AsExpandable() 使用一个自定义的 ExpressionVisitor 叫做 LinqKit.ExpressionExpander 来删除所有对 Invoke 的调用,并用表达式树代替已编译的函数。(我不完全理解它的工作原理,所以来这里了,但我想大致就是这样。)LinqKit 的主页在这里: http://www.albahari.com/nutshell/linqkit.aspx 或者您也可以在 Github 上查看源代码: https://github.com/scottksmith95/LINQKit - Ben Jenkinson
2个回答

0

那么你是在寻找这样的东西:

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

不幸的是,我正在尝试实现包含比较的表达式和指示要比较的参数所在位置的表达式之间的分离。使用情况是,我有几个不同的查询需要进行相同(复杂)的检查,并希望将其提取到一个表达式中;每种情况都将从不同的位置派生其输入值,具体取决于被查询的项目。 - Ben Jenkinson
我刚刚尝试了你的示例代码,它可以很好地查询我的示例,因为虽然它伪装成了一个 IQueryable<T>,但它实际上仍然是一个 List<T>,并且 Linq-To-SQL 实际上并没有参与其中。我不认为它能够针对 EntityFramework DbContext 进行工作。 - Ben Jenkinson
尝试使用EF进行测试。实际上,如果可以消除WhereGreater的第二个泛型参数(这意味着您将预先知道应该比较哪些类型的值),则可以简化实现,并且LinqKit有很好的机会构建可识别EF的表达式树。 - Vitaliy Kalinin
我按照我描述的方式进行了调整,我是正确的,Linq-to-SQL无法处理扩展方法。我正在撰写一个更详细的示例。我认为原始示例太简单了。 - Ben Jenkinson
“LINQ to Entities不识别‘X’方法,该方法无法转换为存储表达式。”这是在进行某些SQL中没有等效项的操作时出现的标准错误。 LinqKit的主要功能之一是提供解决此类异常的方法。” - Ben Jenkinson
显示剩余2条评论

0

好的,你所尝试的是将一个只有一个参数并返回另一个只有一个参数函数 f(x)(y) 转换成一个有两个参数的函数 f(x, y),这被称为“非科里化”,可以查一下! :)

现在,你代码中的问题在于,在 BuildExpressionToCheckTuple 返回的表达式中,有一个方法调用 BuildExpressionToCheckStringLength,这个方法是无法解析的。因为它需要作为参数传递的元组嵌入其中。

解决方案是,不要使用方法调用,而是使用一个与该方法调用等效的 lambda 表达式。

如下:

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接