Чи є місце запитам до бази в бізнес-логіці .NET-застосунку?

💡 Усі статті, обговорення, новини про .NET — в одному місці. Приєднуйтесь до .NET спільноти!

🧣 ЧИ Є МІСЦЕ ЗАПИТАМ ДО БАЗИ В БІЗНЕС-ЛОГІЦІ .NET-ЗАСТОСУНКУ?

Поки ти просто пишеш CRUD — все виглядає «зручно»:

var users = _db.Users.Where(u => u.IsActive).Include(u => u.Roles).ToListAsync();

Але варто лише спробувати покрити це юніт-тестом — і відразу видно, наскільки це погана ідея:

  • ToListAsync() не мокнеться
  • треба тягнути EF InMemory або сторонні бібліотеки типу MockQueryable.Moq
  • весь сервіс змішує бізнес-логіку з SQL-подібною LINQ-кашею

А потім ще хтось каже:

«У нас не DbContext напряму — у нас GenericRepository, який повертає IQueryable»

😅 Та це ж усе та сама штука. Просто обгортка над DbSet, яка нічого не інкапсулює.
Запит як був у бізнес-логіці — так і залишився.

❓ А ЧИ МОЖНА ВВАЖАТИ ЦЕ «РЕПОЗИТОРІЄМ»?

Якщо обʼєкт просто повертає IQueryable — це не репозиторій.

СУХЕ ПОРІВНЯННЯ:

1. Читабельність
✅ Метод у сервісі виглядає як бізнес-дія — GetActiveUsers()
❌ .Where(...), .Include(...), .OrderBy(...) прямо у методі

2. Відокремлення шарів
✅ Сервіс читає дані через API (IUserRepository.GetActiveUsers())
❌ Сервіс будує запит сам через IQueryable

3. Тестування
✅ Легко мокати методи, просте тестування
❌ Потрібно мокати IQueryable, ToListAsync() — складно, часто нестабільно

4. Розподіл обов’язків
✅ Репозиторій відповідає за доступ до даних
❌ Сервіс одночасно вирішує і що зробити, і як отримати дані

5. Контроль над запитом
✅ Централізовано: Include, Where, OrderBy — в одному місці
❌ Логіка розмазана по різних методах, важко відстежити зміни

6. Типові виправдання
🟢 «У нас не DbContext напряму, а GenericRepository»
🔴 Але це та ж суть — ті самі проблеми, інша упаковка

📌 Приклад для порівняння

Погано:

var users = _repository.GetAll()
    .Where(u => u.IsActive && !u.IsDeleted)
    .Include(u => u.Roles)
    .OrderBy(u => u.CreatedAt);

Добре:

var users = await _userRepository.GetActiveUsersSorted();

🗣 ЯК ВИ ЦЕ РОБИТЕ У СЕБЕ?

  • Використовуєте повноцінні репозиторії?
  • Чи прокидаєте IQueryable в бізнес-логіку?
  • Чи вважаєте це окейним підходом?

👇 Діліться досвідом в коментарях!

👍ПодобаєтьсяСподобалось0
До обраногоВ обраному0
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

0. Хотелось бы бы юнит(или почти юнит) тестировать что же там в репозитарии за запросы делаются и не поломал ли кто нить что нить. Да, я в курсе что лучшие собаководы рекомендуют юнит тестировать только бизнес логику. На практике полезно юнит тестировать чуть больше. Если принять это, то проблема остается.
1. Скорей всего количество методов в репозитарии будет разрастаться стремительно. Лекарство стандартное — увеличение количества репозитарием.
2. Кроме Get скорей всего будет и Insert/Update/Delete, и рано или поздно эти операции надо будет делать с разными сущносятми транзакционно — привет UnitOfWork.
3. IQueryable полезен «достраиванием» запроса из разных точек как то

IQueryable<User> AddPermissionsClause(IQueryable<User> source)
{
    return source.Where(u=>u.ManagerId==CurrentIdentity.Id)
}
и вызовом этогометода из разных методов бизнес логики. Можно аналогично и из репозиториев, но это уже холивар, является ли конкретный метод бизнес логикой или механизмом доступа к данным.
4. Существующие библиотеки равно как и in-memory db минимизируют боль от юнит тестирования linq.
---
Суммаризируя: можно и так и эдак, дело вкуса.
ToListAsync() не мокнеться

А нахіба це мокати? docker compose up і будь яка база у вас

ЯК ВИ ЦЕ РОБИТЕ У СЕБЕ?

Так щоб можна було
1. Ідентифікувати кверю
2. Внести зміни в кверю
3. Додати індекси
Не то що без редепоою, навіть без рестарту аплікейшина

А нахіба це мокати? docker compose up і будь яка база у вас

Проблема ж не у тому щоб підняти базу. Проблема її ініціалізувати даними під кожний окремий бізнес кейс який необхідно покрити юніт тестами. А також зачищати дані які змінилися як результат роботи тесту (ну або заново ініціалізувати контейнер перед кожним юніт тестом, що у сумі вже досить довго якщо тестів тисячи). Плюс якщо тести запускаються у паралель на тих самих даних, дані змінені одним тестом легко ломають композицію даних підготовлену для іншого тесту. Підтримувати купу таких ініціалізацій у вигляді SQL скриптів або коду той ще головняк, усе це за рік-другий перетворюється на мес. Для сервіс тестів це ок, але сервіс тести зазвичай покривають лише основні сценарії і їх значно меньше порівняно з юніт тестами.

Проблема її ініціалізувати даними під кожний окремий бізнес кейс який необхідно покрити юніт тестами. А також зачищати дані які змінилися як результат роботи тесту (ну або заново ініціалізувати контейнер перед кожним юніт тестом, що у сумі вже досить довго якщо тестів тисячи).

Та хоч під кожний тест нову схему створюйте це не проблема.
Проблема це коли DBOps головного мозку і під кожний статус/дікшинарі/тощо створюють окремі таблиці з кучею зв’язків і т.д. і т.п.

Вопрос житейский на самом деле. Не раз видел, когда кто-то в уже сформированный репозиторий добавляет метод, возвращающий IQueryable. Бо... Ну так проще же.
Туда же и Expression в аргументах репо.

Лично мое мнение: «или крестик снимите, или трусы наденьте». Если уже есть репозиторий — возвращает только материализованные данные + никаких Expression в аргументах. Единственное что допустимо «просочить» в домен — транзакции/UnitOfWork.

Кто-то может сказать, что репозиторий вообще не нужен и только лишняя работа. Возможно, но не так и много оверхеда.

У загальному випадку Domain Model та DB Model можуть доволі сильно відрізнятися, особливо з часом. Використання IDbSet / IQueryable напряму зв’язує руки у оптимізації DB моделі в майбутньому. При зміні DB моделі усі ці INCLUDE, WHERE і т.п. доведеться змінювати у бізнес логіці в купі місць. Тому на довготривалих проектах де є доменна модель у більшості випадків класичний репозиторій який приймає та повертає об’єкти саме доменної моделі більш гнучкий варіант. У внутрішній реалізації Data Access якщо клас доменної моделі збігається зі структурою таблиць можно і напряму замапити. Ні — додати внутрішні класи ДБ моделі які і будуть мапитися EF + мапінг кодом між класами DB та доменної моделі. Якщо потрібен складний запит та EF генерить єресь — викидаємо запит через EF і додаємо гарно оптимізовану SP. І це все прозоро для бізнес логіки / доменної моделі.

Для read моделі, де потрібна куча різноманітних проекцій тільки для того щоб віддати їх через API, — IQueryable напряму може бути непоганим варіантом.

Якщо проект це 1500 довідників з тупим CRUD, доменної моделі як такої немає, моделі API контрактів майже збігаються з ДБ моделлю, — IDbSet напряму може бути оптимальним варіантом.

Перечитав разів 5, питання так і не зрозумів.

DbSet це і є репозиторій, ніяких інших репозиторіїв не потрібно
DbContext, тобто класс, що його наслідує — і є основна частина DataLayer. Тому його інжектимо в BusinessLayer, там пишемо запити.

А ЧИ МОЖНА ВВАЖАТИ ЦЕ «РЕПОЗИТОРІЄМ»?

martinfowler.com/eaaCatalog
— Query Object — An object that represents a database query.
— Repository — Mediates between the domain and data mapping layers using a collection-like interface for accessing domain objects.

👇 Діліться досвідом в коментарях!

Перед тим як ділитись своєю дуже цінною думкою, добре б почитати книгу, яка вже 20+ років є класикою. Це дозволить вам самостійно відповісти на поставлені питання.

О... Дякую за цінний коментар. Мета посту дещо інша а ніж розібратися що є репозитарієм а що ні. Я це без книжки знаю.

Мета посту дещо інша а ніж розібратися що є репозитарієм а що ні. Я це без книжки знаю.

А яка мета?
Показати, що ви здатні критикувати джунівську ідею? Чи підняти обговорення того, що «ви без книжки знаєте»?

Та ні, не джунівську і не ідею.Ви по суті питання можете щось написати?

Ви по суті питання можете щось написати?

Так в чому суть питання?
Порівняти 2 відомих патерни? Так це є в книзі, яку я навів в першому коментарі. Спойлер: обидва підходи доцільно використовувати, але в різних випадках.

Та ні, не джунівську і не ідею.

Ось це формулювання:

«У нас не DbContext напряму — у нас GenericRepository, який повертає IQueryable»

показує, що людина не в курсі того, що відомо вже 20+ років (різниці 2 типових патернів). Як ви назвете таку людину?

Питання в тому як Ви робите? Навіщо цей понт про знає не знає? Ви який паттернтвикористовуєте в своїх сервісах бізнес логіки? :)))

Ви який паттернтвикористовуєте в своїх сервісах бізнес логіки? :)))

Той який краще підходить. Це не про «понт», а про доцільність. В книжці «без якої ви знаєте» якраз описані трейдофи і особливості кожного підходу.

Так який краще підходить для використання в бізнес логіці? Навіщо ця агресія, цей вкид на вентилятор. Коли для вас доцільно в бізнеслогіці фактично вві##ати «сирі» запити до БД? Можете привести приклади такої доцільності? Чи Ви просто так вирішили тут поагресувати?

Можете привести приклади такої доцільності?

От в книжці ці приклади і є.
Один з них — це необхідність динамічних запитів (пошук по полям). Інший — проста логіка, бананльно немає потреби виділяти окремий шар.

Работали ли вы с Entity Framework ?

необхідність динамічних запитів (пошук по полям)

Прекрасно решается с EF внутри репозитория.

Вот если надо много разных проекций возвращать — вот тут уже да, методы в репе могут начать расти как грибы.

Ну и вопрос «нужен ли дата слой» хороший, но это отдельный вопрос, наверное.

Работали ли вы с Entity Framework ?

Ні.

Прекрасно решается с EF внутри репозитория.

Напевне так (у Фаулера це теж написано :) ). І що? Думаю існує ще кілька інших способів вирішення тієї ж задачі.

Ну и вопрос «нужен ли дата слой» хороший, но это отдельный вопрос, наверное.

Чому окремий?
Та і у вас навіть з DbContext існує «дата шар».

Ну с год назад мой коллега с тайтлом синиор так сделал и...

Тут питання системності. Одна «творча особистість» завжди знайдеться, он у автора теж знайшлась. Просто нормальна реакція — це пояснити йому чому рішення недоцільне (якщо воно недоцільне), а не нести на форум свої аргументи проти творчості буйного г...кодера (джун з тайтлом сеньйора).
В те що люди масово повертають IQueryable з репозиторія якось не хочеться вірити, що так багато людей не знають власне що таке і для чого потрібен репозиторій.

Я оцінив ваш відгук, більше мабуть Вам не варто відволікатися на це питання. Перед тим як виливати свій яд на інших уважно читайте пост. В кого саме «у нас» там мається на увазі і тому подібне. Гарної неділі.

«У нас не DbContext напряму — у нас GenericRepository, який повертає IQueryable»

Автор просто описал типичную ошибку некоторых C# девов, когда пытаются и рыбку сьесть и... пробросить Query Object из репозитория.

Автор просто описал типичную ошибку некоторых C# девов, когда пытаются и рыбку сьесть и... пробросить Query Object из репозитория.

Справді типова для розробників цього стеку? Ви впевнені, що мова йде не про джунів або працівників певної контори?
Фаулер у знайомих дотНетчиків сильно популярніший і відоміший ніж у джавістів. Якось не дуже віриться, що така дурна маніпуляція ( на яку пробує відповідати автор) є типовою

Ну с год назад мой коллега с тайтлом синиор так сделал и... так оно там и осталось, кажись. Теперь все юзают его метод, бо лень новый метод репы делать лол. Вот и думай, может он и прав был.

В чому саме він був правий? Я як Ви думаєте, чи був він правий взагалі?

Ви щось переоцінюєте сеньорів. Я цю прутню спостерігаю не один рік. Те що дехто сильно начитаний і кидається розумними термінами — зовсім не означає що він того дотримується всюди. Люди находять собі випрадання накштал «навіщо плодити файли, в них потім важко орієнтуватися». Таке враження що вони не користуються нормальними IDE або не вміють їх нормально використовувати або ще через якысь причини. Я пыдняв це питання не тому що хочу випендритися а тому що вчергове пригоріло. Хочу побачити що про таку, здавалося б, просту річ думають інші, як вони реально роблять.

Підписатись на коментарі