Наверняка, рано или поздно каждому разработчику приходилось создавать таблицы данных с возможностью сортировки по столбцам и пр. Я не исключение. В нашем проекте подобные таблицы есть чуть ли не на каждой странице, можно сказать что 90% контента выводится через них. Поиск и сортировка по таблицам, естественно, работает без перезагрузки страницы.


Само собой, расширять семантики методов API контроллеров до бесконечной простыни было бы абсолютно глупо непрактично, поэтому нам требовалось универсальное решение для всех наших таблиц. Найти его помогли рефлексия и деревья выражений.



Мы решили, что формировать фильтр для выборки данных на клиенте и передавать его в виде JSON объекта на сервер гораздо удобнее, чем навешивать кучу условий в фабрике запроса.


Как все это выглядит


Допустим мы имеем модели пользователя и автомобилей, которыми он владеет:


public class User 
{
  public int Id { get; set; }
  public string Name { get; set; }
  public int Age { get; set; }
  public IEnumerable<Car> 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<User> query = dataAccess.MyUsers;

query = query.Request(filter);
// или
////query = query.Where(filter.Where).OrderBy(filter.OrderBy);

Вот собственно вся выборка.


Многие знают, а кто не знает, тем расскажу, что ORM типа Entity Framework или Linq2SQL используют деревья выражений для представления структурированных запросов к источникам данных. Поставщик запросов может пройти через структуру данных для дерева выражений и преобразовать ее в язык запросов.


Посредством рефлексии сборщик фильтра рекурсивно строит дерево запросов из соответствующих членов сущности.


Все методы и типы фильтраций рассмотрены в проекте на гитхабе.


P.S. В принципе, я не рассчитывал на звание первооткрывателя в данной задаче, подобное ни раз уже делалось в той или иной форме. В любом случае, это было прекрасным опытом.

Поделиться с друзьями
-->

Комментарии (21)


  1. impwx
    11.10.2016 10:12

    Существует еще библиотека LINQ.Dynamic, которая позволяет делать примерно то же самое, указывая вместо лямбд строки.


    1. mrxten
      11.10.2016 10:25

      Да, слышал про такое. Но нам неудобно было строить строку на клиенте, т.к. используется много условий построений фильтров. И порой результирующее правило получалось из 3-7 операндов разного уровня вложенности. Согласитесь, не всегда удобно будет разбирать и дополнять строку, гораздо удобнее строить js объект. Согласен, можно было бы потом разбирать js объект в строку и использовать dlinq, но нам нужны были доп. условия, которые не вошли в проект на гитхабе. Раз уж сделали, почему бы не поделиться с людьми? Да и в конце я написал, что мы не рассчитывали на звание первооткрывателя, а только лишь делали под свое удобство.


  1. Oxoron
    11.10.2016 10:26
    +1

    А зачем JSON? Те же строки odata выглядят сильно короче (см. тут, первый же пример Uri в разделе 2).
    Плюс, советую добавить логические операции в where фильтры (или в readme на гитхабе).


    1. mrxten
      11.10.2016 10:34

      Говорю же, нам удобнее было использовать именно в виде построения js объекта. Не спорю, есть множество более удобных решений. В любом случае это был отличный опыт.


      1. Oxoron
        11.10.2016 10:46

        В любом случае это был отличный опыт.

        Да я ж не спорю, сам подобный велосипед пилил.

        Говорю же, нам удобнее было использовать именно в виде построения js объекта. Не спорю, есть множество более удобных решений.

        А вот тут уже интересно. Чем именно вам не угодили «более удобные» решения?


        1. mrxten
          11.10.2016 10:58
          +1

          Из-за множества условий построений фильтров, нам было неудобно разбирать строку как в DLINQ, чтобы ее дополнить. Так же мы планируем расширить FilterType в Where-фильтре собственным функционалом.


    1. mrxten
      11.10.2016 10:46
      +1

      Забыл сразу написать, извиняюсь за флуд, но спасибо за предложение, обязательно добавим логические операции.


  1. rishatzak
    11.10.2016 10:34

    Вот еще клиент будет указывать серверу как выборку делать


    1. mrxten
      11.10.2016 10:40
      +1

      Тут надо кое что уточнить. На сервере имеется выборка определенного множества данных. На клиенте есть возможность строить фильтр для получения только подмножества. Расширить выбор данных клиент никак не может.


  1. Vitalii_Panchenko
    11.10.2016 11:11
    +3

    Обычный пример эволюции проэкта и людей которые проэкт пишут.
    Потом внезапно нагугливается OData WEB API, и наступает прозрение.


    1. mrxten
      11.10.2016 11:15
      -2

      Проект очень старый, плюс он используется в разных регионах на отдельных серверах. Первая версия этой библиотеки была написана более трех лет назад. Сейчас ее используют у нас более 50 страниц. Нам было бы очень проблемно сменить технологию, поэтому мы ее только довели до ума.


      1. correy
        11.10.2016 12:47
        +1

        Чем больше вы будете работать над своим API, тем сложнее вам будет его модифицировать при таком подходе. Ни один раз проходили эту ситуацию. OData сущесвенно упращает жизнь, проверено :)


        1. forcewake
          11.10.2016 15:42

          А можете посоветовать адекватные js библиотеки для работы с OData? Желательно Free/Libre and Open-Source Software


          1. Vitalii_Panchenko
            12.10.2016 12:06

            Мой совет просто используйте голый стандарт OData. Когда я стоял перед выбором библиотеки на js, я задал себе вопрос «что учить?» API js библиотеки и API OData запросов или просто API OData запросов.
            Я выбрал второе.

            Код примерно такой:

            var request = {
            $filter: «some filter»,
            $orderby: «field name»,
            $top: 10
            };

            var response = $httpService.get(request);

            P. S. В проэкте используем Angular 1.5, нормальное под Angular не нашел. Возможно для других библиотек чтото есть. Классно было бы получить к примеру angular ui grid кторый с коробки работает с OData endpoint, но увы.


    1. robert_ayrapetyan
      11.10.2016 20:21

      К сожалению, является MS детищем и нормально реализовано только в .NET.


  1. Vitalii_Panchenko
    11.10.2016 11:18
    +1

    Вы бы стали использовать этот подход в новом проэкте?


    1. mrxten
      11.10.2016 11:31

      Да, так как есть уже наработанный функционал.


      1. Vitalii_Panchenko
        11.10.2016 11:58
        +2

        Посмотрите сколько стоит поддержка Вашей библиотеки, и посмотрите на OData к примеру.

        Сравните возможности и думаю все станет понятно.

        P. S. Сам был в такой ситуации, итог новое на OData, старое живет со старым и потихоньку переписывается.
        Создание таких библиотек дело веселое но в целом вредно для проэкта.
        Вы никогда не напишите лучше чем 70+ контрибьюторов с подержкой Microsoft.


  1. AxisPod
    11.10.2016 13:49

    Увы, костыльненькое решение. Как-то жестко напрягает хардкодность. Логические И и ИЛИ напугали если честно. Почему за основу не взять Expression Tree? Есть операнд и есть его параметры, есть к примеру FEqual, FGreater, FLess, FNot, FAnd, FOr и этого достаточно чтобы построить чуть ли не всё что угодно, добавить еще FContains, хватит. В итоге если надо построить выражение типа p1 !=… && (p2 ==… || p2 == ...), описывается легко
    FAnd(FNot(FEqual), FOr(FEqual, FEqual)), а вводить какие-то деревья, в которых указывать как их обрабатывать, это какое-то извращение.


    1. mrxten
      11.10.2016 17:16

      Спасибо, взгляну.


  1. robert_ayrapetyan
    11.10.2016 20:30

    Вообще проблема весьма актуальна.
    Даже server-side ORM обычно «спотыкается» на нестандартных свойствах той или иной БД и иногда проще написать голый SQL-запрос (благо, все ORM позволяют это сделать), чем бороться с вычурными конструкциями.
    На client-side все еще хуже, если вы лишены роскоши «70+ контрибьюторов с подержкой Microsoft», то приходится писать свой велосипед, чем здесь многие и занимаются.