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

Ben*_*son 38 c# linq sql-server linq-to-entities entity-framework

我正在创建一个创建一个搜索数据库的应用程序,并允许用户动态添加任何标准(大约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();
Run Code Online (Sandbox Code Playgroud)

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

SQL

SELECT
    [Id],
    [FirstName],
    [LastName],
    ...etc
FROM [dbo].[Names]
WHERE [FirstName] LIKE '%first%'
  AND [LastName] LIKE '%last%'
Run Code Online (Sandbox Code Playgroud)

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

SQL

SELECT
    [Id],
    [FirstName],
    [LastName],
    ...etc
  FROM [dbo].[Names]
WHERE [FirstName] LIKE '%first%'
  OR [LastName] LIKE '%last%' <-- NOTICE THE "OR"
Run Code Online (Sandbox Code Playgroud)

通常,条件不会大于查询的两个或三个项目,但将它们组合成一个巨大的查询不是一种选择.我尝试过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);
}
Run Code Online (Sandbox Code Playgroud)

Ste*_*n V 21

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

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


B12*_*ter 13

是否有一种简单而干净的方法可以使用实体框架将“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();
Run Code Online (Sandbox Code Playgroud)

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

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

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

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


Str*_*ior 8

尽管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));
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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);
}
Run Code Online (Sandbox Code Playgroud)

这是必要的,因为两个不同表达式中的lambda参数实际上是不同的参数,即使它们具有相同的名称。例如,如果要结束于q => q.first.Contains(first) || q.last.Contains(last),则qin q.last.Contains(last)必须与lambda表达式开头提供的完全相同 q

接下来,我们需要一种通用Join方法,该方法能够将Func<T, TReturn>样式Lambda表达式与给定的Binary Expression生成器连接在一起。

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);
}
Run Code Online (Sandbox Code Playgroud)

我们将与配合使用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();
Run Code Online (Sandbox Code Playgroud)

  • 这有病啊!我为其制作了一个工作示例的 GitHub 要点(至少在 .NET 5.0 中编译)https://gist.github.com/princefishthrower/6620fcded6b2600bbd10f4100c55401c (2认同)