使用匿名筛选方法创建复合条件
本文关键字:创建 复合 条件 方法 筛选 | 更新日期: 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);
而且你的代码很糟糕,不仅仅是因为它工作错误,还因为很难以这种风格编写代码。原因:
- 此代码不遵循 DRY 原则。您有两个相同的 lambda 用于检查
StatusA
(查看您的三元运算符(和两个相同的 lambda 用于检查StatusB
您的三元运算符太长,带有空检查。这很糟糕,因为你看不到大致的图片,你的眼睛集中在语法问题上。您可以为函数编写和扩展方法 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));
读得更好。
您的筛选器是全局筛选器。对于较少的筛选条件,全局筛选器的写入更简单,行数也较少,但了解筛选器则更复杂。以这种方式重写它:
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<..>
来编写过滤器,但我们仍然有问题。
如果结果过滤器为空,我们将遇到什么问题?答:
ArgumentNullException
由于文档。我们必须考虑这个案例。使用简单
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
有什么意见吗?