Clean Architecture у .NET 2026: CQRS, MediatR та питання на співбесідах
Практичний посібник з Clean Architecture у .NET з CQRS та MediatR. Приклади коду, архітектурні патерни та питання для .NET розробників.

Clean Architecture у .NET відокремлює бізнес-логіку від інфраструктурних шарів, що робить застосунки простішими для тестування, підтримки та масштабування. У поєднанні з CQRS (Command Query Responsibility Segregation) та MediatR, цей архітектурний патерн забезпечує структурований підхід до побудови корпоративних застосунків, який часто перевіряється під час технічних співбесід.
Питання про Clean Architecture з'являються у 70% співбесід на позицію senior .NET розробника. Інтерв'юери очікують, що кандидати пояснять правило залежностей: залежності спрямовані всередину, а доменний шар не має жодних залежностей від зовнішніх фреймворків.
Шари Clean Architecture та правило залежностей
Шаблон Ardalis Clean Architecture для ASP.NET Core 10 організовує код у чотири концентричні шари. Кожен шар має визначені обов'язки та суворі правила залежностей.
// Core/Domain Layer - No external dependencies
// src/Core/Domain/Entities/Order.cs
namespace Core.Domain.Entities;
public class Order
{
public Guid Id { get; private set; }
public string CustomerEmail { get; private set; }
public List<OrderLine> Lines { get; private set; } = new();
public OrderStatus Status { get; private set; }
public DateTime CreatedAt { get; private set; }
// Domain logic encapsulated in the entity
public void AddLine(Product product, int quantity)
{
if (Status != OrderStatus.Draft)
throw new InvalidOperationException("Cannot modify a submitted order");
var existingLine = Lines.FirstOrDefault(l => l.ProductId == product.Id);
if (existingLine is not null)
{
existingLine.IncreaseQuantity(quantity);
return;
}
Lines.Add(new OrderLine(product.Id, product.Price, quantity));
}
public void Submit()
{
if (!Lines.Any())
throw new InvalidOperationException("Cannot submit an empty order");
Status = OrderStatus.Submitted;
}
}Доменний шар містить сутності, об'єкти-значення та доменні сервіси. Він не має жодних знань про бази даних, HTTP-протокол чи зовнішні фреймворки.
Патерн CQRS: Розділення операцій читання та запису
CQRS розділяє операції на команди (операції запису, що змінюють стан) та запити (операції читання, що повертають дані). Таке розділення дозволяє незалежно масштабувати та оптимізувати кожен шлях.
// UseCases Layer - Commands and Queries
// src/UseCases/Orders/Commands/CreateOrder/CreateOrderCommand.cs
namespace UseCases.Orders.Commands.CreateOrder;
public record CreateOrderCommand(
string CustomerEmail,
List<OrderLineDto> Lines
) : IRequest<Result<Guid>>;
public record OrderLineDto(Guid ProductId, int Quantity);Команди повертають мінімальні дані, зазвичай лише індикатор успіху або ідентифікатор новоствореного об'єкта. Запити повертають DTO, оптимізовані для споживача.
namespace UseCases.Orders.Queries.GetOrderById;
public record GetOrderByIdQuery(Guid OrderId) : IRequest<Result<OrderDetailsDto>>;
public record OrderDetailsDto(
Guid Id,
string CustomerEmail,
string Status,
decimal TotalAmount,
List<OrderLineDetailsDto> Lines
);MediatR 14: Pipeline Behaviors та реалізація обробників
MediatR 14.2 надає інфраструктуру обміну повідомленнями для CQRS. Кожна команда або запит має рівно один обробник, що забезпечує дотримання принципу єдиної відповідальності.
namespace UseCases.Orders.Commands.CreateOrder;
public class CreateOrderHandler : IRequestHandler<CreateOrderCommand, Result<Guid>>
{
private readonly IOrderRepository _orderRepository;
private readonly IProductRepository _productRepository;
private readonly IUnitOfWork _unitOfWork;
public CreateOrderHandler(
IOrderRepository orderRepository,
IProductRepository productRepository,
IUnitOfWork unitOfWork)
{
_orderRepository = orderRepository;
_productRepository = productRepository;
_unitOfWork = unitOfWork;
}
public async Task<Result<Guid>> Handle(
CreateOrderCommand request,
CancellationToken cancellationToken)
{
// Load products to validate and get prices
var productIds = request.Lines.Select(l => l.ProductId).ToList();
var products = await _productRepository
.GetByIdsAsync(productIds, cancellationToken);
if (products.Count != productIds.Count)
return Result.NotFound("One or more products not found");
// Create order using domain logic
var order = new Order(request.CustomerEmail);
foreach (var line in request.Lines)
{
var product = products.First(p => p.Id == line.ProductId);
order.AddLine(product, line.Quantity);
}
await _orderRepository.AddAsync(order, cancellationToken);
await _unitOfWork.SaveChangesAsync(cancellationToken);
return Result.Success(order.Id);
}
}Готовий до співбесід з .NET?
Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.
Pipeline Behaviors для наскрізних аспектів
Pipeline behaviors у MediatR обробляють логування, валідацію, кешування та управління транзакціями без забруднення обробників додатковим кодом.
namespace UseCases.Common.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 cancellationToken)
{
if (!_validators.Any())
return await next();
var context = new ValidationContext<TRequest>(request);
var validationResults = await Task.WhenAll(
_validators.Select(v => v.ValidateAsync(context, cancellationToken)));
var failures = validationResults
.SelectMany(r => r.Errors)
.Where(f => f is not null)
.ToList();
if (failures.Any())
throw new ValidationException(failures);
return await next();
}
}Behaviors реєструються в порядку їх виконання. Валідація зазвичай виконується першою, потім логування, і нарешті обробка транзакцій.
builder.Services.AddMediatR(cfg =>
{
cfg.RegisterServicesFromAssembly(typeof(CreateOrderCommand).Assembly);
cfg.AddOpenBehavior(typeof(ValidationBehavior<,>));
cfg.AddOpenBehavior(typeof(LoggingBehavior<,>));
cfg.AddOpenBehavior(typeof(TransactionBehavior<,>));
});Інфраструктурний шар: Реалізація репозиторіїв
Інфраструктурний шар реалізує інтерфейси, визначені в шарі Core. Репозиторії Entity Framework Core перетворюють доменні операції на виклики бази даних.
namespace Infrastructure.Data.Repositories;
public class OrderRepository : IOrderRepository
{
private readonly AppDbContext _context;
public OrderRepository(AppDbContext context)
{
_context = context;
}
public async Task<Order?> GetByIdAsync(
Guid id,
CancellationToken cancellationToken = default)
{
return await _context.Orders
.Include(o => o.Lines)
.FirstOrDefaultAsync(o => o.Id == id, cancellationToken);
}
public async Task AddAsync(
Order order,
CancellationToken cancellationToken = default)
{
await _context.Orders.AddAsync(order, cancellationToken);
}
public async Task<IReadOnlyList<Order>> GetByCustomerEmailAsync(
string email,
CancellationToken cancellationToken = default)
{
return await _context.Orders
.Where(o => o.CustomerEmail == email)
.OrderByDescending(o => o.CreatedAt)
.ToListAsync(cancellationToken);
}
}Питання на співбесіді: Що повинен знати senior розробник
Технічні співбесіди на позиції senior .NET часто містять питання про Clean Architecture. Ось патерни, які оцінюють інтерв'юери:
П: Чому доменний шар не має залежностей від зовнішніх фреймворків?
Доменний шар містить бізнес-правила, які повинні залишатися стабільними незалежно від змін інфраструктури. Якщо домен залежить від Entity Framework, перехід на Dapper або іншу базу даних вимагає зміни бізнес-логіки. Правило залежностей гарантує, що інфраструктура є деталлю, яку можна замінити без впливу на основну поведінку.
П: Коли CQRS є надмірним ускладненням?
CQRS додає складність, якої прості CRUD-застосунки не потребують. Якщо моделі читання та запису майже ідентичні, якщо немає потреби в окремому масштабуванні, а команда невелика, простіша архітектура є доречною. CQRS виправдовує себе, коли моделі читання суттєво відрізняються від моделей запису, коли потрібен event sourcing або коли навантаження читання та запису вимагають незалежного масштабування.
П: Як обробляти транзакції між кількома агрегатами?
// Transaction behavior wraps the entire handler execution
public class TransactionBehavior<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private readonly IUnitOfWork _unitOfWork;
public TransactionBehavior(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
// Commands that modify multiple aggregates use explicit transactions
if (request is not ICommand)
return await next();
await using var transaction = await _unitOfWork
.BeginTransactionAsync(cancellationToken);
try
{
var response = await next();
await transaction.CommitAsync(cancellationToken);
return response;
}
catch
{
await transaction.RollbackAsync(cancellationToken);
throw;
}
}
}Для операцій, що охоплюють кілька агрегатів, слід використовувати eventual consistency з domain events, коли це можливо. Синхронні транзакції між агрегатами вказують на потенційну проблему проєктування: агрегати можуть належати один одному.
Оптимізація читання з окремими моделями запитів
Запити повністю обходять доменний шар, коли важлива продуктивність. Прямий доступ до бази даних за допомогою Dapper або чистого SQL уникає накладних витрат на матеріалізацію повних графів сутностей.
namespace Infrastructure.Queries;
public class GetOrderByIdQueryHandler
: IRequestHandler<GetOrderByIdQuery, Result<OrderDetailsDto>>
{
private readonly IDbConnection _connection;
public GetOrderByIdQueryHandler(IDbConnection connection)
{
_connection = connection;
}
public async Task<Result<OrderDetailsDto>> Handle(
GetOrderByIdQuery request,
CancellationToken cancellationToken)
{
const string sql = """
SELECT o.Id, o.CustomerEmail, o.Status, o.CreatedAt,
SUM(ol.UnitPrice * ol.Quantity) AS TotalAmount
FROM Orders o
LEFT JOIN OrderLines ol ON ol.OrderId = o.Id
WHERE o.Id = @OrderId
GROUP BY o.Id, o.CustomerEmail, o.Status, o.CreatedAt
""";
var order = await _connection.QueryFirstOrDefaultAsync<OrderDetailsDto>(
new CommandDefinition(sql, new { request.OrderId }, cancellationToken: cancellationToken));
return order is null
? Result.NotFound($"Order {request.OrderId} not found")
: Result.Success(order);
}
}Починай практикувати!
Перевір свої знання з нашими симуляторами співбесід та технічними тестами.
Підготовка до співбесіди з Clean Architecture: Ключові висновки
- Правило залежностей стверджує, що залежності вихідного коду можуть вказувати лише всередину. Доменний шар не має знань про бази даних, фреймворки чи механізми доставки.
- CQRS розділяє команди (зміни стану) від запитів (отримання даних). Команди проходять через доменну модель та валідацію. Запити можуть обходити доменні сутності для продуктивності.
- MediatR 14 направляє запити до обробників. Pipeline behaviors обробляють наскрізні аспекти, такі як валідація, логування та транзакції, композиційним способом.
- Інтерфейси репозиторіїв знаходяться в шарі Core. Реалізації знаходяться в інфраструктурі. Ця інверсія дозволяє замінити EF Core на Dapper без зміни бізнес-логіки.
- Відповіді на співбесіді повинні включати компроміси. Clean Architecture додає початкову складність, але окупається у великих довготривалих застосунках з багатьма розробниками.
- Модуль ASP.NET Core Clean Architecture на SharpSkill охоплює додаткові патерни, включаючи Specification, об'єкти Result та Domain Events.
Чи знайдеш ти помилку в .NET?
Справжній фрагмент коду, прихована помилка, одна спроба на день. Щоб спробувати, акаунт не потрібен.

Автор:
Anthony Fillion-MailletЗасновник SharpSkill
Fullstack-розробник понад 10 років. Керує SharpSkill і відповідає за все, що тут публікується.
Оновлено 15 вересня 2026 р.
Поділитися
Пов'язані статті

Час життя DbContext в ASP.NET Core: Продуктивність проти потокобезпечності в асинхронних операціях
Опануйте керування часом життя DbContext в ASP.NET Core. Дізнайтеся, коли використовувати scoped проти transient, як безпечно обробляти асинхронні операції та оптимізувати продуктивність за допомогою пулінгу DbContext.

Clean Code Architecture C#: Повний Посібник та Питання Співбесіди 2026
Опануйте Clean Code та Clean Architecture в C# з принципами SOLID, впровадженням залежностей та питаннями співбесіди для .NET 10.

Поглиблений C# LINQ у 2026: Оператори, Продуктивність та Питання на Співбесідах
Опануйте поглиблені оператори LINQ, відкладене виконання та оптимізацію продуктивності. Охоплює GroupBy, SelectMany, оптимізацію запитів та поширені питання на співбесідах для .NET розробників.