Skapa meddelandedrivna affärsprogram med NServiceBus och Azure Service Bus
NServiceBus är ett kommersiellt meddelanderamverk 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 granskar alternativ för att vara värd för dessa tjänster i Azure.
Kommentar
Koden för den här självstudien finns på webbplatsen För särskilda programvarudokument.
Förutsättningar
Exemplet förutsätter att du har skapat ett Azure Service Bus-namnområde.
Viktigt!
NServiceBus kräver minst standardnivån. Nivån Basic fungerar inte.
Ladda ned och förbereda lösningen
Ladda ned koden från webbplatsen För särskilda programvarudokument. 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
- Delat: 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 särskild programvara, visar meddelandeflödet:
Öppna
SendReceiveWithNservicebus.sln
i din favoritkodredigerare (till exempel Visual Studio 2022).Öppna
appsettings.json
i både mottagar- och avsändarprojekten och angeAzureServiceBusConnectionString
till niska veze för ditt Azure Service Bus-namnområde.- Detta finns i Azure-portalen under Service Bus-namnområdesinställningar>>Principer för delad åtkomst>RootManageSharedAccessKey>Primär anslutningssträng .
- Har
AzureServiceBusTransport
också en konstruktor som accepterar ett namnområde och tokenautentiseringsuppgifter, vilket i en produktionsmiljö blir säkrare, men i den här självstudiekursen används den delade åtkomstnyckeln niska veze.
Definiera kontrakten för delat meddelande
I det delade klassbiblioteket definierar du kontrakten 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 har tagits emot.
Och Ping
Pong
ä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 Program.cs
för konfigurerar 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 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();
Det finns mycket att packa upp här så vi granskar 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 niska veze 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-namnområdet när slutpunkten startas, vilket skapar de köer som krävs 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++ });;
logger.LogInformation($"Message #{round}");
await Task.Delay(1_000, stoppingToken);
}
}
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 meddelandetransport) 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å sin 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.
Ping-meddelandehanteraren
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 att återställa från 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 Ping
ska 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år PingHandler
är enkel: när ett Ping
meddelande tas emot loggar du meddelandeinformationen och svarar tillbaka till avsändaren med ett nytt Pong
meddelande, som sedan hanteras i avsändarens PongHandler
.
Kommentar
I avsändarens konfiguration har du angett att Ping
meddelanden ska dirigeras till mottagaren. NServiceBus lägger till metadata i meddelandena 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.
Med både avsändaren och mottagaren 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:
- Högerklicka på lösningen i Istraživač rešenja
- Välj "Ange startprojekt..."
- Välj Flera startprojekt
- 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öksprincip: 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:
- Öppna
Program.cs
i avsändarprojektet .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 avkommentar 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 PingHandler
misslyckas alla våra meddelanden. Du kan se att återförsöksprincipen startar för dessa meddelanden. Första gången ett meddelande misslyckas provas det omedelbart upp till tre gånger:
Naturligtvis fortsätter det att misslyckas, så när de tre omedelbara återförsöken används startar principen för fördröjt återförsök och meddelandet fördröjs i 5 sekunder:
När de fem sekunderna har passerat görs ett nytt försök med meddelandet ytterligare tre gånger (d.s. 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
det 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
.
Begreppet centraliserad felkö skiljer sig från mekanismen med obeställbara bokstäver i Azure Service Bus, som har en kö med obeställbara bokstäver 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 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 helt enkelt 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 ska göra med det. Med hjälp av ServicePulse, ett övervakningsverktyg efter viss programvara, kan vi till exempel visa meddelandeinformationen och orsaken till felet:
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 vi ska distribuera vår lösning i Azure.
Var du ska vara värd för tjänsterna i Azure
I det här exemplet är slutpunkterna Avsändare och Mottagare konfigurerade 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ändarens slutpunkt konfigureras för att köras som en Azure-funktion:
[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();
}
}
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: