[Из песочницы] LINQ: Динамическое построение фильтров запросов
Наверняка, рано или поздно каждому разработчику приходилось создавать таблицы данных с возможностью сортировки по столбцам и пр. Я не исключение. В нашем проекте подобные таблицы есть чуть ли не на каждой странице, можно сказать что 90% контента выводится через них. Поиск и сортировка по таблицам, естественно, работает без перезагрузки страницы.
Само собой, расширять семантики методов API контроллеров до бесконечной простыни было бы абсолютно глупо непрактично, поэтому нам требовалось универсальное решение для всех наших таблиц. Найти его помогли рефлексия и деревья выражений.
Мы решили, что формировать фильтр для выборки данных на клиенте и передавать его в виде JSON объекта на сервер гораздо удобнее, чем навешивать кучу условий в фабрике запроса.
Как все это выглядит
Допустим мы имеем модели пользователя и автомобилей, которыми он владеет:
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
public IEnumerable Cars { get; set; }
}
public class Car
{
public int CarId { get; set; }
public string Model { get; set; }
public int MaxSpeed { get; set; }
}
Допустим, я хочу получить пользователей, у которых имя начинается на A и автомобиль может развивать скорость больше 300 км/ч ну или с Id больше нуля, а потом отсортировать их по имени по убыванию, после по Id по возрастанию. Для это создадим такой объект:
{
"Where": {
"OperatorType": "Or",
"Operands": [
{
"OperatorType": "And",
"Operands": [
{
"Field": "Name",
"FilterType": "StartsWith",
"Value": "A"
},
{
"Field": "Cars.MaxSpeed",
"FilterType": "GreaterThan",
"Value": 300
}
]
},
{
"Field": "Id",
"FilterType": "GreaterThan",
"Value": 0
}
]
},
"OrderBy": [
{
"Field": "Name",
},
{
"Field": "Flag",
"Order": "Desc"
}
],
}
Можно заметить, что получается объект, с неограниченной вложенностью операндов, что позволяет строить довольно большие фильтры.
Теперь осталось применить этот фильтр к выборке данных. Как передать на сервер и десериализовать json объект я расписывать не буду, не маленькие.
FilterContainer filter = ...; // десериализация объекта
IQueryable query = dataAccess.MyUsers;
query = query.Request(filter);
// или
////query = query.Where(filter.Where).OrderBy(filter.OrderBy);
Вот собственно вся выборка.
Многие знают, а кто не знает, тем расскажу, что ORM типа Entity Framework или Linq2SQL используют деревья выражений для представления структурированных запросов к источникам данных. Поставщик запросов может пройти через структуру данных для дерева выражений и преобразовать ее в язык запросов.
Посредством рефлексии сборщик фильтра рекурсивно строит дерево запросов из соответствующих членов сущности.
Все методы и типы фильтраций рассмотрены в проекте на гитхабе.
P.S. В принципе, я не рассчитывал на звание первооткрывателя в данной задаче, подобное ни раз уже делалось в той или иной форме. В любом случае, это было прекрасным опытом.
Комментарии (9)
11 октября 2016 в 10:12
0↑
↓
Существует еще библиотека LINQ.Dynamic, которая позволяет делать примерно то же самое, указывая вместо лямбд строки.11 октября 2016 в 10:25
0↑
↓
Да, слышал про такое. Но нам неудобно было строить строку на клиенте, т.к. используется много условий построений фильтров. И порой результирующее правило получалось из 3–7 операндов разного уровня вложенности. Согласитесь, не всегда удобно будет разбирать и дополнять строку, гораздо удобнее строить js объект. Согласен, можно было бы потом разбирать js объект в строку и использовать dlinq, но нам нужны были доп. условия, которые не вошли в проект на гитхабе. Раз уж сделали, почему бы не поделиться с людьми? Да и в конце я написал, что мы не рассчитывали на звание первооткрывателя, а только лишь делали под свое удобство.
11 октября 2016 в 10:26
0↑
↓
А зачем JSON? Те же строки odata выглядят сильно короче (см. тут, первый же пример Uri в разделе 2).
Плюс, советую добавить логические операции в where фильтры (или в readme на гитхабе).11 октября 2016 в 10:34
0↑
↓
Говорю же, нам удобнее было использовать именно в виде построения js объекта. Не спорю, есть множество более удобных решений. В любом случае это был отличный опыт.11 октября 2016 в 10:46
0↑
↓
В любом случае это был отличный опыт.
Да я ж не спорю, сам подобный велосипед пилил.Говорю же, нам удобнее было использовать именно в виде построения js объекта. Не спорю, есть множество более удобных решений.
А вот тут уже интересно. Чем именно вам не угодили «более удобные» решения?11 октября 2016 в 10:58
+1↑
↓
Из-за множества условий построений фильтров, нам было неудобно разбирать строку как в DLINQ, чтобы ее дополнить. Так же мы планируем расширить FilterType в Where-фильтре собственным функционалом.
11 октября 2016 в 10:46
+1↑
↓
Забыл сразу написать, извиняюсь за флуд, но спасибо за предложение, обязательно добавим логические операции.
11 октября 2016 в 10:34
–1↑
↓
Вот еще клиент будет указывать серверу как выборку делать11 октября 2016 в 10:40 (комментарий был изменён)
+1↑
↓
Тут надо кое что уточнить. На сервере имеется выборка определенного множества данных. На клиенте есть возможность строить фильтр для получения только подмножества. Расширить выбор данных клиент никак не может.