Подтвердить что ты не робот

Динамический запрос с условиями OR в Entity Framework

Я создаю создание приложения, которое ищет базу данных и позволяет пользователю динамически добавлять какие-либо критерии (возможно, около 50), так же как следующий вопрос SO: Создание динамических запросов с помощью структура сущностей. В настоящее время я работаю над поиском, который проверяет каждый критерий, и если он не пуст, он добавляет его в запрос.

С#

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-сервере (я упрощен для более простого понимания)

SQL

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

Теперь я пытаюсь добавить способ сгенерировать следующий SQL с С# через сущность framework, но с OR вместо AND, сохраняя при этом возможность динамически добавлять критерии.

SQL

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

Обычно критерии не будут превышать два или три элемента для запроса, но объединение их в один гигантский запрос не является вариантом. Я пробовал concat, union и пересекаюсь, и они просто дублируют запрос и присоединяют их к UNION.

Есть ли простой и чистый способ добавить условия "ИЛИ" к динамически сгенерированному запросу с использованием сущности framework?

Изменить с помощью моего решения - 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);
}
4b9b3361

Ответ 1

Вероятно, вы ищете что-то вроде Predicate Builder, которое позволяет вам легче управлять символами AND и OR.

Там также Динамический Linq, который позволяет вам представить предложение WHERE как строку SQL и будет анализировать его в правильном предикате для 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);
}

Это необходимо, поскольку лямбда-параметры в двух разных выражениях на самом деле являются разными параметрами, даже если они имеют одинаковые имена. Например, если вы хотите получить q => q.first.Contains(first) || q.last.Contains(last) q => q.first.Contains(first) || q.last.Contains(last), тогда q в q.last.Contains(last) должно быть точно таким же q которое q.last.Contains(last) в начале лямбда-выражения.

Далее нам нужен универсальный метод Join который способен объединять лямбда-выражения Func<T, TReturn> -style вместе с заданным генератором двоичных выражений.

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();

Ответ 3

Существует ли простой и понятный способ добавления условий "ИЛИ" в динамически сгенерированный запрос с использованием структуры сущностей?

Да, вы можете достичь этого, просто полагаясь на одно предложение 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();

Как вы можете видеть в приведенном выше примере, мы динамически включаем или выключаем OR-части выражения where -filter на основе ранее оцененных предпосылок (например, isFirstValid).

Например, если isFirstValid не равно true, тогда name.first.Contains(first) замкнуто и не будет выполнено и не повлияет на набор результатов. Более того, EF Core DefaultQuerySqlGenerator будет дополнительно оптимизировать и уменьшать логическое выражение внутри, where до его выполнения (например, false && x || true && y || false && z может быть уменьшено до простого y посредством простого статического анализа).

Обратите внимание: если ни одна из предпосылок не true, то набор результатов будет пустым - что, я полагаю, является желательным поведением в вашем случае. Однако, если вы по какой-то причине предпочитаете выбирать все элементы из вашего источника IQueryable, то вы можете добавить окончательную переменную к выражению, оценивающему ее как true (например, .Where(... || shouldReturnAll) с помощью var shouldReturnAll = !(isFirstValid || isLastValid) или что-то подобное).

Последнее замечание: Недостатком этого метода является то, что он вынуждает вас создавать "централизованное" логическое выражение, которое находится в том же теле метода, в котором находится ваш запрос (точнее, часть запроса where). Если по какой-то причине вы хотите децентрализовать процесс сборки своих предикатов и внедрить их в качестве аргументов или объединить их в цепочку с помощью построителя запросов, то вам лучше придерживаться построителя предикатов, как это предлагается в других ответах. В противном случае, наслаждайтесь этой простой техникой :)