【问题标题】:Entity Framework LINQ complex query - combine multiple predicatesEntity Framework LINQ 复杂查询 - 组合多个谓词
【发布时间】:2021-08-20 18:33:27
【问题描述】:

我正在尝试创建一个复杂的 Linq 查询,如下所示: 获取所有员工符合给定过滤参数的组织。

过滤器示例:

  • 名字:约翰
  • 姓名:史密斯

我的第一次尝试:

if (!filter.Name.IsNullOrWhiteSpace())
{
    query = query.Where(o => o.Persons.Any(p => p.Name.ToLower().Contains(filter.Name.ToLower())));
}

if (!filter.Firstname.IsNullOrWhiteSpace())
{
    query = query.Where(o => o.Persons.Any(p => p.Firstname.ToLower().Contains(filter.Firstname.ToLower())));
}

if (!filter.ContactNumber.IsNullOrWhiteSpace())
{
    query = query.Where(o => o.Persons.Any(p => p.ContactNumber.contains(filter.ContactNumber)));
}

这种方法的问题在于,当组织 A 中有人姓 John(例如 John Johnson),而组织 A 中有人姓 Smith (Jenny Smith) 时,组织 (A) 包含那两个人被退回。这是不应该的。我只希望组织中的人员名字为“john”且姓氏为“Smith”

我找到了一种可行但肮脏且不可扩展的方法:

if (!filter.ContactNumber.IsNullOrWhiteSpace())
{
    if (!filter.Name.IsNullOrWhiteSpace() && !filter.Firstname.IsNullOrWhiteSpace())
    {
        query = query.Where(o => o.Persons.Any(p => p.ContactNumber.contains(filter.ContactNumber)
                                                && p.Name.ToLower().Contains(filter.Name.ToLower())
                                                && p.Firstname.ToLower().Contains(filter.Firstname.ToLower())));
    }
    else if (!filter.Name.IsNullOrWhiteSpace())
    {
        query = query.Where(o => o.Persons.Any(p => p.ContactNumber.contains(filter.ContactNumber)
                                                && p.Name.ToLower().Contains(filter.Name.ToLower())));
    } else if (!filter.Firstname.IsNullOrWhiteSpace())
    {
        query = query.Where(o => o.Persons.Any(p => p.ContactNumber.contains(filter.ContactNumber)
                                                && p.Firstname.ToLower().Contains(filter.Firstname.ToLower())));
    } else
    {
        query = query.Where(o => o.Persons.Any(p => p.ContactNumber.contains(filter.ContactNumber));
    }
} else if(!filter.Name.IsNullOrWhiteSpace())
{
    if (!filter.Firstname.IsNullOrWhiteSpace())
    {
        query = query.Where(o => o.Persons.Any(p => p.Firstname.ToLower().Contains(filter.Firstname.ToLower()) && p.Name.ToLower().Contains(filter.Name.ToLower())));
    } else
    {
        query = query.Where(o => o.Persons.Any(p => p.Name.ToLower().Contains(filter.Name.ToLower())));
    }
} else if (!filter.Firstname.IsNullOrWhiteSpace())
{
    query = query.Where(o => o.Persons.Any(p => p.Firstname.ToLower().Contains(filter.Firstname.ToLower())));
}

如您所见,这不是一个非常干净的解决方案。

我也尝试在表达式中使用方法调用,但 Linq 无法翻译。有什么办法可以制作一个谓词表达式列表并将它们合并为一个?还是有其他更好的解决方案?

顺便说一句,由于我需要一个分页列表,所以所有内容都必须在一个查询中。

供您参考,这就是我的过滤器类的样子。它只是从我的前端发送的一个类,包含所有需要过滤的字段。

public class ContactFilter
{
    public string Name{ get; set; }
    public string Firstname{ get; set; }
    public string ContactNummer { get; set; }
}

【问题讨论】:

  • filter 到底是什么类型?看起来不像Predicate<T>。它的属性来自哪里?
  • Filter 只是一个简单的类,其中包含需要过滤的字段。有些字段可能为空,有些不是,取决于填写的字段。我编辑了描述
  • 为什么您的第一个代码不起作用?它似乎是过滤器中每个值的 AND 条件。
  • 顺便说一句,包含可能需要替换为相等。
  • 由于下面的解释,第一个代码不起作用。我需要包含,因为我想允许部分匹配

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


【解决方案1】:

最简单的解决方案之一是使用LINQKit 库:

var predicate = PredicateBuilder.New<Person>();

if (!filter.Name.IsNullOrWhiteSpace())
{
    predicate = predicate.And(p => p.Name.ToLower().Contains(filter.Name.ToLower()));
}

if (!filter.Firstname.IsNullOrWhiteSpace())
{
    predicate = predicate.And(p => p.Firstname.ToLower().Contains(filter.Firstname.ToLower()));
}

if (!filter.ContactNumber.IsNullOrWhiteSpace())
{
    predicate = predicate.And(p => p.ContactNumber.contains(filter.ContactNumber));
}

Expression<Func<Person, bool>> exp = predicate;

query = query
    .AsExpandable()
    .Where(o => o.Persons.Any(exp.Compile()));

【讨论】:

  • 谢谢,我选择了无依赖解决方案,但我会记住这一点。
【解决方案2】:

有什么方法可以制作一个谓词表达式列表并将它们合并为一个?

是的,这是我在这种情况下更喜欢的方法。

首先建立列表:

var filterExpressions = new List<Expression<Func<Person, bool>>();
if (!filter.Name.IsNullOrWhiteSpace())
{
    filterExpressions.Add(p => p.Name.ToLower().Contains(filter.Name.ToLower()));
}

if (!filter.Firstname.IsNullOrWhiteSpace())
{
    filterExpressions.Add(p => p.Firstname.ToLower().Contains(filter.Firstname.ToLower()));
}

if (!filter.ContactNumber.IsNullOrWhiteSpace())
{
    filterExpressions.Add(p => p.ContactNumber.contains(filter.ContactNumber));
}

从那里,您可以一起使用this implementationAnd 任意表达式。如果没有要应用的过滤器,您还需要决定要做什么(我将使用默认的无过滤器,但您可能想做其他事情)。

var predicate = filterExpressions.DefaultIfEmpty(p => true)
    .Aggregate((a, b) => a.And(b));

现在我们进入最难的部分。我们有一个表达式,它表示您要传递给Any 的调用的 lambda。如果我们可以这样做就好了:

query = query.Where(o => o.Persons.Any(predicate));

但遗憾的是,这不起作用,因为o.Persons 的类型不是IQueryable。所以现在我们有一个表达式,我们想要嵌入到另一个表达式中,其中内部表达式需要是一个 lambda。幸运的是,这并不是复杂:

public static Expression<Func<TSource, TResult>> EmbedLambda
    <TSource, TResult, TFunc1, TFunc2>(
    this Expression<Func<TFunc1, TFunc2>> lambda,
    Expression<Func<TSource, Func<TFunc1, TFunc2>, TResult>> expression)
{
    var body = expression.Body.Replace(
        expression.Parameters[1],
        lambda);
    return Expression.Lambda<Func<TSource, TResult>>(
        body, expression.Parameters[0]);
}

(使用上述链接中的辅助类)

现在我们只需要调用该方法。请注意,由于这一切的运作方式,我们将无法完全依赖类型推断,因此需要明确指定某些类型。

query = query.Where(predicate.EmbedLambda((UnknownType o, Func<Person, bool> p) => o.Persons.Any(p)));

【讨论】:

  • “但是很遗憾,这行不通,因为o.Persons 的类型不是IQueryable...” 但是它可以很容易地通过添加.AsQueryable(),从而消除了对具有丑陋调用语法的额外表达式助手的需要。
  • @IvanStoev 假设查询提供者知道如何翻译,但很多人不知道。它不会产生与内联 lambda 相同的查询,而这是查询提供程序通常期望发生的。
  • 嗯,一般来说你是对的,但这个问题有一个上下文([entity-framework-core] 标签,这就是我登陆这里的原因)。幸运的是,目标查询提供程序 (EF Core) 认识到此类功能的需求,并且属性会处理它。
猜你喜欢
  • 1970-01-01
  • 2011-04-16
  • 1970-01-01
  • 2010-09-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-07-12
相关资源
最近更新 更多