Clean Architecture + Vertical Slices + DDD
Такая комбинация показала себя хорошо на практике, поэтому я считаю её наиболее удачной для реализации сложной предметной области.
Тема достаточно обширная, поэтому будет состоять из множества блоков :) Начнем.
Из чистой архитектуры я беру в основном Dependency Rule, UseCases и некоторые идеи из остальных.
Для изоляции разных уровней абстракций выделяю следующие сборки:
Api
↓
Infrastructure
↓
Application
↓
Domain
В дополнение к Api еще выделяется сборка BackgroundJobs, которая полезна когда требуется отделить асинхронную или фоновую работу чтобы масштабировать независимо от API.
Рассмотрим что в каждой сборке находится
в Api/BackgroundJobs:
* Authentication/Authorization
* DI Composition Root
* HealthChecks
* ProblemDetails
* Swagger
* Middlewares
* Controllers, Minimal API Endpoints
* Consumer, Producer
* Outbox
* И т.п.
Остановимся подробнее на основных вещах:
* Controllers & Minimal API Endpoints
Здесь начинаем применять Vertical Slices и изолируем фичи друг от друга структурой папок и неймспейсами:
Api
ProblemDetails
Swagger
HealthChecks
...
Features
WithdrawMoney
WithdrawMoneyController.cs
WithdrawMoneyEndpoint.cs
WithdrawMoneyModel.cs
WithdrawMoneyModelValidator.cs
WithdrawMoneyModelExample.cs
WithdrawMoneyResponseModel.cs
WithdrawMoneyResponseModelExample.cs
OtherFeatureName
...
Заметьте, что все что связано с конкретной фичей располагается в одной папке внутри папки Features. Обращаться к соседним фичам категорически нельзя (для этого можно сделать архитектурный тест).
Модели объявляются для каждой фичи свои и используют только примитивы или другие модели этой фичи. Это позволяет независимо развивать фичи и избегать неожиданных поломок из-за изменений в другой фиче.
Так как эти модели предназначены только для десериализации данных из методов контроллера, то мы можем им придать любую форму для того чтобы сделать максимально удобный API.
Валидатор проверит поля на корректность и обязательность их заполнения. А файлы с суффиксом Example - это примеры моделей для сваггера для наглядности.
Если Вам удобно, можно не делить всё это на отдельные файлы и расположить всё в одном, тут уж дело вкуса и соглашений в команде. Здесь я их все отделил для наглядности.
Еще хочется отметить, что здесь встречается одна из самых частых ошибок проектирования, когда объявляется DTO в слое Application (хуже в Domain или еще хуже - используется доменная модель в качестве DTO)
и эта модель используется в контроллере для десериализации данных и последующей передачи в нижележащие слои.
И начинаются настоящие танцы с бубном - пишутся кастомные ModelBinders, навешиваются атрибуты [FromBody], [FromForm], [FromQuery] и т.п. в полях DTO чтобы все замапилось как надо, так как часть параметров в QueryParams, часть в теле, часть в пути, часть заголовках, а UserId лежит где-нибудь в HttpContext.
Со временем требования меняются, код рефакторится, приходят новые люди в команду со своими подходами.. и эти модели тоже могут внезапно измениться, а контракты API - нет и в итоге сломается маппинг.
Тут на лицо нарушение SRP, а также протекание абстракций. Так как эти DTO объявлены в Application/Domain. А эти слои не должны знать о внешних слоях (особенно про AspNetCore, контроллеры и прочее).
* Consumer, Producer
Здесь в целом та же самая история, единственное - для сообщений из шины часто используют proto/avro и генерируют из них DTO.
Это очень удобно и решает сразу большинство описанных выше проблем - модели генерируются под конкретный Consumer/Producer,
а также они не ссылаются на другие модели из других фич или модели других слоёв. Являются полностью автономными.
Также, расположив эти модели на внешнем слое (Api/BackgroundJobs), мы не позволяем к ним обращаться из слоёв ниже. Если бы они объявлялись в Application или Domain, то программисты часто проигрывают лени
и MQ модели просачиваются сквозь все слои к которым они имеют доступ и потом их очень сложно оттуда убрать.
Продолжение следует...
Такая комбинация показала себя хорошо на практике, поэтому я считаю её наиболее удачной для реализации сложной предметной области.
Тема достаточно обширная, поэтому будет состоять из множества блоков :) Начнем.
Из чистой архитектуры я беру в основном Dependency Rule, UseCases и некоторые идеи из остальных.
Для изоляции разных уровней абстракций выделяю следующие сборки:
Api
↓
Infrastructure
↓
Application
↓
Domain
В дополнение к Api еще выделяется сборка BackgroundJobs, которая полезна когда требуется отделить асинхронную или фоновую работу чтобы масштабировать независимо от API.
Рассмотрим что в каждой сборке находится
в Api/BackgroundJobs:
* Authentication/Authorization
* DI Composition Root
* HealthChecks
* ProblemDetails
* Swagger
* Middlewares
* Controllers, Minimal API Endpoints
* Consumer, Producer
* Outbox
* И т.п.
Остановимся подробнее на основных вещах:
* Controllers & Minimal API Endpoints
Здесь начинаем применять Vertical Slices и изолируем фичи друг от друга структурой папок и неймспейсами:
Api
ProblemDetails
Swagger
HealthChecks
...
Features
WithdrawMoney
WithdrawMoneyController.cs
WithdrawMoneyEndpoint.cs
WithdrawMoneyModel.cs
WithdrawMoneyModelValidator.cs
WithdrawMoneyModelExample.cs
WithdrawMoneyResponseModel.cs
WithdrawMoneyResponseModelExample.cs
OtherFeatureName
...
Заметьте, что все что связано с конкретной фичей располагается в одной папке внутри папки Features. Обращаться к соседним фичам категорически нельзя (для этого можно сделать архитектурный тест).
Модели объявляются для каждой фичи свои и используют только примитивы или другие модели этой фичи. Это позволяет независимо развивать фичи и избегать неожиданных поломок из-за изменений в другой фиче.
Так как эти модели предназначены только для десериализации данных из методов контроллера, то мы можем им придать любую форму для того чтобы сделать максимально удобный API.
Валидатор проверит поля на корректность и обязательность их заполнения. А файлы с суффиксом Example - это примеры моделей для сваггера для наглядности.
Если Вам удобно, можно не делить всё это на отдельные файлы и расположить всё в одном, тут уж дело вкуса и соглашений в команде. Здесь я их все отделил для наглядности.
Еще хочется отметить, что здесь встречается одна из самых частых ошибок проектирования, когда объявляется DTO в слое Application (хуже в Domain или еще хуже - используется доменная модель в качестве DTO)
и эта модель используется в контроллере для десериализации данных и последующей передачи в нижележащие слои.
И начинаются настоящие танцы с бубном - пишутся кастомные ModelBinders, навешиваются атрибуты [FromBody], [FromForm], [FromQuery] и т.п. в полях DTO чтобы все замапилось как надо, так как часть параметров в QueryParams, часть в теле, часть в пути, часть заголовках, а UserId лежит где-нибудь в HttpContext.
Со временем требования меняются, код рефакторится, приходят новые люди в команду со своими подходами.. и эти модели тоже могут внезапно измениться, а контракты API - нет и в итоге сломается маппинг.
Тут на лицо нарушение SRP, а также протекание абстракций. Так как эти DTO объявлены в Application/Domain. А эти слои не должны знать о внешних слоях (особенно про AspNetCore, контроллеры и прочее).
* Consumer, Producer
Здесь в целом та же самая история, единственное - для сообщений из шины часто используют proto/avro и генерируют из них DTO.
Это очень удобно и решает сразу большинство описанных выше проблем - модели генерируются под конкретный Consumer/Producer,
а также они не ссылаются на другие модели из других фич или модели других слоёв. Являются полностью автономными.
Также, расположив эти модели на внешнем слое (Api/BackgroundJobs), мы не позволяем к ним обращаться из слоёв ниже. Если бы они объявлялись в Application или Domain, то программисты часто проигрывают лени
и MQ модели просачиваются сквозь все слои к которым они имеют доступ и потом их очень сложно оттуда убрать.
Продолжение следует...