Clean Architecture (Чистая архитектура) -- подход Роберта Мартина, при котором бизнес-логика изолирована от фреймворков, баз данных и пользовательского интерфейса. Главная идея: зависимости направлены внутрь -- внешние слои зависят от внутренних, но не наоборот.
Концепция слоёв
┌──────────────────────────────────────┐
│ Frameworks & Drivers │ <- API, EF Core, Azure SDK
│ ┌──────────────────────────────┐ │
│ │ Interface Adapters │ │ <- Controllers, Repository impl
│ │ ┌──────────────────────┐ │ │
│ │ │ Application Logic │ │ │ <- Use Cases, Commands, Queries
│ │ │ ┌──────────────┐ │ │ │
│ │ │ │ Domain │ │ │ │ <- Entities, Value Objects
│ │ │ └──────────────┘ │ │ │
│ │ └──────────────────────┘ │ │
│ └──────────────────────────────┘ │
└──────────────────────────────────────┘
Внутренние слои ничего не знают о внешних. Domain не знает, что данные хранятся в SQL Server. Application не знает, что запрос пришёл по HTTP. Это делает ядро системы независимым и легко тестируемым.
Четыре слоя в проекте
1. Domain Layer -- ядро
Содержит бизнес-правила, Entity, Value Objects, Aggregates, Domain Events, интерфейсы репозиториев и исключения домена. Не содержит ссылок на EF Core, ASP.NET, Azure SDK или MediatR (за исключением интерфейса INotification для событий).
OrderManagement.Domain/
├── Common/
│ ├── Entity.cs
│ ├── ValueObject.cs
│ ├── IAggregateRoot.cs
│ └── IDomainEvent.cs
├── Entities/
│ ├── Order.cs
│ └── OrderItem.cs
├── ValueObjects/
│ ├── Money.cs
│ └── ShippingAddress.cs
├── Events/
│ ├── OrderCreatedEvent.cs
│ └── OrderConfirmedEvent.cs
├── Exceptions/
│ └── DomainException.cs
└── Interfaces/
├── IOrderRepository.cs
└── IUnitOfWork.cs
2. Application Layer -- сценарии использования
Содержит Commands и Queries (паттерн CQRS), DTO для передачи данных, валидаторы, Pipeline Behaviors и интерфейсы внешних сервисов. Зависит только от Domain Layer.
OrderManagement.Application/
├── Common/
│ ├── Behaviors/
│ │ ├── ValidationBehavior.cs
│ │ └── LoggingBehavior.cs
│ └── Interfaces/
│ └── IEmailService.cs
├── Orders/
│ ├── Commands/
│ │ ├── CreateOrder/
│ │ └── ConfirmOrder/
│ ├── Queries/
│ │ ├── GetOrder/
│ │ └── GetCustomerOrders/
│ └── DTOs/
│ └── OrderDto.cs
└── DependencyInjection.cs
3. Infrastructure Layer -- реализация
Содержит реализацию репозиториев через EF Core, DbContext с конфигурациями, внешние интеграции (email, кэш, очереди). Зависит от Domain и Application Layer.
OrderManagement.Infrastructure/
├── Persistence/
│ ├── OrderDbContext.cs
│ ├── Configurations/
│ │ ├── OrderConfiguration.cs
│ │ └── OrderItemConfiguration.cs
│ └── Interceptors/
│ └── DomainEventDispatcherInterceptor.cs
├── Repositories/
│ └── OrderRepository.cs
└── DependencyInjection.cs
4. API Layer -- точка входа
Содержит контроллеры ASP.NET Core, middleware для обработки ошибок, конфигурацию DI и документацию API. Зависит от Application Layer, а на Infrastructure ссылается только для регистрации зависимостей.
CQRS: разделение чтения и записи
Command Query Responsibility Segregation разделяет операции изменения состояния (Commands) и чтения данных (Queries):
// Command -- changes state, returns ID or nothing
public record CreateOrderCommand(
string CustomerId,
string Street,
string City,
string PostalCode,
string Country,
List<OrderItemDto> Items) : IRequest<Guid>;
// Query -- does not change state, returns data
public record GetOrderQuery(Guid OrderId) : IRequest<OrderDto>;
Команды проходят через валидацию и бизнес-логику. Запросы могут быть оптимизированы отдельно (кэширование, денормализованные представления).
Pipeline Behaviors
Pipeline Behaviors в MediatR -- это цепочка обработчиков, через которую проходит каждая команда или запрос перед попаданием в Handler. Типичные Behaviors: валидация и логирование.
public class ValidationBehavior<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private readonly IEnumerable<IValidator<TRequest>> _validators;
public ValidationBehavior(
IEnumerable<IValidator<TRequest>> validators)
{
_validators = validators;
}
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken ct)
{
if (!_validators.Any()) return await next();
var context = new ValidationContext<TRequest>(request);
var results = await Task.WhenAll(
_validators.Select(v => v.ValidateAsync(context, ct)));
var failures = results
.SelectMany(r => r.Errors)
.Where(f => f != null)
.ToList();
if (failures.Any())
throw new ValidationException(failures);
return await next();
}
}
Если для команды CreateOrderCommand зарегистрирован CreateOrderCommandValidator, валидация сработает автоматически до вызова Handler. При ошибках клиент получит HTTP 400 с детальным описанием проблем.
Dispatch Domain Events
Доменные события диспатчатся после сохранения изменений в базу данных через EF Core interceptor:
public class DomainEventDispatcherInterceptor : SaveChangesInterceptor
{
private readonly IMediator _mediator;
public override async ValueTask<int> SavedChangesAsync(
SaveChangesCompletedEventData eventData,
int result,
CancellationToken ct = default)
{
if (eventData.Context is null) return result;
var entities = eventData.Context.ChangeTracker
.Entries<Entity>()
.Where(e => e.Entity.DomainEvents.Any())
.Select(e => e.Entity)
.ToList();
var events = entities
.SelectMany(e => e.DomainEvents)
.ToList();
entities.ForEach(e => e.ClearDomainEvents());
foreach (var domainEvent in events)
await _mediator.Publish(domainEvent, ct);
return result;
}
}
Событие публикуется после успешного SaveChanges, что гарантирует: если сохранение в БД провалилось, обработчики событий не будут вызваны.
Преимущества Clean Architecture
- Тестируемость. Domain и Application тестируются без БД и HTTP.
- Независимость от фреймворков. Замена EF Core на Dapper или SQL Server на PostgreSQL затрагивает только Infrastructure.
- Масштабируемость. Чтение и запись можно масштабировать независимо.
- Читаемость. Структура проекта отражает бизнес-логику, а не технические детали.