Skapa meddelandedrivna affärsprogram med NServiceBus och Azure Service Bus

NServiceBus är ett ramverk för kommersiella meddelanden som tillhandahålls av Särskild programvara. Den bygger på Azure Service Bus och hjälper utvecklare att fokusera på affärslogik genom att abstrahera infrastrukturproblem. I den här guiden skapar vi en lösning som utbyter meddelanden mellan två tjänster. Vi visar också hur du automatiskt försöker igen med misslyckade meddelanden och granska alternativ för att vara värd för dessa tjänster i Azure.

Anteckning

Koden för den här självstudien finns på webbplatsen För särskilda programdokument.

Förutsättningar

Exemplet förutsätter att du har skapat ett Azure Service Bus namnområde.

Viktigt

NServiceBus kräver minst standardnivån. Basic-nivån fungerar inte.

Ladda ned och förbereda lösningen

  1. Ladda ned koden från webbplatsen För särskilda programdokument. Lösningen SendReceiveWithNservicebus.sln består av tre projekt:

    • Avsändare: ett konsolprogram som skickar meddelanden
    • Mottagare: ett konsolprogram som tar emot meddelanden från avsändaren och svarar tillbaka
    • Delad: ett klassbibliotek som innehåller meddelandekontrakten som delas mellan avsändaren och mottagaren

    Följande diagram, som genereras av ServiceInsight, ett visualiserings- och felsökningsverktyg från viss programvara, visar meddelandeflödet:

    Bild som visar sekvensdiagrammet

  2. Öppna SendReceiveWithNservicebus.sln i din favoritkodredigerare (till exempel Visual Studio 2019).

  3. Öppna appsettings.json i både mottagar- och avsändarprojekten och ange AzureServiceBusConnectionString anslutningssträngen för Azure Service Bus namnområde.

Definiera kontrakten för delade meddelanden

I det delade klassbiblioteket definierar du de kontrakt som används för att skicka våra meddelanden. Den innehåller en referens till NServiceBus NuGet-paketet som innehåller gränssnitt som du kan använda för att identifiera våra meddelanden. Gränssnitten krävs inte, men de ger oss lite extra validering från NServiceBus och tillåter att koden självdokumenteras.

Först ska vi granska Ping.cs klassen

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

Klassen Ping definierar ett meddelande som avsändaren skickar till mottagaren. Det är en enkel C#-klass som implementerar NServiceBus.ICommand, ett gränssnitt från NServiceBus-paketet. Det här meddelandet är en signal till läsaren och till NServiceBus att det är ett kommando, även om det finns andra sätt att identifiera meddelanden utan att använda gränssnitt.

Den andra meddelandeklassen i delade projekt är Pong.cs:

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

Pong är också ett enkelt C#-objekt även om det här implementerar NServiceBus.IMessage. Gränssnittet IMessage representerar ett allmänt meddelande som varken är ett kommando eller en händelse och som ofta används för svar. I vårt exempel är det ett svar som mottagaren skickar tillbaka till avsändaren för att indikera att ett meddelande togs emot.

Och PingPong är de två meddelandetyperna som du ska använda. Nästa steg är att konfigurera avsändaren att använda Azure Service Bus och skicka ett Ping meddelande.

Konfigurera avsändaren

Avsändaren är en slutpunkt som skickar vårt Ping meddelande. Här konfigurerar du avsändaren så att den använder Azure Service Bus som transportmekanism och skapar sedan en Ping instans och skickar den.

Main I metoden för Program.cskonfigurerar du avsändarslutpunkten:

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();

Det finns mycket att packa upp här så vi går igenom det steg för steg.

Konfigurera en värd för slutpunkten

Värd- och loggning konfigureras med standardalternativ för Microsoft Generic Host. För tillfället är slutpunkten konfigurerad att köras som ett konsolprogram, men den kan ändras så att den körs i Azure Functions med minimala ändringar, vilket vi kommer att diskutera senare i den här artikeln.

Konfigurera NServiceBus-slutpunkten

Sedan uppmanar du värden att använda NServiceBus med .UseNServiceBus(…) tilläggsmetoden. Metoden tar en återanropsfunktion som returnerar en slutpunkt som startas när värden körs.

I slutpunktskonfigurationen anger AzureServiceBus du för vår transport och tillhandahåller en anslutningssträng från appsettings.json. Därefter konfigurerar du routningen så att meddelanden av typen Ping skickas till en slutpunkt med namnet "Mottagare". Det gör att NServiceBus kan automatisera processen med att skicka meddelandet till målet utan att mottagarens adress krävs.

Anropet till EnableInstallers konfigurerar vår topologi i Azure Service Bus namnrymd när slutpunkten startas, vilket skapar nödvändiga köer där det behövs. I produktionsmiljöer är driftskript ett annat alternativ för att skapa topologin.

Konfigurera bakgrundstjänsten för att skicka meddelanden

Den sista delen av avsändaren är SenderWorker, en bakgrundstjänst som är konfigurerad för att skicka ett Ping meddelande varje sekund.

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
        }
    }
}

Den IMessageSession som används i ExecuteAsync matas in i SenderWorker och gör att vi kan skicka meddelanden med NServiceBus utanför en meddelandehanterare. Den routning som du konfigurerade i Sender anger målet för meddelandena Ping . Den behåller topologin för systemet (vilka meddelanden dirigeras till vilka adresser) som ett separat problem från affärskoden.

Avsändarprogrammet innehåller också en PongHandler. Du kommer tillbaka till det när vi har diskuterat mottagaren, vilket vi ska göra härnäst.

Konfigurera mottagaren

Mottagaren är en slutpunkt som lyssnar efter ett Ping meddelande, loggar när ett meddelande tas emot och svarar tillbaka till avsändaren. I det här avsnittet ska vi snabbt granska slutpunktskonfigurationen, som liknar avsändaren, och sedan rikta vår uppmärksamhet mot meddelandehanteraren.

Precis som avsändaren konfigurerar du mottagaren som ett konsolprogram med hjälp av Microsoft Generic Host. Den använder samma loggnings- och slutpunktskonfiguration (med Azure Service Bus som meddelandetransporten) men med ett annat namn för att skilja den från avsändaren:

var endpointConfiguration = new EndpointConfiguration("Receiver");

Eftersom den här slutpunkten bara svarar på dess upphovsman och inte startar nya konversationer krävs ingen routningskonfiguration. Den behöver inte heller någon bakgrundsarbetare som avsändaren gör, eftersom den bara svarar när den tar emot ett meddelande.

Pingmeddelandehanteraren

Mottagarprojektet innehåller en meddelandehanterare med namnet 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);
    }
}

Nu ska vi ignorera den kommenterade koden. Vi återkommer till det senare när vi pratar om återställning efter fel.

Klassen implementerar IHandleMessages<Ping>, som definierar en metod: Handle. Det här gränssnittet talar om för NServiceBus att när slutpunkten tar emot ett meddelande av typen Pingska det bearbetas av Handle metoden i den här hanteraren. Metoden Handle tar själva meddelandet som en parameter och en IMessageHandlerContext, som tillåter ytterligare meddelandeåtgärder, till exempel att svara, skicka kommandon eller publicera händelser.

Vårt PingHandler är enkelt: när ett Ping meddelande tas emot loggar du meddelandeinformationen och svarar tillbaka till avsändaren med ett nytt Pong meddelande.

Anteckning

I avsändarens konfiguration har du angett att Ping meddelanden ska dirigeras till mottagaren. NServiceBus lägger till metadata i meddelanden som bland annat anger meddelandets ursprung. Det är därför du inte behöver ange några routningsdata för Pong svarsmeddelandet. De dirigeras automatiskt tillbaka till dess ursprung: avsändaren.

När både avsändaren och mottagaren är korrekt konfigurerade kan du nu köra lösningen.

Kör lösningen

För att starta lösningen måste du köra både avsändaren och mottagaren. Om du använder Visual Studio Code startar du konfigurationen "Felsök alla". Om du använder Visual Studio konfigurerar du lösningen för att starta både avsändar- och mottagarprojekten:

  1. Högerklicka på lösningen i Solution Explorer
  2. Välj "Ange startprojekt..."
  3. Välj Flera startprojekt
  4. För både avsändaren och mottagaren väljer du "Start" i listrutan

Starta lösningen. Två konsolprogram visas, ett för avsändaren och ett för mottagaren.

Observera att ett Ping meddelande skickas varje sekund i avsändaren tack vare bakgrundsjobbet SenderWorker . Mottagaren visar information om varje Ping meddelande som den tar emot och avsändaren loggar information om varje Pong meddelande som den tar emot som svar.

Nu när du har allt som fungerar, låt oss bryta det.

Motståndskraft i praktiken

Fel är ett faktum i programvarusystem. Det är oundvikligt att koden misslyckas och den kan göra det av olika skäl, till exempel nätverksfel, databaslås, ändringar i ett API från tredje part och vanliga gamla kodfel.

NServiceBus har robusta återställningsfunktioner för hantering av fel. När en meddelandehanterare misslyckas görs automatiskt nya försök baserat på en fördefinierad princip. Det finns två typer av återförsöksprinciper: omedelbara återförsök och fördröjda återförsök. Det bästa sättet att beskriva hur de fungerar är att se dem i praktiken. Nu ska vi lägga till en återförsöksprincip i mottagarslutpunkten:

  1. Öppna Program.cs i avsändarprojektet
  2. .EnableInstallers Lägg till följande kod efter raden:
endpointConfiguration.SendFailedMessagesTo("error");
var recoverability = endpointConfiguration.Recoverability();
recoverability.Immediate(
    immediate =>
    {
        immediate.NumberOfRetries(3);
    });
recoverability.Delayed(
    delayed =>
    {
        delayed.NumberOfRetries(2);
        delayed.TimeIncrease(TimeSpan.FromSeconds(5));
    });

Innan vi diskuterar hur den här principen fungerar ska vi se hur den fungerar. Innan du testar återställningsprincipen måste du simulera ett fel. PingHandler Öppna koden i mottagarprojektet och ta bort kommentaren på den här raden:

throw new Exception("BOOM");

Nu, när mottagaren hanterar ett Ping meddelande, kommer det att misslyckas. Starta lösningen igen och låt oss se vad som händer i mottagaren.

Med vår mindre tillförlitliga PingHandlermisslyckas alla våra meddelanden. Du kan se att återförsöksprincipen startar för dessa meddelanden. Första gången ett meddelande misslyckas försöker det omedelbart igen upp till tre gånger:

Bild som visar principen för omedelbar återförsök som försöker skicka meddelanden igen upp till 3 gånger

Naturligtvis fortsätter det att misslyckas, så när de tre omedelbara återförsöken används startar den fördröjda återförsöksprincipen och meddelandet fördröjs i 5 sekunder:

Bild som visar den fördröjda återförsöksprincipen som fördröjer meddelandena i steg om 5 sekunder innan en ny omgång omedelbara återförsök görs

När dessa 5 sekunder har passerat görs ett nytt försök att köra meddelandet igen tre gånger (det vill sa en annan iteration av principen för omedelbart återförsök). Dessa misslyckas också och NServiceBus fördröjer meddelandet igen, den här gången i 10 sekunder, innan du försöker igen.

Om PingHandler fortfarande inte lyckas efter att du har kört igenom den fullständiga återförsöksprincipen placeras meddelandet i en central felkö med namnet error, enligt definitionen i anropet till SendFailedMessagesTo.

Bild som visar det misslyckade meddelandet

Konceptet med en centraliserad felkö skiljer sig från mekanismen med obeställbara bokstäver i Azure Service Bus, som har en kö med obeställbara meddelanden för varje bearbetningskö. Med NServiceBus fungerar köerna med obeställbara meddelanden i Azure Service Bus som sanna giftmeddelandeköer, medan meddelanden som hamnar i den centraliserade felkön kan bearbetas på nytt vid ett senare tillfälle, om det behövs.

Återförsöksprincipen hjälper till att åtgärda flera typer av fel som ofta är tillfälliga eller delvis tillfälliga. Det innebär att fel som är tillfälliga och ofta försvinner om meddelandet bara bearbetas på nytt efter en kort fördröjning. Exempel är nätverksfel, databaslås och API-avbrott från tredje part.

När ett meddelande finns i felkön kan du granska meddelandeinformationen i det verktyg du väljer och sedan bestämma vad du vill göra med det. Med hjälp av ServicePulse, ett övervakningsverktyg från Particular Software, kan vi till exempel visa meddelandeinformationen och orsaken till felet:

Bild som visar ServicePulse, från särskild programvara

När du har granskat informationen kan du skicka tillbaka meddelandet till den ursprungliga kön för bearbetning. Du kan också redigera meddelandet innan du gör det. Om det finns flera meddelanden i felkön, som misslyckades av samma anledning, kan alla skickas tillbaka till sina ursprungliga mål som en batch.

Nu är det dags att ta reda på var lösningen ska distribueras i Azure.

Var tjänsterna ska hanteras i Azure

I det här exemplet konfigureras slutpunkterna Avsändare och Mottagare för att köras som konsolprogram. De kan också finnas i olika Azure-tjänster, inklusive Azure Functions, Azure App Services, Azure Container Instances, Azure Kubernetes Services och virtuella Azure-datorer. Så här kan till exempel avsändarslutpunkten konfigureras att köras som en Azure-funktion:

[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;
        });
    }
}

Mer information om hur du använder NServiceBus med Functions finns i Azure Functions med Azure Service Bus i NServiceBus-dokumentationen.

Nästa steg

Mer information om hur du använder NServiceBus med Azure-tjänster finns i följande artiklar: