Clean Architecture .NET en 2026 : CQRS, MediatR et Questions d'Entretien Développeur
Clean Architecture en .NET avec CQRS et MediatR 14. Maîtrisez les patterns en couches, les pipeline behaviors et préparez les entretiens développeur senior.

La Clean Architecture en .NET sépare la logique métier des préoccupations d'infrastructure, rendant les applications plus faciles à tester, maintenir et faire évoluer. Combinée avec CQRS (Command Query Responsibility Segregation) et MediatR, cette architecture fournit une approche structurée pour construire des applications d'entreprise que les recruteurs évaluent fréquemment.
Les questions sur la Clean Architecture apparaissent dans 70% des entretiens pour développeurs .NET seniors. Les recruteurs attendent des candidats qu'ils expliquent la règle de dépendance : les dépendances pointent vers l'intérieur, avec la couche domaine n'ayant aucune dépendance vers les frameworks externes.
Les Couches de la Clean Architecture et la Règle de Dépendance
Le template Ardalis Clean Architecture pour ASP.NET Core 10 organise le code en quatre couches concentriques. Chaque couche possède des responsabilités spécifiques et des règles de dépendance strictes.
// 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;
}
}La couche domaine contient les entités, les objets valeur et les services de domaine. Elle ne connaît rien des bases de données, du HTTP ou des frameworks externes.
Pattern CQRS : Séparer les Lectures des Écritures
Le CQRS divise les opérations en Commandes (opérations d'écriture qui modifient l'état) et Requêtes (opérations de lecture qui retournent des données). Cette séparation permet un scaling et une optimisation indépendants de chaque chemin.
// 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);Les commandes retournent un minimum de données, typiquement juste un indicateur de succès ou un ID nouvellement créé. Les requêtes retournent des DTOs optimisés pour le consommateur.
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 et Implémentation des Handlers
MediatR 14.2 fournit l'infrastructure de messagerie pour CQRS. Chaque commande ou requête possède exactement un handler, imposant le principe de responsabilité unique.
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);
}
}Prêt à réussir tes entretiens .NET ?
Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.
Pipeline Behaviors pour les Préoccupations Transversales
Les pipeline behaviors dans MediatR gèrent la journalisation, la validation, la mise en cache et la gestion des transactions sans polluer les handlers.
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();
}
}Les behaviors sont enregistrés dans l'ordre d'exécution souhaité. La validation s'exécute généralement en premier, suivie de la journalisation, puis de la gestion des transactions.
builder.Services.AddMediatR(cfg =>
{
cfg.RegisterServicesFromAssembly(typeof(CreateOrderCommand).Assembly);
cfg.AddOpenBehavior(typeof(ValidationBehavior<,>));
cfg.AddOpenBehavior(typeof(LoggingBehavior<,>));
cfg.AddOpenBehavior(typeof(TransactionBehavior<,>));
});Couche Infrastructure : Implémentation du Repository
La couche infrastructure implémente les interfaces définies dans la couche core. Les repositories Entity Framework Core traduisent les opérations du domaine en appels base de données.
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);
}
}Questions d'Entretien : Ce Que les Développeurs Seniors Doivent Savoir
Les entretiens techniques pour les postes .NET seniors incluent fréquemment des questions sur la Clean Architecture. Voici les patterns que les recruteurs évaluent :
Q : Pourquoi la couche domaine n'a-t-elle aucune dépendance vers les frameworks externes ?
La couche domaine contient les règles métier qui doivent rester stables indépendamment des changements d'infrastructure. Si le domaine dépend d'Entity Framework, passer à Dapper ou une autre base de données nécessiterait de modifier la logique métier. La règle de dépendance garantit que l'infrastructure est un détail qui peut être échangé sans affecter le comportement central.
Q : Quand le CQRS est-il excessif ?
Le CQRS ajoute une complexité dont les applications CRUD simples n'ont pas besoin. Si les modèles de lecture et d'écriture sont presque identiques, s'il n'y a pas besoin de scaling séparé, et si l'équipe est petite, une architecture plus simple est appropriée. Le CQRS excelle quand les modèles de lecture diffèrent significativement des modèles d'écriture, quand l'event sourcing est requis, ou quand les charges de lecture et d'écriture nécessitent un scaling indépendant.
Q : Comment gérer les transactions sur plusieurs agrégats ?
// 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;
}
}
}Pour les opérations s'étendant sur plusieurs agrégats, utiliser la cohérence à terme avec des événements de domaine quand c'est possible. Les transactions synchrones à travers les agrégats indiquent un problème de conception potentiel : les agrégats appartiennent peut-être ensemble.
Optimisation Côté Lecture avec des Modèles de Requête Séparés
Les requêtes contournent entièrement la couche domaine quand la performance compte. L'accès direct à la base de données avec Dapper ou du SQL brut évite le surcoût de matérialisation des graphes d'entités complets.
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);
}
}Passe à la pratique !
Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.
Préparation aux Entretiens Clean Architecture : Points Clés à Retenir
- La règle de dépendance stipule que les dépendances du code source ne peuvent pointer que vers l'intérieur. La couche domaine n'a aucune connaissance des bases de données, frameworks ou mécanismes de livraison.
- Le CQRS sépare les commandes (changements d'état) des requêtes (récupération de données). Les commandes passent par le modèle de domaine et la validation. Les requêtes peuvent contourner les entités du domaine pour la performance.
- MediatR 14 route les requêtes vers les handlers. Les pipeline behaviors gèrent les préoccupations transversales comme la validation, la journalisation et les transactions de manière composable.
- Les interfaces de repository vivent dans la couche core. Les implémentations vivent dans l'infrastructure. Cette inversion permet d'échanger EF Core contre Dapper sans toucher à la logique métier.
- Les réponses aux entretiens doivent inclure les compromis. La Clean Architecture ajoute une complexité initiale mais s'amortit dans les grandes applications à longue durée de vie avec plusieurs développeurs.
- Le module Clean Architecture ASP.NET Core sur SharpSkill couvre des patterns supplémentaires incluant Specification, les objets Result et les Domain Events.
Tu saurais repérer le bug en .NET ?
Un vrai bout de code, un bug caché, une tentative par jour. Sans compte pour essayer.

Écrit par
Anthony Fillion-MailletFondateur de SharpSkill
Développeur fullstack depuis plus de 10 ans. Il dirige SharpSkill et répond de tout ce qui y est publié.
Mis à jour le 15 septembre 2026
Partager
Articles similaires

Durée de Vie de DbContext dans ASP.NET Core : Performance vs Thread Safety en Async
Maîtrisez la gestion de la durée de vie de DbContext dans ASP.NET Core. Apprenez quand utiliser scoped vs transient, comment gérer les opérations async en toute sécurité et optimiser les performances avec le pooling de DbContext.

Clean Code Architecture C# : Guide Complet et Questions d'Entretien 2026
Maîtrisez les principes Clean Code et Clean Architecture en C# pour créer des applications .NET maintenables et testables. Préparez vos entretiens techniques avec des exemples pratiques.

LINQ avancé en C# en 2026 : Opérateurs, Performance et Questions d'Entretien
Maîtriser les opérateurs LINQ avancés, optimiser les performances des requêtes et se préparer aux questions d'entretien technique C# avec des exemples pratiques.