Dynamic Queries with LINQ

"LINQ классная штука, но т.к. там все строго типизировано я не могу строить динамические запросы, а это большой минус" – такие утверждения слышу все чаще от различных разработчиков. Правильный ответ – динамические запросы генерировать можно.

Вообще такие вопросы вижу как в онлайн (e-mail), раз пять отвечал на этот вопрос в зоне "Спроси Эксперта" на Платформе 2009, но последней каплей стало то, что мой блог нашли по запросу "dynamic queries with LINQ". =))))))

Expression Tree

Итак, в основе всего лежит интерфейс IQueryable и Expression Trees. Что это такое? Давайте подумаем, что на самом деле представляет собой выражение LINQ – фактически оно трансформируется в цепочку вызовов Extension-методов, который в свою очередь – в вызовы статических методов. Происходит это примерно в следующей последовательности:

Шаг 1: Выражение LINQ:

var q = from d in data
        where d.Length > 5
        select d;

Шаг 2: Цепочка методов расширения:

var q = data.Where(d => d.Length > 5).Select(d => d);

Шаг 3: Вызовы статических методов:

var q = Enumerable.Select(Enumerable.Where(data, d => d.Length > 5), d => d);

Все хорошо когда мы работаем с in-memory коллекциями, но что делать LINQ когда необходимо работать, например, с СУБД? Фактически, нужно трансформировать запрос LINQ в запрос T-SQL (в нашем случае). Вместо T-SQL может быть все что угодно, но давайте пока остановимся именно на этом варианте. Итак, как же LINQ будет осуществлять эту трансформацию? Ответ прост – для этого необходимо иметь представление нашего запроса в виде некоторой объектной модели – Expression Tree. Эта модель описывает все моменты запроса – условия, порядок сортировки, результат выборки и т.д. Основываясь на данных этой модели уже можно строить запрос на каноническом языке, в нашем случае T-SQL.

Причем же здесь IQueryable? Все просто — помните IEnumerable? Этот интерфейс позволяет нам пробежаться по коллекции в однонаправленном режиме. Именно на основе IEnumerable строятся все коллекции. IQueryable – это наследник IEnumerable, который включает также модель Expression Tree.

Генерируем Expression Tree

Возвращаясь к теме, давайте поговорим о том, каким образом мы можем формировать динамические запросы. Для этого, собственно, нам необходимо как раз сформировать Expression Tree и получить из этого лямбда выражение (-ия).

Для формирования Expression Tree существует класс Expression, который содержит различные статические методы для формирования конструкций. Здесь проще показать на примере.

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

Логично, что для решения этой задачи нам нужно сформировать лямбда-выражение примерно такого вида:

c => c.Contains("a") || c.Contains("b") || c.Contains("c") || итд

Давайте попробуем это сделать по шагам:

Шаг 1: Формируем параметр:

var param = Expression.Parameter(typeof(string), "c");

Шаг 2: Формируем вызов метода:

var condition = Expression.Call(param, typeof (string).GetMethod("Contains"), Expression.Constant("a"));

Шаг 3: Получаем лямбда-выражение:

var l = Expression.Lambda<Func<string, bool>>(condition, param);

Теперь, если мы выведем полученное лямбда-выражение на экран, то увидим следующую вещь:

c => c.Contains("a")

Отлично, теперь никто нам не мешает создать несколько таких условий, используя оператор "ИЛИ" (Expression.Or). Финальный код может выглядеть следующим образом:

var param = Expression.Parameter(typeof(string), "c");

Expression condition = null;
string input;
while (String.IsNullOrEmpty(input = Console.ReadLine()) == false)
{
  if (condition == null)
  {
    condition = Expression.Call(param,
                                typeof(string).GetMethod("Contains"),
                                Expression.Constant(input));
  }
  else
  {
    condition = Expression.Or(condition,
                    Expression.Call(param,
                                    typeof (string).GetMethod("Contains"),
                                    Expression.Constant(input)));
  }
}

var l = Expression.Lambda<Func<string, bool>>(condition, param);

Теперь, если мы выведем на экран получившееся выражение, то увидим, что все условия объединены через "ИЛИ".

Compile

Если мы посмотрим на переменную l, то увидим, что там содержится не Func<string, bool>, а Expression<Func<string, bool>>. Давайте исправим это, для этого необходимо вызвать метод Compile(). После этого можно использовать лямбда-выражение в ваших запросах:

string[] data = new[] { "qwerty", "asdfgh", "zxcvb", "wertyu" };

var param = Expression.Parameter(typeof(string), "c");

Expression condition = null;
string input;
while (String.IsNullOrEmpty(input = Console.ReadLine()) == false)
{
    if (condition == null)
    {
        condition = Expression.Call(param,
                                    typeof(string).GetMethod("Contains"),
                                    Expression.Constant(input));
    }
    else
    {
        condition = Expression.Or(condition,
                                  Expression.Call(param,
                                                  typeof (string).GetMethod("Contains"),
                                                  Expression.Constant(input)));
    }
}

var l = Expression.Lambda<Func<string, bool>>(condition, param);

Console.WriteLine(l);

var q = data.Where(l.Compile());

foreach (var s in q)
{
    Console.WriteLine(s);
}