В LINQ есть конструкция, которая позволяет делать SQL запрос с выражением IN. Все примеры я буду приводить на всем известной базе данных Northwind.
Например чтоб получить запрос на SQL в виде
SELECT c.*
FROM Customers c
WHERE c.Region IN ("WA", "SP", "BC")
нужно написать на LINQ следующий код
string[] filters = new [] { "WA", "SP", "BC" };
context.Customers.Where(p => filters.Contains(p.Region));
Данная конструкция нечитабельная, лично меня она напрягает. Мне хотелось бы чтоб она выглядела более естественно, как то так:
string[] filters = new [] { "WA", "SP", "BC" };
context.Customers.Where(p => p.Region.In(filters));
Конечно так написать нельзя и данных код нескомпилируется, но сделать запрос более естественным можно. Нам помогут Expression Trees, примеры которых вы могли видеть в предыдущих статьях. Далее будет рассказано, как сделать код вида:
string[] filters = new [] { "WA", "SP", "BC" };
var customers = context.Customers.In(p => p.Region, filters);
Погуглив, я нашел пример в котором делается примерно тоже самое, только проще и с обманом. В моем случае, на выходе мы получим SQL запрос с конструкцией WHERE IN, а в примере получается … AND (c.Region = “WA” OR c.Region = “SP” …). Я думаю парень не выдержал, задача все-таки нелегкая. Я тоже понервничал пока разобрался как транслировать в конструкцию IN. Вам будет проще :)
Начнем.
Ниже приведен код “In” Extenstion method’a и подробное его описание по шагам.
public static class LinqExtensions
{
/// <summary>
/// Filters the specified query with input filters.
/// </summary>
/// <typeparam name="T">The result entity type.</typeparam>
/// <typeparam name="TKey">The type of column to filter.</typeparam>
/// <param name="query" />The query.</param>
/// <param name="column" />The column.</param>
/// <param name="filters" />The in sequence.</param>
public static IQueryable<T> In<T, TKey>(
this IQueryable<T> query,
Expression<Func<T, TKey>> column,
IEnumerable<TKey> filters)
{
MethodInfo containsMethod = typeof(System.Linq.Enumerable)
.GetMethods()
.Where(m => m.Name == "Contains" && m.GetParameters().Length == 2)
.Single();
containsMethod = containsMethod.MakeGenericMethod(new[] { typeof(TKey) });
List<expression> values = new List<expression>();
foreach (TKey value in filters)
{
values.Add(Expression.Constant(value));
}
NewArrayExpression filtersArray = Expression.NewArrayInit(typeof(TKey), values);
List<expression> arguments = new List<expression>();
arguments.Add(filtersArray);
arguments.Add(column.Body);
MethodCallExpression containsMethodCall = Expression.Call(containsMethod, arguments.ToArray());
LambdaExpression lambda = Expression.Lambda(containsMethodCall, column.Parameters.ToArray());
var result = Expression.Call(
typeof(Queryable),
"Where",
new Type[] { query.ElementType },
query.Expression,
lambda);
return query.Provider.CreateQuery<T>(result);
}
}
Рассмотрим подробней:
- Сначала нам надо найти метод Contains (17-20). Поскольку этот метод является Extenstion Method, то его нужно искать в классе, в котором хранятся эти расширения. В данном случае System.Linq.Enumerable. Методов Contains два:
Contains>TSource<(IEnumerable>TSource<, TSource) Contains>TSource<(IEnumerable>TSource<, TSource, IEqualityComparer>TSource<)
нам нужен [1] с двумя параметрами.
- После того как мы его нашли, у нас будет нетипизированный метод в виде Contains[TSource], а нам нужен Contains, который работает с типом поля, которое нам передается. В данном примере мы фильтровали по Region у которого тип System.String. Делаем с общего метода, типизированный (23). В результате получаем сигнатуру вида Contains<String>(<IEnumerable<String>, String>).
- Затем нам нужен список значений, по которым будет фильтроваться выборка. Для этого создаем набор констант (24-28) и помещаем их в массив (30). В результате мы получаем динамическое выражение вида new[] { “WA”, “BC”, “SP”}.
- Согласно сигнатуре метода Contains [1] мы должны передать в него набор фильтров (33) и параметр-поле (34), по которым будет фильтроваться выборка. Делаем вызов метода Contains (36). В результате мы получаем динамическое выражение вида new[] { “WA”, “BC”, “SP”}.Contains(p.Region). Тут мы как раз делаем саму IN конструкцию.
- После этого нам нужно создать лямбда выражение (38), которое будет передаваться в конструкцию Where. В результате мы получаем динамическое выражение вида p => new[] { “WA”, “BC”, “SP”}.Contains(p.Region). Входящий параметр лямбда выражения будет таким, как вы его назовете при использовании нашего “In” Exstension method. В данном примере он называется p, т.к. я делал вызов вида context.Customers.In(p => p.Region, filters).
- Все. Нам осталось добавить в существующую цепочку вызовов наш метод Where (40-45).
Думаю вы почерпнулия для себя что-то новое и полезное.
Ох как ты будешь рад узнать, что в EF 1.0 нет поддержки метода Contains()...
ОтветитьУдалитьЭтот комментарий был удален автором.
ОтветитьУдалитьИ кстати полученный вариант не особо то читабельнее. Одна несчастная рюшечка, т.е. "syntax sugar" по-моему усилий не стоили.
ОтветитьУдалитьНу так оно хоть выглядит более естественно, не с ног на голову. Мне данный вариант больше нравится чем существующий, в другом случае я б не делал пост. Если предложишь другой синтаксис - я рассмотрю.
ОтветитьУдалить//context.Customers.In(p => p.Region, filters);
ОтветитьУдалитья бы заменил на:
context.Customers
.Property(p => p.Region)
.In(filters);
Так хоть читая слева на право понятно, что происходит.
Не получится. После .Property мы теряем всю цепочку вызова самого IQueryable. Из данного предложения мы возвращает максимум Func<T, S>, что в результате дает нам p => p.Region, а сам запрос context.Customers... мы тут потеряем.
ОтветитьУдалитьОтпадает
Получится, если их "пропихнуть" через свой класс, который возвращается из Property() и содержит два свойства - "сам запрос" и FuncT, S>
ОтветитьУдалитьрабочее решение я тебе выслал =)