Oyun Kitabı
Comment fonctionne le pipeline CQRS (Command Query Responsibility Segregation) ? Anatomie du flux de commande et de requête (Comment Fonctionne Le Pipeline CQRS Command Query Responsibility Segregation Anatomie Du Flux DE Commande Et DE Requete)
Qu’est-ce que le pipeline de requêtes CQRS ? Comment une requête HTTP se déroule-t-elle via Controller, MediatR, le comportement du pipeline, le gestionnaire, la boîte d'envoi et le modèle de lecture ?
CQRS- Anatomie des décisions
Partie 2 de 4
CQRS pas avec la syntaxe du framework ; Un livre d'ingénierie en quatre parties examinant les tensions liées à la décision, à l'échelle et à la production.
Que se passe-t-il lorsqu'une requête HTTP parvient au système ? Où fonctionne la validation, quand la transaction est-elle ouverte, à quel niveau les règles de domaine sont-elles appliquées ? Dans un système utilisant CQRS, ce parcours est différent de l'architecture CRUD classique. Dans cet article, nous examinerons les étapes suivies par une requête depuis l'API vers la base de données jusqu'au modèle de lecture.
Pour un lecteur peu familier avec CQRS, le dictionnaire le plus court est celui-ci : Command est l'intention de changer le système ; Requête demande des informations sans modifier le système ; Handler est le gestionnaire de cette requête. Un médiateur comme MediatR permet au contrôleur de transmettre la demande au flux approprié plutôt que de reconnaître directement le bon gestionnaire.```text POST /orders ↓ Controller ↓ Mediator.Send(command) ↓ Pipeline behaviors ↓ Command handler ↓ Aggregate + Repository ↓ Database
Le comportement du pipeline correspond à des contrôles techniques courants que nous ne souhaitons pas réécrire dans chaque commande. Aucune de ces règles n’est une règle commerciale ; La règle métier réside dans le gestionnaire et l’agrégat.```text
ValidationBehavior
↓
AuthorizationBehavior
↓
LoggingBehavior
↓
MetricsBehavior
↓
RetryBehavior (yalnızca güvenli işlemlerde)
↓
TransactionBehavior
↓
Handler
```La transaction s'ouvre à la fin ; car nous ne voulons pas déplacer inutilement la connexion à la base de données et verrouiller une demande qui pourrait être invalide, non autorisée ou rejetée prématurément. Une fois que nous avons atteint Handler, nous sommes maintenant prêts à mettre en œuvre la décision commerciale de manière atomique.
**Aggregate** préserve les règles métier du système (c'est-à-dire les invariants) qui ne doivent jamais être enfreintes. Par exemple, si une commande terminée ne peut pas être annulée à nouveau, cette règle doit être conservée dans l'agrégat de la commande, et non dans le contrôleur.
Le côté requête suit un chemin différent. Pour la même commande, l'utilisateur n'a peut-être pas besoin de l'objet de domaine entier, mais d'une courte vue adaptée à l'écran :```text
GET /orders
↓
Authorization
↓
Cache (uygunsa)
↓
Read database / Search index
↓
OrderSummary DTO
↓
Frontend
Concepts à la première mention```text
📦 Handler Tek bir command veya query'nin uygulama iş akışını yürütür.
📦 Mediator Controller'ın handler'ı doğrudan bilmeden isteği doğru işleyiciye göndermesini sağlar.
📦 Pipeline Behavior Validation, authorization veya logging gibi her istekte çalışan ortak teknik katmandır.
📦 Repository Aggregate'i yükleyen ve kalıcı olarak saklayan uygulama sınırıdır.
📦 DTO Domain modelin tamamını değil, bir ekranın ihtiyacı olan veriyi taşır.
Lorsque vous lisez l'exemple du gestionnaire, gardez cette distinction à l'esprit : le gestionnaire met simplement en œuvre la décision commerciale. La validation, l'autorisation et la journalisation ne sont pas incluses dans le gestionnaire ; Ceux-ci sont résolus par les comportements du pipeline. De cette façon, le gestionnaire reste petit et chaque requête passe les mêmes règles de sécurité/traçabilité.
Nous voyons souvent CQRS comme deux dossiers, quelques gestionnaires MediatR et un cache. Cette image est trompeuse. La tâche principale de CQRS n'est pas de diviser le code ; Il s'agit de faire la distinction entre les **décisions qui modifient l'état d'un système** et les **demandes qui demandent des informations sur cet état**.
Il s'agit de la deuxième partie de la collection CQRS- Anatomie des Décisions. Dans une première partie, nous expliquerons pourquoi la ségrégation est devenue nécessaire. Nous entrons ici dans le mécanisme : par quelles portes passe une commande, pourquoi une requête ne devrait-elle pas suivre le même chemin, et quand le modèle de lecture devient-il un système distinct ?
## Point de départ : une demande, deux intentions différentes
`CreateOrder` est une commande. Il veut ajouter une nouvelle réalité au système. Il exécute les règles, demande l'autorisation, ouvre les transactions et doit être auditable.
`GetOrderSummary` est une requête. Cela ne produit pas une nouvelle réalité. Veut renvoyer une vue rapide, étroite et conviviale pour l’écran. Il n'est pas nécessaire de charger le même agrégat, d'exécuter des règles de domaine ou d'entrer en concurrence avec des verrous en écriture.
L'implication algorithmique de cette distinction est claire : du côté de la commande, le coût est souvent pris en compte pour des raisons de validation et de cohérence ; Côté requête, le coût augmente avec la quantité de données interrogées. L'utilisation d'un modèle de lecture personnalisé au lieu de parcourir le graphique agrégé pour un écran de liste réduit à la fois les E/S inutiles et la charge cognitive.
## Pipeline de commandes : chemin sûr vers la décision
Côté Commande, le but n'est pas seulement d'appeler `handler`. La demande doit passer par les règles communes du système avant d’aboutir à la décision commerciale.```text
API
→ Authentication / Authorization
→ Validation
→ Idempotency check
→ Transaction
→ Command Handler
→ Aggregate + Domain Rules
→ Persist + Outbox
→ Commit
```Pseudo-code :```text
handle(command):
authorize(command.actor)
validate(command)
return idempotency.execute(command.key):
begin transaction
aggregate = repository.load(command.aggregateId)
aggregate.apply(command)
repository.save(aggregate)
outbox.store(aggregate.domainEvents)
commit transaction
```Chaque étape a une seule responsabilité. La validation ne remplace pas une règle métier ; rejette le formulaire invalide plus tôt. L'autorisation vérifie si la décision appartient au propriétaire. L'agrégat préserve les invariants. Outbox, quant à lui, garantit que l'état permanent et l'événement à publier se produisent dans la même limite de transaction.
Côté C#, Mediator rend ce flux pratique :```csharp
public sealed record PlaceOrder(Guid CustomerId, IReadOnlyList<OrderLine> Lines) : IRequest<OrderId>;
public sealed class PlaceOrderHandler : IRequestHandler<PlaceOrder, OrderId>
{
public async Task<OrderId> Handle(PlaceOrder command, CancellationToken ct)
{
var order = Order.Place(command.CustomerId, command.Lines);
await repository.AddAsync(order, ct);
await outbox.AddAsync(order.DomainEvents, ct);
return order.Id;
}
}
```Le gestionnaire reste court car les responsabilités transversales telles que l'audit, la mesure, la validation ou la nouvelle tentative sont déplacées vers les comportements du pipeline. L’avantage est que tous les gestionnaires ne reproduisent pas manuellement le même comportement. Le prix est le risque de perdre la visibilité de la chaîne d'appel. Par conséquent, la séquence de comportement doit être clairement documentée et observée.
## Pipeline de requêtes : lisez autant que vous en avez besoin
Du côté des requêtes, la question fondamentale est la suivante : quelles informations l'utilisateur souhaite-t-il, dans les limites de quel budget de latence ? La réponse à cette question n’est pas le modèle de domaine, mais le scénario d’utilisation.```text
API
→ Authorization for the view
→ Query Handler
→ Read model / cache / search index
→ DTO shaped for the screen
```Il est souvent inutile de recharger l'agrégat `Order`, les relations clients et les règles d'inventaire pour une liste de commandes. Le gestionnaire de requêtes sélectionne directement les champs dont l'écran a besoin. Ainsi, le coût est approximativement limité au nombre de lignes et de colonnes renvoyées plutôt qu'à l'ensemble du graphique de domaine.```csharp
public sealed record GetOrderSummary(Guid OrderId) : IRequest<OrderSummaryDto?>;
public sealed class GetOrderSummaryHandler : IRequestHandler<GetOrderSummary, OrderSummaryDto?>
{
public Task<OrderSummaryDto?> Handle(GetOrderSummary query, CancellationToken ct) =>
readDb.OrderSummaries
.Where(x => x.Id == query.OrderId)
.Select(x => new OrderSummaryDto(x.Id, x.Status, x.Total, x.UpdatedAt))
.SingleOrDefaultAsync(ct);
}
```Ici `DTO` n'est pas une décoration pour cacher le domaine ; Il s'agit d'un accord de lecture. Le modèle de lecture peut changer lorsque l'écran change. Le modèle de commande peut changer lorsque les règles métier changent. Les deux changements ne doivent pas nécessairement se produire au même rythme.
## CQRS logique et CQRS physique
La première étape est un CQRS logique pour la plupart des équipes : la commande et la requête sont séparées dans le code, mais elles utilisent la même base de données relationnelle. Les transactions ACID sont protégées, les coûts d'exploitation sont faibles et le débogage est simple.
Dans le CQRS physique, le modèle de lecture est déplacé vers un magasin de données, un cache ou un index de recherche distinct. Cela donne la possibilité de mettre à l'échelle le côté lecture de manière indépendante ; Mais en retour, il y a la fraîcheur des données, les erreurs de projection et les processus de reconstruction. La séparation physique n’est donc pas un fantasme de performance mais la réponse à un goulot d’étranglement avéré.
| Élection | Gains | Prix accepté |
| --- | --- | --- |
| CQRS logique | Faible coût d'exploitation, forte cohérence | La lecture et l'écriture partagent la même infrastructure |
| CQRS physiques | Mise à l'échelle indépendante, modèles adaptés à l'écran | Cohérence finale, opération de projection |
## Du modèle d'écriture au modèle de lecture : CDC ou Outbox ?
La question cruciale en matière de séparation physique est de savoir comment les données circulent. Change Data Capture peut capturer les modifications de la base de données à partir des journaux ; Il peut être puissant pour rejouer sans toucher aux systèmes existants. Mais cela rapproche le contrat de message du schéma de base de données.
L'approche de la boîte d'envoi écrit l'événement de domaine dans la table de la boîte d'envoi dans la même transaction. Un relais transmet ces enregistrements au courtier ; Les consommateurs se comportent idempotents. Ainsi, le dilemme `database write başarılı, event publish başarısız` devient gérable. Mais le relais, les nouvelles tentatives, la surveillance des lettres mortes et l'idempotence du consommateur font désormais partie de la conception.```text
Command commit
→ Outbox record
→ Relay publishes event
→ Projection consumes event
→ Read model updates
```Dans ce flux, il est plus réaliste de concevoir au moins une livraison et un traitement idempotent plutôt que des promesses exactement une fois. Par exemple, la projection enregistre l'ID de l'événement ; Si le même événement se reproduit, cela ne change pas la vue une seconde fois.
## Pourquoi les solutions naïves échouent-elles ?
- Envoi d'un message directement au courtier depuis le Handler : le message peut avoir déjà été consommé lors de l'annulation de la transaction.
- Chargement de l'agrégat pour chaque requête : les écrans de liste deviennent inutilement coûteux avec les règles de domaine et les jointures.
- Considérer le délai de lecture du modèle comme une erreur : certains écrans nécessitent des données fraîches, d'autres tolèrent des secondes de délai. Cette distinction doit être déterminée avec le produit.
- Résoudre tous les problèmes avec le CQRS physique : le deuxième magasin de données n'est pas seulement technologique mais une surcharge opérationnelle permanente.
## Liste de contrôle de décision
1. La condition de réussite de la commande et le commutateur d'idempotence sont-ils activés ?
2. Chaque invariant est-il préservé à la limite globale ?
3. La requête est-elle façonnée en fonction du DTO du scénario d'utilisation plutôt que du domaine ?
4. Une attente est-elle définie dans le langage du produit concernant le délai de lecture du modèle ?
5. Comment le système sera-t-il surveillé et vérifié lorsque Projection sera reconstruit ?
CQRS n’est pas un ticket automatique pour une échelle illimitée. Lorsqu’elles sont utilisées correctement, les décisions peuvent être prises dans un pipeline ; Il transforme la lecture en un modèle adapté aux besoins. Le système devient ainsi à la fois plus visible et plus résistant au changement.
Dans la section suivante, cette logique ira au-delà de la limite d'un seul service : nous examinerons le CQRS avec des courtiers d'événements, des projections et des stratégies de restauration dans les systèmes distribués.
## Qu'est-ce que le modèle de lecture ?
Le modèle de lecture n'est pas une copie du modèle de domaine côté écriture ; C'est la vue préparée pour les besoins d'un écran.```text
Orders (write model)
Id | CustomerId | Status | Lines | Rules
↓ Projection
OrderSummary (read model)
Id | CustomerName | Status | Total | BadgeColor
```## CDC vs Boîte d'envoi
| Critère | CDC | Boîte d'envoi |
| --- | --- | --- |
| Intention de l'événement de domaine | Indirect | Ouvrir |
| Dépendance du schéma de base de données | Élevé | inférieur |
| Rejouer | Fort | Fort |
| Contrôle du contenu des événements | Limité | Complet |
| Communication par microservices | Cela dépend | Très abordable |
## Pourquoi les solutions naïves échouent-elles en production ?```text
Handler
→ DbContext.SaveChanges()
→ Message publish
```Si la deuxième étape échoue, les données sont écrites et l'événement est perdu. Ou bien, pendant l'annulation de la transaction, l'appel suivant peut déjà avoir produit un effet secondaire :```text
await emailService.Send(...)
```Par conséquent, Outbox considère l'événement à publier avec le changement permanent dans la même limite de transaction ; Les effets secondaires tels que le courrier électronique sont gérés après la validation, chez les consommateurs résistants aux nouvelles tentatives.
## Comment se déroule une requête réelle ?```text
POST /orders
↓
Controller
↓
Mediator.Send()
↓
Validation → Authorization → Logging → Metrics → Transaction
↓
Handler
↓
Aggregate
↓
Repository + Outbox
↓
Commit
↓
Relay → Broker / Kafka
↓
Projection
↓
Read database
↓
GET /orders → Query handler → DTO → Frontend
```Le CQRS n'est pas une balance automatique. Mais si vous pouvez expliquer pourquoi chaque étape du pipeline existe, cela introduit une séparation consciente des responsabilités dans votre système. Si vous ne pouvez pas l'expliquer, vous n'êtes peut-être pas encore prêt à utiliser CQRS.
## Pourquoi devrais-je connaître ce flux ?
Si vous savez par quelles couches passe une requête :
- Vous n'écrivez pas de validation au hasard dans le contrôleur ou le gestionnaire.
- Vous ne mettez pas la règle métier dans la couche HTTP, vous la protégez à la limite agrégée.
- Vous n'ouvrez pas la transaction plus tôt que nécessaire.
- Vous n'agrandissez pas les gestionnaires avec des codes de journalisation, d'autorisation et de mesure.
- Vous déplacez les préoccupations transversales vers le pipeline.
La différence côté requête est tout aussi pratique :```text
Sipariş detayı
→ Order aggregate
→ kurallar ve davranış için doğru model
Sipariş listesi
→ OrderSummaryDto
→ ekrana hızlı ve dar bir görünüm
```Voir cette distinction signifie que CQRS n'est pas une présentation de dossier ; Cela vous permet de l’utiliser comme une discipline pour mettre chaque responsabilité à la bonne place.
FAQ
Frequently asked questions
Qu’est-ce que le gestionnaire ?
Exécute le flux de travail d'application d'une seule commande ou requête.
Qu’est-ce que le Médiateur ?
Il garantit que le contrôleur envoie la demande au bon gestionnaire sans connaître directement le gestionnaire.
Que vous dit « Comment fonctionne le pipeline CQRS (Command Query Responsibility Segregation) ? Anatomie du flux de commande et de requête » ?
Qu’est-ce que le pipeline de requêtes CQRS ? Comment une requête HTTP se déroule-t-elle via Controller, MediatR, le comportement du pipeline, le gestionnaire, la boîte d'envoi et le modèle de lecture ?
Principes d'ingénierie appris
- CQRS n'est pas deux bases de données ; C’est accepter que lire et écrire sont des responsabilités différentes.
- Pipeline supprime les contrôles répétitifs des gestionnaires ; laisse la décision commerciale visible.
- La séparation physique n'est utile que lorsque la latence, les nouvelles tentatives et la fraîcheur des données sont conçues comme une décision produit.
Continuer la lecture
Continuer la lecture
Suivant en série
CQRS (Command Query Responsibility Segregation) dans les systèmes distribués : événement, courtier et projection
Comment fonctionne CQRS dans les systèmes distribués ? Examinez les événements de domaine, le courtier de messages, la projection, la boîte d'envoi,…
Suivant en série
De CRUD (Créer, Lire, Mettre à jour, Supprimer) à CQRS (Command Query Responsibility Segregation) : le problème est le modèle, pas le code
Qu'est-ce que CQRS, quelle est la différence entre CRUD et CQRS et quand faut-il utiliser CQRS ? Un guide expliquant pourquoi un seul modèle ne suffit pas…
Même série
CQRS (Command Query Responsibility Segregation) en production : cohérence, erreurs et stratégies de récupération
Comment CQRS fonctionne-t-il en toute sécurité dans un environnement de production ? Décalage de cohérence, événement en double, corruption de séquence,…