В 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>
ОтветитьУдалитьрабочее решение я тебе выслал =)