Архитектура приложения на C#
Архитектура — это не раскладка папок и не список проектов в решении. Это набор ограничений на направление зависимостей, который решает, во что вам обойдётся следующее изменение: можно ли поменять СУБД, не трогая бизнес-правила; можно ли добавить поле в сущность, не сломав контракт API; можно ли протестировать сценарий, не поднимая базу. Всё остальное — следствия.
Отсюда честная позиция по трём самым цитируемым вещам в этой теме. Clean/Onion — это правило зависимостей, направленных внутрь, а не четыре проекта с определёнными именами: решение с проектами Domain, Application, Infrastructure, Web, где Domain ссылается на Microsoft.EntityFrameworkCore, не является Clean-архитектурой ни в каком смысле. Паттерн Repository поверх EF Core чаще всего избыточен: DbSet<T> уже является репозиторием, а DbContext — уже Unit of Work, поэтому обёртка-переходник не абстрагирует ничего, а лишь прячет IQueryable. DTO существует, чтобы отвязать контракт передачи от доменной модели: возвращая сущности из контроллеров, вы делаете схему БД публичным API — и получаете случайные breaking change и over-posting. Ниже — по слоям.
Карта темы
- Слои приложения — presentation, application, domain, infrastructure; Clean/Onion как правило зависимостей, направленных внутрь.
- DTO и доменные сущности — плоский контракт границы против сущности с идентичностью и инвариантами; чем платят за утечку сущности в API.
- Инверсия зависимостей —
DIP, внедрение через конструктор, composition root и почему интерфейс должен принадлежать домену. - Паттерн Repository — когда обёртка над EF Core оправдана, а когда это лишний слой поверх уже существующих репозитория и Unit of Work.
Частые ошибки и ловушки
| Ошибка | Последствие |
|---|---|
Ссылаться на DbContext или типы ASP.NET Core из доменного проекта | Домен зависит от инфраструктуры — смена ORM или хоста тянет за собой бизнес-правила; правило зависимостей нарушено, как бы ни назывались проекты |
| Класть бизнес-правила в контроллеры | Домен становится анемичным набором свойств, а правила дублируются в каждом endpoint |
| Считать слои раскладкой по папкам | Правило зависимостей не проверяется — «слои» есть, а связность прежняя |
| Объявлять интерфейс репозитория в Infrastructure | Домен снова зависит от инфраструктуры: зависимость не инвертирована, а лишь переименована |
Считать, что любое использование DI-контейнера удовлетворяет DIP | Service locator внутри метода (provider.GetService<T>()) прячет зависимость и не инвертирует ничего |
Считать DIP приёмом ради тестируемости | Тестируемость — следствие; правило — про направление зависимостей |
Оборачивать DbSet<T> в репозиторий-переходник | Лишний слой без выгоды: DbSet<T> уже репозиторий, DbContext уже Unit of Work |
Возвращать IQueryable из репозитория | Наружу утекает та самая технология, которую репозиторий должен был скрыть, — вызывающий код диктует SQL |
| Возвращать сущности EF Core из контроллеров | Схема БД становится контрактом API: миграция колонки — breaking change; лишние поля попадают в биндинг (over-posting) |
Класть бизнес-правила и инварианты в DTO | Инварианты уезжают из домена на границу и перестают действовать для остальных вызывающих |
Значение для собеседований
Архитектурные вопросы на C#-собеседовании — водораздел между middle и senior. Junior перечисляет слои. Middle объясняет, зачем внедрять через конструктор. Senior называет правило — зависимости направлены внутрь, к домену, — и, что важнее, честно говорит, где паттерн не нужен: именно способность сказать «Repository поверх EF Core здесь избыточен, потому что…» отличает инженера от человека, пересказавшего статью.
Что обычно проверяют:
- Слои типового решения ASP.NET Core и правило «слой обращается только к нижележащему».
- Что Clean/Onion — это направление зависимостей внутрь, а не имена проектов, и что домен не ссылается ни на что.
- Как внедрение через конструктор реализует
DIPи почему интерфейс объявляет потребитель (домен), а не реализация. - Что такое composition root и чем он отличается от service locator.
- Оправдан ли
Repositoryповерх EF Core — и умеете ли вы назвать два случая, когда да. - Зачем нужен
DTOи что происходит, когда сущность утекает в контракт API.
Типичный неверный ответ: «Мы сделали Clean-архитектуру — у нас есть проекты Domain, Application, Infrastructure и Web». Это описание папок. Следующий вопрос — «а на что ссылается Domain?» — и если в ответе звучит DbContext, архитектуры нет: есть слоистая раскладка с зависимостью, направленной наружу.