Перейти к основному содержимому

Clean Architecture на ASP.NET Core

Разработчику Архитектору
Теория данных (раздел 3)
Связь с теорией

Круги зависимостей, сущности и use cases — в чистой архитектуре.

Здесь — как это выглядит в Visual Studio на базе открытого шаблона jasontaylordev/CleanArchitecture и документации cleanarchitecture.jasontaylor.dev.


Где применяют эталонный шаблон

Теория Uncle Bob отвечает на вопрос "куда направлены зависимости". Шаблон Jason Taylor показывает рабочую раскладку решения — отдельные проекты Domain, Application, Infrastructure, Web, оркестрация через MediatR, валидация FluentValidation, данные через EF Core, локальный запуск через .NET Aspire (AppHost).

Шаблон устанавливается как NuGet-шаблон:

dotnet new install Clean.Architecture.Solution.Template
dotnet new ca-sln -cf none -db sqlite -o MyApp
dotnet run --project .\src\AppHost

Параметры -cf (Angular / React / API only) и -db (SQLite, PostgreSQL, SQL Server) меняют оболочку, ядро слоёв остаётся тем же.


Четыре проекта и правило зависимостей

ПроектПапкаРольЗависит от
Domainsrc/DomainСущности, value objects, domain events, перечисления
Applicationsrc/ApplicationКоманды, запросы, обработчики, интерфейсы портов, валидаторыDomain
Infrastructuresrc/InfrastructureDbContext, EF-конфигурации, внешние сервисыApplication
Websrc/WebHTTP endpoints, middleware, OpenAPI, DI-регистрацияApplication, Infrastructure

Дополнительно — AppHost (Aspire) — оркестратор для разработки — поднимает Web, БД, дашборд наблюдаемости.

Правило: ссылки на проекты идут только внутрь. Domain не знает про ASP.NET и EF. Application объявляет IApplicationDbContext; Infrastructure его реализует.

Сопоставление с построением систем на классах: там та же логика может жить в папках одного репозитория; шаблон жёстко разносит сборки, чтобы компилятор не дал "случайно" импортировать Microsoft.EntityFrameworkCore в домен.


Vertical slice — одна папка — один сценарий

В Application код группируют по фиче, а не по типу файла:

Application/
└── TodoLists/
├── Commands/
│ ├── CreateTodoList/
│ │ ├── CreateTodoList.cs ← команда + handler
│ │ └── CreateTodoListCommandValidator.cs
│ └── DeleteTodoList/
└── Queries/
└── GetTodos/
├── GetTodosQuery.cs
└── GetTodosQueryHandler.cs

Открыть CreateTodoList — значит увидеть весь сценарий "создать список" без поиска по Services/ и Repositories/.


Domain

Сущность хранит инварианты; value object Colour инкапсулирует допустимые значения:

// Domain/Entities/TodoList.cs (упрощённо)
public class TodoList
{
public int Id { get; set; }
public string? Title { get; set; }
public Colour Colour { get; set; } = Colour.White;
public IList<TodoItem> Items { get; private set; } = new List<TodoItem>();
}

Application — команда и обработчик

Код ITЗагрузка примера кода…

Обработчик оркестрирует домен и порт сохранения. SQL и HTTP здесь отсутствуют.


Web — тонкий endpoint

public static async Task<Created<int>> CreateTodoList(ISender sender, CreateTodoListCommand command)
{
var id = await sender.Send(command);
return TypedResults.Created($"/{nameof(TodoLists)}/{id}", id);
}

ISender — фасад MediatR: endpoint не знает конкретный CreateTodoListCommandHandler. Подробнее про pipeline и валидацию — в MediatR и cross-cutting в Application.


Где живут интерфейсы

АбстракцияСлойПример
Репозиторий / контекст БДApplicationIApplicationDbContext
Реализация EFInfrastructureApplicationDbContext
HTTP, OpenAPIWebTodoLists endpoints
Регистрация DIInfrastructure + WebAddInfrastructure(), AddWebServices()

Интерфейс порта в Application — каноничный вариант для Clean Architecture: прикладной слой задаёт контракт, инфраструктура подстраивается. В Построение систем на основе классов и объектов встречаются примеры с интерфейсом в Infrastructure — это допустимо для маленьких проектов, но в enterprise-шаблоне предпочтительнее порт в Application.


Cross-cutting — pipeline behaviours

HTTP-слой (middleware, endpoint filters) и бизнес-цепочка (policy pipeline) разведены в ASP.NET — где жить логике. Ниже — реализация policy pipeline через MediatR в Application.

Валидация, логирование и замер времени выполнения вешаются на MediatR pipeline, а не в каждый handler:

Код ITЗагрузка примера кода…

Команда проходит цепочку до Handle: сначала FluentValidation, затем бизнес-логика. Контроллер остаётся без if (!ModelState.IsValid) для доменных правил.


Aspire и точка входа

Локально приложение стартует из src/AppHost: Aspire поднимает Web API, контейнер с БД (при PostgreSQL/SQL Server) и открывает dashboard с URL и логами. Это связывает чистую архитектуру с узлом приложений .NET Aspire: топология "сервис + БД" описана в коде, а не в README.

В проде Web деплоится отдельно; AppHost — инструмент разработки и интеграционных сценариев.


Тестирование

УровеньЧто проверяемЗависимости
UnitHandler + доменMock IApplicationDbContext или in-memory
IntegrationHTTP → pipeline → БДWebApplicationFactory, Testcontainers / Respawn

Шаблон использует NUnit, Shouldly, Moq; для БД — сброс состояния между тестами (Respawn). См. также интеграционные тесты ASP.NET Core.

Юнит-тест handler'а CreateTodoList без Kestrel и Docker — прямой вызов Handle с подменённым контекстом; тот же приём, что в сквозном примере todo в 2132.


Когда шаблон уместен

СитуацияРекомендация
Корпоративный API, 2+ года жизни, несколько разработчиковЧетыре проекта + vertical slices оправданы
CRUD на 3–5 сущностей, один разработчик, срок до 6 месяцевДостаточно одного проекта со слоями в папках или Minimal API без четырёх сборок
Нужны отдельные read/write модели и масштаб чтенияДобавить CQRS осознанно, не "по умолчанию"
Микросервис с одной ответственностьюУрезать: Domain + Application + один host, без SPA-проектов

MediatR, AutoMapper и четыре сборки — инструменты, не требование чистой архитектуры. Минимальный каркас из Чистая архитектура выражает те же правила зависимостей.


Антипаттерны

СимптомПочему ломает CAЧто делать
[Table], [JsonProperty] на entityДомен привязан к EF/JSONКонфигурация в IEntityTypeConfiguration, DTO в Web
DbContext в handler через newНет DI и подмены в тестахИнжектировать IApplicationDbContext
SQL в endpointPresentation знает персистентностьsender.Send(command)
"God" ApplicationService на 800 строкСмешаны сценарииРазбить на команды/запросы по папкам
Shared со всем подрядСкрытые зависимости наружуТолько примитивы ядра; фичи — в slice
Валидация только в UIДомен обходят через APIFluentValidation + инварианты в entity
AutoMapper в DomainЗависимость от инфраструктуры маппингаПрофили в Application или Web

ADR и эволюция

В репозитории шаблона решения фиксируют как Architecture Decision Records (docs/decisions/) — выбор БД, аутентификация, структура тестов. Тот же приём описан в документации как инструмент проектирования и оценке альтернатив.

При форке шаблона под продукт имеет смысл оставить формат ADR — через год команда вспомнит, почему выбрали SQLite, а не "потому что так в примере".


Карта материалов энциклопедии

ТемаСтатья
Теория кругов и портыЧистая архитектура — Чистая архитектура
Папки, типы классов, DIПостроение систем на основе классов и объектов — Построение на классах
CQRSCQRS
MediatR, pipelineMediatR и pipeline в слое Application — MediatR
ASP.NET Web APIПервая программа на ASP.NET Core
EF CoreEntity Framework Core — первая программа
AspireЭкосистема .NET-приложений — платформа .NET
GRASP на границе HTTPGRASP и паттерн ADR для веб-бэкенда

Итог

Шаблон jasontaylordev/CleanArchitecture — практический эталон для .NET — зависимости внутрь, сценарии в vertical slices, порты в Application, адаптеры в Infrastructure и Web. Его стоит разбирать после Чистая архитектура, проходя один сценарий (например CreateTodoList) от endpoint до таблицы в БД. Для собственного продукта шаблон можно упростить, сохранив правило зависимостей — оно важнее количества проектов в solution.

Внешние источники: GitHub · Документация · Architecture overview