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.

LAB 01 · FLUX RÉSILIENT C# 14 · .NET 10 · Azure Service Bus · Amazon SQS
Décisions dans l’article 0/4 Prenez les décisions proposées au fil des chapitres.

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.

01

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.

01Accepter

Enregistrez l’intention et attribuez-lui un identifiant stable.

02Découpler

Ne faites pas attendre l’utilisateur à cause d’un système tiers.

03Réessayer

La reprise fait partie du fonctionnement normal, pas des exceptions à la conception.

04Prouver

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

flux actif
01APIcommand + requestId
02DB + Outboxun seul COMMIT
03Dispatcherpublication après enregistrement
AAzure Service BusPeekLock · Sessions · DLQ
BAmazon SQSVisibility · FIFO · DLQ
04Consumer + Inboxidempotence
05Effetreceipt + télémétrie
Retrybackoff + jitterDLQ
QR code vers la carte mobile du flux de messages
OUVRIR SUR MOBILE

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 →
Décision 1/4

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.

Une migration Azure importante

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 pour .NET

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

BesoinAzure Service BusAmazon SQSDécision de conception
Livraison de basePeekLock offre une livraison au moins une foisStandard Queue offre une livraison au moins une foisLe consommateur doit supporter les répétitions
Ordre au sein d’un groupeSessions avec SessionIdFIFO avec MessageGroupIdRegroupez par agrégat, pas à l’échelle de tout le système
Publications en doubleDuplicate detection fondée sur MessageIdDéduplication FIFO fondée sur le deduplication IDUne protection complémentaire, qui ne remplace pas l’inbox
Temps d’exécutionDurée du verrou et renouvellementVisibility timeout et ajustement de sa duréeFixez une durée supérieure au p95 et surveillez les dépassements
Messages problématiquesFile de lettres mortes intégréeRedrive policy vers une DLQAlerte, responsable, politique de reprise et procédure de rejeu
Données volumineusesBlob Storage + clé dans le messageS3 + clé dans le messageClaim-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.

02

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ù ».

STRATEGY + ADAPTER

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);
}
UNE POLITIQUE, PAS UNE FORÊT DE IF

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.

Décision 2/4

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.

uploadBlob / S3objectKey + SHA-256Queueworker
03

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.

04

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);
}
Décision 3/4

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.

4 incidents×3 personnes×2,5 h×180 PLN= 5 400 PLN / mois

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.

Progression0/7

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

Décision 4/4

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

APIOUTBOXBROKERRETRYINBOXEFFECT

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

01

Â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.

02

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.

03

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.

04

Lock lost / timeout

Indique un mauvais ajustement entre la durée du travail et celle du verrou, ou un consommateur surchargé.

05

Duplicate prevented

Ce compteur confirme que l’idempotence protège réellement le résultat et ne se limite pas à la documentation.

06

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.

DÉPART

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.
PRATIQUE

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.
SENIOR

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 ?
FINAL · 10 QUESTIONS

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 :