使用匿名过滤器方法创建复合条件

时间:2013-07-11 18:39:18

标签: c# linq filter anonymous-function

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

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

但是现在,它就像:

我喜欢where子句中的过滤器(ItemNumber == X AND St​​atementStatus == SatusA或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{ ....
        }
}

2 个答案:

答案 0 :(得分: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 principle。你有两个相同的lambdas检查StatusA(查看你的三元运算符)和两个检查StatusB
  2. 的lambda
  3. 你有太长的三元运算符和空检查。这很糟糕,因为你没有看到一般的图片,你的眼睛专注于语法问题。您可以为funcs编写extension method 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));
    

    阅读更好。

  4. 您的过滤器是全局过滤器。对于很少的滤波器条件,全局滤波器的写入更简单,并且线的数量很少,但是理解滤波器会更复杂。用这种方式重写它:

    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);
    
  5. 好的,我们已经认为我们可以通过将它们组合为Func<..>来编写过滤器,但我们仍有问题。

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

    2. 使用简单Func<...>可以带来哪些其他问题?那么,你必须知道IEnumerable<T>IQueryable<T>接口之间的区别。简单来说,IEnumerable上的所有操作都会导致所有元素的简单迭代(好吧,它是懒惰的,IEnumerable真的比IQueryable慢)。因此,例如,对于具有10000个对此过滤器不利的元素的集合中的Where(filter),Take(100),ToList()和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>
      }
      
    3. 好。现在你明白了这个问题。因此,可以通过这种方式编写简单正确的代码:

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

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

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

      Expression

      对于And而言也一样。现在我们可以编写过滤器了:

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

      您可能遇到问题,因为.NET中的{ 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); } 中的类ExpressionVisitor&lt; 4.0是密封的。您可以编写自己的ExpressionVisitor,也可以从this article复制它。

答案 1 :(得分:0)

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

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

有任何意见吗?