Паттерн спецификация в .NET

от автора

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

Свою идею я реализовывал постепенно на основании опыта использования в реальном проекте. Требования оформились следующие:

  • Минимальный API;

  • Не использовать методы расширения;

  • Совместимость с существующим кодом;

  • Использование как с провайдерами баз данных так и просто в обычном коде;

  • Композиция;

  • Не должно быть привязано ни к какому фреймворку;

Основные ограничения существующих решений:

  • Неоправданно объемные определения (например, надо надо создавать целый отдельный класс);

  • Кучи разных методов и методов расширений (загрязняет код, усложняет отказ от библиотеки, сбивает с толку если выбраны названия мимикрирующие под LINQ);

  • Лишний функционал вроде поддержки пагинации или сортировки;

  • Фокус только на деревьях выражений либо только на варианте с методами/делегатами;

  • Лишние компиляции делегатов без намека на оптимизацию;

  • Слабые возможности по композиции, например, вложенные условия не поддерживаются;

  • Возможно использовать только в некоторых контекстах;

Может возникнуть вопрос: Почему просто не использовать набор методов расширений или даже просто выражения (Expression<Func<T, bool>>)? Разумеется, есть случаи когда и этого будет достаточно, но часто одни и те же условия необходимо проверять как при запросе в базу, так и в обычном коде, поэтому очевидно, что надо поддерживать оба сценария.

Еще один вариант — создать некий сервис(ы), в котором будут методы вроде IsUserActive, IsUserRegistered и так далее. Опять же, в каких-то случаях это тоже оправдано, но с композицией и переиспользованием у такого подхода может быть еще хуже. Могут быть условия, которые просто не возможно проверить в одном запросе или внутри спецификации, но эти проблемы можно решить или сгладить.

Перейдем к примерам объявления:

public class User {     public int Id { get; set; }      public string Name { get; set; }      public bool Active { get; set; }      public Subscription Subscription { get; set; }      public List<Department> Departments { get; set; } }  public class Department {     public int Id { get; set; }      public string Name { get; set; }      public bool Active { get; set; } }  public enum Subscription {     Subscribed,      Unsubscribed }  public static class Specifications {     // в базе сравнение не учитывает регистр     public static readonly Specification<Department> CustomerServiceDepartment = new(         x => x.Name == "Customer Service",         x => string.Equals(x.Name?.TrimEnd(), "Customer Service", StringComparison.InvariantCultureIgnoreCase)     );      // делагат скомпилируется при вызове     public static readonly Specification<Department> ActiveDepartment = new(x => x.Active);      public static readonly Specification<User> ActiveUser = new(         default,         x => x.Active // передаем только делегат, выражение будет вычислено     );      // инвертируем     public static readonly Specification<User> InactiveUser = !ActiveUser;          // комбинируем     public static readonly Specification<User> SubscribedUser = ActiveUser && new Specification<User>(x => x.Subscription == Subscription.Subscribed);      public static readonly Specification<User> VasiliyUser = new(x => x.Name == "Vasiliy");      // можем даже использовать спецификации внутри других спецификаций     public static readonly Specification<User> UserInCustomerServiceDepartment = new(x => x.Departments.Any(CustomerServiceDepartment && ActiveDepartment)); }

Упрощенный пример использования:

public class UserController : Controller {     private readonly DbContext _context;      public UserController(DbContext context)     {         _context = context;     }      // option 1: DB     public Task<User> GetUser(int id)     {         return _context.Set<User>()             .Where(Specifications.UserInCustomerServiceDepartment)             .Where(x => x.Id == id)             .SingleOrDefaultAsync();     }      // option 2: in-memory     public async Task<User> GetUser(int id)     {         var user = await _context.Set<User>()             .Include(x => x.Departments)             .Where(x => x.Id == id)             .SingleAsync();          return Specifications.UserInCustomerServiceDepartment.IsSatisfiedBy(user) ? user : null;     } }

Основные инструменты — деревья выражений + визиторы и библиотека DelegateDecompiler.

Весь код доступен на гитхабе. Он включает в себя еще и проекции из моей другой статьи.

Если лень читать код — можно установить пакет из nuget и попробовать у себя.


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


Комментарии

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

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