首页 文章

在Entity Framework中使用OR条件进行动态查询

提问于
浏览
28

我正在创建一个创建一个搜索数据库的应用程序,并允许用户动态添加任何标准(大约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”条件?

Edit with my solution - 9/29/2015

自发布以来,我注意到这已经得到了一点关注,所以我决定发布我的解决方案

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

2 回答

  • 4

    您可能正在寻找像Predicate Builder这样的东西,它允许您更容易地控制where语句的AND 's and OR' .

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

  • 16

    虽然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.last.Contains(last) 中的 q 必须与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();
    

相关问题