Комментарии 34
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, если статья — обучающая (а это в данном случае так: и по структуре изложения — крайне разжеванного, и по плашке в начале), то она должна описывать готовое решение. Подходы описывать — это в статьях для уже обученной аудитории.
Иначе есть большой риск научить новичка плохому. Не надо так — плохому новичок сам научится.
Я посмотрел код - там совсем не 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, а не доменная сущность. И упомянул, как это решается - вводом маппинга.
А зачем в классе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 сильно нагружает БД.
public interface IRepository where TEntity : IEntity
{
...
IQueryable GetAll();
...
}
IQuerable, если его использовать с LINQ - это ведь весьма неинтуитивная вещь для новичков. Методы LINQ для него, хоть и выглядят так же, как LINQ для IEnumerable, но принимают в качестве параметров не делегаты (то есть любые методы в коде), а выражения, причем - такие, которым провадер СУБД может подобрать эквивалент в языке запросов. А использование лямбд это до поры, до времени затушевывает. А потом однажды новичку захочется вставить в лямбду вызов своего метода, получит он сообщение об ошибке, что такое не поддерживается, и придется ему репу чесать.
Я бы так не делал, а через GetAll отдавал бы IEnumerable и добавил бы специальный метод для фильтрации.
Я объяснил, для чего я решил возвращать IQueryable, чтобы можно было вынести пэйджинг и сортировку в методы расширения, но при этом, выполнять сортировку и выборку страницы на стороне БД.
Я это понимаю, как возврат IQueryable может облегчить написание остального кода.
Но у IQueryable есть и другая сторона, я о ней. Пару лет назад на Хабре уже была статья, в которой эта, оборотная сторона IQueryable была разобрана подробнее.
IMHO эта оборотная сторона и как с ней жить — это не то, что, с чем должен иметь дело новичок с самого начала. Поэтому я считаю, что в обучащей статье для новичков возвращать IQueryable не стоит. Подрастут — сами научатся, а пока пусть лучше используют IEnumerable, у которого нет этой обратной стороны.
Странная аргументация. Отличия IQuerable<> от IEnumerable<> - дефолтный вопрос на любом собеседовании на уровень джуна. Ну, один раз забудется и попадётся, но не более того. Если уж говорить об разграничении слоёв, то нечего делать репе в контроллере.
Ничего странного.
Одно дело - заучить теорию для собеса (кстати, интересно было бы посмотреть в плане статистики, как в реальности на собесах новички на этот вопрос отвечают - там возможно много интересных вариантов разной степени правильности).
Другое дело - осознать, что в .NET и C# есть такая сущность - Expression Tree, и понимать, что лямбда двулика.
Третье, самое трудное - осозновать, с чем имеешь дело здесь и сейчас, в конкретном месте. Ну да, всплывающие подсказки есть - но и там это лишь вскользь упомянуто, а уж про определения методов в коде или в документации я и не говорю - их читать совершенно некогда (то есть лень). И если это выражение - понимать, какой компонент его там дальше использовать будет и какие ограничения он налагает. И компилятор тут - плохой помощник: если, допустим, вставить вместо лямбды, которая используется как выражение, делегат он не даст, то проверить эти ограничения он не поможет, и они всплывут уже только при работе программы, в худшем случае - на бою.
А самое неприятное - что у разных СУБД эти ограничения на выражения разные. И при попытке сменить СУБД они могут внезапно вылезти там, где раньше их не было.
Всему этому подвержены, конечно, не только новички. Но новички - больше. И не из-за того, что они не знают (теоретически-то они знают, скорее всего), а из-за того, что из-за недостатка опыта они могут не ждать подвоха.
Всё это - основание спрятать IQueryable подальше и не держать его в публичном интерфейсе.
Тогда уж лучше CQRS описать и реализовать generic reposotory для CUD и query для R
Помню делал что-то такое, давно.
В результате - слишком много возни, когда нужно не только круд-операции делать.
Дополнительно, когда происходит разделение фронтенда и бекенда на самостоятельные приложения, возникает желание - не как в базе отдавать, а так как надо фронтенду. Ну и тут - вообще такая пляска начинается с мапперами, валидаторами и т.д.
Но таки не считаю этот подход прям плохим-плохим. Если все простыми крудами ограничивается - почему нет?
C# Generic-подход к разработке web API