Комментарии 53
Под конец статьи все собралось в картину.
Как я вижу - вы всю статью прикрывает свои же ошибки, и ищите их везде кроме своих решений))
1 В статье вся боль начинается с того что для бизнес аналитики одна база представляет простой механизм, а во второй надо разбираться самому и писать запрос (обратите внимание что здесь эта проблема приписывается репозиторию!!!) Хотя возможно тут проблема что ОРМ через которую пишутся запросы не предоставляет возможности на такой запрос)
2 По поводу возврашаемых данных через репозиторий - там может быть любой тип, если вы в репозитории просто отправляете ответ ОРМ, то не надо сетовать на то что репозиторий возвращает IQueryable
По итогу получился какой-то ком грязи (чисто моё мнение по прочитаному из статьи) где вместо решения проблем , которые на прямую к репозиторию не относятся, предлагается разделять запросы по принципу CQRS, мотивируя это тем что по другому думать надо как аналитику писать))
Прекрасная иллюстрация как проблема привалирует над архитектурой)
Главная ловушка Generic Repository — IQueryable наружу. Вроде абстракция, а на деле ты привязан к EF жёстче, чем без репо. Для сложных запросов давно перешёл на спецификации, репозитории оставил только для CRUD.
Спасибо за разбор по пунктам, отвечу так же.
Как я вижу - вы всю статью прикрывает свои же ошибки, и ищите их везде кроме своих решений))
Так в этом и пост! Это "постмортем" про ошибки на том проекте, я в этих граблях сидел сам. И главная наша ошибка в том, что вынесли Generic Repository<T> наружу как публичный контракт. Вот это решение сейчас и разбираю. Только провалы из него - структурные, а не следствие "кривых рук": в ту же развилку упирается любая реализация такого контракта, будь она аккуратная или нет.
По пункту 1. Боль в этом кейсе не в том, что "базу надо понимать и писать запрос". $facet Mongo считает сам, в один проход на стороне БД - ни сама база, ни драйвер к ней тут ничего "не умеют". Я не приписываю репозиторию сложность запроса, а показываю обратное: контракт generic-репозитория не даёт воспользоваться тем, что база уже умеет. Чтобы построить фасеты, приходится либо протащить провайдер-специфику в интерфейс, либо выгрузить выборку в память и считать в приложении. Узкое место не база и не драйвер, а контракт между ними.
По пункту 2. Согласен: тип на выходе - выбор разработчика, а не то что диктует паттерн. Поэтому в посте разобраны оба выхода. Отдаёте IQueryable - наружу протёк провайдер: Mongo и EF транслируют его в разные запросы и по-разному падают. Не отдаёте - значит либо материализуете выборку (выгрузка в память), либо добавляете под это чтение отдельный метод (хоть проекция в DTO, хоть aggregation). И вот этот второй выход ключевой: метод под конкретное чтение проблему чинит, но в ту же секунду репозиторий перестаёт быть generic. "Возвращай любой тип" развилку не убирает, а переводит на вторую проблему.
написать датамапер который IQueryable завернет в какую-то структуру которая будет вам удобнее
Датамаппер из соседней ветки - тот же случай:
если материализует выборку и вы теряете push-down, $facet не отработает
если остаётся ленивым и композируемым, тогда обязан отдать наружу примитивы построения запроса, то есть снова IQueryable или спецификацию, которую провайдер либо переведёт, либо уронит в рантайме уже на проде
а если это отдельная типизированная структура под конкретное чтение, отдающая сразу read-DTO - это и есть read-side query object из финала статьи - и тогда мы с вами заодно :)
Вопрос в том, остаётся ли эта штука под вывеской Repository<T>. По мне нет - потому что generic она уже не является 💁♂️
вместо решения проблем , которые на прямую к репозиторию не относятся, предлагается разделять запросы по принципу CQRS
Про CQRS. Называйте это хоть CQRS - суть не в ярлыке. В посте оговорено явно: это не event sourcing, не отдельная база на чтение и не шина, всё та же одна Mongo, просто чтение и запись разведены на уровне запросов. И развожу я не "несвязанные с репозиторием проблемы", а ровно ту, что создал generic-контракт: это он загнал чтение и запись в одну дверь. Разведёшь - он растворяется.
И чтобы граница была честной: generic-база как приватная деталь под доменными репозиториями - это нормально, об этом в посте сказано прямо. Анти-паттерн - это Generic Repository<T>, который торчит наружу публичным контрактом сразу под запись агрегатов и под произвольные проекции на чтение.
Что вы имеете ввиду под репозиторий торчит наружу? У репозитория есть чёткая цель и места где он может размещаться по проекту и конечно его интерфейс должен "торчать" для клиентского кода.
То что репозиторий перестаёт быть генерик это что значит? На сколько я понял генерик это именно реализация c# который в себе содержит методы crud, ну допустим у вас не генерик репозиторий, а обычный с кучей операций
Mongo считает сам, в один проход на стороне БД - ни сама база, ни драйвер к ней тут ничего "не умеют". Я не приписываю репозиторию сложность запроса, а показываю обратное: контракт generic-репозитория не даёт воспользоваться тем, что база уже умеет.
Тут вы вообще последовательность теряете, то у вас это делает монго, в середине никто ничего "не умеет", а в конце опять монго все делает(
Ну я если честно не знаю зачем себя ограничивать генерик репозиторем, когда можно пользоваться обычным))
зачем себя ограничивать генерик репозиторем, когда можно пользоваться обычным))
О, на последней фразе мы как раз сходимся, я ровно это и предлагаю как вывод в финале статьи: вместо Generic Repository<T> взять обычные конкретные репозитории.
Теперь про термины, чтобы не путаться:
репозиторий торчит наружу
окей, тут сказал неудачно. Да, интерфейс репозитория должен быть виден клиентскому коду, иначе он бесполезен. Я не про видимость, а про форму контракта:
с одной стороны generic Repository<T> с CRUD по типу T
с другой - конкретный IBookingRepository с методами по смыслу
...оба видны клиенту, но первый предлагает один набор операций на все типы сразу, второй - набор конкретных операций к конкретному агрегату. Generic-репо торчит наружу всеми методами, даже теми, которых по смыслу у агрегата не должно быть.
репозиторий перестаёт быть генерик
уточню, чтобы не путать - тут про контракт, от которого зависит клиентский код. Как только он зависит от именованного метода по смыслу, а не от Repository<T> с GetById/Find по любому T, контракт в этой точке уже конкретный. Это и есть ваш "обычный с кучей операций". Я против дженерика по T наружу, не против репозитория как такового.
Тут вы вообще последовательность теряете...
в пункте 1 - ок, скомкал, попробую развернуть:
про саму базу - $facet Mongo умеет, считает в один проход
про обёртку над базой как "обобщённый хранилищенезависимый репозиторий" - к $facet через неё не подобраться, не протекая провайдером или не выгружая всю выборку в память.
Мой тезис: умеют и движок, и драйвер - не умеет generic-обёртка поверх них. Поэтому в решении мы и спускаемся к драйверу напрямую. Никакого "Mongo → никто → Mongo": база умеет всегда, а нам мешает только обобщённый контракт над ней.
Так что в главном мы с вами заодно. Единственное уточню: запись я веду через конкретный репозиторий, а тяжёлое чтение - мимо него, тонким запросом напрямую к БД. Но это уже деталь поверх того, в чём мы сошлись.
Ну если честно дженерик репо это просто сахар для разработчика, должно быть очевидно что crud это не все чем должен заниматься источник данных)) И это очевидно не все что нужно вашим агрегатам))
А вообще, я просто пытаюсь разобраться - что и для чего вы написали :) Раз уж архитектура это и моя работа)
это радует, разбор от коллег это одна из целей для чего вообще я начал писать ))
я много лет всё откладывал и прокрастинировал, хотя материалов и кейсов накопилось много, есть о чем рассказать, вот пытаюсь писать так, чтобы было интересно коллегам и читать, и вовлекаться в обсуждение, надеюсь это так )
Поэтому в решении мы и спускаемся к драйверу напрямую. Никакого "Mongo → никто → Mongo": база умеет всегда, а нам мешает только обобщённый контракт над ней.
Извините за такой вопрос, если что, а вы комменты пишите и читаете с нейронкой вместе или мне показалось?)
Проекты и кейсы на них, каркас статьи, рассуждения, тезисы и выводы, грабли что я видел и наступал сам, решения и ошибки, что я сделал - это всё мой опыт за годы работы в ИТ.
Что касается нейронки - длинные ответы накидываю с ассистентом и правлю, да. Но кейсы, рассуждения и выводы мои, про facet на поиске отелей и мой проект нейронка ничего не знает. Юзаю как инструмент, но голова своя 🙂
Извините за такой вопрос, если что, а вы комменты пишите и читаете с нейронкой вместе или мне показалось?)
Один раз случайность, два - совпадение, три - … 😀
Но в случае с flannery_gold там сильнее заметно, а тут только одна эта последовательность выдала
>Хотя возможно тут проблема что ОРМ через которую пишутся запросы не предоставляет возможности на такой запрос)
Т.е. у вас "архитектура" приложения возможностями ORM диктуется?
На моем опыте мысль “репозиторий позволит абстрагироваться от базы данных” - это несбыточная мечта, которая очень дорого обходится в процессе. Если мы говорим о смене одной SQL-базы на другую, то современный EF как правило справляется достаточно хорошо - в разных проектах пробовали разные сочетания SQL Server, PostgreSQL и SQLite, потребовались только минимальные модификации (и то в основном в коде миграций). Вообще говоря, у SQLite in-memory достаточно высокая совместимость с PostgreSQL, чтобы ее можно было использовать в юнит-тестах вместо живой базы на 99% тестах - очень сильно экономит время по сравнению с теми же testcontainers, на которых прогоняется оставшийся 1% сложных тестов. Ну а пытаться написать абстракцию, которая одинаково хорошо скрывает как SQL, так и NoSQL-базы, имхо гиблое дело - либо абстракция будет течь, либо это будет работать страшно неэффективно.
Мне кажется это частая ошибка в понимании репозитория.
Если у вас данные абстрагированны репозиторием, это не значит что вы можете просто в проекте переключать источники данных как захотите.
При необходимости сменить или добавить источник данных - пишем новую реализацю репозитория, подставляем в алиас новый репозиторий и все)
По моему очень элегантно и удобно)
Ну и по реализации репозиторий явно не самый сложный паттерн)) Но один из самых важных, как по мне
В том и проблема, что “элегантно и удобно” на практике не получается. Нельзя заранее заложить такой интерфейс, чтобы в него хорошо ложился и текущий, и любой будущий источник данных. Даже уже SQL и NoSQL слишком разные подходы, чтобы их можно было унифицировать единым интерфейсом, не жертвуя производительностью.
В том и проблема, что “элегантно и удобно” на практике не получается
Не говорите за всех, у кого-то получается, в книгах куча примеров.
Вы интерфейсом закладываете потребности вашего получателя данных (домен, агрегат как у автора), а не детали реализации sql, nosql. Детали никак не влияют на абстракцию)
GetById в sql и nosql очевидно имеют разную реализацию, но интерфейс репозитория содержит сигнатуру GetById
Окей, вот конкретный пример. У нас был репозиторий примерно такого вида, когда первая версия проекта работала поверх CosmosDB:
interface IRepository<T>
{
T GetById(int id);
void Remove(int id);
void Update(int id, T value);
}
Все очень просто: взял запись, поправил, сохранил. С NoSQL отлично работает. Дальше пытаемся переехать на SQL + EF, и сразу возникают вопросы:
Внутри
GetByIdнужно делатьAsTrackingилиAsNoTracking? Оба варианта имеют проблемы с производительностью, третий вариант “выставить флаг наружу” нарушает абстракциюRemoveдолжен вызыватьExecuteDelete(быстро, но ломает стейт) илиSaveChanges(требует подгрузки, может применить другие изменения)?Нужно поменять две сущности вместе, атомарно, в одной транзакции. В интерфейсе такого нет - не было нужно, база не умеет и т.д., что делать? Нарушить абстракцию и использовать транзакции, или писать саги-реверты по старинке с горой бойлерплейта?
И это только то, что пришло в голову, на двух достаточно хорошо известных технологиях. А если через два года потребуется переехать на что-то еще более экзотическое?
Конкретный пример с какими-то абстрактными уточнениями)
По каждому уточнению принимать решение конкретному человек по конкретному проекту, а не демогогию разводить, не один паттерн не даст вам ответы на такие вопросы, причём тут вообще репозиторий)
Если у вас что-то умеет база или не умеет вы можете переносить обработку данных хоть в код проекта хоть в базу. Если sql - больше логики в базе (сложные запросы) если nosql - больше обрабатывается кодом (более бедная модель данных)
Репозиторий помогает направить зависимости и отделяет код бизнес логики от кода получения данных)
Так вы же выше утверждаете, что всё очень просто, и если не получается сделать красиво - это просто skill issue. Вот я вам пример задачи привожу, минимальный и конкретный: как бы вы в этом случае сделали красиво? Только по существу, пожалуйста, а не цитаты абстрактных определений из книг
На моем опыте мысль “репозиторий позволит абстрагироваться от базы данных” - это несбыточная мечта, которая очень дорого обходится в процессе.
Я вам отвечал на конкретную фразу, поэтому не надо передергивать и говорить за меня. Темболее что это супер легко я не писал.
Я уже вам ответил на ваши "конкретные" вопросы, их нужно решать по факту
Быстрых ответов на сложные вопросы вы не получте, не тут, не из книг, только от нейронки наверное
Но я бы рекомендовал книги :)
пишем новую реализацю репозитория, подставляем в алиас новый репозиторий и все
репозиторий явно не самый сложный паттерн
Темболее что это супер легко я не писал.
Судя по вашему вопросу в отдельной ветке про “маппинг для IQueryable” я могу сделать вывод, что вы вообще не имеете никакого представления о C# \ EF, однако решили поучаствовать в дискуссии, цитируя прописные истины. А как только я предложил обсудить вопрос по существу, запас тотчас иссяк и началось - “вопрос недостаточно конкретный”, “я не я и лошадь не моя” и т.д… Жаль, что так получилось.
Тут же речь про паттерн, вопрос про маппер был скорее с сарказмом))
По существу я от вас ничего не увидел, интерфейс и какие-то рассуждения вокруг него которые нужны конкретно вам)
А мне, если честно, все понравилось :)
пишем новую реализацю репозитория, подставляем в алиас новый репозиторий и все
Под "и все" я имею ввиду что бизнес логика не затрагивается, а пишется только обёртка для данных и все))
репозиторий явно не самый сложный паттерн
Не самый сложный, не значит что он лёгкий или вообще средний)) Вроде такое должно быть очевидно
Возможно произошла путаница. Часто паттерн репозиторий используют в ddd и вот как раз там нет проблем с переходом на другую бд
путаницы нет, статья как раз вокруг этой темы
мне очень интересно услышать вашу версию: расскажите подробнее как вы пишете слой данных и не имеете проблем с БД под капотом у доменного репозитория 🤔
Проблемы в любом случае будут, просто потому что перевести нетривиальный проект с одной базы на другую - это само по себе сложная инженерная задача, вне зависимости от используемой методологии, DDD или что угодно еще. Репозиторий позволяет разве что упростить миграцию в одном слое за счет усложнения другого: условно, логика приложения переезжает вообще без изменений, зато на слое доступа к данным теперь творится ад. Во многих случаях это оправдано, но нужно учесть, что некоторые проблемы (в частности с производительностью) с такими ограничениями становятся вообще нерешаемыми, поэтому менять интерфейс в любом случае придется
А версия no sql была без ef? Как бы получается, что вопросы эти касаются именно ef. Грубо говоря, для get-by-id - если в первой реализации вы не трекали изменения, значит во второй используете as-no-tracking
Все же чистый репозиторий (в том числе без всяких дженериков) + unit of work дает больше гибкости, чем orm или связка репозиторий + orm внутри.
Добавлю - пожалуй orm типа ef и репозиторий даже в какой то мере мешают друг другу.
Это хорошо видно на методе update. В классическом репозитории - туда приходит dto и внутри формируется sql запрос update. В случае с ef, если был включен трекинг - метод update вырождается до вызова save changes. А если еще есть транзакционная обвязка, то метод update вообще пустой, потому что save changes вызывается где-то выше по стеку, там где begin / commit transaction
Отвечу сразу на оба комментария:
А версия no sql была без ef? Как бы получается, что вопросы эти касаются именно ef. Грубо говоря, для get-by-id - если в первой реализации вы не трекали изменения, значит во второй используете as-no-tracking
Да, изначально была самопальная реализация репозитория. Разумеется, можно было бы везде использовать AsNoTracking, но это противоречит принципам EF и приводит к неоптимальному коду, о чем я говорил выше: придется создавать контекст отдельно на каждую операцию, явно прикреплять к нему объект и т.д.
чистый репозиторий + unit of work дает больше гибкости, чем orm
EF сам по себе уже предоставляет эти абстракции: DbContext = Unit of work, DbSet<T> = Repository.
если был включен трекинг - метод update вырождается до вызова save changes
Верно - в таком контексте метод update вообще не имеет смысла, потому что обновление состояния происходит при изменении любого свойства (в том числе произвольного другого объекта, входящего в отслеживаемый граф)
>в книгах куча примеров
"- Так моему соседу 85, а он говорит, что с женщинами ещё ого-го.
- Вот и вы _говорите_" (c)
Если мы говорим о смене одной SQL-базы на другую, то современный EF как правило справляется достаточно хорошо
Про SQL→SQL полностью согласен: переносимость между реляционными базами даёт сам EF, репозиторий тут ничего не добавляет. Вы это на трёх СУБД и показали - правки в основном в миграциях, а остальное EF тащит сам.
Вообще говоря, у SQLite in-memory достаточно высокая совместимость с PostgreSQL, чтобы ее можно было использовать в юнит-тестах вместо живой базы
Про тесты хочу развить мысль. В посте я "ругал" именно EF InMemory и подобные ему: это не реляционный провайдер, нет констрейнтов, своя трансляция, своё поведение на GroupBy. SQLite in-memory - совсем другое, это настоящий SQL-движок с реальной семантикой. Поэтому ваш расклад честный: 99% на sqlite , потому что там настоящий SQL, и 1% на testcontainers. И вот этот 1% как раз должен ловить места, где sqlite и postgres расходятся - collation, отдельные типы, конкурентность. Тот самый разъезд сортировки по collation, на котором я в посте обжёгся, живёт в этом 1%.
Ну а пытаться написать абстракцию, которая одинаково хорошо скрывает как SQL, так и NoSQL-базы, имхо гиблое дело - либо абстракция будет течь, либо это будет работать страшно неэффективно.
Про мысль "сменить источник = написать новую реализацию и подставить" - по-моему, тут про 2 разные вещи. Репозиторий как шов для DI и подмены реализации - полезно, не спорю. Но мечта "сменим базу, домен не заметит" - про другое и она-то и дорогая: новую реализацию написать мало, под новый стор переписываются сами запросы, а часто и модель данных, потому что возможности у сторов разные. Абстракция переносит сигнатуры, а не запросы. Для SQL→SQL это и не нужно (EF справляется), а для SQL→NoSQL, как вы и говорите, она либо течёт, либо работает неэффективно.
Это один из самых полезных комментов, спасибо :)
новую реализацию написать мало, под новый стор переписываются сами запросы
А что для вас реализация репозитория если не воплощение запроса в его конкретном виде?) Да и возвращаемые значения должны быть по сигнатуре одинаковые...
И DI тут не причём...
Так да, в том и суть: "написать новую реализацию" = "переписать все запросы под новый стор", это и есть дорогая часть, а паттерн репозиторий продавали как то, что её экономит! а в nosql даже сигнатура одинаковой не остаётся
Вас явно пытались обмануть, потому что репозиторий разделяет код и задаёт правильное направление зависимостям, а не экономия на переходе с sql на nosql, но в свою очередь такая возможность тоже имеется
Про сигнатуру это ваши заморочки которые я до сих пор не могу понять) Если вы в репозитории отдаёте просто что вам вернула ОРМ, это не проблема репозитория как паттерна)
репозиторий разделяет код и задаёт правильное направление зависимостям, а не экономия на переходе с sql на nosql
Так меня и не обманули, в посте я написал в точности это: репозиторий ценен разделением кода и направлением зависимостей, а "экономия на смене БД" - это ложное обещание
Про сигнатуру по кругу не пойду, выше уже отвечал
В последних примерах кода в статье автор смешивает между собой понятия репозитория и объектов типа Query / Command. Репозиторий IRepository<T> таким и должен быть, как описан в начале статьи. Использую генерик-тип Т его можно конкретизировать для нужного типа сущности - например BookingRepository. Но Command объект это не будет наследник IBookingRepository. Это будет объект BookingCommand, использующий репозиторий BookingRepository.
автор смешивает между собой понятия репозитория и объектов типа Query / Command
Что query не должен наследлвать репозиторий - тут согласен, это разные штуки. HotelSearchQuery - это отдельный класс для чтение из БД, а не наследник репозитория. В финале я предлагаю такую концепцию:
IBookingRepository - запись делаем через репозиторий с методами по смыслу для конкретно этого агрегата, он не содержит методы для тех действий, которые с ним совершить нельзя
HotelSearchQuery - читаем через read-query прямо из провайдера, а не через IRepository<T>, это намеренно
потому что $facet через контракт репозитория невозможно вызвать, с этого примера статья и начинается. Как только читающий query начнёт использовать репозиторий, на чтении вернётся та проблема, ради которой всё и затевалось.
Generic IRepository<T> торчащий наружу методы не допустимые для конкретного агрегата - это то, что я оспариваю. Ну а если юзать в качестве приватной базы под конкретным репозиторием - вопросов нет.
Command не может быть классом-наследником репозитория. Command и репозиторий находятся в разных слоях приложения. Command использует функционал репозитория для изменения данных в бд.
как в обсуждение репозитория у вас попали команды? про них речи вообще не было
В последнем примере статьи для извлечения данных используется Query объект HotelSearchQuery. Поэтому для изменения данных логично использовать объект типа Command. Использование связки объектов Query / Command для работы с данными в бд детально описано в технологии CQRS.
мм, понял о чём вы 👌
CQRS в статью я сознательно не добавлял, чтобы не уводить в сторону, а HotelSearchQuery - это не совсем "классический query из cqrs", хотя и называется схоже - просто как демонстрация варианта прочитать напрямую из провайдера БД, в обход Repository, обертка может быть любой
В целом, тут я согласен с вами - опционально, для записи можно написать CommandHandler который внутри будет использовать IBookingRepository с соблюдением всех инвариантов для Booking - они не противоречат друг другу
Про IQueryable, как протекающую абстракцию и про тестирование запросов in-memory +много. Сам на собесах такие вещи проверяю.
А вот про загрузуку всего поста для того, чтобы добавить один коммент (работа через агрегаты) - стоило бы отдельно поговорить.
Ну и в целом, для того чтобы паттерн "загрузли агрегат из репы -> внесли изменения в агрегат -> сохранили агрегат целиком" нужна либо ORM, либо key-value хранилище. И отсутствие вопросов с конкурентностью. А если на той стороне базёнка с процедурами, то уложиться в такой паттерн становится намного труднее. Так что инфраструктурные ограничения далеко не всегда DDD-паттернами устраняются.
спасибо, рад что вам зашло ))
Про сохранение агрегата по всем пунктам согласен: load-save агрегат держится на change tracking ORM-ки (как UoW) или атомарном документе (в монге или kv) и вменяемой конкурентности, а на базе с хранимками вообще начинает воевать с инфраструктурой. DDD моделирует домен - агрегат и есть граница согласованности с оптимистичным локом на версию его корня, но только если хранилище в эту версию умеет.
А вот про загрузуку всего поста для того, чтобы добавить один коммент (работа через агрегаты) - стоило бы отдельно поговорить.
С удовольствие поддержу эту дискуссию. Только хочу набросить встречный тезис: по-моему проблема этого примера в том что граница агрегата проведена по вложенности, а не по инварианту. Между постом и комментом нет общего инварианта, который надо проверять на каждом изменении - значит коммент это отдельный агрегат со ссылкой на PostId, и грузить ради него весь пост незачем 🤷♂️
Предположу, что это лучше ложится на другой пример: корзина с товарными позициями - вот это наоборот, классический настоящий агрегат - там инвариант есть (итог, лимиты, резерв стока, скидки), поэтому грузишь корзину и позиции целиком. У поста с комментами такого инварианта нет, и связки "корень - часть" тоже нет.
Ну или допустим, повесим на пост правило "не больше n комментов" или кэш счётчика - и инвариант как-будто появлякется, и тут можно подумать дальше... А у вас в проде попадался случай, где он там реально возникал и заставлял загружать целое?
В общем случае говорят о разделении доменной модели и persistence model. ORM и change tracking работают как раз с persistence model. Если в юз кейсе предполагается изменение данных в объектах доменной модели, то функционал юз кейса должен обеспечить копирование данных из изменённых доменных объектов в соответствующие объекты persistence model и далее сохранить их в бд. В подобном алгоритме уже не имеет значения используется ORM или нет.
Разделять доменную модель и модель хранения это ок.
Но проблему, о которой говорит @monco83 это не убирает. Чтобы сохранить, всё равно надо понимать что именно поменялось - или это отслеживает ORM или ты сам сравниваешь и обновляешь или переписываешь весь агрегат целиком. Так что ORM тут по-прежнему важен, просто работа по отслеживанию изменений переезжает в тот код, который копирует данные из домена в модель хранения.
А загрузку всего агрегата ради одной правки, конкурентность и базу на хранимых процедурах разделение моделей не решает вообще - это отдельные задачи.
Если мы говорим о Stateless-приложениях, а обычно веб-приложения являются такими, то ORM объектов, которые извлекли на предыдущем шаге юз кейса (например извлечение данных из бд, которые на следующем шаге надо изменить и записать в бд) уже не существует. И объект persistence model надо создавать с нуля с помощью данных соответствующего доменного объекта.
Если у вас доменнная модель отделена от моделей EF, то изменения в доменной модели не трекаются EF. А ведь на этом "эффекте" (изменения "в памяти" плавно перетекут в БД) базируется уже логика доменного слоя.
Если вы попробуете добавить прослойку, которая будет делать маппинг из доменной модели в модель EF, то окажется, что это дело не такое уж и простое. Прямолинейный маппинг из одной сущности в другую будет работать, только если у вас вариант key-value хранилища. Маппинг, который "апдайтит" сущность написать не намного проще, чем собственный change-tracker.
В общем, от того, что под капотом репозитория на самом деле очень многое зависит на уровне бизнес-слоя. И от этой зависимости невозможно полностью абстрагироваться.
Блог с комментом, конечно, учебный пример, но если отвечать кратко, то частая проблема с агрегатами именно в том, что для разных задач необходим различный набор данных.
Там, где данных немного, размером можно пренебречь. Когда полный агрегат становится достаточно большим, эта проблема появляется. И главная сложность в том, что границы таких агрегатов подвижны.
Если нет правил на добавление коммента, тогда коммент - агрегат. Если появляются правила, заданные постом, тогда агрегат уже пост. И эти правила могут меняться. Сначала вводится ограничение для ролевой модели (френд-не френд), потом добавляется ограничение на максимальное количество комментов к посту, потом для серебряных пользователей вводится расширенный лимит на добавление комментариев, а для золотых пользователей лимит и вовсе снимается. Грузить текст с картинками для определения правил всё равно не нужно.
На практике это часто приводит к протекающим моделям. В EF можно заинклюдить какие-то связи, а можно грузить сущность без них. В репу можно передать это флагами. На выходе будет "единая" модель, которая будет вести себя по разному в зависимости от того, каким образом был выгружен агрегат.
Таким образом, задача добавления комментария уже свою собственную модель доменной области потянет, под капотом которой, возможно, будет прятаться сущность EF. В общем, "агрегат" на практике получается более подвижная штука, чем это принято считать, и определяется во многом сценариями использования. Не сценарии от агрегатов пляшут, а агрегаты от сценариев.
Вот это в точку, особенно "агрегаты от сценариев". Уточню только, что границу двигают инварианты, а сценарии их и задают.
И тот самый "единый" агрегат, который по флагам грузится то с одними связями, то с другими и ведёт себя по-разному - по мне это та же протечка, что у меня в посте в сценарии чтения чтения, только уже на записи - одну модель растянули на разные сценарии. Поэтому когда правила расходятся, лучше разводить на модели под задачу, чем гнать всё через один агрегат.
Спасибо, давно в комментах не было такого предметного разговора :)

Generic Repository<T> обещал три вещи — не сдержал ни одной и забрал доменную модель