使用匿名筛选方法创建复合条件

本文关键字:创建 复合 条件 方法 筛选 | 更新日期: 2023-09-27 18:21:57

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

我喜欢 where 子句中的过滤器是什么(项目编号 == X AND ( 语句状态 == SatusA 或语句状态 == 状态 B ( (

但是现在,它就像:

我喜欢子句中的过滤器(项目编号 == X 和语句状态 == SatusA 或语句状态 == 状态 B(

因为 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{ ....
        }
}

使用匿名筛选方法创建复合条件

发生这种情况不是因为 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 原则。您有两个相同的 lambda 用于检查StatusA(查看您的三元运算符(和两个相同的 lambda 用于检查StatusB
  2. 您的三元运算符太长,带有空检查。这很糟糕,因为你看不到大致的图片,你的眼睛集中在语法问题上。您可以为函数编写和扩展方法 AndNull。喜欢这个:

    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. 使用简单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);

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

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

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

和也是如此。现在我们可以编写过滤器:

{
    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 中PredicateBuilder中的类ExpressionVisitor是密封的。您可以编写自己的ExpressionVisiter,也可以从本文中复制它。

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

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

有什么意见吗?