Des files de messages sans heures supplémentaires : C#, Azure Service Bus et AWS SQS dans un flux résilient
Un atelier interactif sur C# 14, .NET 10, Azure Service Bus et Amazon SQS : outbox, idempotence, claim-check avec Blob/S3, DLQ, coût des incidents, carte animée et quiz avec rapport par e-mail.
L’article complet est disponible ci-dessous. Activez JavaScript pour les quiz et les simulations.
Effacer le résultat
Vendredi, 16 h 47
Une file ne doit pas masquer la panne. Elle doit laisser à l’équipe le temps de réagir sans risque
Le client valide une commande. L’API répond correctement, mais le système de facturation externe ralentit. Le worker perd sa connexion juste après avoir exécuté l’opération : le même message revient donc une seconde fois. Si votre conception suppose qu’un message n’arrivera qu’une seule fois, votre samedi matin commencera par un effet produit en double et un nettoyage manuel des données.
Ce contenu est un atelier, pas le récit d’une réussite client fictive. Nous allons construire un flux qui tient compte des répétitions, des retards et des pannes partielles. Nous utiliserons un port commun en C#, des stratégies de transport, une transactional outbox, un consommateur idempotent, des reprises, une file de lettres mortes et le modèle claim-check pour les fichiers dans Azure Blob Storage ou Amazon S3. Un examen de dix questions et un rapport individuel par e-mail vous attendent à la fin.
Comprendre le modèle
Une file est une salle d’attente maîtrisée, pas un accélérateur magique
Dans un système synchrone, l’utilisateur attend la fin de chaque étape dont dépend sa demande. Dans un système asynchrone, l’API accepte une intention, enregistre son propre état et délègue l’exécution. Une panne temporaire de l’intégration ne bloque donc pas nécessairement tout le parcours. Cette liberté a un coût précis : il faut reconnaître les doublons, reprendre le travail, surveiller le retard et décider du sort des messages que les automatismes ne savent plus réparer.
Enregistrez l’intention et attribuez-lui un identifiant stable.
Ne faites pas attendre l’utilisateur à cause d’un système tiers.
La reprise fait partie du fonctionnement normal, pas des exceptions à la conception.
Les métriques, les logs et la preuve de traitement doivent montrer un seul effet.
Une carte à conserver
De la requête à son effet : un contrat, deux transports
Scannez la carte et gardez-la comme liste de vérification pour vos revues
Le code mène directement à cette carte animée. Ouvrez-la pendant la conception d’un worker ou l’analyse d’un incident, sans avoir à retrouver le bon chapitre.
Ouvrir la carte sans scanner →L’API a enregistré la commande, mais le broker est temporairement indisponible. Que faites-vous ?
J’attends et je retente la publication dans la requête HTTP.
J’enregistre l’état et l’outbox dans une même transaction ; le dispatcher reprendra l’envoi.
Choisissez la solution qui ne perdra pas une commande déjà acceptée.
État des technologies à la publication
Un nouveau C# ne corrige pas une mauvaise répartition des responsabilités, mais peut simplifier son expression
C# 14 est livré avec .NET 10, une version LTS. Les extension members, l’affectation conditionnelle à null et les améliorations autour des spans rendent le code plus agréable à écrire ; la fiabilité dépend toujours du contrat et des données. Dans une intégration, il est plus important de donner au message un identifiant, une version, une corrélation, une date d’événement et les données minimales nécessaires au consommateur que de gagner quelques caractères de syntaxe.
Microsoft a annoncé la fin du support des anciennes bibliothèques
WindowsAzure.ServiceBus et Microsoft.Azure.ServiceBus
au 30 septembre 2026. Le nouveau code doit utiliser Azure.Messaging.ServiceBus, et les systèmes existants doivent prévoir une migration et un test de réception.
AWS SDK for .NET V4 expose directement les API SQS, tandis qu’AWS Message Processing Framework for .NET peut structurer la publication et le traitement des messages. Le framework ne dispense toutefois ni de l’idempotence, ni d’un visibility timeout adapté, ni de la surveillance de la DLQ.
Choisir le transport
Azure Service Bus et Amazon SQS résolvent un problème semblable avec des outils différents
| Besoin | Azure Service Bus | Amazon SQS | Décision de conception |
|---|---|---|---|
| Livraison de base | PeekLock offre une livraison au moins une fois | Standard Queue offre une livraison au moins une fois | Le consommateur doit supporter les répétitions |
| Ordre au sein d’un groupe | Sessions avec SessionId | FIFO avec MessageGroupId | Regroupez par agrégat, pas à l’échelle de tout le système |
| Publications en double | Duplicate detection fondée sur MessageId | Déduplication FIFO fondée sur le deduplication ID | Une protection complémentaire, qui ne remplace pas l’inbox |
| Temps d’exécution | Durée du verrou et renouvellement | Visibility timeout et ajustement de sa durée | Fixez une durée supérieure au p95 et surveillez les dépassements |
| Messages problématiques | File de lettres mortes intégrée | Redrive policy vers une DLQ | Alerte, responsable, politique de reprise et procédure de rejeu |
| Données volumineuses | Blob Storage + clé dans le message | S3 + clé dans le message | Claim-check, somme de contrôle et accès limité dans le temps |
« Exactly once » ne doit pas être un simple vœu dans la conception. L’objectif sûr est le suivant : un message peut être reçu plusieurs fois, mais son effet métier observable ne se produit qu’une fois.
Contrat C#
L’enveloppe du message d’abord, le choix du cloud ensuite
Le modèle de domaine ne doit importer aucun type des SDK Azure ou AWS. Définissons d’abord notre propre contrat, réduit à l’essentiel. L’adaptateur de transport le traduit en propriétés du broker.
public sealed record WorkEnvelope<T>(
Guid MessageId,
Guid CorrelationId,
string TenantId,
int SchemaVersion,
DateTimeOffset OccurredAt,
T Payload);
public interface IWorkQueue
{
Task PublishAsync<T>(
WorkEnvelope<T> message,
QueueRoute route,
CancellationToken cancellationToken);
}
MessageId évite de produire l’effet une seconde fois, CorrelationId relie la trace entre l’API et le worker, TenantId permet d’imposer l’isolation, tandis que
SchemaVersion permet au contrat d’évoluer sans risque. L’enveloppe ne contient ni secrets, ni documents complets, ni données ajoutées « au cas où ».
Un transport choisi par une politique
public interface IQueueStrategy
{
bool Supports(CloudBroker broker);
Task PublishAsync<T>(
WorkEnvelope<T> message,
QueueRoute route,
CancellationToken ct);
}
public sealed class QueueRouter(
IEnumerable<IQueueStrategy> strategies)
{
public Task PublishAsync<T>(
CloudBroker broker,
WorkEnvelope<T> message,
QueueRoute route,
CancellationToken ct)
=> strategies.Single(x => x.Supports(broker))
.PublishAsync(message, route, ct);
}
Quand un routeur est-il utile ?
- lorsque des régions ou des tenants nécessitent des transports différents ;
- pendant une migration d’un cloud vers un autre ;
- lorsqu’un basculement maîtrisé est nécessaire ;
- lorsque les tests doivent utiliser un transport en mémoire.
Si tout le système utilise toujours le même broker, un seul adaptateur sera plus simple. Une abstraction doit absorber une variation réelle, pas en inventer par précaution.
Un message doit transporter un PDF de 18 Mo. Que placez-vous dans la file ?
Le fichier entier en Base64, pour que le worker ait immédiatement toutes les données.
La clé de l’objet, une somme de contrôle et les métadonnées ; le fichier va dans Blob Storage ou S3.
Choisissez la solution qui garde un contrat léger et sûr.
Claim-check
Blob Storage ou S3 porte le poids des données. La file porte l’intention
L’API stocke le fichier sous une clé imprévisible, calcule son SHA-256 et ne publie qu’une référence. Le worker récupère l’objet avec une identité managée ou un rôle IAM, vérifie le hash et le traite en streaming. L’accès public est désactivé ; la conservation et le chiffrement dépendent de la classification des données.
Atomicité
L’outbox ferme la fenêtre entre le COMMIT et la publication
Il n’existe pas de transaction distribuée entre PostgreSQL et le broker dans ce flux. Au lieu de prétendre le contraire, nous enregistrons la modification métier et l’entrée à publier dans une seule transaction locale.
await using var transaction =
await db.Database.BeginTransactionAsync(ct);
var order = WorkOrder.Accept(command.RequestId, command.CustomerId);
db.WorkOrders.Add(order);
db.OutboxMessages.Add(OutboxMessage.Create(
messageId: command.RequestId,
type: "work-order.accepted.v1",
payload: serializer.Serialize(new WorkOrderAccepted(order.Id))));
await db.SaveChangesAsync(ct);
await transaction.CommitAsync(ct);
// Dispatcher odczyta OutboxMessages dopiero po COMMIT.
Le dispatcher sélectionne les entrées non traitées par lots, les publie, puis marque leur exécution. Deux dispatchers ne doivent pas s’approprier la même entrée sans contrôle de concurrence. Dans PostgreSQL, on peut utiliser un verrou de ligne avec SKIP LOCKED ou un statut explicite de bail. Il faut toujours mesurer l’âge de la plus ancienne entrée outbox : le nombre d’entrées ne dit pas depuis combien de temps le client attend.
Réception idempotente
L’inbox tranche le cas du doublon avant de produire un effet irréversible
Service Bus en mode PeekLock et SQS Standard peuvent livrer un message plusieurs fois. Le consommateur vérifie d’abord sa propre preuve de traitement, produit ensuite l’effet, puis ne confirme le message qu’après un enregistrement durable.
public async Task HandleAsync(
WorkEnvelope<WorkOrderAccepted> message,
CancellationToken ct)
{
if (await db.InboxReceipts.AnyAsync(
x => x.MessageId == message.MessageId, ct))
return;
await using var tx = await db.Database.BeginTransactionAsync(ct);
await pricing.CreateQuoteAsync(
message.Payload.WorkOrderId,
idempotencyKey: message.MessageId.ToString("N"),
ct);
db.InboxReceipts.Add(new InboxReceipt(message.MessageId));
await db.SaveChangesAsync(ct);
await tx.CommitAsync(ct);
}
Le worker a exécuté l’opération, mais a perdu la connexion avant Complete/Delete. Le message revient. Que doit-il faire ?
Retrouver la preuve de traitement par MessageId et terminer sans reproduire l’effet.
Tout exécuter à nouveau, puisque le broker a renvoyé le message volontairement.
Réfléchissez à ce qui prouve l’achèvement lorsque la confirmation du broker s’est perdue.
Le coût des pannes
Une file ne fait économiser de l’argent que si elle réduit le coût des interventions manuelles
Le calcul suivant est un exemple à adapter à vos données, pas le résultat d’une étude de cas ArcCloud Studio. Supposons quatre incidents par mois, trois personnes mobilisées pendant 2,5 heures en moyenne et un coût horaire complet de 180 PLN.
Ajoutez la concentration perdue, le retard de la feuille de route et le risque pour le client. Comparez ce coût à celui du broker, de la télémétrie et des tests automatisés. L’architecture la moins chère n’est pas celle dont la facture cloud est la plus basse, mais celle qui minimise le coût total d’un résultat fiable.
Jeu d’architecture
Remettez dans l’ordre les sept étapes d’un traitement sûr
Cliquez sur les tuiles dans l’ordre. Une erreur ne retire aucun point : elle indique la responsabilité encore manquante.
BrokerASB ou SQS conserve le message
CommandL’API accepte un requestId stable
Effet métierexécution avec une idempotency key
Dispatcherpublie l’outbox enregistrée
SettlementComplete ou Delete après un résultat durable
DB + Outboxétat et message dans un même COMMIT
Consumer + Inboxvérification du doublon et de la corrélation
Commencez par l’intention, dotée d’un identifiant stable.
Recommencer
37 messages sont arrivés en DLQ. Quelle est la première réaction appropriée ?
Renvoyer immédiatement tous les messages dans la file principale.
Identifier la cause, la corriger et rejouer un petit lot de façon contrôlée.
La DLQ est une file de travail opérationnel, pas une corbeille à vider en un clic.
Simulateur d’incident
Faites passer un message par un timeout, une reprise et un doublon
Le parcours contrôlé vérifie requestId, l’outbox, le transport, l’inbox, l’effet métier, la preuve de traitement et le settlement.
Lancer le parcours
Le laboratoire est prêt. Lancez le parcours contrôlé.
Runbook de production
Que surveiller avant que le téléphone sonne en dehors des heures de travail
Âge des messages
p50, p95 et message le plus ancien. La longueur de la file, sans connaître le rythme d’arrivée, peut induire en erreur.
Retry rate
Tentatives par type d’erreur, dépendance et version de contrat. Déclenchez une alerte sur un changement de tendance, pas sur une reprise isolée.
DLQ ingress
Nombre de nouvelles entrées, reason, responsable et délai entre l’arrivée et la décision. Une DLQ sans responsable grossit discrètement.
Lock lost / timeout
Indique un mauvais ajustement entre la durée du travail et celle du verrou, ou un consommateur surchargé.
Duplicate prevented
Ce compteur confirme que l’idempotence protège réellement le résultat et ne se limite pas à la documentation.
Outbox lag
L’âge de la plus ancienne entrée non publiée et le débit du dispatcher constituent un SLO fondamental de l’intégration.
Pour débuter
- Distinguez command, event et document message.
- Apprenez quand Complete/Delete peut être exécuté sans risque.
- Ajoutez MessageId, correlationId et un test de doublon.
Pour un niveau intermédiaire
- Implémentez une outbox et une inbox avec un index unique.
- Adaptez le verrou ou le visibility timeout au p95.
- Construisez des reprises avec backoff, jitter et DLQ maîtrisée.
Pour les profils expérimentés
- Définissez un SLO, un budget d’erreur et un responsable de la file.
- Testez l’évolution du schéma et le rejeu des données historiques.
- Calculez le coût opérationnel et concevez un rollback sûr.
Vérifications avant la production
Si une seule réponse est « je ne sais pas », la file n’est pas encore prête
- Chaque message possède-t-il un MessageId stable et un correlationId ?
- L’enregistrement de l’état et de l’outbox est-il atomique ?
- Un doublon produit-il le même résultat observable ?
- Les charges utiles volumineuses utilisent-elles Blob Storage/S3 et le claim-check ?
- Les reprises distinguent-elles les erreurs temporaires des erreurs permanentes ?
- La DLQ possède-t-elle une alerte, un responsable et une procédure de rejeu ?
- Le verrou ou le visibility timeout correspond-il au p95 réel ?
- Le tableau de bord montre-t-il l’âge des messages, et pas seulement leur nombre ?
- Pouvons-nous arrêter le consommateur sans perdre les commandes acceptées ?
- Un test reproduit-il un timeout après l’effet mais avant le settlement ?
Vérifiez si vous concevez une file robuste ou si vous ne faites que reporter la panne
Le quiz ci-dessous porte sur Azure Service Bus, Amazon SQS, l’outbox, l’inbox, le claim-check, l’idempotence, les reprises et la télémétrie. Après avoir répondu à toutes les questions, vous confirmerez votre adresse e-mail par un code à usage unique. Un rapport dédié vous donnera votre résultat, votre niveau et les prochaines étapes. Votre adresse n’est pas publiée sur le blog.
Sources techniques
Cet atelier s’appuie sur la documentation des éditeurs à jour au moment de sa publication :
- Microsoft Learn: What's new in C# 14
- Microsoft Learn: What's new in .NET 10
- Azure Service Bus: message transfers, locks and duplicates
- Azure Service Bus: duplicate detection
- Azure Service Bus: message sessions
- AWS SDK for .NET V4: Amazon SQS APIs
- AWS Message Processing Framework for .NET
- Amazon SQS: at-least-once delivery
- Amazon SQS: visibility timeout
- Amazon SQS: dead-letter queues