Динамический запрос с условиями 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);
}
Ответы
Ответ 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
). Если по какой-то причине вы хотите децентрализовать процесс сборки своих предикатов и внедрить их в качестве аргументов или объединить их в цепочку с помощью построителя запросов, то вам лучше придерживаться построителя предикатов, как это предлагается в других ответах. В противном случае, наслаждайтесь этой простой техникой :)