将lambda查询中的Where子句与OR组合在一起

huangapple go评论65阅读模式
英文:

Combine Where clauses with OR in lambda query

问题

if (filtersMinDate != default)
{
    query = query.Where(tv => tv.TimeStamp >= filtersMinDate);
}

if (filtersMaxDate != default)
{
    query = query.Where(tv => tv.TimeStamp <= filtersMaxDate);
}

如果两个日期都设置了,我会得到一个最终的查询,其中使用了 "and",与以下代码相同:

query = query.Where(tv => tv.TimeStamp >= filtersMinDate && tv.TimeStamp <= filtersMaxDate);

但实际上我想要动态生成一个查询,将所有条件都用 "or" 组合在一起,所以应该与以下代码相同:

query = query.Where(tv => tv.TimeStamp >= filtersMinDate || tv.TimeStamp <= filtersMaxDate);
英文:

I try to dynamically create a query but want to use or between the conditions. Currently my code looks like this:

if (filtersMinDate != default)
{
    query = query.Where(tv =&gt; tv.TimeStamp &gt;= filtersMinDate);
}

if (filtersMaxDate != default)
{
    query = query.Where(tv =&gt; tv.TimeStamp &lt;= filtersMaxDate);
}

If both dates are set, I get a final query with an and, would be the same as following:

query = query.Where(tv =&gt; tv.TimeStamp &gt;= filtersMinDate &amp;&amp; tv.TimeStamp &lt;= filtersMaxDate);

But what I actually want is to dynamically generate a query that combines all conditions with an or, so should be the same as following:

query = query.Where(tv =&gt; tv.TimeStamp &gt;= filtersMinDate || tv.TimeStamp &lt;= filtersMaxDate);

答案1

得分: 2

你可以用两种方式实现(Linq 或 LinqKit)

  1. Linq
context.MyClass.Where(tv =>
               (filtersMinDate != default && tv.Date >= filtersMinDate)
            || (filtersMaxDate != default && tv.Date <= filtersMaxDate)).ToList();

我使用 Profiler 获取查询

exec sp_executesql N'SELECT [m].[Id], [m].[Date], [m].[LetterEnum]
FROM [MyClass] AS [m]
WHERE ([m].[Date] >= @__filtersMinDate_0) OR ([m].[Date] <= @__filtersMaxDate_1)'
,N'@__filtersMinDate_0 datetime2(7),@__filtersMaxDate_1 datetime2(7)'
,@__filtersMinDate_0='2023-06-12 03:54:02.6118428'
,@__filtersMaxDate_1='2023-06-12 03:54:03.3649045'
  1. LinqKit(LINQKit 是 LINQ to SQL 和 Entity Framework 高级用户的免费扩展)efcore-linqkit
var pre = PredicateBuilder.New<MyClass>(true);
var query = context.MyClass;
if (filtersMinDate != default)
    pre.Or(tv => tv.Date >= filtersMinDate);

if (filtersMaxDate != default)
    pre.Or(tv => tv.Date <= filtersMaxDate);

我使用 Profiler 获取查询

exec sp_executesql N'SELECT [m].[Id], [m].[Date], [m].[LetterEnum]
FROM [MyClass] AS [m]
WHERE ([m].[Date] >= @__filtersMinDate_0) OR ([m].[Date] <= @__filtersMaxDate_1)'
,N'@__filtersMinDate_0 datetime2(7),@__filtersMaxDate_1 datetime2(7)'
,@__filtersMinDate_0='2023-06-12 03:54:02.6118428'
,@__filtersMaxDate_1='2023-06-12 03:54:03.3649045'
       
英文:

You can implement with 2 ways (Linq or LinqKit)

1.Linq

context.MyClass.Where(tv =&gt;
               (filtersMinDate != default &amp;&amp; tv.Date &gt;= filtersMinDate)
            || (filtersMaxDate != default &amp;&amp; tv.Date &lt;= filtersMaxDate)).ToList();

I qet Query with Profiler

exec sp_executesql N&#39;SELECT [m].[Id], [m].[Date], [m].[LetterEnum]
FROM [MyClass] AS [m]
WHERE ([m].[Date] &gt;= @__filtersMinDate_0) OR ([m].[Date] &lt;= @__filtersMaxDate_1)&#39;
,N&#39;@__filtersMinDate_0 datetime2(7),@__filtersMaxDate_1 datetime2(7)&#39;
,@__filtersMinDate_0=&#39;2023-06-12 03:54:02.6118428&#39;
,@__filtersMaxDate_1=&#39;2023-06-12 03:54:03.3649045&#39;

2.LinqKit(LINQKit is a free set of extensions for LINQ to SQL and Entity Framework power users)efcore-linqkit

var pre = PredicateBuilder.New&lt;MyClass&gt;(true);
var query = context.MyClass;
 if (filtersMinDate != default) 
pre.Or(tv =&gt; tv.Date &gt;= filtersMinDate);

if (filtersMaxDate != default) 
pre.Or(tv =&gt; tv.Date &lt;= filtersMaxDate);

I qet Query with Profiler

exec sp_executesql N&#39;SELECT [m].[Id], [m].[Date], [m].[LetterEnum]
FROM [MyClass] AS [m]
WHERE ([m].[Date] &gt;= @__filtersMinDate_0) OR ([m].[Date] &lt;= @__filtersMaxDate_1)&#39;
,N&#39;@__filtersMinDate_0 datetime2(7),@__filtersMaxDate_1 datetime2(7)&#39;
,@__filtersMinDate_0=&#39;2023-06-12 03:54:02.6118428&#39;
,@__filtersMaxDate_1=&#39;2023-06-12 03:54:03.3649045&#39;

答案2

得分: 1

这似乎是Expression组合的任务。

当您构造一个IQueryable&lt;&gt;时,您编写的各种lambda表达式(比如Where谓词的lambda表达式)并不会编译成IL或本机代码,而是用于构建LINQ表达式:代表您编写的代码的语法树的对象。它们的功能方式非常有趣,但我会在以后的时间里保留这个讲座。重要的部分是,您可以将这些表达式拆分开来,以有趣的方式将它们重新组合在一起。

在这种情况下,我们想要将两个兼容的表达式合并到第三个表达式中,使用OrElse表达式节点。在这样做的同时,我们必须确保对行对象参数(在您的示例中是tv)的引用被规范化,以便整个表达式引用正确的对象。

这是一个我最喜欢的小表达式辅助类之一,用于进行子表达式替换,在表达式组合过程中很有用:

// using System.Linq.Expression;

// 实现简单的子表达式替换
internal class ReplaceVisitor : ExpressionVisitor
{
	private Expression _from;
	private Expression _to;
	
	private ReplaceVisitor(Expression from, Expression to) 
	{
		_from = from;
		_to = to;
	}
	
	// 返回一个具有'from'替换为'to'的相同类型的新表达式
	public static T Replace&lt;T&gt;(T expression, Expression from, Expression to)
		where T : Expression
	{
		var visitor = new ReplaceVisitor(from, to);
		return (T)visitor.Visit(expression)!;
	}
	
	// 当找到'from'时,执行实际的替换
	public override Expression? Visit(Expression? node)
	{
		if (node == _from)
			return _to;
		return base.Visit(node);
	}
}

这将为我们修复参数引用,现在进入有趣的部分。

让我们创建一个名为Or的扩展方法,将两个谓词表达式合并在一起:

internal static class ExpressionComposition
{
	// 返回一个等效于'left || right'的谓词
	public static Expression&lt;Func&lt;T, bool&gt;&gt;? Or&lt;T&gt;(this Expression&lt;Func&lt;T, bool&gt;&gt;? left, Expression&lt;Func&lt;T, bool&gt;&gt;? right)
	{
		// 首先消除空子句
		if (left is null)
			return right;
		if (right is null)
			return left;
		
		// 现在获取右子句的兼容版本的主体
		var rightBody = ReplaceVisitor.Replace
		(
			right.Body, 
			right.Parameters[0], 
			left.Parameters[0]
		);
		
		// 使用'OrElse'创建新表达式
		return Expression.Lambda&lt;Func&lt;T, bool&gt;&gt;
		(
			Expression.OrElse(left.Body, rightBody), 
			left.Parameters[0]
		);
	}
}

现在,您的筛选方法可以简单地将多个谓词组合在一起:

// 'RowType' 这里是您的... 行类型。
Expression&lt;Func&lt;RowType, bool&gt;&gt;? predicate = null;

if (filtersMinDate != default)
	predicate = predicate.Or(tv =&gt; tv.TimeStamp &gt;= filtersMinDate);

if (filtersMaxDate != default)
{
	// 捕获过滤值以避免可变性
	DateTime maxDate = filtersMaxDate;	
	predicate = predicate.Or(tv =&gt; tv.TimeStamp &lt;= maxDate);
}

// 最后,使用构建的谓词过滤查询:
if (predicate is not null)
	query = query.Where(predicate);

这里需要小心的一件事是捕获的变量可以在创建表达式和最终调用表达式之间发生更改。在上面的代码中,我已经将filtersMaxDate捕获到了一个作用域变量中,然后才创建了表达式。由于您不能在封闭的if语句的范围之外轻松地更改该变量,所以其值会立即固定。在执行查询之前更改filterMaxDate不会更改查询使用的值。然而,更改filterMinDate将更改查询的结果。

选择其中一种方法,并在所有情况下坚持使用它。要么您的查询应该对过滤值的更改做出响应,要么它应该生成(相对)不可变的输出。

如果您愿意,您可以编译表达式并直接针对内存中的对象执行它:

Func&lt;RowType, bool&gt; fn = predicate.Compile();

if (fn(row)) 
{
	// ...
}
英文:

Sounds like a job for Expression composition.

When you're constructing an IQueryable&lt;&gt; the various lambdas you write (like the lambda for the Where predicate) aren't compiled to IL or native code, they are instead used to build LINQ Expressions: objects that represent the code you write as syntax trees. The way they function is all kinds of interesting, but I'll save that lecture for another time. The important part is that you can tease those expressions apart and weave them back together again in interesting ways.

In this case we want to take two compatible expressions and combine their bodies into a third using the OrElse expression node. While doing so we'll have to make sure that references to the row object parameter (tv in your examples) are normalized so that the whole expression references the correct object.

Here's one of my favorite little expression helper classes to do sub-expression replacement, which is handy for getting the parameter instances to match up during expression composition:

// using System.Linq.Expression;

// Implement simple sub-expression replacement
internal class ReplaceVisitor : ExpressionVisitor
{
	private Expression _from;
	private Expression _to;
	
	private ReplaceVisitor(Expression from, Expression to) 
	{
		_from = from;
		_to = to;
	}
	
	// Return a new expression of the same type with &#39;from&#39; replaced with &#39;to&#39;
	public static T Replace&lt;T&gt;(T expression, Expression from, Expression to)
		where T : Expression
	{
		var visitor = new ReplaceVisitor(from, to);
		return (T)visitor.Visit(expression)!;
	}
	
	// This does the actual replacing when &#39;from&#39; is located.
	public override Expression? Visit(Expression? node)
	{
		if (node == _from)
			return _to;
		return base.Visit(node);
	}
}

That will fix up our parameter references for us, now onto the fun part.

Let's make an extension method named Or that will combine two predicate expressions together:

internal static class ExpressionComposition
{
	// Return a predicate equivalent to &#39;left || right&#39;
	public static Expression&lt;Func&lt;T, bool&gt;&gt;? Or&lt;T&gt;(this Expression&lt;Func&lt;T, bool&gt;&gt;? left, Expression&lt;Func&lt;T, bool&gt;&gt;? right)
	{
		// First let&#39;s eliminate null clauses
		if (left is null)
			return right;
		if (right is null)
			return left;
		
		// Now get a compatible version of the right clause&#39;s body
		var rightBody = ReplaceVisitor.Replace
		(
			right.Body, 
			right.Parameters[0], 
			left.Parameters[0]
		);
		
		// This creates the new expression using &#39;OrElse&#39;
		return Expression.Lambda&lt;Func&lt;T, bool&gt;&gt;
		(
			Expression.OrElse(left.Body, rightBody), 
			left.Parameters[0]
		);
	}
}

Now your filter method can combine multiple predicates together simply:

// &#39;RowType&#39; here is your... row type.
Expression&lt;Func&lt;RowType, bool&gt;&gt;? predicate = null;

if (filtersMinDate != default)
	predicate = predicate.Or(tv =&gt; tv.TimeStamp &gt;= filtersMinDate);

if (filtersMaxDate != default)
{
	// Capture filter value to avoid mutability
	DateTime maxDate = filtersMaxDate;	
	predicate = predicate.Or(tv =&gt; tv.TimeStamp &lt;= maxDate);
}

// Finally, filter your query with the constructed predicate:
if (predicate is not null)
	query = query.Where(predicate);

One thing to be careful of here is that captured variables can change between when you create the expression and when it is finally invoked. In the code above I've captured filtersMaxDate into a scoped variable before creating the expression. Since you can't alter that variable outside of the scope of the enclosing if statement (at least not easily) the value is fixed immediately. Changing filterMaxDate before the query is executed won't change the value used by the query. Changing filterMinDate however will change the results of the query.

Pick one of the two and stick with it in all the cases. Either your query should be responsive to changes in the filter values or it should produce a (relatively) immutable output.

And if you want you can compile the expression and execute it directly against objects in memory:

Func&lt;RowType, bool&gt; fn = predicate.Compile();

if (fn(row)) 
{
	// ...
}

huangapple
  • 本文由 发表于 2023年6月12日 05:52:19
  • 转载请务必保留本文链接:https://go.coder-hub.com/76452643.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定