В смысле «дергать» разработчика? Есть класс, есть описанные методы. Если нужно что-то добавить уже не в процессе планирования задача на команду, то да, дергать разработчика, но это уже из другой области.
Тут смысл как раз в том, чтобы избежать фильтрации в памяти. Чтобы все было за кулисами, за репозиториями. В этом и суть шаблона. Нужны отфильтрованные, разбитые на страницы данные? Пожалуйста. Нужен объект по идентификатору? Пожалуйста. Нужно создать, удалить объект? Пожалуйста. А с целью оптимизации для каждой реализации репозитория будет собственный подход к каждому методу. Где-то достаточно Where-запроса, а где-то нужно выполнит хранимую процедуру или же обратиться ко вьюхе в базе или еще миллион моментов, но все это вас, как разраотчика, например, контроллера, не волнует и не касается.
согласитесь, отсутствие 100 методов в одном классе не означает, что нигде не будет кода, который должен был бы там размещаться, раз уж у нас есть эти 100 сценариев. правильно? т. е. этот код все-равно будет. либо это будет 100 методов расширения, либо это будет непосредственное использование конструкций Where прямо в коде где-то минимум 100 раз, но скорее всего это будет запутанная и неподдерживаемая смесь этих двух подходов. словом, плохо. но я не навязываю вам свое мнение.
Нет, это не мусорка, а понятный класс с большим количеством методов. Они все сосредоточены в одном месте, поэтому их легко поддерживать, сопровождать и документировать. Если у вас реально возникнет необходимость в 100 методах в репозитории, думаю, это будет не самая сложная часть приложения. И все-равно правильнее иметь 100 методов в одном месте, чем размазанную логику взаимодействия с БД.
В реальности бывают разные ситуации. Например, «отчеты» вообще часто генерируются на стороне сервера и затем представляются в виде «фейковых» сущностей из вьюх баз данных или как-то еще. Суть не в этом.
Так а где достаточно? У нас статья о примере реализации 2х шаблонов, а не «Лучший способ доступа к данным в простом приложении». Т. е. суть статьи — посмотреть, как реализовать эти два шаблона на ASP.NET Core. Я не утверждаю, что это единственно правильный подход во всех ситуациях.
Для меня важно, чтобы интерфейс доступа к данным предоставлял ровно тот набор возможностей, который необходим, и ничего больше. Если над реализацией взаимодействия с БД работает один человек, а использует его другой человек, то у другого человека не будет возможности и соблазна вместо метода расширения (не зная, что он существует или же что он подходит в данном случае) писать непосредственно запросы на Linq либо добавлять свои методы расширения. Также важно иметь все взаимодействие на одном уровне. Имею в виду, чтобы не было такого, что часть метода напрямую вызывается на DbSet, часть — расширения Linq, а еще часть — ваши расширения. По сути, я использовал бы ваш подход только для чего-то простого.
var names = dbContext.Employee.Where(e => e.Salary > 5000).Select(e => e.Name).ToArray();
Можно, конечно, писать эти условия Where прямо в контроллере, но это примерно то же самое, что и писать там непосредственно SQL-запросы. Посмотрите сами на этот код. А если там будет больше условий и если вы не писали этот запрос, как вам понять, что он делает, быстро? И сравните его с понятным названием метода, и тогда вы, возможно, поймете, зачем используются репозитории (или сервисы, кто как любит).
Также представьте, что вы написали несколько таких конструкций по проекту, а затем вам поступает задача изменить условие с БОЛЬШЕ на БОЛЬШЕ ИЛИ РАВНО. И что тогда? Будете везде лазить и изменять. И так делают, к сожалению, большинство.
По поводу EF уже абстракция повторюсь, существует не только EF. Посмотрите хотя бы на пример. Если вы на 100% уверены, что вашему проекту не потребуется что-то другое и знаете, что его ждет дальше — прекрасно, можно все упростить и использовать напрямую DbContext и linq-запросы в контроллерах.
В репозитории находятся только те методы, которые относятся к конкретной сущности, поэтому вряд ли их будет часто слишком много. Обычно около 5-10 в моих проектах в среднем. Но даже если их будет 100 в этом нет ничего страшного, если это действительно необходимые и полезные методы. Если вы будете строить ваши запросы как методы расширения на IQueryable то придется при необходимости реализовывать этот интерфейс для различных источников данных, т.к. не всегда можно обойтись EF, как я уже писал не раз. В общем, не вижу смысла и особой разницы. Просто нужно выбирать подход исходя из задачи.
Что касается GetFiltered, то это пример, просто подразумевается функция, которая отбирает отфильтрованные объекты по одному или нескольким параметрам.
Выше уже ответил на ваш комментарий. Сейчас не понимаю до конца, что вы имеете в виду. Будет здорово, если сможете более детально описать, в чем именно состоит недостаток подхода по-вашему и как избежать этих недостатков, сохранив слабую связанность системы.
Если мы будем расширять общие интерфейсы методами, которые относятся к различным сущностям, получим в итоге мусорку, которую невозможно будет сопровождать и использовать. Представьте, что у вас будет 100 методов расширения, понадобится вставлять название сущности в их имена и так далее. Короче, это не вариант. Если честно, я пытаюсь понять, в чем недостаток, например, репозиториев, чтобы изменить свой подход, если он действительно не оптимален. Но пока что я к этому не пришел. Во множестве проектов не было проблем с реляционной моделью, даже наоборот. Сложные вещи, действительно, необходимо решать отдельными решениями, но так в любом случае будет, мне кажется. Приведите, пожалуйста, если можно, какой-то пример удачной реализации, чтобы понят вашу идею.
Не понимаю, при чем тут IQueryable. Если вы о методе All, то это просто пример, суть ведь не в этом методе. Я написал там, что в реальности там бы были методы Create, Edit, Delete и так далее. Методы для выборки одного и многих элементов по различным параметрам.
Слой абстракций необходим именно для этого, верно, для ослабления связывания. Но я не могу сказать, что он несет какую-то лишнюю нагрузку или усложнение даже в простых проектах, в которых, возможно, никогда не потребуется что-то еще, кроме EF. Такой подход просто разделяет приложение на отдельные части, которые легко и удобно сопровождать и понимать. Чем вы за это платите? Парочкой «дополнительных» интерфейсов. Я считаю, что положительный эффект несравнимо больше.
Боюсь, я чрезмерно упростил пример, вы правы. Я сконцентрировался на решении понятной для меня задачи имплементации знакомого шаблона на новой технологии и не стал углубляться в саму суть этих паттернов и в их сценарии использования. Могу привести несколько примеров применения практически идентичного подхода в реальных проектах, если это поможет.
Для «старого» ASP.NET есть отличная статья по реализации этих шаблонов из 10 шагов.
В этой статье я не пытался заново рассказать о самих шаблонах (по сути, здесь вообще нет их детального описания). Лишь показать простой пример реализации на новой технологии со встроенным DI. Сейчас я думаю, что стоило бы более детально рассказать о том, когда их следует применять, а когда — нет.
1. Т. к. мы реализуем шаблон Единица работы, точкой входа у нас все-таки должен быть соответствующий объект, который существует в единственном экземпляре в контексте одного запроса. Теоретически можно инжектить репозитории и вообще все что угодно, но какой смысл? Инжектить десятки различных типов, затем по отдельности их все запрашивать. А где выполнять сохранение? А как иметь единый контекст? Это все решает UoW.
2. Интерфейс репозитория не знает об IStorage. Он знает только о контексте хранилища (о самом «представлении» базы данных, например, что бы это ни было в конкретном случае), т. к. в противном случае ему просто не с чем было бы работать.
Насчет выделения IStorage и EF. Я не согласен, что все прибито к EF. В простых случаях, где просто нужно сохранить немного объектов где-нибудь, можно всегда обходиться EF. Но в чем-то побольше источниками данных вполне могут выступать и HTTP-сервисы, и файловая система и так далее. Если мы завяжем реализацию на EF, придется потом все переписывать, если возникнет необходимость в чем-то еще. Вот и все.
Насчет обязанности репозиториев. Обязанностью репозитория является агрегация всех методов для манипуляции с данными определенного типа (одной сущностью) и сокрытие особенностей реализации этих манипуляций. Иногда есть смысл разделять CRUD-операции и операции предметной области на репозитории и сервисы, но чаще последние являются избыточными и я помещаю все на один уровень — уровень репозиториев. Т.е. необходимо вам получить объект по идентификатору или же набор объектов по определенным признакам (например, 10 самых популярных товаров в интернет-магазине за вчера) — все это делается одним методом одного репозитория. Т.е. запросы одного уровня.
Насчет абстракций в трехзвенном приложении. В общем-то я с вами согласен. За исключением того, что часто нет смысла в разделении репозиториев и сервисов.
Storage является реализацией Единицы работы, поэтому именно его мы и инжектим в контроллер. Это ведь удобно — иметь единую точку доступа ко всем репозиториям с единым контекстом и возможностью фиксации всех изменений. Инжектить StorageContext в контексте приведенной архитектуры не имеет смысла.
Насчет дублирования готового функционала EF. Вы правы, DbContext также является реализацией Unit of work и можно использовать его напрямую. В простых приложениях это вполне применимо. Но что если на каком-то этапе потребуется, как в примере, использовать другой источник данных, вообще не основанный на EF. Например, JSON HTTP API или же, снова-таки случай из примера, перечисление в памяти для тестирования. Что тогда? Тогда придется либо признать, что сделать этого уже нельзя (без переписывания всего, что связано с механизмом взаимодействия с данными), либо сесть и все-таки внедрить слой абстракций, например, с помощью описанного в статье подхода, но все-равно с переписыванием всего. А ведь в реальных долгоиграющих проектах задачи и условия меняются постоянно.
Смотрите, предположим, у вас есть необходимость выбрать всех людей со средней зарплатой больше Х. Вы получаете соответствующий репозиторий из единицы работы и затем выполняете 1 метод, который просто принимает параметр Х. Вам не нужно заботиться о том, как именно реализован этот метод с точки зрения предметной области или с точки зрения базы данных или другого хранилища. Вы просто его используете. Он вполне может быть написан кем-то другим и его логика может 10 раз измениться в процессе, но остальной код менять не потребуется. Также если вам нужен конкретный человек, вы получаете его по идентификатору также из репозитория. Часто для решения первой задачи вводится еще слой сервисов, но он практически всегда избыточен и попросту дублирует наборы методов из репозиториев, поэтому почти всегда я размещаю ВСЕ методы для манипуляции объектами определенного типа в репозиторий. Представьте, что вы используете в контроллерах DbContext и DataSet напрямую. Вы будете везде писать один и тот же Linq-запрос, чтобы выбрать необходимые объекты? Или как вы сказали, создадите просто висящий в воздухе ItemManager? Тогда наверняка часть методов будут выполняться на DbSet, а часть — на ItemManager, что приведет к путанице и сделает еще более сложными описанные выше сценарии.
Совершенно верно, я согласен. Но было бы неправильным делать в качестве примера огромный проект только для того, чтобы продемонстрировать, что подход вряд ли стоит использовать в проектах на 3 страницы. Эти шаблоны очень удобно применять когда есть вероятность использования различных хранилищ, для тестирования и так далее, когда даже наперед неизвестно, как требования будут изменяться дальше и хочется достигнуть минимальной связанности системы, чтобы можно было сопровождать ее максимально просто.
Дело в том, что такой подход дает универсальность. Не всегда в качестве хранилища используется база данных и не всегда в качестве ORM используется EF. А использование дополнительного слоя репозиториев позволят скрыть за ними реализацию тех или иных методов. Например, если есть метод фильтрации объектов, то гораздо удобнее использовать метод вроде GetFiltered(query), чем в каждом месте, где это необходимо, писать Linq-выражения. Кроме того, с точки зрения командной разработки такой подход позволяет разработчику, например, какого-то контроллера вообще не знать, как именно производится фильтрация объектов, просто вызывая один метод, который написан кем-то другим. Это важно и удобно.
Тут смысл как раз в том, чтобы избежать фильтрации в памяти. Чтобы все было за кулисами, за репозиториями. В этом и суть шаблона. Нужны отфильтрованные, разбитые на страницы данные? Пожалуйста. Нужен объект по идентификатору? Пожалуйста. Нужно создать, удалить объект? Пожалуйста. А с целью оптимизации для каждой реализации репозитория будет собственный подход к каждому методу. Где-то достаточно Where-запроса, а где-то нужно выполнит хранимую процедуру или же обратиться ко вьюхе в базе или еще миллион моментов, но все это вас, как разраотчика, например, контроллера, не волнует и не касается.
В реальности бывают разные ситуации. Например, «отчеты» вообще часто генерируются на стороне сервера и затем представляются в виде «фейковых» сущностей из вьюх баз данных или как-то еще. Суть не в этом.
значительно лучше, чем
Можно, конечно, писать эти условия Where прямо в контроллере, но это примерно то же самое, что и писать там непосредственно SQL-запросы. Посмотрите сами на этот код. А если там будет больше условий и если вы не писали этот запрос, как вам понять, что он делает, быстро? И сравните его с понятным названием метода, и тогда вы, возможно, поймете, зачем используются репозитории (или сервисы, кто как любит).
Также представьте, что вы написали несколько таких конструкций по проекту, а затем вам поступает задача изменить условие с БОЛЬШЕ на БОЛЬШЕ ИЛИ РАВНО. И что тогда? Будете везде лазить и изменять. И так делают, к сожалению, большинство.
По поводу EF уже абстракция повторюсь, существует не только EF. Посмотрите хотя бы на пример. Если вы на 100% уверены, что вашему проекту не потребуется что-то другое и знаете, что его ждет дальше — прекрасно, можно все упростить и использовать напрямую DbContext и linq-запросы в контроллерах.
Что касается GetFiltered, то это пример, просто подразумевается функция, которая отбирает отфильтрованные объекты по одному или нескольким параметрам.
В этой статье я не пытался заново рассказать о самих шаблонах (по сути, здесь вообще нет их детального описания). Лишь показать простой пример реализации на новой технологии со встроенным DI. Сейчас я думаю, что стоило бы более детально рассказать о том, когда их следует применять, а когда — нет.
Согласен, что в каждом случае необходимо исходить из задачи.
1. Т. к. мы реализуем шаблон Единица работы, точкой входа у нас все-таки должен быть соответствующий объект, который существует в единственном экземпляре в контексте одного запроса. Теоретически можно инжектить репозитории и вообще все что угодно, но какой смысл? Инжектить десятки различных типов, затем по отдельности их все запрашивать. А где выполнять сохранение? А как иметь единый контекст? Это все решает UoW.
2. Интерфейс репозитория не знает об IStorage. Он знает только о контексте хранилища (о самом «представлении» базы данных, например, что бы это ни было в конкретном случае), т. к. в противном случае ему просто не с чем было бы работать.
Насчет выделения IStorage и EF. Я не согласен, что все прибито к EF. В простых случаях, где просто нужно сохранить немного объектов где-нибудь, можно всегда обходиться EF. Но в чем-то побольше источниками данных вполне могут выступать и HTTP-сервисы, и файловая система и так далее. Если мы завяжем реализацию на EF, придется потом все переписывать, если возникнет необходимость в чем-то еще. Вот и все.
Насчет обязанности репозиториев. Обязанностью репозитория является агрегация всех методов для манипуляции с данными определенного типа (одной сущностью) и сокрытие особенностей реализации этих манипуляций. Иногда есть смысл разделять CRUD-операции и операции предметной области на репозитории и сервисы, но чаще последние являются избыточными и я помещаю все на один уровень — уровень репозиториев. Т.е. необходимо вам получить объект по идентификатору или же набор объектов по определенным признакам (например, 10 самых популярных товаров в интернет-магазине за вчера) — все это делается одним методом одного репозитория. Т.е. запросы одного уровня.
Насчет абстракций в трехзвенном приложении. В общем-то я с вами согласен. За исключением того, что часто нет смысла в разделении репозиториев и сервисов.
Насчет дублирования готового функционала EF. Вы правы, DbContext также является реализацией Unit of work и можно использовать его напрямую. В простых приложениях это вполне применимо. Но что если на каком-то этапе потребуется, как в примере, использовать другой источник данных, вообще не основанный на EF. Например, JSON HTTP API или же, снова-таки случай из примера, перечисление в памяти для тестирования. Что тогда? Тогда придется либо признать, что сделать этого уже нельзя (без переписывания всего, что связано с механизмом взаимодействия с данными), либо сесть и все-таки внедрить слой абстракций, например, с помощью описанного в статье подхода, но все-равно с переписыванием всего. А ведь в реальных долгоиграющих проектах задачи и условия меняются постоянно.
Смотрите, предположим, у вас есть необходимость выбрать всех людей со средней зарплатой больше Х. Вы получаете соответствующий репозиторий из единицы работы и затем выполняете 1 метод, который просто принимает параметр Х. Вам не нужно заботиться о том, как именно реализован этот метод с точки зрения предметной области или с точки зрения базы данных или другого хранилища. Вы просто его используете. Он вполне может быть написан кем-то другим и его логика может 10 раз измениться в процессе, но остальной код менять не потребуется. Также если вам нужен конкретный человек, вы получаете его по идентификатору также из репозитория. Часто для решения первой задачи вводится еще слой сервисов, но он практически всегда избыточен и попросту дублирует наборы методов из репозиториев, поэтому почти всегда я размещаю ВСЕ методы для манипуляции объектами определенного типа в репозиторий. Представьте, что вы используете в контроллерах DbContext и DataSet напрямую. Вы будете везде писать один и тот же Linq-запрос, чтобы выбрать необходимые объекты? Или как вы сказали, создадите просто висящий в воздухе ItemManager? Тогда наверняка часть методов будут выполняться на DbSet, а часть — на ItemManager, что приведет к путанице и сделает еще более сложными описанные выше сценарии.