S

super-dev — full-stack

@super-dev.app · Full-stack · ★ ouvert aux missions
12,8k abonnés·47 vidéos·Actif depuis 2017

Développeur full-stack .NET / Angular avec une casquette DevOps sur Azure Cloud. Aussi à l'aise sur Flutter + Firebase. Je construis des produits, je les déploie, je les surveille.

Retour aux articles
.NET
.NET

CQRS et vertical slices sans usine à gaz

Article 3 sur 5 — Le .NET moderne
APIs minimales, CQRS, gRPC, source generators : du .NET 8 net et testable, sans usine à gaz.

Dès qu'on prononce CQRS , beaucoup d'équipes sortent l'artillerie lourde : event sourcing, bus de messages, deux bases de données. Pourtant CQRS, c'est d'abord une idée modeste — séparer les lectures des écritures — qu'on peut appliquer sans aucune usine à gaz, en organisant le code par vertical slices .

Découper par fonctionnalité, pas par couche

L'architecture en couches éclate une fonctionnalité dans cinq dossiers : Controllers , Services , Repositories , DTOs , Validators . Pour comprendre « créer une commande », on saute de fichier en fichier. La vertical slice inverse la logique : un dossier par fonctionnalité, tout ce qui la concerne au même endroit.

Bash
1Features/
2 Orders/
3 CreateOrder.cs # commande + handler + validateur
4 GetOrderById.cs # requête + handler
5 ListOrders.cs

Chaque slice est autonome. On la lit de haut en bas, on la supprime sans effet de bord, et deux slices ne partagent que le domaine — jamais un « service » fourre-tout.

Commande et requête, deux intentions distinctes

Une commande modifie l'état et ne renvoie (idéalement) qu'un identifiant. Une requête ne lit rien d'autre que ce dont la vue a besoin, souvent en court-circuitant le domaine pour projeter directement vers un DTO. Les modéliser séparément clarifie l'intention :

C#
1public sealed record CreateOrder(Guid CustomerId, IReadOnlyList<LineItem> Items)
2 : IRequest<Guid>;
3
4public sealed class CreateOrderHandler(AppDbContext db)
5 : IRequestHandler<CreateOrder, Guid>
6{
7 public async Task<Guid> Handle(CreateOrder command, CancellationToken ct)
8 {
9 var order = Order.Create(command.CustomerId, command.Items);
10 db.Orders.Add(order);
11 await db.SaveChangesAsync(ct);
12
13 return order.Id;
14 }
15}

Le handler reste mince : il orchestre, il ne raisonne pas. La logique métier vit dans Order.Create , pas dans le handler — sinon on a juste déplacé le « service » dans un autre fichier.

Le médiateur, optionnel

On voit souvent CQRS collé à MediatR . Le médiateur découple l'endpoint du handler et offre un point d'accroche pour les pipeline behaviors (validation, logging, transaction). C'est pratique, mais ce n'est pas CQRS : on peut très bien injecter le handler directement.

C#
1group.MapPost("/", async (CreateOrder command, ISender sender) =>
2{
3 var id = await sender.Send(command);
4
5 return TypedResults.Created($"/orders/{id}", new { id });
6});

Si l'application est petite, sauter le médiateur et appeler le handler à la main reste parfaitement légitime — moins d'indirection, moins de magie.

Ne pas sur-concevoir

La question à se poser à chaque slice : ai-je vraiment besoin de ça ? Bases séparées, projections asynchrones, event sourcing — ce sont des réponses à des problèmes d'échelle précis (lectures massivement supérieures aux écritures, audit immuable). Sans ce problème, ils n'ajoutent que de la latence et des bugs de cohérence. Le bon CQRS, dans 90 % des cas, c'est : commandes et requêtes distinctes, un seul DbContext , des slices lisibles.

CQRS n'est pas une architecture, c'est une discipline de nommage . Séparez les intentions, gardez les handlers minces, et n'ajoutez un bus de messages que le jour où une métrique vous y force.
super-dev — portfolio.app
// Continuer dans .NET
{ }
Étrangler un monolithe .NET 8 sans tout casser
9 min • 3,2k lectures
ƒ()
Minimal APIs + EF Core : une API .NET 8 propre
8 min • 3,1k lectures
gRPC entre microservices .NET
9 min • 1,8k lectures