Pull to refresh

Comments 32

TL;DR
Допустим, для самого примитивного варианта это подойдет, но, зачастую, мы имеем дело с разными моделями, причем модель в api может вообще не быть похожа на то, как оно в БД лежит. И так ли нужна вся эта куча кода, когда можно посадить интерна пилить менее примитивные круды и прокачиваться?


return DbSet; Я тоже люблю делать select * from veryhugetable, но так ли это необходимо?


P.S. Заботиться о валидации данных это таки хорошо, но делать это вот так вот if(!ModelState.IsValid) не надо, правда.

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

У него контроллер помечен как [ApiController], так что до ModelState.IsValid дело даже не дойдет - оно в этом месте всегда будет true. Просто лишний код. Ну а про то, что "generic repository" это антипаттерн в интернете уже только ленивый не писал.

На это и был намёк, что фреймворк все сделает за нас и эта проверка просто раздувает кодовую базу почем зря.

Про антипаттерн полностью согласен. Слишком уж он неповоротливый.

Ну а про то, что «generic repository» это антипаттерн в интернете уже только ленивый не писал.

Писали. Только вот у автора статьи — не тот «generic repository», про который писали. Он у него только называется GenericRepository, а по факту ничего, кроме EF использовать не умеет:
class GenericRepository<TDbContext, TEntity> ... where TDbContext : DbContext ...

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

Если убрать ограничение

where TDbContext : DbContext

то вы сможете передавать любой класс в качестве контекста. Но придется по-другому реализовать свойство DbSet, на котором построен GenericRepository.

IMHO, если статья — обучающая (а это в данном случае так: и по структуре изложения — крайне разжеванного, и по плашке в начале), то она должна описывать готовое решение. Подходы описывать — это в статьях для уже обученной аудитории.
Иначе есть большой риск научить новичка плохому. Не надо так — плохому новичок сам научится.

Ленивый не ленивый, но Ardalis продолжает успешно декларировать "generic repository" и демонстрировать в "образцовом" магазине правильный дизайн.

Я посмотрел код - там совсем не generic repository, а specifications - которые как раз и являются нормальным подходом в случае когда хочется избежать кучи репозиторных интерфейсов на каждый чих. Generic repository - это когда IQueryable из источника данных (например EF) выставляются голым задом наружу (аз есъм протечка абстракции).

Repository Design Pattern in C# with Examples - Dot Net Tutorials

What is Repository?

A repository is nothing but a class defined for an entity, with all the possible database operations. For example, a repository for an Employee entity will have the basic CRUD operations and any other possible operations related to the Employee entity. 

Так что, я считаю, что это, все-таки, репозиторий.

Да, это репозиторий, но это не тот обобщенный репо, который антипаттерном считается. Сам по себе репо это паттерн вполне нормальный. Вы прочитайте внимательней что я писал - плох не сам репозиторий, а когда он протекает.

Вы правы, по-умолчанию, в случае невалидной модели мы даже не попадем в action-метод. Но это поведение может быть отключено (по разным причинам).
Больше информации можно найти, например, здесь: How to Use ModelState Validation in ASP.NET Core Web API - Code Maze (code-maze.com)

Очень интересная статья, которая даёт импульс к собственным экспериментам в рассматриваемом автором направлении (обобщение при разработке web API) с целью сокращения кодовой базы.

Такое вот сокращение кодовой базы весьма обманчиво - при мало-мальском развитии проекта это "сокращение" вам обязательно выйдет боком.

Не стоит забывать, что описанный подход задуман, как реализация админки, где зачастую, данные отображаются, так, как они хранятся в БД.

В случае, когда generic controller/repository не подходит, можно добавить custom реализацию.

Да там вон даже в комментарии, на котором я отвечал, товарищ явно будет пытаться этот подход реализовывать не только для админки (хотя и для админки, честно говоря, весьма сомнительный подход, не представляю как в админке может быть отображение данных из БД один к одному)

Обобщение ради чего? Каждый тип требует набор методов "репозитория", это ограничение. Обобщения задуманы для реализации гибкости в архитектуре, тут этим и не пахнет. Репозиторий задуман чтобы отделить доменную логику от инфраструктурной, которая знает как сохранить объект. IQueryable это протечка через абстракции, так как он связан с конкретной реализацией и бд. Да и в целом "репозиторий" протек: инфраструктурные типы в ДТО.

Даже если забить на простейшую архитектуру и все лепить вместе, то это решение минимум не практично, объекты не бывают плоские, они ссылаются на другие и запросы бывают сложные. Сами программные модели не бывают такими простыми. Вы определили пару методов, добавили новый который не вписывается в ограничение и придется еще делать один репозиторий. Есть принцип: интерфейсы принадлежат клиентам. А у вас он тупое и беспощадное ограничение.

Спасибо за комментарий. Если вы дочитали до конца, то там я упомянул, что возможны более сложные случаи, когда отдается DTO, а не доменная сущность. И упомянул, как это решается - вводом маппинга.

У вас нет доменных сущностей и домена, детали orm протекают через всё.

Ещё не надо давать вредные советы про мапперы, мапперы всегда создают проблемы.

А зачем в классеGenericRepositoryпараметр TDbContext? Зачем в примере нужен интерфейс IRepository<TEntity>? Запутать свой код?

А как вы предлагаете без конкретного типа DB-контекста реализовать свойство DbSet?

Масса вариантов. Если мы реализуем слой бизнес-логики в программе, то знаем, что у нас только один тип реализует DbSet. Тогдв generic-параметр для DbContext в процессе реализации известен. Меняется только DbSet, который всегда TEntity. Но даже если задачку усложнить и будем реализовывать промежуточный framework для своих проектов (DbContext в разных проектах свой). В данном классе бизнес-логики не нужен DbContext, ему нужен DbSet. Так передайте этот DbSet в фабрике класса. Измените конструктор. Вот такая фабрика у вас будет:

services.AddScoped<GenericRepository<Employees>>(sp => new GenericRepository<Employees>(sp.GetRequiredService<EmployeesDbContext>().Employees));

Да, можно. Только в этом случае придется каждый DbSet регистрировать в DI. В случае же передачи DbContext - такой необходимости нет. Кроме того, в случае перехода на DTO, может потребоваться доступ ко всему DbContext, чтобы скомпоновать DTO из нескольких сущностей. И тут одним DbSet уже не обойтись.

Зачем каждый DbSet регистрировать DI? Вы внимательно посмотрели мой код? В фабрике сервиса получаем из DI нужный DbContext и берем у него нужный DbSet. Пока DbSet не используется в коде логики, нечего ему там делать. 90% кода написанного "на всякий случай" превращается в мусор. Я бы не стал смешивать логику работы с DTO и бизнес-логику CrUD. Тем более что в данном примере вообще об этом ни слова.

После прочтения статьи есть только один вопрос - если в "ITQ Group" такие "ведущие" разработчики, какие же там джуны?

Если хочется делать гибкие выборки с сортировки и/или поиском, то не лучше ли для этого использовать уже готовые решения как OData?

Плюс к тому же есть немало статей, где описывается, что Paging через Offset сильно нагружает БД.

Повторюсь, что цель проекта была - разработка Web API для админки. Там нет сложных запросов. А вот некоторый поиск и сортировка по столбцам, как это реализовано, например, в ReactAdmin, прекрасно решается предложенным подходом.

IMHO Не стоит в обучающей статье демонстрировать новичкам возврат IQueryable из метода публичного интерфейса, отдаваемого наружу:

public interface IRepository where TEntity : IEntity
{
...
IQueryable GetAll();
...
}



IQuerable, если его использовать с LINQ - это ведь весьма неинтуитивная вещь для новичков. Методы LINQ для него, хоть и выглядят так же, как LINQ для IEnumerable, но принимают в качестве параметров не делегаты (то есть любые методы в коде), а выражения, причем - такие, которым провадер СУБД может подобрать эквивалент в языке запросов. А использование лямбд это до поры, до времени затушевывает. А потом однажды новичку захочется вставить в лямбду вызов своего метода, получит он сообщение об ошибке, что такое не поддерживается, и придется ему репу чесать.
Я бы так не делал, а через GetAll отдавал бы IEnumerable и добавил бы специальный метод для фильтрации.

Я объяснил, для чего я решил возвращать IQueryable, чтобы можно было вынести пэйджинг и сортировку в методы расширения, но при этом, выполнять сортировку и выборку страницы на стороне БД.

Я это понимаю, как возврат IQueryable может облегчить написание остального кода.
Но у IQueryable есть и другая сторона, я о ней. Пару лет назад на Хабре уже была статья, в которой эта, оборотная сторона IQueryable была разобрана подробнее.
IMHO эта оборотная сторона и как с ней жить — это не то, что, с чем должен иметь дело новичок с самого начала. Поэтому я считаю, что в обучащей статье для новичков возвращать IQueryable не стоит. Подрастут — сами научатся, а пока пусть лучше используют IEnumerable, у которого нет этой обратной стороны.

Тогда уж лучше CQRS описать и реализовать generic reposotory для CUD и query для R

Помню делал что-то такое, давно.

В результате - слишком много возни, когда нужно не только круд-операции делать.

Дополнительно, когда происходит разделение фронтенда и бекенда на самостоятельные приложения, возникает желание - не как в базе отдавать, а так как надо фронтенду. Ну и тут - вообще такая пляска начинается с мапперами, валидаторами и т.д.

Но таки не считаю этот подход прям плохим-плохим. Если все простыми крудами ограничивается - почему нет?

Sign up to leave a comment.