Crie aplicativos de negócios orientados por mensagem com o NServiceBus e o Azure Service Bus

NServiceBus é uma estrutura de mensagens comerciais fornecida pela Particular Software. Ele foi criado com base no Barramento de Serviço do Azure e ajuda os desenvolvedores a se concentrarem na lógica de negócios, abstraindo preocupações com a infraestrutura. Neste guia, criaremos uma solução que troca mensagens entre dois serviços. Também mostraremos como repetir automaticamente mensagens com falha e revisar as opções para hospedar esses serviços no Azure.

Nota

O código para este tutorial está disponível no site Particular Software Docs.

Pré-requisitos

O exemplo pressupõe que você criou um namespace do Barramento de Serviço do Azure.

Importante

O NServiceBus requer pelo menos a camada Standard. A camada Básica não funcionará.

Faça o download e prepare a solução

  1. Faça o download do código do site Particular Software Docs. A solução SendReceiveWithNservicebus.sln consiste em três projetos:

    • Remetente: um aplicativo de console que envia mensagens
    • Recetor: um aplicativo de console que recebe mensagens do remetente e responde de volta
    • Compartilhada: uma biblioteca de classes que contém os contratos de mensagens compartilhados entre o remetente e o recetor

    O diagrama a seguir, gerado pelo ServiceInsight, uma ferramenta de visualização e depuração da Particular Software, mostra o fluxo de mensagens:

    Imagem mostrando o diagrama de sequência

  2. Abra SendReceiveWithNservicebus.sln no seu editor de código favorito (por exemplo, Visual Studio 2022).

  3. Abra appsettings.json nos projetos Receiver e Sender e defina AzureServiceBusConnectionString como a cadeia de conexão para seu namespace do Barramento de Serviço do Azure.

    • Isso pode ser encontrado no portal do Azure em Configurações>de namespace>do Service Bus Políticas de acesso>compartilhado RootManageSharedAccessKey>Cadeia de conexão primária .
    • O AzureServiceBusTransport também tem um construtor que aceita uma credencial de namespace e token, que em um ambiente de produção será mais seguro, no entanto, para os fins deste tutorial, a cadeia de conexão de chave de acesso compartilhado será usada.

Definir os contratos de mensagens compartilhadas

A biblioteca de classes compartilhadas é onde você define os contratos usados para enviar nossas mensagens. Ele inclui uma referência ao NServiceBus pacote NuGet, que contém interfaces que você pode usar para identificar nossas mensagens. As interfaces não são necessárias, mas nos dão alguma validação extra do NServiceBus e permitem que o código seja autodocumentado.

Primeiro vamos rever a Ping.cs aula

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

A Ping classe define uma mensagem que o remetente envia para o destinatário. É uma classe C# simples que implementa NServiceBus.ICommand, uma interface do pacote NServiceBus. Esta mensagem é um sinal para o leitor e para o NServiceBus de que é um comando, embora existam outras maneiras de identificar mensagens sem usar interfaces.

A outra classe de mensagem nos projetos compartilhados é Pong.cs:

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

Pong também é um objeto C# simples, embora este implemente NServiceBus.IMessage. A IMessage interface representa uma mensagem genérica que não é um comando nem um evento, e é comumente usada para respostas. Em nosso exemplo, é uma resposta que o destinatário envia de volta ao remetente para indicar que uma mensagem foi recebida.

O Ping e Pong são os dois tipos de mensagem que você usará. A próxima etapa é configurar o Remetente para usar o Barramento de Serviço do Azure e enviar uma Ping mensagem.

Configurar o remetente

O remetente é um ponto de extremidade que envia nossa Ping mensagem. Aqui, você configura o Sender para usar o Barramento de Serviço do Azure como o mecanismo de transporte, constrói uma Ping instância e a envia.

Main No método de , você configura o ponto de Program.csextremidade 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 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();

Há muito para descompactar aqui, então vamos analisá-lo passo a passo.

Configurar um host para o ponto de extremidade

A hospedagem e o registro em log são configurados usando as opções padrão do Host Genérico da Microsoft. Por enquanto, o ponto de extremidade está configurado para ser executado como um aplicativo de console, mas pode ser modificado para ser executado no Azure Functions com alterações mínimas, que discutiremos mais adiante neste artigo.

Configurar o ponto de extremidade NServiceBus

Em seguida, você diz ao host para usar NServiceBus com o .UseNServiceBus(…) método de extensão. O método usa uma função de retorno de chamada que retorna um ponto de extremidade que será iniciado quando o host for executado.

Na configuração do ponto final, você especifica AzureServiceBus para o nosso transporte, fornecendo uma cadeia de conexão de appsettings.json. Em seguida, você configurará o roteamento para que mensagens do tipo Ping sejam enviadas para um ponto de extremidade chamado "Recetor". Ele permite que o NServiceBus automatize o processo de envio da mensagem para o destino sem exigir o endereço do destinatário.

A chamada para EnableInstallers configurará nossa topologia no namespace do Barramento de Serviço do Azure quando o ponto de extremidade for iniciado, criando as filas necessárias quando necessário. Em ambientes de produção, o script operacional é outra opção para criar a topologia.

Configurar o serviço em segundo plano para enviar mensagens

A parte final do remetente é SenderWorker, um serviço em segundo plano configurado para enviar uma Ping mensagem a cada segundo.

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

O IMessageSession usado em é injetado e ExecuteAsync SenderWorker nos permite enviar mensagens usando NServiceBus fora de um manipulador de mensagens. O roteamento configurado em Sender especifica o Ping destino das mensagens. Ele mantém a topologia do sistema (quais mensagens são roteadas para quais endereços) como uma preocupação separada do código comercial.

O aplicativo Sender também contém um PongHandlerarquivo . Você voltará a ele depois de discutirmos o Recetor, o que faremos a seguir.

Configurar o recetor

O recetor é um ponto de extremidade que escuta uma Ping mensagem, registra quando uma mensagem é recebida e responde ao remetente. Nesta seção, analisaremos rapidamente a configuração do ponto de extremidade, que é semelhante ao Remetente, e voltaremos nossa atenção para o manipulador de mensagens.

Como o remetente, configure o recetor como um aplicativo de console usando o Microsoft Generic Host. Ele usa a mesma configuração de log e ponto de extremidade (com o Barramento de Serviço do Azure como o transporte de mensagens), mas com um nome diferente, para distingui-lo do remetente:

var endpointConfiguration = new EndpointConfiguration("Receiver");

Como esse ponto de extremidade responde apenas ao seu originador e não inicia novas conversas, nenhuma configuração de roteamento é necessária. Ele também não precisa de um trabalhador em segundo plano como o remetente, uma vez que só responde quando recebe uma mensagem.

O manipulador de mensagens Ping

O projeto Receiver contém um manipulador de mensagens chamado 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);
    }
}

Vamos ignorar o código comentado por enquanto; Voltaremos a isso mais tarde, quando falarmos sobre a recuperação de um fracasso.

A classe implementa IHandleMessages<Ping>, que define um método: Handle. Essa interface informa ao NServiceBus que, quando o ponto de extremidade recebe uma mensagem do tipo Ping, ela deve ser processada Handle pelo método neste manipulador. O Handle método usa a própria mensagem como um parâmetro e um IMessageHandlerContext, que permite outras operações de mensagens, como responder, enviar comandos ou publicar eventos.

O nosso PingHandler é simples: quando uma Ping mensagem é recebida, registre os detalhes da mensagem e responda ao remetente com uma nova Pong mensagem, que é posteriormente tratada no .PongHandler

Nota

Na configuração do remetente, você especificou que Ping as mensagens devem ser roteadas para o destinatário. O NServiceBus adiciona metadados às mensagens indicando, entre outras coisas, a origem da mensagem. É por isso que você não precisa especificar nenhum dado de roteamento para a Pong mensagem de resposta, ela é automaticamente roteada de volta à sua origem: o Remetente.

Com o Remetente e o Recetor configurados corretamente, agora você pode executar a solução.

Executar a solução

Para iniciar a solução, você precisa executar o remetente e o recetor. Se você estiver usando o Visual Studio Code, inicie a configuração "Depurar tudo". Se você estiver usando o Visual Studio, configure a solução para iniciar os projetos Sender e Receiver:

  1. Clique com o botão direito do mouse na solução no Gerenciador de Soluções
  2. Selecione "Definir projetos de inicialização..."
  3. Selecione Vários projetos de inicialização
  4. Para o remetente e o destinatário, selecione "Iniciar" na lista suspensa

Inicie a solução. Aparecerão duas aplicações de consola, uma para o Remetente e outra para o Recetor.

No Remetente, observe que uma Ping mensagem é enviada a cada segundo, graças ao trabalho em SenderWorker segundo plano. O Destinatário exibe os detalhes de cada Ping mensagem que recebe e o Remetente registra os detalhes de cada Pong mensagem que recebe em resposta.

Agora que você tem tudo funcionando, vamos quebrá-lo.

Resiliência em ação

Os erros são uma realidade nos sistemas de software. É inevitável que o código falhe e pode fazê-lo por vários motivos, como falhas de rede, bloqueios de banco de dados, alterações em uma API de terceiros e erros de codificação antigos.

O NServiceBus tem recursos robustos de capacidade de recuperação para lidar com falhas. Quando um manipulador de mensagens falha, as mensagens são automaticamente repetidas com base em uma política predefinida. Existem dois tipos de política de retentativas: repetições imediatas e tentativas atrasadas. A melhor maneira de descrever como eles funcionam é vê-los em ação. Vamos adicionar uma política de repetição ao nosso endpoint do Receiver:

  1. Abrir Program.cs no projeto Sender
  2. Após a .EnableInstallers linha, adicione o seguinte código:
endpointConfiguration.SendFailedMessagesTo("error");
var recoverability = endpointConfiguration.Recoverability();
recoverability.Immediate(
    immediate =>
    {
        immediate.NumberOfRetries(3);
    });
recoverability.Delayed(
    delayed =>
    {
        delayed.NumberOfRetries(2);
        delayed.TimeIncrease(TimeSpan.FromSeconds(5));
    });

Antes de discutirmos como essa política funciona, vamos vê-la em ação. Antes de testar a política de recuperabilidade, você precisa simular um erro. Abra o PingHandler código no projeto Receiver e descomente esta linha:

throw new Exception("BOOM");

Agora, quando o recetor lida com uma Ping mensagem, ela falhará. Inicie a solução novamente e vamos ver o que acontece no recetor.

Com a nossa menos fiável PingHandler, todas as nossas mensagens falham. Você pode ver a política de repetição entrando em ação para essas mensagens. A primeira vez que uma mensagem falha, ela é imediatamente repetida até três vezes:

Imagem mostrando a política de repetição imediata que repete mensagens até 3 vezes

É claro que ele continuará a falhar, então quando as três tentativas imediatas forem esgotadas, a política de repetição atrasada entra em ação e a mensagem é atrasada por 5 segundos:

Imagem mostrando a política de repetição atrasada que atrasa as mensagens em incrementos de 5 segundos antes de tentar outra rodada de tentativas imediatas

Após esses 5 segundos, a mensagem é repetida novamente mais três vezes (ou seja, outra iteração da política de repetição imediata). Eles também falharão e o NServiceBus atrasará a mensagem novamente, desta vez por 10 segundos, antes de tentar novamente.

Se PingHandler ainda não tiver êxito depois de executar a política de repetição completa, a mensagem será colocada em uma fila de erros centralizada , chamada error, conforme definido pela chamada para SendFailedMessagesTo.

Imagem mostrando a mensagem com falha

O conceito de uma fila de erros centralizada difere do mecanismo de letras mortas no Barramento de Serviço do Azure, que tem uma fila de letras mortas para cada fila de processamento. Com o NServiceBus, as filas de mensagens mortas no Barramento de Serviço do Azure atuam como verdadeiras filas de mensagens suspeitas, enquanto as mensagens que acabam na fila de erros centralizada podem ser reprocessadas posteriormente, se necessário.

A política de repetição ajuda a resolver vários tipos de erros que geralmente são transitórios ou semitransitórios por natureza. Ou seja, erros que são temporários e muitas vezes desaparecem se a mensagem for simplesmente reprocessada após um pequeno atraso. Os exemplos incluem falhas de rede, bloqueios de banco de dados e interrupções de API de terceiros.

Quando uma mensagem estiver na fila de erros, você poderá examinar os detalhes da mensagem na ferramenta de sua escolha e decidir o que fazer com ela. Por exemplo, usando o ServicePulse, uma ferramenta de monitoramento da Particular Software, podemos visualizar os detalhes da mensagem e o motivo da falha:

Imagem mostrando ServicePulse, de Particular Software

Depois de examinar os detalhes, você pode enviar a mensagem de volta para sua fila original para processamento. Você também pode editar a mensagem antes de fazê-lo. Se houver várias mensagens na fila de erros, que falharam pelo mesmo motivo, todas elas poderão ser enviadas de volta aos seus destinos originais como um lote.

Em seguida, é hora de descobrir onde implantar nossa solução no Azure.

Onde hospedar os serviços no Azure

Neste exemplo, os pontos de extremidade Remetente e Recetor são configurados para serem executados como aplicativos de console. Eles também podem ser hospedados em vários serviços do Azure, incluindo Azure Functions, Serviços de Aplicativo do Azure, Instâncias de Contêiner do Azure, Serviços Kubernetes do Azure e VMs do Azure. Por exemplo, veja como o ponto de extremidade Sender pode ser configurado para ser executado como uma Função do 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();
    }
}

Para obter mais informações sobre como usar o NServiceBus com o Functions, consulte Azure Functions with Azure Service Bus na documentação do NServiceBus.

Próximos passos

Para obter mais informações sobre como usar o NServiceBus com serviços do Azure, consulte os seguintes artigos: