8wDlpd.png
8wDFp9.png
8wDEOx.png
8wDMfH.png
8wDKte.png

实体框架中使用 OR 条件的动态查询

zest16 2月前

71 0

我正在创建一个搜索数据库的应用程序,并允许用户动态添加任何条件(大约 50 个可能),类似于以下 SO 问题:使用创建动态查询

我正在创建一个搜索数据库的应用程序,并允许用户动态添加任何条件(大约 50 个),就像以下 SO 问题一样: 使用实体框架创建动态查询 。我目前正在进行一个检查每个条件的搜索,如果它不为空,它会将其添加到查询中。

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);
}
帖子版权声明 1、本帖标题:实体框架中使用 OR 条件的动态查询
    本站网址:http://xjnalaquan.com/
2、本网站的资源部分来源于网络,如有侵权,请联系站长进行删除处理。
3、会员发帖仅代表会员个人观点,并不代表本站赞同其观点和对其真实性负责。
4、本站一律禁止以任何方式发布或转载任何违法的相关信息,访客发现请向站长举报
5、站长邮箱:yeweds@126.com 除非注明,本帖由zest16在本站《asp.net-mvc》版块原创发布, 转载请注明出处!
最新回复 (0)
  • Predicate Builder 确实是这里的答案,但我只是好奇...为什么将它们组合起来“不是一个选择”?您说它不会大于两个或三个项目。SQL Server 很可能能够优化您的大型组合查询,使其以与具有相同条件的单个查询类似的速度运行。您是否对此进行了测试并发现连接查询是性能瓶颈?

  • 查看谓词生成器,我相信这就是答案。谢谢 Steven V,如果你想提交答案,我会将其标记为已回答。将它们组合成一个大型查询不是一个选择,因为我需要检查每个条件的行内空白,然后我会进行实际过滤,而这有超过 50 个条件。这会使查询变得缓慢且难以管理。

  • 您可能正在寻找类似 Predicate Builder 的 ,它可以让您更轻松地控制 where 语句的 AND 和 OR。

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

  • 虽然 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 => q.first.Contains(first) || q.last.Contains(last) ,则 q in q.last.Contains(last) 必须与 lambda 表达式开头提供的 q 完全相同

    接下来,我们需要一种通用 Join 方法,能够将 Func<T, TReturn> -style 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/…

  • 我怎样才能像 query.Where(entity => entity.Collection.Any(joinedSearchCriteria) 那样使用它?我怎样才能为 where 查询构建表达式?我已经尝试了几个小时,但没有找到获取 Any 方法信息的方法。

  • @Snaketec:只要您的标准的通用类型与 entity.Collection 匹配,它应该可以正常工作。听起来也许您应该提出一个单独的问题,其中包含有关您尝试过的内容以及您看到的行为/错误的详细信息。

  • 是否有一种简单干净的方法可以使用实体框架向动态生成的查询添加“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();
    

    根据先前评估的前提(例如 where 动态地打开或关闭过滤表达式的 OR 部分 isFirstValid

    例如,如果 短路FirstValid 不是 true ,则被 name.first.Contains(first) is既不会被执行,也不会影响结果集。此外,EF Core DefaultQuerySqlGenerator 在执行之前 进一步 优化和减少 where 其中的布尔表达式 false && x || true && y || false && z 可以 y 通过简单的静态分析将其简化为简单)。

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

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

  • Kas 2月前 0 只看Ta
    引用 10

    是的,动态的,因为你有两个可选的 OR 语句,但不是真正的动态,因为你仍然必须在 Where() 函数中写入你需要的任何内容......

  • @fullStackChris 是的,这就是我在答案末尾的“免责声明”中提到的缺点。但通常这种可选的 OR 语句非常方便,并且足够“动态”以解决手头的问题。但是,当然,对于更复杂的查询谓词链接,人们会诉诸其他技术之一。

  • 根据 StriplingWarrior 的回答 ,我编写了我的 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="IQueryable{T}"/> 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="IQueryable{T}"/> to <see cref="IQueryOr{T}"/> to make next condition append as or predicate.
            /// Call <see cref="IQueryOr{T}.AsQueryable"/> back to <see cref="IQueryable{T}"/> 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,并且每个返回的对象都是一个不可变的查询。

  • rbp 2月前 0 只看Ta
    引用 14

    另外,请仔细考虑当 WhereOr() 从未被调用时用户会有什么期望。他们应该得到异常吗?还是没有应用任何过滤器的原始查询?

  • @StriplingWarrior 我修复了问题:返回相同的对象 - 现在它返回新对象;当用户从未调用 WhereOr 时抛出异常 - 现在将返回原始查询。

  • 不过,您仍在更改原始对象的谓词。因此,即使您不执行赋值 (queryOr = ...),调用 queryOr.WhereOr(...) 也会更改 queryOr 对象。考虑使用不可变集合而不是列表作为谓词吗?

返回
作者最近主题: