Упрощение сложной фильтрации в LINQ с помощью шаблона «Спецификация» и библиотеки XSpecification

от автора

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

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

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

Исходники традиционно на 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 🙂


ссылка на оригинал статьи https://habr.com/ru/post/663808/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *