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

NServiceBus è un framework di messaggistica commerciale fornito da Software specifico. Si basa su 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 Della documentazione software specifica.

Prerequisiti

L'esempio presuppone che sia stato creato uno spazio dei nomi 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: 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 da Software specifico, mostra il flusso del messaggio:

    Immagine che mostra il diagramma di sequenza

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

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

Definire i contratti di messaggio condivisi

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

Prima di tutto si esaminerà la Ping.cs classe

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

La Ping classe definisce un messaggio inviato dal mittente 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 si tratta di 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 restituisce al mittente per indicare che è stato ricevuto un messaggio.

e PingPong sono i due tipi di messaggio che verranno usati. Il passaggio successivo consiste nel configurare il mittente in modo da usare bus di servizio di Azure e per inviare un Ping messaggio.

Configurare il mittente

Il mittente è un endpoint che invia il Ping messaggio. In questo caso, si configura il mittente in modo da usare bus di servizio di Azure come meccanismo di trasporto, quindi costruire un'istanza Ping e inviarla.

Main Nel metodo di Program.csconfigurare l'endpoint Sender:

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 transport = endpointConfiguration.UseTransport<AzureServiceBusTransport>();
        var connectionString = context.Configuration.GetConnectionString("AzureServiceBusConnectionString");
        transport.ConnectionString(connectionString);

        transport.Routing().RouteToEndpoint(typeof(Ping), "Receiver");

        endpointConfiguration.EnableInstallers();
        endpointConfiguration.AuditProcessedMessagesTo("audit");

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

await host.RunAsync();

C'è molto da decomprimere 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 .UseNServiceBus(…) metodo di estensione. 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 "Receiver". Consente a NServiceBus di automatizzare il processo di invio del messaggio alla destinazione senza richiedere l'indirizzo del ricevitore.

La chiamata a EnableInstallers configurerà la topologia nello spazio dei nomi 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 componente finale del mittente è SenderWorker, un servizio in background configurato per inviare un Ping messaggio 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++ })
                    .ConfigureAwait(false);

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

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

L'oggetto 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 Ping messaggi. Mantiene la topologia del sistema (i messaggi indirizzati agli indirizzi) come un problema separato dal codice aziendale.

L'applicazione Sender contiene anche un oggetto PongHandler. Si tornerà ad esso dopo aver discusso il ricevitore, che faremo successivamente.

Configurare il ricevitore

Il ricevitore è un endpoint in ascolto di un Ping messaggio, 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 messaggi.

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

var endpointConfiguration = new EndpointConfiguration("Receiver");

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

Gestore messaggi Ping

Il progetto Receiver 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à ad 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 Handle metodo in questo gestore. Il Handle metodo accetta il messaggio stesso come parametro e , IMessageHandlerContextche consente ulteriori operazioni di messaggistica, ad esempio la risposta, l'invio di comandi o la pubblicazione di eventi.

È PingHandler semplice: quando viene ricevuto un Ping messaggio, registrare i dettagli del messaggio e rispondere al mittente con un nuovo Pong messaggio.

Nota

Nella configurazione del mittente è stato specificato che Ping i messaggi devono essere indirizzati al ricevitore. NServiceBus aggiunge metadati ai messaggi che indicano, tra l'altro, l'origine del messaggio. Questo è il motivo per cui non è necessario specificare dati di routing per il Pong messaggio di risposta. 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 Sender e Receiver:

  1. Fare clic con il pulsante destro del mouse sulla soluzione in Esplora soluzioni
  2. Selezionare "Imposta progetti di avvio..."
  3. Selezionare Più progetti di avvio
  4. Per Sender e Receiver (Mittente) e Receiver (Ricevitore), selezionare "Start" nell'elenco a discesa

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

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

Ora che tutto funziona, interrompiamolo.

Resilienza in azione

Gli errori sono un fatto di vita nei sistemi software. È inevitabile che il codice avrà esito negativo e possa farlo per diversi motivi, ad esempio errori di rete, blocchi di database, modifiche in un'API di terze parti e semplici errori di codifica precedenti.

NServiceBus offre funzionalità di ripristino affidabili per la gestione degli errori. Quando un gestore messaggi ha esito negativo, i messaggi vengono riprovati 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 mittente
  2. Dopo la .EnableInstallers riga 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, vediamolo in azione. Prima di testare i criteri di ripristinabilità, è necessario simulare un errore. Aprire il codice nel progetto Ricevitore e annullare ilcommentazione PingHandler seguente:

throw new Exception("BOOM");

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

Con il nostro meno affidabile PingHandler, tutti i nostri 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 riprovato 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 in modo che i tre tentativi immediati vengano usati, i criteri di ripetizione ritardati vengono avviati e il messaggio viene ritardato per 5 secondi:

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

Dopo aver superato questi 5 secondi, il messaggio viene riprovato un'altra tre volte ,ovvero un'altra iterazione dei criteri di ripetizione immediata. Questi errori avranno esito negativo e NServiceBus ritarderà di nuovo il messaggio, questa volta per 10 secondi, prima di riprovare.

Se PingHandler non riesce ancora dopo aver eseguito il criterio di ripetizione dei tentativi completo, il messaggio viene inserito in una coda di errori centralizzata, denominata , come errordefinito dalla chiamata a SendFailedMessagesTo.

Immagine che mostra il messaggio non riuscito

Il concetto di una coda di errori centralizzata differisce dal meccanismo di messaggi non recapitabili in bus di servizio di Azure, che dispone di una coda di messaggi non recapitabili per ogni coda di elaborazione. Con NServiceBus, le code di messaggi non recapitabili in 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 riprocessati in un secondo momento, se necessario.

Il criterio di ripetizione dei tentativi consente di risolvere diversi tipi di errori che sono spesso temporanei o semi-temporanei in natura. Ovvero, gli errori temporanei e spesso vanno via se il messaggio viene semplicemente riprocessato dopo un breve ritardo. Alcuni esempi includono errori di rete, blocchi di database e interruzioni dell'API di terze parti.

Una volta che un messaggio si trova nella coda di errori, è possibile esaminare i dettagli del messaggio nello strumento scelto, 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, da Particolare software

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

Successivamente, è necessario 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 Azure, Istanze di Azure Container, servizi Azure Kubernetes e macchine virtuali di Azure. Ecco ad esempio come è possibile configurare l'endpoint mittente per l'esecuzione come funzione di Azure:

[assembly: FunctionsStartup(typeof(Startup))]
[assembly: NServiceBusEndpointName("Sender")]

public class Startup : FunctionsStartup
{
    public override void Configure(IFunctionsHostBuilder builder)
    {
        builder.UseNServiceBus(() =>
        {
            var configuration = new ServiceBusTriggeredEndpointConfiguration("Sender");
            var transport = configuration.AdvancedConfiguration.Transport;
            transport.Routing().RouteToEndpoint(typeof(Ping), "Receiver");

            return configuration;
        });
    }
}

Per altre informazioni sull'uso di NServiceBus con Funzioni, vedere Funzioni di Azure con 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: