实体框架中带有 OR 条件的动态查询

Posted

技术标签:

【中文标题】实体框架中带有 OR 条件的动态查询【英文标题】:Dynamic query with OR conditions in Entity Framework 【发布时间】:2013-12-02 00:19:52 【问题描述】:

我正在创建一个搜索数据库并允许用户动态添加任何条件(大约 50 个可能)的应用程序,就像下面的 SO 问题:Creating dynamic queries with entity framework。我目前正在检查每个条件的搜索,如果它不为空,则将其添加到查询中。

C#

var query = Db.Names.AsQueryable();
  if (!string.IsNullOrWhiteSpace(first))
      query = query.Where(q => q.first.Contains(first));
  if (!string.IsNullOrWhiteSpace(last))
      query = query.Where(q => q.last.Contains(last));
  //.. around 50 additional criteria
  return query.ToList();

此代码在 sql server 中产生类似于以下内容(为了便于理解,我进行了简化)

SQL

SELECT
    [Id],
    [FirstName],
    [LastName],
    ...etc
FROM [dbo].[Names]
WHERE [FirstName] LIKE '%first%'
  AND [LastName] LIKE '%last%'

我现在正在尝试添加一种方法,通过实体框架使用 C# 生成以下 SQL,但使用 OR 而不是 AND,同时仍然能够动态添加条件。

SQL

SELECT
    [Id],
    [FirstName],
    [LastName],
    ...etc
  FROM [dbo].[Names]
WHERE [FirstName] LIKE '%first%'
  OR [LastName] LIKE '%last%' <-- NOTICE THE "OR"

通常,查询的条件不会大于两个或三个项目,但不能将它们组合成一个巨大的查询。我尝试过 concat、union 和 intersect,它们都只是复制查询并将它们与 UNION 连接起来。

是否有一种简单而干净的方法可以将“OR”条件添加到使用实体框架动态生成的查询中?

使用我的解决方案进行编辑 - 2015 年 9 月 29 日

自从发布了这个,我注意到这个问题已经引起了一些关注,所以我决定发布我的解决方案

// Make sure to add required nuget
// PM> Install-Package LinqKit

var searchCriteria = new 

    FirstName = "sha",
    LastName = "hill",
    Address = string.Empty,
    Dob = (DateTime?)new DateTime(1970, 1, 1),
    MaritalStatus = "S",
    HireDate = (DateTime?)null,
    LoginId = string.Empty,
;

var predicate = PredicateBuilder.False<Person>();
if (!string.IsNullOrWhiteSpace(searchCriteria.FirstName))

    predicate = predicate.Or(p => p.FirstName.Contains(searchCriteria.FirstName));


if (!string.IsNullOrWhiteSpace(searchCriteria.LastName))

    predicate = predicate.Or(p => p.LastName.Contains(searchCriteria.LastName));


// Quite a few more conditions...

foreach(var person in this.Persons.Where(predicate.Compile()))

    Console.WriteLine("First: 0 Last: 1", person.FirstName, person.LastName);

【问题讨论】:

您可能需要查看Predicate Builder 之类的内容,这样可以更轻松地进行与和或的操作。 Predicate Builder 确实是这里的答案,但我只是好奇......为什么将它们结合起来“不是一种选择”?你说它不会超过两三个项目。 SQL Server 可能能够优化您的大型组合查询,使其以与具有相同条件的单个查询相似的速度运行。您是否对此进行了测试并发现加入查询是性能瓶颈? 查看谓词构建器,我相信这是答案。谢谢 Steven V,如果您想提交答案,我会将其标记为已回答。将它们组合成一个大型查询不是一种选择,因为我需要检查每个内联空白条件,然后我会进行实际过滤,即超过 50 个条件。这会使查询变得缓慢且难以管理。 Predicate Builder 的替代方案是此处接受的答案中的代码:***.com/questions/15677492/… 【参考方案1】:

您可能正在寻找类似 @​​987654321@ 的东西,它可以让您更轻松地控制 where 语句的 AND 和 OR。

还有 Dynamic Linq 允许您像 SQL 字符串一样提交 WHERE 子句,它会将其解析为 WHERE 的正确谓词。

【讨论】:

【参考方案2】:

虽然 LINQKit 及其 PredicateBuilder 相当通用,但可以使用一些简单的实用程序更直接地执行此操作(每个实用程序都可以作为其他表达式操作操作的基础):

首先,一个通用的表达式替换器:

public class ExpressionReplacer : ExpressionVisitor

    private readonly Func<Expression, Expression> replacer;

    public ExpressionReplacer(Func<Expression, Expression> replacer)
    
        this.replacer = replacer;
    

    public override Expression Visit(Expression node)
    
        return base.Visit(replacer(node));
    

接下来,一个简单的实用方法可以用给定表达式中的另一个参数替换一个参数的用法:

public static T ReplaceParameter<T>(T expr, ParameterExpression toReplace, ParameterExpression replacement)
    where T : Expression

    var replacer = new ExpressionReplacer(e => e == toReplace ? replacement : e);
    return (T)replacer.Visit(expr);

这是必要的,因为两个不同表达式中的 lambda 参数实际上是不同的参数,即使它们具有相同的名称。例如,如果您想以q =&gt; q.first.Contains(first) || q.last.Contains(last) 结尾,那么q.last.Contains(last) 中的q 必须完全相同q 是在 lambda 表达式开头提供的。 p>

接下来,我们需要一个通用的Join 方法,该方法能够将Func&lt;T, TReturn&gt; 样式的 Lambda 表达式与给定的二进制表达式生成器连接在一起。

public static Expression<Func<T, TReturn>> Join<T, TReturn>(Func<Expression, Expression, BinaryExpression> joiner, IReadOnlyCollection<Expression<Func<T, TReturn>>> expressions)

    if (!expressions.Any())
    
        throw new ArgumentException("No expressions were provided");
    
    var firstExpression = expressions.First();
    var otherExpressions = expressions.Skip(1);
    var firstParameter = firstExpression.Parameters.Single();
    var otherExpressionsWithParameterReplaced = otherExpressions.Select(e => ReplaceParameter(e.Body, e.Parameters.Single(), firstParameter));
    var bodies = new[]  firstExpression.Body .Concat(otherExpressionsWithParameterReplaced);
    var joinedBodies = bodies.Aggregate(joiner);
    return Expression.Lambda<Func<T, TReturn>>(joinedBodies, firstParameter);

我们会将其与Expression.Or 一起使用,但您可以将相同的方法用于多种目的,例如将数字表达式与Expression.Add 结合使用。

最后,把它们放在一起,你可以得到这样的东西:

var searchCriteria = new List<Expression<Func<Name, bool>>();

  if (!string.IsNullOrWhiteSpace(first))
      searchCriteria.Add(q => q.first.Contains(first));
  if (!string.IsNullOrWhiteSpace(last))
      searchCriteria.Add(q => q.last.Contains(last));
  //.. around 50 additional criteria
var query = Db.Names.AsQueryable();
if(searchCriteria.Any())

    var joinedSearchCriteria = Join(Expression.Or, searchCriteria);
    query = query.Where(joinedSearchCriteria);

  return query.ToList();

【讨论】:

这病了!我为它制作了一个工作示例的 GitHub 要点(至少在 .NET 5.0 中编译)gist.github.com/princefishthrower/… 我的回答基于你的回答:***.com/a/69156702/6859121。谢谢。【参考方案3】:

是否有一种简单而干净的方法可以将“OR”条件添加到使用实体框架动态生成的查询中?

是的,您可以通过简单地依赖包含单个布尔表达式的单个 where 子句来实现此目的,该子句的 OR 部分在运行时动态“禁用”或“启用”,从而避免安装 LINQKit 或编写自定义谓词构建器。

参考你的例子:

var isFirstValid = !string.IsNullOrWhiteSpace(first);
var isLastValid = !string.IsNullOrWhiteSpace(last);

var query = db.Names
  .AsQueryable()
  .Where(name =>
    (isFirstValid && name.first.Contains(first)) ||
    (isLastValid && name.last.Contains(last))
  )
  .ToList();

正如您在上面的示例中所见,我们正在根据先前评估的前提(例如 isFirstValid)动态地“打开”或“关闭”where-filter 表达式的 OR 部分。

例如,如果isFirstValid 不是true,那么name.first.Contains(first) 就是short-circuited,既不会被执行也不会影响结果集。此外,EF Core 的DefaultQuerySqlGenerator 在执行之前会进一步optimize and reduce 内部的布尔表达式where(例如,false &amp;&amp; x || true &amp;&amp; y || false &amp;&amp; z 可以通过简单的静态分析简化为简单的y)。

请注意:如果所有前提都不是true,则结果集将为空——我认为这是您的情况所需的行为。但是,如果您出于某种原因更喜欢从IQueryable 源中选择所有元素,那么您可以在表达式中添加一个最终变量,以评估为true(例如.Where( ... || shouldReturnAll)var shouldReturnAll = !(isFirstValid || isLastValid) 或类似的东西)。

最后一点:这种技术的缺点是它迫使您构建一个“集中式”布尔表达式,该表达式驻留在您的查询所在的同一方法体中(更准确地说是查询的where 部分)。如果您出于某种原因想要分散谓词的构建过程并将它们作为参数注入或通过查询构建器链接它们,那么您最好按照其他答案中的建议坚持使用谓词构建器。否则,请享受这个简单的技术:)

【讨论】:

我喜欢这个最简单的动态查询。谢谢! 是的,动态的意思是你有两个可选的 OR 语句,但不是真正动态的,因为你仍然需要在 Where() 函数中编写你需要的任何东西...... @fullStackChris 是的,这是我在答案末尾的“免责声明”中提到的缺点。但通常这种可选的 OR 语句非常方便,并且“动态”足以解决手头的问题。但是,当然,对于更复杂的查询谓词链接,人们会求助于其他技术之一。【参考方案4】:

基于StriplingWarrior's answer,我编写了我的 linq 扩展来以 linq 方式完成这项工作:

https://github.com/Flithor/ReusableCodes/blob/main/EFCore/OrPredicate.cs

代码(可能不是最新的):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;

namespace Flithors_ReusableCodes

    /// <summary>
    /// Make <see cref="IQueryableT"/> support or predicate in linq way
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public interface IQueryOr<T>
    
        IQueryOr<T> WhereOr(Expression<Func<T, bool>> predicate);
        IQueryable<T> AsQueryable();
    
    /// <summary>
    /// The extension methods about or predicate
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public static class OrPredicate
    
        /// <summary>
        /// Private or predicate builder
        /// </summary>
        /// <typeparam name="T"></typeparam>
        private class OrPredicateBuilder<T> : IQueryOr<T>
        
            List<Expression<Func<T, bool>>> predicates = new List<Expression<Func<T, bool>>>();
            IQueryable<T> sourceQueryable;

            #region private methods
            internal OrPredicateBuilder(IQueryable<T> sourceQueryable) => this.sourceQueryable = sourceQueryable;
            private OrPredicate(IQueryable<T> sourceQueryable, IEnumerable<Expression<Func<T, bool>>> predicates)
            
                this.sourceQueryable = sourceQueryable;
                this.predicates.AddRange(predicates);
            

            //===============================================
            // Code From: https://***.com/a/50414456/6859121
            private class ExpressionReplacer : ExpressionVisitor
            
                private readonly Func<Expression, Expression> replacer;

                public ExpressionReplacer(Func<Expression, Expression> replacer)
                
                    this.replacer = replacer;
                

                public override Expression Visit(Expression node)
                
                    return base.Visit(replacer(node));
                
            
            private static TExpression ReplaceParameter<TExpression>(TExpression expr, ParameterExpression toReplace, ParameterExpression replacement) where TExpression : Expression
            
                var replacer = new ExpressionReplacer(e => e == toReplace ? replacement : e);
                return (TExpression)replacer.Visit(expr);
            
            private static Expression<Func<TEntity, TReturn>> Join<TEntity, TReturn>(Func<Expression, Expression, BinaryExpression> joiner, IReadOnlyCollection<Expression<Func<TEntity, TReturn>>> expressions)
            
                if (!expressions.Any())
                
                    throw new ArgumentException("No expressions were provided");
                
                var firstExpression = expressions.First();
                if (expressions.Count == 1)
                
                    return firstExpression;
                
                var otherExpressions = expressions.Skip(1);
                var firstParameter = firstExpression.Parameters.Single();
                var otherExpressionsWithParameterReplaced = otherExpressions.Select(e => ReplaceParameter(e.Body, e.Parameters.Single(), firstParameter));
                var bodies = new[]  firstExpression.Body .Concat(otherExpressionsWithParameterReplaced);
                var joinedBodies = bodies.Aggregate(joiner);
                return Expression.Lambda<Func<TEntity, TReturn>>(joinedBodies, firstParameter);
            
            //================================================
            private Expression<Func<T, bool>> GetExpression() => Join(Expression.Or, predicates);
            #endregion

            #region public methods
            public IQueryOr<T> WhereOr(Expression<Func<T, bool>> predicate)
            
                return new OrPredicate<T>(sourceQueryable, predicates.Append(predicate));
            
            public IQueryable<T> AsQueryable()
            
                if (predicates.Count > 0)
                    return sourceQueryable.Where(GetExpression());
                else // If not any predicates exists, returns orignal query
                    return sourceQueryable;
            
            #endregion
        

        /// <summary>
        /// Convert <see cref="IQueryableT"/> to <see cref="IQueryOrT"/> to make next condition append as or predicate.
        /// Call <see cref="IQueryOrT.AsQueryable"/> back to <see cref="IQueryableT"/> linq.
        /// </summary>
        /// <typeparam name="TSource"></typeparam>
        /// <param name="source"></param>
        /// <returns></returns>
        public static IQueryOr<TSource> AsWhereOr<TSource>(this IQueryable<TSource> source)
        
            return new OrPredicateBuilder<TSource>(source);
        
    

使用方法:

// IQueryable<ClassA> myQuery = ....;
  
var queryOr = myQuery.AsWhereOr();
// for a condition list ...
// queryOr = queryOr.WhereOr(a => /*some condition*/)

myQuery = queryOr.AsQueryable();

享受吧!

【讨论】:

有趣的方法。让WhereOr 更改状态并返回相同的对象是一种反模式,尤其是在 LINQ 语法中。考虑遵循OrderBy().ThenBy() 使用的模式,其中返回的接口扩展了 IQueryable,并且每个返回的对象都是一个不可变的查询。 还要仔细考虑当WhereOr() 永远不会被调用时用户会期待什么。他们应该例外吗?还是没有应用任何过滤器的原始查询? @StriplingWarrior 我修复了问题:返回相同的对象 - 现在它返回新对象;当用户从未调用 WhereOr 时抛出异常 - 现在将返回原始查询。 不过,您仍在更改原始对象的 predicates。所以调用queryOr.WhereOr(...) 会改变queryOr 对象,即使你不做赋值(queryOr = ...)。考虑对谓词使用不可变集合而不是列表? @StriplingWarrior 哎呀,我的错

以上是关于实体框架中带有 OR 条件的动态查询的主要内容,如果未能解决你的问题,请参考以下文章

如何动态构建实体框架查询?

改善使用实体框架时的搜索功能延迟

动态 SQL 到 LINQ 实体框架

动态执行SQL语句,拼接字符串,select中带有一个变量

java框架之mybatis(动态SQL)

MongoDB动态条件之分页查询