使用LinqKit PredicateBuilder处理相关模型(EF Core)。

13

我想使用LinqKit的PredicateBuilder并将谓词传递到相关模型的.Any方法中。

因此我想要构建一个谓词:

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

然后使用它来过滤与谓词中的模型相关的模型:

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

但是这会导致一个 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'.

我看到了类似的问题,那里的答案建议使用.Compile或者构建额外的谓词

所以我尝试使用了额外的谓词。

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

或者直接使用编译

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

但我遇到了编译错误:System.NotSupportedException: Could not parse expression 'n.CastInfo.Any(__Compile_0)'

那么,是否有可能将PredicateBuilder的结果转换为可以传递给Any的形式呢?

注意:我已经成功组合表达式以实现所需的行为,但我不喜欢需要额外的变量。

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

所以我认为我对构建器有所遗漏。

版本更新: 我使用的是 dotnet core 2.0 和 LinqKit.Microsoft.EntityFrameworkCore 1.1.10

2个回答

15
看代码,人们会认为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/表达式。但是,它也防止了在表达式树中使用时的编译时错误,在您的情况下,编译器会发出类似于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)));

但现在由于前面解释的原因,这段代码无法编译通过。因此,你需要先将其转换为查询之外的 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();

这仍然需要在查询之外完成,即以下内容不起作用

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

2
感谢您的详细解释。我尝试了您的代码,但不幸的是我收到了一个警告:“LINQ表达式'where __castCondition_0.Invoke([c])' 无法翻译并将在本地评估。”虽然代码可以编译和运行,但条件没有添加到SQL中,查询选择了所有行。您有什么建议吗? - Igor
实际上,您提到ExpressionStarter可以转换为Expression。因此,以下语句有效:var castExpr = (Expression>)castCondition; context.Name.AsExpandable().Where(n => n.CastInfo.Any(castExpr.Compile())).Count();由于某种原因,内联变量不起作用。 - Igor
内联不起作用,因为“扩展器”无法识别ExpressionStarter.Compile()。听起来像是LinqKit工作不完整 :( - Ivan Stoev
3
啊,这就是让我留下来的问答类型!(有时候我感觉想要放弃)。 - Gert Arnold
上面的建议对我也起作用了。我遇到了另一个错误,暗示着与async/await的组合是不可能的。在应用以上提示并删除await和To...Async()之后,一切都正常工作了。谢谢! - Jan C. de Graaf

0

这可能与原始问题不完全相关,但考虑以下模型:

public Class Music
{
    public int Id { get; set; }
    public List<Genre> Genres { get; set; }
}
public Class Genre
{
    public int Id { get; set; }
    public string Title { get; set; }
}

List<string> genresToFind = new() {"Pop", "Rap", "Classical"};

如果您正在尝试查找所有流派存在于genresToFind列表中的音乐,可以执行以下操作:
在Genre模型上创建PredicateBuilder表达式链:
var pre = PredicateBuilder.New<Genre>();
foreach (var genre in genresToFind)
{
    pre = pre.Or(g => g.Title.Contains(genre));
}

然后像这样执行您的查询:

var result = await _db.Musics.AsExpandable()
    .Where(m => m.Genres
        .Any(g => pre.ToExpression().Invoke(g)))
    .ToListAsync();

ToExpression() 是我们创建的通用扩展方法,用于将 ExpressionStarter<Genre> 类型转换为 Expression<Func<Genre, bool>>

public static class ExpressionExtensions
{
    public static Expression<Func<T, bool>> ToExpression<T> (this 
        ExpressionStarter<T> exp) => exp;
}

另外,您需要安装 LinqKit.Microsoft.EntityFrameworkCore 软件包。


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