Как гласит Википедия:

«Спецификация» в программировании  — это шаблон проектирования, посредством которого представление правил бизнес логики может быть преобразовано в виде цепочки объектов, связанных операциями булевой логики.

Реализация и преимущества данного шаблона уже были описаны в нескольких статьях, но т.к. у меня в проекте уже была своя реализация, которая, на мой взгляд, удобнее и позволяет убрать кучу повторяющегося кода, то я решил поделиться своим вариантом (который, возможно, не совсем чистая Спецификация).

Исходники традиционно на GitHub, пакеты на Nuget.

Теперь к деталям: данная либа будет полезна, в первую очередь, для тех, у кого есть большое количество бизнес-логики при фильтрации или множество параметров фильтрации. Как пример, бэкенд для грида типа такого https://reactdatagrid.io/demo или фильтра типа такого https://i.imgur.com/Jw5UAFz.png.

Итак, как может выглядеть типичный код для получения данных в апи:

// Модель данных
public class Employee
{
    public decimal Salary { get; set; }
    public string Name { get; set; }
    public DateTime? Date { get; set; }
}

// Фильтр на фронте
public class SomeApiFilter
{
    public DateTime? Date { get;set }
    public string Name { get;set }
    public string NameContains { get;set }
    public decimal? SalaryFrom { get; set; }
    public decimal? SalaryTo { get; set; }
    // И еще 100500 полей, которые хочет заказчик
}
// Пришел в запросе
var filter = new SomeApiFilter
{
  Date = DateTime.Today,
  NameContains = "complex",
  IdFrom = 0,
  IdTo = 5
};

// В коде репозитория (или контроллера -_o)
var where = PredicateBuilder.New<Employee>();
if (filter.Date.HasValue)
{
  where.And(f => f.Date == filter.Date.Value);
}
if (!string.IsNullOrEmpty(filter.Name))
{
  where.And(f => f.Name == filter.Name);
}
if (!string.IsNullOrEmpty(filter.NameContains))
{
  where.And(f => f.Name.Contains(filter.NameContains));
}
if (filter.SalaryFrom.HasValue)
{
  where.And(f => f.Id >= filter.SalaryFrom);
}
if (filter.SalaryTo.HasValue)
{
  where.And(f => f.Id <= filter.SalaryTo);
}
// И еще 100500 if

//Получаем данные
var data = dbcontext.Set<Employee>().Where(where);

Как может помочь моя библиотека, при условии соблюдения конвенций наименования и типов полей фильтра:

// где-то в DAL создаем обработчик фильтра
public class GetByFilterSpec : SpecificationBase<Employee, SomeApiFilter>
{
    public GetByFilterSpec(ILogger<GetByFilterSpec> logger, IOptions<Options> options)
        : base(logger, options)
    {
    // можно добавить явную обработку полей фильтра, но по умолчанию не надо
    }
}
// Подключаем библиотеку
services.AddLinqSpecification();
//Регистрируем спецификацию
services.AddSingleton<GetByFilterSpec>();
// модифицируем фильтр, используя новые типы свойств
public class SomeApiFilter
{  
  public RangeFilter<decimal> Salary { get; set; }
  public StringFilter Name { get; set; }
  public DateTime? Date { get;set }
  // И еще 100500 полей, которые хочет заказчик
}
//Используем

// Пришел с фронта 
var filter = new SomeApiFilter
{
    Date = DateTime.Today,
    Salary = new RangeFilter<decimal> { Start = 0, End = 5 },
    Name = new StringFilter("complex") { Contains = true }
}

// В коде репозитория (или контроллера -_o)
var spec = serviceProvider.GetRequiredService<GetByFilterSpec>();
var expression = spec.CreateFilterExpression(filter);
var data = dbcontext.Set<Employee>().Where(expression);

Таким образом, в оптимистичном варианте и для больших фильтров, количество кода может уменьшиться в десятки раз, буквально до 2 строк:

var expression = spec.CreateFilterExpression(filter);
var data = dbcontext.Set<Employee>().Where(expression);

Чуть больше примеров и вариантов использования описано в ридми к проекту и в тестах.
Буду рад выслушать конструктивную критику и предложения по вариантам реализации TODO из README :)

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


  1. pil0t
    30.04.2022 11:49

    Как вариант, для фильтрации FE-грида можно посмотреть в сторону OData. Там уже есть готовые инструменты для этого.


    1. XuMiX Автор
      01.05.2022 01:37

      OData и GraphQL это всегда начало длинного путешествия, моя либа делает 1 вещь, но хорошо :) Применять ее также можно не только в контроллерах


  1. OkunevPY
    01.05.2022 08:01

    Да либа делает одну вещь и хорошо, сажает производительность.


    1. XuMiX Автор
      01.05.2022 11:33
      -1

      Аргументы на уровне мамкиного хайлоадера. Пока вижу только, что твой комментарий сажает ненависть.

      Исходники открыты, багтрекер активен, pr's welcome, как говорится.


      1. mvv-rus
        02.05.2022 23:54

        Нет, ваш проект развивайте сами. А лично я не вижу для него экологической ниши.

        Потому что реально основные ниши суть следующие:
        1. Простые фильтры и/или ответственный за фильтр разработчик — уровня повыше джуна с опытом SO-Driven development
        Фильтр тогда будет набран, как минимум, из базовых функций LINQ — а то ещё и оптимизирован вручную, если там реально high-load
        2. Реально сложный фильтр и/или ответственный за фильтр разработчик — джун с опытом SO-Driven development only
        Для такого случая кому-то постарше стоит потратить силы на то, чтобы прикрутить парсер, разбирающий строку с выражением и формирующий из нее фильтр. И что-то (например — расшифровка названия древней утилиты yacc) мне подсказывает, что такой компонент уже написан, и, возможно — не единожды (так или нет — не смотрел). А если не написан — я и сам готов получит свою толику славы (и, может быть, денег). Ну, а строчку с выражением потом не только джун напишет, но и вообще вайтишник.
        Так что, для вашей библиотеки остается довльно узкий зазор, где ее в нашей реальной жизни целесообразно использовать. Сомневаюсь, что вам удастся в него влезть.

        PS Впрочем, код этого проекта может сильно помочь лично вам где-нибудь на собеседовании. И вообще, писать программы — это приятно (по крайне мере — мне), так что не принимайте мое старческое брюзжание близко к сердцу.


        1. XuMiX Автор
          03.05.2022 00:13

          Забыл перелогиниться? :)

          1. Если реальный хайлоад - то там вряд ли есть linq или вообще sql.

          2. Если реально сложный фильтр - в моем случае он в 99% случаев реализуется и вполне себе используется. Но ты же примеры не смотрел, да? ;)

          Ниша очень простая - экономия человекочасов, унификация интерфейсов, уменьшение связности.

          ЗЫ я давно сам собеседую людей и код именно этой библиотеки точно не поможет мне на собеседованиях, которые я прохожу :)


      1. Vadim_Aleks
        02.05.2022 23:57

        Аргументы на уровне мамкиного хайлоадера

        Значит ли это, что не нужно вставлять результаты бенчмарка, чтобы при выборе библиотеки пользователи видели трейдоф?

        К слову, ваш пакет нельзя скачать с nuget


        1. XuMiX Автор
          03.05.2022 00:00

          Значит ли это, что нужно? Бремя доказательства всегда на том, кто делает заявление. Я никого никому не обещаю, а предлагаю избавление от рутины, использовать или нет - каждый решает сам.