简体   繁体   English

实体框架 LINQ 复杂查询 - 组合多个谓词

[英]Entity Framework LINQ complex query - combine multiple predicates

I'm trying to create a complex Linq query that goes like this: Get all organisations which have employees that match the given filter parameters.我正在尝试创建一个复杂的 Linq 查询,如下所示:获取所有员工与给定过滤器参数匹配的组织。

Example filter:示例过滤器:

  • Firstname: John姓名:约翰
  • Name: Smith姓名:史密斯

My first attempt:我的第一次尝试:

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

The problem with this approach is that when there is someone with the firstname John (ex. John Johnson) in organisation A, and someone with the last name Smith (Jenny Smith) in organisation A. The organisation (A) that contains those two persons gets returned.这种方法的问题在于,当组织 A 中有人姓 John(例如 John Johnson),而组织 A 中有人姓 Smith(Jenny Smith)时。包含这两个人的组织 (A)被退回。 Which it shouldn't.这是不应该的。 I only want organisations that have people with the firstname "john" AND the lastname "Smith"我只希望组织中的人员名字为“john”,姓氏为“Smith”

I found a working, but dirty and non-scalable approach:我找到了一种有效但肮脏且不可扩展的方法:

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

As you can see this not a very clean solution.如您所见,这不是一个非常干净的解决方案。

I also tried using method calls inside the expression but Linq couldnt translate that.我也尝试在表达式中使用方法调用,但 Linq 无法翻译。 Is there any way I can can make a list of predicate expressions an merge them to one?有什么办法可以制作一个谓词表达式列表并将它们合并为一个? Or is there a another, better solution?还是有其他更好的解决方案?

By the way, since I need a paginated list, it all has to be in one query.顺便说一句,因为我需要一个分页列表,所以它必须在一个查询中。

For your information, this is what my filter class looks like.供您参考,这就是我的过滤器 class 的样子。 It is just a class send from my front-end with all the fields that need to be filtered.它只是从我的前端发送的 class ,其中包含所有需要过滤的字段。

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

One of the easiest solution is using LINQKit library:最简单的解决方案之一是使用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()));

Is there any way I can can make a list of predicate expressions an merge them to one?有什么办法可以制作一个谓词表达式列表并将它们合并为一个?

Yes, and that's the approach I'd prefer in this situation.是的,这是我在这种情况下更喜欢的方法。

First build the list:首先构建列表:

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

From there, you can use this implementation to And arbitrary Expressions together.从那里,您可以将此实现And任意表达式一起使用。 You'll also need to decide what to do if there are no filters to apply (I'll use a default of no filter, but you may want to do something else).如果没有要应用的过滤器,您还需要决定要做什么(我将使用默认的无过滤器,但您可能想做其他事情)。

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

Now we get to the hard part.现在我们到了最难的部分。 We have an expression that represents the lambda you want to pass to a call to Any .我们有一个表达式,它代表您想要传递给Any的调用的 lambda。 It would be nice if we could just do:如果我们可以这样做就好了:

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

But sadly, this won't work because the type of o.Persons isn't an IQueryable .但遗憾的是,这不起作用,因为o.Persons的类型不是IQueryable So now we have an expression that we want to embed in another expression in which the inner expression needs to be a lambda.所以现在我们有一个表达式,我们想要嵌入另一个表达式,其中内部表达式需要是 lambda。 Fortunately this isn't too complicated:幸运的是,这并不太复杂:

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

(Using a helper class from the above link) (使用上述链接中的帮助程序 class)

Now we just need to call the method.现在我们只需要调用该方法。 Note we won't be able to rely entirely on type inference due to the way this all works out, so some types need to be specified explicitly.请注意,由于这一切的运作方式,我们将无法完全依赖类型推断,因此需要明确指定某些类型。

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM