Понимаю, что тема избитая, есть масса статей на хабре (например раз, два, три) и если с теорией все гладко, то все попавшиеся мне на глаза реализации (не только на хабре, но и на гитхабе в том числе) этого паттерна обладали теми или иными ограничениями.
Свою идею я реализовывал постепенно на основании опыта использования в реальном проекте. Требования оформились следующие:
Минимальный 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 и попробовать у себя.
Комментарии (8)
Dotarev
04.07.2023 06:33Offtop: Допустимо ли правилами Хабра размещать статью о своей библиотеке где-то, кроме хаба "Я пиарюсь"? Это не упрек, сам хотел бы такое сделать, но насколько я понял - чревато баном.
moderator
04.07.2023 06:33+1Допустимо. Правило про размещение статей в хабе "Я пиарюсь" распространяется только на публикации в которых рассказывается про коммерческий продукт, услугу, сервис, мероприятие.
nronnie
04.07.2023 06:33+5В статье ни слова ни про сам паттерн, ни про какие-либо (интересные или необычные) детали его реализации. Все можно было бы уложить в одно предложение: "Посмотрите, как я реализовал паттерн спецификация: <ссылка на GitHub>".
zartarn
04.07.2023 06:33Про спецификацию хорошо было написано у marshinov https://habr.com/ru/articles/325280/ + смежная статья https://habr.com/ru/articles/313394/
Dotarev
Что есть
Specification<T>? Чтобы понять, надо заглянуть на GitHub?
kemsky Автор
Именно так, реализация слишком тривиальна, чтобы размещать ее в статье.