1

我正在尝试使用 linq 编辑搜索工具,

我喜欢 where 子句中的过滤器是 (ItemNumber == X AND (StatementStatus == SatusA Or StatementStatus == StatusB))

但现在,它是这样的:

我喜欢 where 子句中的过滤器是 (ItemNumber == X AND St​​atementStatus == SatusA Or StatementStatus == StatusB )

因为 AND 比 OR 具有更高的操作优先级,所以结果不是我想要的。:) 能否请你帮忙?

using (var ctx = new MyContext())    {
    Func<Statement, bool> filter = null;

    if (!string.IsNullOrEmpty(request.ItemNumber))
        filter = new Func<Statement, bool>(s => s.StatementDetails.Any(sd => sd.ItemNumber == request.ItemNumber));

    if (request.StatusA)
        filter = filter == null ? new Func<Statement, bool>(s => s.StatementStatus == StatementStatusType.StatusA) : 
            filter.And(s => s.StatementStatus == StatementStatusType.StatusA);

    if (request.StatusB)
        filter = filter == null ? new Func<Statement, bool>(s => s.StatementStatus == StatementStatusType.StatusB) :
            filter.Or(s => s.StatementStatus == StatementStatusType.StatusB);

    var results = ctx.Statements
        .Include("StatementDetails")
        .Include("StatementDetails.Entry")
        .Where(filter)
        .Take(100)
        .Select(s => new StatementSearchResultDTO{ ....
        }
}

4

2 回答 2

1

发生这种情况并不是因为 AND 的优先级高于 OR。现实中会发生什么:

var firstFilter = ...; // itemNumber
var secondFilter = ...; // statusA
var firstAndSecondFilter = firstFilter.And(secondFilter); // itemNumber && statusA
var thirdFilter = ...; // statusB
var endFilter = firstAndSecondFilter.Or(thirdFilter) // (itemNumber && statusA) || statusB.

问题 - 错误的控制流程。你必须这样做:

var filterByA = ...;
var filterByB = ...;
var filterByAorB = filterByA.Or(filterByB);
var filterByNumber = ...;
var endFiler = filterByNumber.And(filterByAorB);

而且您的代码很糟糕,不仅因为它工作错误,还因为很难以这种风格编写代码。原因:

  1. 此代码不遵循DRY 原则。您有两个相同的 lambdas 检查StatusA(查看您的三元运算符)和两个相同的 lambdas 检查StatusB
  2. 您的三元运算符太长,带有空检查。这很糟糕,因为您看不到一般情况,您的眼睛专注于语法问题。您可以为函数编写和扩展方法AndNullable。像这样:

    static Func<T1, TOut> AndNullable<T1, TOut>(this Func<T1, TOut> firstFunc, Func<T1, TOut> secondFunc) {
        if (firstFunc != null) {
             if (secondFunc != null)
                return firstFunc.And(secondFunc);
             else
                return firstFunc;
        }
        else {
             if (secondFunc != null)
                return secondFunc;
             else
                return null;
        }
    }
    

    Or 也一样。现在你的代码可以这样写:

    Func<Statement, bool> filter = null;
    
    if (request.StatusA)
        filter = s => s.StatementStatus == StatementStatusType.StatusA;
    
    if (request.StatusB)
        filter = filter.OrNullable(s => s.StatementStatus == StatementStatusType.StatusB);
    
    if (!string.IsNullOrEmpty(request.ItemNumber))
        filter = filter.AndNullable(s => s.StatementDetails.Any(sd => sd.ItemNumber == request.ItemNumber));
    

    越读越好。

  3. 您的过滤器是全局过滤器。全局过滤器的编写在过滤条件少且行数较少的情况下更简单,但理解您的过滤器更复杂。以这种方式重写它:

    Func<Statement, bool> filterByStatusA = null;
    Func<Statement, bool> filterByStatusB = null;
    
    if (request.StatusA)
        filterByStatusA = s => s.StatementStatus == StatementStatusType.StatusA;
    
    if (request.StatusB)
        filterByStatusB = s => s.StatementStatus == StatementStatusType.StatusB;
    
    Func<Statement, bool> filterByStatuses = filterByStatusA.OrNullable(filterByStatusB);
    
    Func<Statement, bool> filterByItemNumber = null;
    if (!string.IsNullOrEmpty(request.ItemNumber))
        filterByItemNumber = s => s.StatementDetails.Any(sd => sd.ItemNumber == request.ItemNumber);
    
    Func<Statement, bool> endFilter = filterByItemNumber.And(filterByStatuses);
    

好的,我们已经超越了如何通过组合它们来编写过滤器,Func<..>但我们仍然有问题。

  1. 如果结果过滤器为空,我们会遇到什么问题?答:ArgumentNullException由于文档。我们必须思考这个案例。

  2. 使用 simple 还会遇到哪些问题Func<...>IEnumerable<T>那么,你必须知道和IQueryable<T>接口之间的区别。简而言之,对 IEnumerable 的所有操作都会导致对所有元素的简单迭代(嗯,它很懒,IEnumerable 确实比 IQueryable 慢)。因此,例如,将 Where(filter)、Take(100)、ToList() 组合到具有 10000 个对该过滤器有害的元素和 400 个有益的元素的集合上,将导致迭代超过 10100 个元素。如果您为 IQueryable 编写了类似的代码,则过滤请求将在数据库服务器上发送,并且如果您在数据库上配置了索引,该服务器将仅迭代 ~400(或 1000,但不是 10100)。那么你的代码会发生什么。

    var results = ctx.Statements // you are getting DbSet<Statement> that implements interface IQueryable<Statement> (and IQueryable<T> implements IEnumerable<T>)
                     .Include("StatementDetails") // still IQueryable<Statement>
                     .Include("StatementDetails.Entry") // still IQueryable<Statement>
                     .Where(filter) // Cuz your filter is Func<..> and there are no extension methods on IQueryable that accepts Func<...> as parameter, your IQueryable<Statement> casted automatically to IEnumerable<Statement>. Full collection will be loaded in your memory and only then filtered. That's bad
                     .Take(100) // IEnumerable<Statement>
    .Select(s => new StatementSearchResultDTO { .... // IEnumerable<Statement> -> IEnumerable<StatementSearchResultDTO>
    }
    

好的。现在你明白问题所在了。因此,可以这样编写适合您的简单正确代码:

using (var ctx = new MyContext())    {
    results = ctx.Statements
        .Include("StatementDetails")
        .Include("StatementDetails.Entry")
        .AsQueryable();

    if (!string.IsNullOrEmpty(request.ItemNumber))
        results = results.Where(s => s.StatementDetails.Any(sd => sd.ItemNumber == request.ItemNumber));

    if (request.StatusA) {
        if (request.StatusB)
            results = results.Where(s => s.StatementStatus == StatementStatusType.StatusA || 
                                         s.StatementStatus == StatementStatusType.StatusA);
        else 
            results = results.Where(s => s.StatementStatus == StatementStatusType.StatusA);
    }
    else {
        if (request.StatusB) {
            results = results.Where(s => s.StatementStatus == StatementStatusType.StatusB);
        }
        else {
            // do nothing
        }
    }

    results = .Take(100)
              .Select(s => new StatementSearchResultDTO{ ....
              };

    // .. now you can you results.
}

是的,完全丑陋,但现在你的数据库解决了如何找到满足过滤器的语句。因此,这个请求是尽可能快的。现在我们必须了解我在上面编写的代码中发生了什么神奇的事情。让我们比较两个代码示例:

results = results.Where(s => s.StatementDetails.Any(sd => sd.ItemNumber == request.ItemNumber));

还有这个:

Func<Statement, bool> filter = s => s.StatementDetails.Any(sd => sd.ItemNumber == request.ItemNumber);
results = results.Where(filter);

有什么区别?为什么首先更快?答:当编译器看到第一个代码时,它会检查resultsis的类型IQueryable<T>IEnumerable<T>因此括号内的条件可以有类型Func<Statement, bool>(编译函数)或Expression<Func<Statement, bool>>(数据,可以在函数中编译)。编译器选择Expression(为什么 - 真的不知道,只是选择)。第一个对象查询的请求不是在 C# 语句中编译,而是在 SQL 语句中编译并发送到服务器。由于存在索引,您的 SQL 服务器可以优化请求。

好吧,更好的方法 - 编写自己的表达式。有不同的方法可以编写自己的表达式,但是有一种方法可以用不难看的语法来编写它。您不能只从另一个表达式调用一个表达式的问题 - 实体框架不支持并且另一个 ORM 不支持。因此,我们可以使用 Pete Montgomery 的 PredicateBuilder:链接。然后在适合我们的表达式上写两个简单的扩展。

public static Expression<Func<T, bool>> OrNullable<T>(this Expression<Func<T, bool>> first, Expression<Func<T, bool>> second)
{
    if (first != null && second != null)
        return first.Compose(second, Expression.OrElse);

    if (first != null)
        return second;

    if (second != null)
}

And 也一样。现在我们可以编写我们的过滤器了:

{
    Expression<Func<Statement, bool>> filterByStatusA = null;
    Expression<Func<Statement, bool>> filterByStatusB = null;

    if (request.StatusA)
        filterByStatusA = s => s.StatementStatus == StatementStatusType.StatusA;

    if (request.StatusB)
        filterByStatusB = s => s.StatementStatus == StatementStatusType.StatusB;

    Expression<Func<Statement, bool>> filterByStatuses = filterByStatusA.OrNullable(filterByStatusB);

    Expression<Func<Statement, bool>> filterByItemNumber = null;
    if (!string.IsNullOrEmpty(request.ItemNumber))
        filterByItemNumber = s => s.StatementDetails.Any(sd => sd.ItemNumber == request.ItemNumber);

    Expression<Func<Statement, bool>> endFilter = filterByItemNumber.And(filterByStatuses);

    requests = ...;
    if (endFilter != null)
       requests = requests.Where(endFilter);
}

您可能会遇到问题,因为 .NET < 4.0 中的类ExpressionVisitorPredicateBuilder密封的。您可以编写自己的 ExpressionVisitor 或从本文中复制它。

于 2013-07-11T22:57:56.753 回答
0

好的,这是我解决它的方法:

filter.And(s => (request.StatusA && s.StatementStatus == StatementStatusType.StatusA) ||
                            (request.StatusB && s.StatementStatus == StatementStatusType.StautsB) ||
                            !(request.StatusA || request.StatusB)); //None selected = All selected

任何意见?

于 2013-07-11T21:48:22.283 回答