Понимаю, что тема избитая, есть масса статей на хабре (например раз, два, три) и если с теорией все гладко, то все попавшиеся мне на глаза реализации (не только на хабре, но и на гитхабе в том числе) этого паттерна обладали теми или иными ограничениями.
Свою идею я реализовывал постепенно на основании опыта использования в реальном проекте. Требования оформились следующие:
Минимальный 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 и попробовать у себя.
