Use LinqKit PredicateBuilder for related model (EF Core)

时间:2017-10-12 09:38:55

标签: c# linq .net-core entity-framework-core linqkit

I want to use LinqKit's PredicateBuilder and pass the predicate into .Any method for related model.

So I want to build a predicate:

var castCondition = PredicateBuilder.New<CastInfo>(true);

if (movies != null && movies.Length > 0)
{
    castCondition = castCondition.And(c => movies.Contains(c.MovieId));
}
if (roleType > 0)
{
    castCondition = castCondition.And(c => c.RoleId == roleType);
}

And then use it to filter model that has relation to model in predicate:

IQueryable<Name> result = _context.Name.AsExpandable().Where(n => n.CastInfo.Any(castCondition));
return await result.OrderBy(n => n.Name1).Take(25).ToListAsync();

But this causes a System.NotSupportedException: Could not parse expression 'n.CastInfo.Any(Convert(__castCondition_0, Func``2))': The given arguments did not match the expected arguments: Object of type 'System.Linq.Expressions.UnaryExpression' cannot be converted to type 'System.Linq.Expressions.LambdaExpression'.

I saw similar question and answer there suggests to use .Compile. Or one more question that build an extra predicate.

So I tried to use extra predicate

var tp = PredicateBuilder.New<Name>(true);
tp = tp.And(n => n.CastInfo.Any(castCondition.Compile()));
IQueryable<Name> result = _context.Name.AsExpandable().Where(tp);

Or use compile directly

IQueryable<Name> result = _context.Name.AsExpandable().Where(n => n.CastInfo.Any(castCondition.Compile()));

But I have an error about Compile: System.NotSupportedException: Could not parse expression 'n.CastInfo.Any(__Compile_0)'

So is it possible to convert the result from PredicateBuilder to pass into Any?

Note: I was able to build the desired behavior combining expressions, but I don't like that I need extra variables.

System.Linq.Expressions.Expression<Func<CastInfo,bool>> castExpression = (c => true);
if (movies != null && movies.Length > 0)
{
    castExpression = (c => movies.Contains(c.MovieId));
}
if (roleType > 0)
{
    var existingExpression = castExpression;
    castExpression = c => existingExpression.Invoke(c) && c.RoleId == roleType;
}
IQueryable<Name> result = _context.Name.AsExpandable().Where(n => n.CastInfo.Any(castExpression.Compile()));
return await result.OrderBy(n => n.Name1).Take(25).ToListAsync();

So I assume I just miss something about builder.

Update about versions: I use dotnet core 2.0 and LinqKit.Microsoft.EntityFrameworkCore 1.1.10

1 个答案:

答案 0 :(得分:7)

查看代码,可以假设castCondition变量的类型为Expression<Func<CastInfo, bool>>(就像早期版本的PredicateBuilder中那样)。

但如果是这种情况,那么n.CastInfo.Any(castCondition)甚至不应该编译(假设CastInfo是一个集合导航属性,因此编译器将命中Enumerable.Any,期望Func<CastInfo, bool> ,而不是Expression<Func<CastInfo, bool>>)。那么这里发生了什么?

在我看来,这是C#隐式操作符滥用的一个很好的例子。 PredicateBuilder.New<T>方法实际上返回一个名为ExpressionStarter<T>的类,其中有许多方法模拟Expression,但更重要的是,隐式转换为Expression<Func<T, bool>>并且Func<CastInfo, bool>。后者允许该类用于顶级Enumerable / Queryable方法,以替换相应的lambda func / expression。但是,它也可以防止在表达式树中使用时出现编译时错误 - 编译器会发出类似n.CastInfo.Any((Func<CastInfo, bool>)castCondition)的内容,这当然会在运行时导致异常。

LinqKit AsExpandable方法的整个想法是允许通过自定义Invoke扩展方法“调用”表达式,然后在表达式树中“展开”。所以回到开头,如果变量类型是Expression<Func<CastInfo, bool>>,则预期用法是:

_context.Name.AsExpandable().Where(n => n.CastInfo.Any(c => castCondition.Invoke(c)));

但是由于之前解释的原因,现在这不能编译。因此,您必须先将其转换为查询的<{1}}

Expression<Func<T, bool>

然后使用

Expression<Func<CastInfo, bool>> castPredicate = castCondition;

_context.Name.AsExpandable().Where(n => n.CastInfo.Any(c => castPredicate.Invoke(c)));

为了让编译器推断表达式类型,我会创建一个这样的自定义扩展方法:

_context.Name.AsExpandable().Where(n => n.CastInfo.Any(castPredicate.Compile()));

然后只需使用

using System;
using System.Linq.Expressions;

namespace LinqKit
{
    public static class Extensions
    {
        public static Expression<Func<T, bool>> ToExpression<T>(this ExpressionStarter<T> expr) => expr;
    }
}

仍然需要在查询 之外完成,即以下工作:

var castPredicate = castCondition.ToExpression();