Condividi tramite


Creare Applicazioni aziendali basate su messaggi con NServiceBus e il bus di servizio di Azure

NServiceBus è un framework di messaggistica commerciale fornito da Particular Software. Si basa sul bus di servizio di Azure e consente agli sviluppatori di concentrarsi sulla logica di business astraendo i problemi dell'infrastruttura. In questa guida verrà creata una soluzione che scambia messaggi tra due servizi. Verrà inoltre illustrato come ripetere automaticamente i messaggi con esito negativo ed esaminare le opzioni per l'hosting di questi servizi in Azure.

Nota

Il codice per questa esercitazione è disponibile nel sito Web Particular Software Docs.

Prerequisiti

L'esempio presuppone che sia stato creato uno spazio dei nomi del bus di servizio di Azure.

Importante

NServiceBus richiede almeno il livello Standard. Il livello Basic non funzionerà.

Scaricare e preparare la soluzione

  1. Scaricare il codice dal sito Web Particular Software Docs. La soluzione SendReceiveWithNservicebus.sln è costituita da tre progetti:

    • Mittente: un'applicazione console che invia messaggi
    • Ricevitore: un'applicazione console che riceve messaggi dal mittente e risponde
    • Condiviso: una libreria di classi contenente i contratti di messaggio condivisi tra il mittente e il destinatario

    Il diagramma seguente, generato da ServiceInsight, uno strumento di visualizzazione e debug di Particolare software, mostra il flusso dei messaggi:

    Immagine che mostra il diagramma di sequenza

  2. Aprire SendReceiveWithNservicebus.sln nell'editor di codice preferito (ad esempio, Visual Studio 2022).

  3. Aprire appsettings.json sia nei progetti Ricevitore che Mittente e impostare AzureServiceBusConnectionString sulla stringa di connessione per lo spazio dei nomi del bus di servizio di Azure.

    • Questo è disponibile nel portale di Azure in Spazio dei nomi del bus di servizio>Impostazioni>Criteri di accesso condivisi>RootManageSharedAccessKey>Stringa di connessione primaria .
    • AzureServiceBusTransport include anche un costruttore che accetta uno spazio dei nomi e credenziali token, che in un ambiente di produzione sarà più sicuro, tuttavia ai fini di questa esercitazione verrà usata la stringa di connessione della chiave di accesso condiviso.

Definire i contratti di messaggio condivisi

La libreria di classi condivise consente di definire i contratti usati per inviare i messaggi. Include un riferimento al pacchetto NuGet NServiceBus, che contiene interfacce che è possibile usare per identificare i messaggi. Le interfacce non sono necessarie, ma offrono una convalida aggiuntiva da NServiceBus e consentono di auto-documentare il codice.

Prima di tutto esamineremo la classe Ping.cs

public class Ping : NServiceBus.ICommand
{
    public int Round { get; set; }
}

La classe Ping definisce un messaggio che il mittente invia al ricevitore. Si tratta di una semplice classe C# che implementa NServiceBus.ICommand, un'interfaccia del pacchetto NServiceBus. Questo messaggio è un segnale al lettore e a NServiceBus che è un comando, anche se esistono altri modi per identificare i messaggi senza usare le interfacce.

L'altra classe messaggio nei progetti Condivisi è Pong.cs:

public class Pong : NServiceBus.IMessage
{
    public string Acknowledgement { get; set; }
}

Pong è anche un semplice oggetto C# anche se questo implementa NServiceBus.IMessage. L'interfaccia IMessage rappresenta un messaggio generico che non è né un comando né un evento e viene comunemente usato per le risposte. Nell'esempio è una risposta che il ricevitore invia al mittente per indicare che è stato ricevuto un messaggio.

Ping e Pong sono i due tipi di messaggio che verranno usati. Il passaggio successivo consiste nel configurare il mittente per l'uso del bus di servizio di Azure e per inviare un messaggio Ping.

Configurare il mittente

Il mittente è un endpoint che invia il messaggio Ping. In questo caso viene configurato il mittente per l'uso del bus di servizio di Azure come meccanismo di trasporto, quindi creare un'istanza Ping e inviarla.

Nel metodo Main di Program.cs, configurare l'endpoint Mittente:

var host = Host.CreateDefaultBuilder(args)
    // Configure a host for the endpoint
    .ConfigureLogging((context, logging) =>
    {
        logging.AddConfiguration(context.Configuration.GetSection("Logging"));

        logging.AddConsole();
    })
    .UseConsoleLifetime()
    .UseNServiceBus(context =>
    {
        // Configure the NServiceBus endpoint
        var endpointConfiguration = new EndpointConfiguration("Sender");

        var connectionString = context.Configuration.GetConnectionString("AzureServiceBusConnectionString");
        // If token credentials are to be used, the overload constructor for AzureServiceBusTransport would be used here
        var routing = endpointConfiguration.UseTransport(new AzureServiceBusTransport(connectionString));
        endpointConfiguration.UseSerialization<SystemJsonSerializer>();

        endpointConfiguration.AuditProcessedMessagesTo("audit");
        routing.RouteToEndpoint(typeof(Ping), "Receiver");

        endpointConfiguration.EnableInstallers();

        return endpointConfiguration;
    })
    .ConfigureServices(services => services.AddHostedService<SenderWorker>())
    .Build();

await host.RunAsync();

C'è molto da spiegare qui, quindi lo esamineremo passo dopo passo.

Configurare un host per l'endpoint

L'hosting e la registrazione vengono configurati usando le opzioni standard dell'host generico Microsoft. Per il momento, l'endpoint è configurato per l'esecuzione come applicazione console, ma può essere modificato per l'esecuzione in Funzioni di Azure con modifiche minime, che verranno illustrate più avanti in questo articolo.

Configurare l'endpoint NServiceBus

Successivamente, si indica all'host di usare NServiceBus con il metodo di estensione .UseNServiceBus(…). Il metodo accetta una funzione di callback che restituisce un endpoint che verrà avviato all'esecuzione dell'host.

Nella configurazione dell'endpoint specificare AzureServiceBus per il trasporto, fornendo una stringa di connessione da appsettings.json. Successivamente, si configurerà il routing in modo che i messaggi di tipo Ping vengano inviati a un endpoint denominato "Ricevitore". Consente a NServiceBus di automatizzare il processo di invio del messaggio alla destinazione senza richiedere l'indirizzo del destinatario.

La chiamata a EnableInstallers configurerà la topologia nello spazio dei nomi del bus di servizio di Azure all'avvio dell'endpoint, creando le code necessarie, se necessario. Negli ambienti di produzione, lo scripting operativo è un'altra opzione per creare la topologia.

Configurare il servizio in background per l'invio di messaggi

Il frammento finale del mittente è SenderWorker, un servizio in background configurato per inviare un messaggio Ping ogni secondo.

public class SenderWorker : BackgroundService
{
    private readonly IMessageSession messageSession;
    private readonly ILogger<SenderWorker> logger;

    public SenderWorker(IMessageSession messageSession, ILogger<SenderWorker> logger)
    {
        this.messageSession = messageSession;
        this.logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        try
        {
            var round = 0;
            while (!stoppingToken.IsCancellationRequested)
            {
                await messageSession.Send(new Ping { Round = round++ });;

                logger.LogInformation($"Message #{round}");

                await Task.Delay(1_000, stoppingToken);
            }
        }
        catch (OperationCanceledException)
        {
            // graceful shutdown
        }
    }
}

IMessageSession usato in ExecuteAsync viene inserito in SenderWorker e consente di inviare messaggi usando NServiceBus all'esterno di un gestore di messaggi. Il routing configurato in Sender specifica la destinazione dei messaggi Ping. Mantiene la topologia del sistema (i messaggi indirizzati agli indirizzi) come preoccupazione separata dal codice aziendale.

L'applicazione Mittente contiene anche PongHandler. Si tornerà ad esso dopo aver discusso il Ricevitore, che verrà eseguito successivamente.

Configurare il ricevitore

Il ricevitore è un endpoint in ascolto di un messaggio Ping, registra quando viene ricevuto un messaggio e risponde al mittente. In questa sezione si esaminerà rapidamente la configurazione dell'endpoint, simile al mittente e quindi si rivolgerà l'attenzione al gestore dei messaggi.

Come il mittente, configurare il ricevitore come applicazione console usando l'host generico Microsoft. Usa la stessa configurazione di registrazione ed endpoint (con il bus di servizio di Azure come trasporto messaggi), ma con un nome diverso, per distinguerlo dal mittente:

var endpointConfiguration = new EndpointConfiguration("Receiver");

Poiché questo endpoint risponde solo al relativo origine e non avvia nuove conversazioni, non è necessaria alcuna configurazione di routing. Non è inoltre necessario un ruolo di lavoro in background come il mittente, perché risponde solo quando riceve un messaggio.

Gestore messaggi Ping

Il progetto Ricevitore contiene un gestore di messaggi denominato PingHandler:

public class PingHandler : NServiceBus.IHandleMessages<Ping>
{
    private readonly ILogger<PingHandler> logger;

    public PingHandler(ILogger<PingHandler> logger)
    {
        this.logger = logger;
    }

    public async Task Handle(Ping message, IMessageHandlerContext context)
    {
        logger.LogInformation($"Processing Ping message #{message.Round}");

        // throw new Exception("BOOM");

        var reply = new Pong { Acknowledgement = $"Ping #{message.Round} processed at {DateTimeOffset.UtcNow:s}" };

        await context.Reply(reply);
    }
}

Ignorare il codice commentato per il momento; si tornerà a esso in un secondo momento quando si parla di ripristino da un errore.

La classe implementa IHandleMessages<Ping>, che definisce un metodo: Handle. Questa interfaccia indica a NServiceBus che quando l'endpoint riceve un messaggio di tipo Ping, deve essere elaborato dal metodo Handle in questo gestore. Il metodo Handle accetta il messaggio stesso come parametro e IMessageHandlerContext, che consente ulteriori operazioni di messaggistica, ad esempio la risposta, l'invio di comandi o la pubblicazione di eventi.

PingHandler è semplice: quando viene ricevuto un messaggio Ping, registrare i dettagli del messaggio e rispondere al mittente con un nuovo messaggio Pong, che viene successivamente gestito in PongHandler.

Nota

Nella configurazione del mittente è stato specificato che i messaggi Ping devono essere indirizzati al ricevitore. NServiceBus aggiunge metadati ai messaggi che indicano, tra le altre cose, l'origine del messaggio. Questo è il motivo per cui non è necessario specificare dati di routing per il messaggio di risposta Pong. Viene automaticamente instradato all'origine: il mittente.

Con il mittente e il ricevitore configurati correttamente, è ora possibile eseguire la soluzione.

Eseguire la soluzione

Per avviare la soluzione, è necessario eseguire sia il mittente che il ricevitore. Se si usa Visual Studio Code, avviare la configurazione "Debug tutto". Se si usa Visual Studio, configurare la soluzione per avviare i progetti Mittente e Ricevitore:

  1. Fare clic con il pulsante destro del mouse sulla soluzione in Esplora soluzioni
  2. Selezionare "Imposta progetti di avvio..."
  3. Selezionare Progetti di avvio multipli
  4. Per Mittente e Ricevitore, selezionare "Avvia" nell'elenco a discesa

Avviare la soluzione. Verranno visualizzate due applicazioni console, una per il mittente e una per il ricevitore.

Nel mittente si noti che un messaggio Ping viene inviato ogni secondo, grazie al processo SenderWorker in background. Il ricevitore visualizza i dettagli di ogni messaggio Ping ricevuto e il mittente registra i dettagli di ogni messaggio Pong ricevuto in risposta.

Ora che funziona tutto, è il momento di romperlo.

Resilienza in azione

Gli errori sono una costante nei sistemi software. È inevitabile che il codice fallisca e può succedere per vari motivi, ad esempio errori di rete, blocchi del database, modifiche in un'API di terze parti e semplici errori di codifica precedenti.

NServiceBus offre funzionalità di recuperabilità affidabili per la gestione degli errori. Quando un gestore di messaggi ha esito negativo, i messaggi vengono ritentati automaticamente in base a un criterio predefinito. Esistono due tipi di criteri di ripetizione dei tentativi: tentativi immediati e tentativi ritardati. Il modo migliore per descrivere il funzionamento consiste nel vederli in azione. Aggiungere un criterio di ripetizione dei tentativi all'endpoint ricevitore:

  1. Aprire Program.cs, nel progetto Ricevitore
  2. Dopo la riga .EnableInstallers, aggiungere il codice seguente:
endpointConfiguration.SendFailedMessagesTo("error");
var recoverability = endpointConfiguration.Recoverability();
recoverability.Immediate(
    immediate =>
    {
        immediate.NumberOfRetries(3);
    });
recoverability.Delayed(
    delayed =>
    {
        delayed.NumberOfRetries(2);
        delayed.TimeIncrease(TimeSpan.FromSeconds(5));
    });

Prima di illustrare il funzionamento di questo criterio, è possibile vederlo in azione. Prima di testare i criteri di recupero, è necessario simulare un errore. Aprire il codice PingHandler nel progetto Ricevitore e rimuovere il commento da questa riga:

throw new Exception("BOOM");

Ora, quando il ricevitore gestisce un messaggio Ping, avrà esito negativo. Avviare di nuovo la soluzione e vedere cosa accade nel ricevitore.

Con il meno affidabile PingHandler, tutti i messaggi hanno esito negativo. È possibile visualizzare i criteri di ripetizione dei tentativi per tali messaggi. La prima volta che un messaggio ha esito negativo, viene immediatamente ritentato fino a tre volte:

Immagine che mostra i criteri di ripetizione immediata dei tentativi che riprovano i messaggi fino a 3 volte

Naturalmente, continuerà a non riuscire, quindi quando vengono usati i tre tentativi immediati, i criteri di ripetizione ritardati vengono attivati e il messaggio viene ritardato per 5 secondi:

Immagine che mostra i criteri di ripetizione ritardati che ritarda i messaggi in incrementi di 5 secondi prima di avviare un altro round di tentativi immediati

Dopo aver superato questi 5 secondi, il messaggio viene ritentato altre tre volte (vale a dire un'altra iterazione del criterio di ripetizione immediata dei tentativi). Questi errori avranno esito negativo e NServiceBus ritarderà nuovamente il messaggio, questa volta per 10 secondi, prima di riprovare.

Se PingHandler ancora non riesce dopo l'esecuzione dei criteri di ripetizione completi, il messaggio viene inserito in una coda di errori centralizzata, denominata error, come definito dalla chiamata a SendFailedMessagesTo.

Immagine che mostra il messaggio non riuscito

Il concetto di una coda di errori centralizzata è diverso dal meccanismo di messaggi non recapitabili nel bus di servizio di Azure, che include una coda di messaggi non recapitabili per ogni coda di elaborazione. Con NServiceBus, le code di messaggi non recapitabili nel bus di servizio di Azure fungono da vere code di messaggi non elaborabili, mentre i messaggi che terminano nella coda di errori centralizzata possono essere rielaborati in un secondo momento, se necessario.

I criteri di ripetizione dei tentativi consentono di risolvere diversi tipi di errori che sono spesso temporanei o semi-temporanei. Vale a dire, gli errori temporanei e spesso vanno via se il messaggio viene semplicemente rielaborato dopo un breve ritardo. Ad esempio, errori di rete, blocchi del database e interruzioni dell'API di terze parti.

Una volta che un messaggio si trova nella coda degli errori, è possibile esaminare i dettagli del messaggio nello strumento preferito, quindi decidere cosa fare con esso. Ad esempio, usando ServicePulse, uno strumento di monitoraggio di Particolare Software, è possibile visualizzare i dettagli del messaggio e il motivo dell'errore:

Immagine che mostra ServicePulse, from Particular Software

Dopo aver esaminato i dettagli, è possibile inviare di nuovo il messaggio alla relativa coda originale per l'elaborazione. È anche possibile modificare il messaggio prima di farlo. Se nella coda di errori sono presenti più messaggi, che hanno avuto esito negativo per lo stesso motivo, possono essere tutti inviati alle destinazioni originali come batch.

A questo punto, è possibile capire dove distribuire la soluzione in Azure.

Dove ospitare i servizi in Azure

In questo esempio gli endpoint Mittente e Ricevitore sono configurati per l'esecuzione come applicazioni console. Possono anche essere ospitati in vari servizi di Azure, tra cui Funzioni di Azure, Servizi app di Azure, Istanze di Azure Container, Servizi Azure Kubernetes e macchine virtuali di Azure. Ad esempio, ecco come configurare l'endpoint mittente per l'esecuzione come funzione di Azure:

[assembly: NServiceBusTriggerFunction("Sender")]
public class Program
{
    public static async Task Main()
    {
        var host = new HostBuilder()
            .ConfigureFunctionsWorkerDefaults()
            .UseNServiceBus(configuration =>
            {
                configuration.Routing().RouteToEndpoint(typeof(Ping), "Receiver");
            })
            .Build();

        await host.RunAsync();
    }
}

Per altre informazioni sull'uso di NServiceBus con Funzioni, vedere Funzioni di Azure con il bus di servizio di Azure nella documentazione di NServiceBus.

Passaggi successivi

Per altre informazioni sull'uso di NServiceBus con i servizi di Azure, vedere gli articoli seguenti: