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

Clean Architecture на ASP.NET Core

Разработчику Архитектору
Связь с теорией

Круги зависимостей, сущности и 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 — команда и обработчик

public record CreateTodoListCommand : IRequest<int>
{
public string? Title { get; init; }
public string? Colour { get; init; }
}

public class CreateTodoListCommandHandler : IRequestHandler<CreateTodoListCommand, int>
{
private readonly IApplicationDbContext _context;

public CreateTodoListCommandHandler(IApplicationDbContext context) => _context = context;

public async Task<int> Handle(CreateTodoListCommand request, CancellationToken cancellationToken)
{
var entity = new TodoList
{
Title = request.Title,
Colour = Colour.From(request.Colour ?? Colour.Grey)
};

_context.TodoLists.Add(entity);
await _context.SaveChangesAsync(cancellationToken);
return entity.Id;
}
}

Обработчик оркестрирует домен и порт сохранения. 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: прикладной слой задаёт контракт, инфраструктура подстраивается. В 113.md встречаются примеры с интерфейсом в Infrastructure — это допустимо для маленьких проектов, но в enterprise-шаблоне предпочтительнее порт в Application.


Cross-cutting — pipeline behaviours

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

public class ValidationBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : notnull
{
private readonly IEnumerable<IValidator<TRequest>> _validators;

public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken ct)
{
if (_validators.Any())
{
var results = await Task.WhenAll(
_validators.Select(v => v.ValidateAsync(new ValidationContext<TRequest>(request), ct)));
var failures = results.SelectMany(r => r.Errors).Where(e => e != null).ToList();
if (failures.Count != 0)
throw new ValidationException(failures);
}
return await next();
}
}

Команда проходит цепочку до 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 и четыре сборки — инструменты, не требование чистой архитектуры. Минимальный каркас из 2132 выражает те же правила зависимостей.


Антипаттерны (чеклист)

СимптомПочему ломает 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, а не «потому что так в примере».


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

ТемаСтатья
Теория кругов и порты2132 — Чистая архитектура
Папки, типы классов, DI113 — Построение на классах
CQRS2122
MediatR, pipeline4518 — MediatR
ASP.NET Web API4511
EF Core441
Aspire173 — платформа .NET
GRASP на границе HTTP2139

Итог

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

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


См. также

Другие статьи этого же раздела в боковом меню (как на странице «О разделе»).