Programmazione

Il Pattern Saga in .NET con Wolverine: gestire workflow distribuiti a lungo termine

Dario Fadda Aprile 14, 2026

Nei sistemi distribuiti, la gestione di processi di business che si estendono su più servizi e nel tempo rappresenta una delle sfide più complesse. Il Pattern Saga nasce proprio per risolvere questo problema: coordinare una sequenza di operazioni distribuite in modo affidabile, garantendo la consistenza dei dati anche in caso di errori parziali.

In questo articolo esploriamo come implementare il Pattern Saga in .NET utilizzando Wolverine, un framework moderno che semplifica notevolmente la gestione di messaggi e workflow complessi grazie al suo approccio convention-driven.

Cos’è il Pattern Saga?

Il Pattern Saga è un meccanismo di gestione delle transazioni distribuite che sostituisce le transazioni ACID tradizionali nei sistemi a microservizi. Invece di eseguire una sequenza di operazioni come un’unica transazione atomica, la saga suddivide il processo in una serie di passi indipendenti, ciascuno con la propria logica di compensazione in caso di fallimento.

Il principio fondamentale è semplice: se un passo fallisce o va in timeout, la saga esegue la logica di compensazione invece di lasciare il sistema in uno stato inconsistente. Questo approccio è particolarmente utile per processi di lunga durata come:

  • Onboarding di nuovi utenti con email di verifica
  • Processi di ordine e pagamento in e-commerce
  • Workflow di approvazione multi-step
  • Processi di provisioning di risorse cloud

Perché Wolverine?

Wolverine è un framework per .NET che adotta un approccio convention-driven alla messaggistica e ai workflow. A differenza di soluzioni come MassTransit o Rebus, Wolverine gestisce automaticamente routing dei messaggi, persistenza dello stato e correlazione, senza richiedere un’estesa configurazione tramite DSL per state machine.

Le principali dipendenze per iniziare sono:

WolverineFx (5.16.2)
WolverineFx.Postgresql (5.16.2)
WolverineFx.RabbitMQ (5.16.2)

Configurazione del progetto

La configurazione di Wolverine richiede pochi passaggi. Nell’entry point dell’applicazione, si configura il framework per utilizzare RabbitMQ come message broker e PostgreSQL per la persistenza dello stato:

builder.Host.UseWolverine(options =>
{
    options.UseRabbitMqUsingNamedConnection("rmq")
        .AutoProvision()
        .UseConventionalRouting();

    options.Policies.DisableConventionalLocalRouting();
    options.PersistMessagesWithPostgresql(connectionString!);
});

Dettagli importanti di questa configurazione:

  • AutoProvision(): crea automaticamente exchange e code in RabbitMQ
  • UseConventionalRouting(): instrada i messaggi in base ai nomi dei tipi
  • DisableConventionalLocalRouting(): forza tutti i messaggi attraverso RabbitMQ
  • PersistMessagesWithPostgresql(): archivia stato della saga e messaggi; crea una tabella per saga con serializzazione JSON

Definizione dei messaggi

Ogni passo della saga è rappresentato da un messaggio. Definiamo tutti i tipi di messaggio per un processo di onboarding utente:

public record SendVerificationEmail(Guid UserId, string Email);
public record VerificationEmailSent(Guid Id);
public record VerifyUserEmail(Guid Id);
public record SendWelcomeEmail(Guid UserId, string Email, string FirstName);
public record WelcomeEmailSent(Guid Id);
public record OnboardingTimedOut(Guid Id) : TimeoutMessage(5.Minutes());

Il record OnboardingTimedOut estende TimeoutMessage di Wolverine: questo fa sì che il messaggio venga consegnato automaticamente dopo 5 minuti, eliminando la necessità di scheduler esterni per gestire i timeout.

Implementazione della classe Saga

La saga viene implementata come una classe che estende Saga. Lo stato viene mantenuto come proprietà della classe:

public class UserOnboardingSaga : Saga
{
    public Guid Id { get; set; }
    public string Email { get; set; } = string.Empty;
    public string FirstName { get; set; } = string.Empty;
    public string LastName { get; set; } = string.Empty;
    public bool IsVerificationEmailSent { get; set; }
    public bool IsEmailVerified { get; set; }
    public bool IsWelcomeEmailSent { get; set; }
}

Il metodo Start: avvio della saga

Il metodo statico Start è il factory method che inizia la saga. Restituisce una tupla contenente l’istanza della saga, il comando iniziale e il messaggio di timeout pianificato:

public static (
    UserOnboardingSaga,
    SendVerificationEmail,
    OnboardingTimedOut) Start(
        UserRegistered @event,
        ILogger<UserOnboardingSaga> logger)
{
    var saga = new UserOnboardingSaga
    {
        Id = @event.Id,
        Email = @event.Email,
        FirstName = @event.FirstName,
        LastName = @event.LastName,
    };

    return (
        saga,
        new SendVerificationEmail(saga.Id, saga.Email),
        new OnboardingTimedOut(saga.Id));
}

Wolverine persiste automaticamente la saga e consegna tutti i messaggi restituiti. Elegante e senza boilerplate.

Metodi Handle: gestione degli eventi

I metodi Handle elaborano i messaggi in arrivo. Se restituiscono void, aggiornano solo lo stato; se restituiscono un messaggio, causano l’invio del passo successivo:

public void Handle(VerificationEmailSent @event, ILogger<UserOnboardingSaga> logger)
{
    logger.LogInformation("Email di verifica inviata per l'utente {UserId}", Id);
    IsVerificationEmailSent = true;
}

public SendWelcomeEmail Handle(VerifyUserEmail command, ILogger<UserOnboardingSaga> logger)
{
    logger.LogInformation("Email verificata per l'utente {UserId}", Id);
    IsEmailVerified = true;
    return new SendWelcomeEmail(Id, Email, FirstName);
}

public void Handle(WelcomeEmailSent @event, ILogger<UserOnboardingSaga> logger)
{
    logger.LogInformation("Onboarding completato per l'utente {UserId}", Id);
    IsWelcomeEmailSent = true;
    MarkCompleted(); // Elimina lo stato dal database
}

Gestione del timeout e compensazione

Il timeout è un cittadino di prima classe in Wolverine. Se l’utente non verifica l’email entro 5 minuti, il handler del timeout gestisce la compensazione:

public void Handle(OnboardingTimedOut timeout, ILogger<UserOnboardingSaga> logger)
{
    if (IsEmailVerified)
    {
        logger.LogInformation(
            "Timeout ignorato - email già verificata per {UserId}", Id);
        return;
    }

    logger.LogWarning(
        "Onboarding scaduto per {UserId} - email non verificata", Id);
    MarkCompleted();
}

Gestione dei messaggi “orfani” con NotFound

Wolverine richiede la gestione esplicita del caso in cui arrivi un messaggio per una saga già terminata, tramite metodi statici NotFound:

public static void NotFound(VerifyUserEmail command, ILogger<UserOnboardingSaga> logger)
{
    logger.LogWarning("VerifyEmail ricevuto ma la saga {Id} non esiste più", command.Id);
}

public static void NotFound(OnboardingTimedOut timeout, ILogger<UserOnboardingSaga> logger)
{
    logger.LogInformation("Timeout per la saga già completata {Id}", timeout.Id);
}

Correlazione dei messaggi e gestione della concorrenza

Wolverine correla automaticamente i messaggi alle istanze della saga cercando nell’ordine: attributo [SagaIdentity], proprietà {SagaTypeName}Id, oppure proprietà Id. Non è necessaria alcuna configurazione esplicita per i casi standard.

Per la concorrenza, Wolverine applica di default il controllo di concorrenza ottimistico: quando più messaggi per la stessa saga arrivano contemporaneamente, uno riesce mentre gli altri vengono ritentati automaticamente. Attenzione: non invocare IMessageBus.InvokeAsync() all’interno dei handler della stessa saga, ma usare sempre i messaggi in cascata (valori di ritorno) per evitare problemi con dati obsoleti.

Opzioni di persistenza

Wolverine supporta tre strategie per la persistenza dello stato della saga:

  • Lightweight Storage: serializza lo stato come JSON in tabelle dedicate per saga, zero configurazione ORM
  • Marten: archivia le saghe come documenti con concorrenza ottimistica e ID fortemente tipizzati
  • Entity Framework Core: mappa le saghe su tabelle queryabili, abilitando commit in singola transazione con altri dati

Il flusso completo

Il percorso “happy path” dell’onboarding segue questi passi:

  1. L’evento UserRegistered attiva il metodo Start()
  2. Viene creata l’istanza della saga, inviato SendVerificationEmail e pianificato OnboardingTimedOut
  3. VerificationEmailSent aggiorna lo stato della saga
  4. VerifyUserEmail ricevuto, viene inviato in cascata SendWelcomeEmail
  5. WelcomeEmailSent completa il workflow, la saga viene eliminata

Se VerifyUserEmail non arriva entro 5 minuti, OnboardingTimedOut gestisce la compensazione e termina la saga.

Conclusione

Wolverine offre un approccio sorprendentemente pulito all’implementazione del Pattern Saga in .NET. La scelta convention-driven elimina gran parte del boilerplate tipico di altri framework, consentendo di concentrarsi sulla logica di business. La gestione automatica di persistenza, routing e correlazione dei messaggi, unita al supporto nativo per timeout e compensazione, lo rende una scelta solida per workflow distribuiti complessi.

Per i team che lavorano con architetture a microservizi in .NET, Wolverine merita certamente una valutazione approfondita come alternativa moderna ai pattern tradizionali di orchestrazione.

Fonte originale: Implementing the Saga Pattern With Wolverine — Milan Jovanović

💬 Unisciti alla discussione!


Questo è un blog del Fediverso: puoi trovare quindi questo articolo ovunque con @blog@spcnet.it e ogni commento/risposta apparirà qui sotto.

Se vuoi commentare su Il Pattern Saga in .NET con Wolverine: gestire workflow distribuiti a lungo termine, utilizza la discussione sul Forum.
Condividi la tua esperienza, confrontati con altri professionisti e approfondisci i dettagli tecnici nel nostro 👉 forum community