Implementando Publish-Subscribe com ASP.NET Core, RabbitMQ e Docker

Neste artigo eu vou falar sobre o padrão Publish-Subscribe, e como implementá-lo utilizando uma API ASP.NET Core 7 com o RabbitMQ como message broker.

Caso não conheça o RabbitMQ e seus conceitos mais básicos, indico ler este artigo meu: Introdução ao RabbitMQ

Código-fonte do exemplo apresentado no artigo: Clique aqui

Sobre o padrão Publish-Subscribe

O padrão Publish-Subscribe é um padrão de projeto de software que permite que componentes enviem mensagens uns aos outros sem precisar conhecer diretamente quem as receberá. Em vez disso, os componentes simplesmente publicam suas mensagens em um tópico, e outros se inscrevem para receber notificações sobre esses tópicos.

Ele é útil em situações em que há dois ou mais objetos que podem estar interessados em receber notificações sobre determinados eventos ou atualizações de estado em seu sistema. Ele permite que esses objetos se inscrevam para receber apenas as notificações que lhes interessam, ao invés de ter que receber todas as atualizações enviadas por um determinado componente. Além disso, o padrão Publish-Subscribe ajuda a evitar dependências fortes entre os objetos, pois eles não precisam conhecer diretamente uns aos outros, resultando em menor acoplamento entre os componentes do sistema.

O padrão Publish-Subscribe é implementado com a utilização de um mediador, que é responsável por gerenciar a publicação de mensagens e a inscrição de objetos para receber notificações. Ele pode ser implementado de várias maneiras, como por exemplo, com a utilização de um Message Broker como o RabbitMQ.

Configurando o ambiente

Certifique-se de que o Docker está instalado em sua máquina. Se ainda não o tiver, você pode seguir as instruções de instalação em https://docs.docker.com/get-docker/.

Eu utilizo o Docker para subir uma instância do RabbitMQ e sua interface de gerenciamento local.

Para isso eu executo o seguinte comando.

docker run -d --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:3-management

Como acessar a interface de gerenciamento:

  1. Abra um navegador e acesse a UI do RabbitMQ usando o endereço http://localhost:15672/. Você deverá ver a tela de login da UI do RabbitMQ.
  2. Use o usuário “guest” e a senha “guest” para fazer login na UI do RabbitMQ.

Antes de começarmos a implementação, vamos criar a Exchange de tipo tópico que nossas APIs vão utilizar, além de duas filas e dois bindings.

  1. Primeiramente, na UI do RabbitMQ, navegue para a aba “Exchanges”.
  2. Expanda a sessão “Add a new exchange”, e preencha com os seguintes dados.
    1. Name: customers-service
    2. Type: topic
  3. Clique em “Add exchange”
  4. Navegue para a aba “Queues”
  5. Expanda a sessão “Add a new queue”, e preencha com os seguintes dados:
    1. Name: notifications-service/customer-created
    2. Type: Classic
  6. Clique em “Add queue”
  7. Repita o processo para os seguintes dados:
    1. Name: sales-service/customer-created
    2. Type: Classic
  8. Acesse a fila criada “notifications-service/customer-created”, expanda a aba “Bindings” e preencha os seguintes dados:
    1. From exchange: customers-service
    2. Routing key: customer-created
  9. Repita o processo anterior, mas dessa vez para a fila “sales-service/customer-created”, com os mesmos dados.

Com isso, vamos começar nossa implementação.

Implementação

No início do artigo foi repassado o código-fonte utilizado, que contém uma solução com 3 projetos ASP.NET Core 7:

  • AwesomeShop.Customers: vai fazer o papel de publisher do evento CustomerCreatedEvent
  • AwesomeShop.Notifications e AwesomeShop.Sales: vão fazer o papel de subscribers do evento CustomerCreatedEvent

Vou apresentar abaixo o código das principais classes utilizadas.

Primeiramente, o código do Publisher do projeto AwesomeShop.Customers, que está contido em um endpoint HTTP POST, que inicia a conexão com o RabbitMQ e publica uma mensagem no Exchange customers-service, com a Routing Key customer-created.

    [ApiController]
    [Route("api/customers")]
    public class CustomersController : ControllerBase
    {
        private readonly IConnection _connection;
        private readonly IModel _channel;
        private const string Exchange = "customers-service";

        public CustomersController()
        {
            var connectionFactory = new ConnectionFactory
            {
                HostName = "localhost"
            };

            _connection = connectionFactory.CreateConnection("customers-service-publisher");

            _channel = _connection.CreateModel();
        }

        [HttpPost]
        public IActionResult Post(CustomerInputModel model)
        {
            var customerCreated = new CustomerCreatedEvent(model.FullName, model.Email);

            var payload = JsonConvert.SerializeObject(customerCreated);
            var byteArray = Encoding.UTF8.GetBytes(payload);

            Console.WriteLine("CustomerCreatedEvent Published");

            _channel.BasicPublish(Exchange, "customer-created", null, byteArray);

            return NoContent();
        }
    }

O que está sendo feito no código acima:

  • Instanciamos um ConnectionFactory, utilizando uma instância local do RabbitMQ “localhost”;
  • Criamos uma conexão, e um canal através dessa conexão;
  • Preparamos o objeto para ser publicado como mensagem, convertendo-o para uma cadeia de caracteres em formato JSON, e logo para matriz de bytes;
  • Publicamos a mensagem passando o Exchange, o Routing Key e a matriz de bytes que representa a mensagem.

Em seguida, para criar Subscribers eu utilizo classes que herdem de BackgroundService, configurando-as como Hosted Services na classe Program.cs para que possam ser iniciadas quando a aplicação iniciar.

Começamos pelo Subscriber NotifyCustomerCreatedSubscriber, no projeto AwesomeShop.Notifications.

public class NotifyCustomerCreatedSubscriber : BackgroundService
    {
        private readonly IConnection _connection;
        private readonly IModel _channel;
        private const string Queue = "notifications-service/customer-created";
        public NotifyCustomerCreatedSubscriber()
        {
            var connectionFactory = new ConnectionFactory
            {
                HostName = "localhost"
            };

            _connection = connectionFactory.CreateConnection("notifications-service-customer-created-consumer");

            _channel = _connection.CreateModel();
        }
        protected override Task ExecuteAsync(CancellationToken stoppingToken)
        {
            var consumer = new EventingBasicConsumer(_channel);

            consumer.Received += async (sender, eventArgs) =>
            {
                var contentArray = eventArgs.Body.ToArray();
                var contentString = Encoding.UTF8.GetString(contentArray);
                var message = JsonConvert.DeserializeObject<CustomerCreatedEvent>(contentString);

                Console.WriteLine($"Message CustomerCreatedEvent received with Email {message.Email}");

                _channel.BasicAck(eventArgs.DeliveryTag, false);
            };

            _channel.BasicConsume(Queue, false, consumer);

            return Task.CompletedTask;
        }
    }

O que está sendo feito no código acima:

  • Instanciamos um ConnectionFactory, utilizando uma instância local do RabbitMQ “localhost”;
  • Criamos uma conexão, e um canal através dessa conexão;
  • Instanciamos um EventingBasicConsumer, onde configuramos em seguida tratamento para o evento Received;
  • Convertemos a matriz de bytes do corpo da mensagem para um objeto de tipo CustomerCreatedEvent;
    • Ao final dele invocamos o método BasicAck, que reconhece a mensagem como processada ao Message Broker;
  • Iniciamos o consumo ao invocar o método BasicConsume, passando a fila e o consumer definido;

E agora, para o Subscriber OnboardingCustomerCreatedSubscriber, no projeto AwesomeShop.Sales.

public class OnboardingCustomerCreatedSubscriber : BackgroundService
    {
        private readonly IConnection _connection;
        private readonly IModel _channel;
        private const string Queue = "sales-service/customer-created";
        public OnboardingCustomerCreatedSubscriber()
        {
            var connectionFactory = new ConnectionFactory
            {
                HostName = "localhost"
            };

            _connection = connectionFactory.CreateConnection("sales-service-customer-created-consumer");

            _channel = _connection.CreateModel();
        }
        protected override Task ExecuteAsync(CancellationToken stoppingToken)
        {
            var consumer = new EventingBasicConsumer(_channel);

            consumer.Received += async (sender, eventArgs) =>
            {
                var contentArray = eventArgs.Body.ToArray();
                var contentString = Encoding.UTF8.GetString(contentArray);
                var message = JsonConvert.DeserializeObject<CustomerCreatedEvent>(contentString);

                Console.WriteLine($"Message CustomerCreatedEvent received with Email {message.Email}");

                _channel.BasicAck(eventArgs.DeliveryTag, false);
            };

            _channel.BasicConsume(Queue, false, consumer);

            return Task.CompletedTask;
        }
    }

A mesma explicação anterior se aplica aqui.

Finalmente, antes de executar as aplicações e ver a “mágica” ocorrendo, precisamos configurar os Subscribers nos projetos AwesomeShop.Notifications e AwesomeShop.Sales.

É algo bem simples, basta adicionar a linha abaixo no Program.cs do AwesomeShop.Notifications.

builder.Services.AddHostedService<NotifyCustomerCreatedSubscriber>();

E a linha abaixo no Program.cs do AwesomeShop.Sales.

builder.Services.AddHostedService<OnboardingCustomerCreatedSubscriber>();

O resultado

Executamos as 3 aplicações separadamente, e realizando uma requisição para o endpoint api/customers HTTP POST do AwesomeShop.Customers, vemos a escrita no Console das aplicações AwesomeShop.Notifications e AwesomeShop.Sales.

Execução das aplicações e Publish-Subscribe na prática

Quer alavancar sua carreira como Desenvolvedor(a) .NET?

Além de Desenvolvedor .NET Sênior, eu sou instrutor de mais de 700 alunos e também tenho dezenas de mentorados.

Conheça o Método .NET Direto ao Ponto, minha plataforma com mais de 800 videoaulas, com cursos cobrindo temas relacionados a linguagem C# e Programação Orientada a Objetos, APIs REST com ASP NET Core, Microsserviços com ASP NET Core, HTML, CSS e JavaScript, Desenvolvimento Front-end com Angular, Desenvolvimento Front-end com React, JavaScript Intermediário, TypeScript, Formação Arquitetura de Software, Microsoft Azure, Agile, SQL, e muito mais.

Inclui comunidade de centenas de alunos, suporte por ela, plataforma e e-mail, atualizações regulares e muito mais.

Clique aqui para ter mais informações e garantir sua vaga


Conclusão

Neste artigo vimos os conceitos relacionados ao padrão Publish-Subscribe, bem como uma implementação dele na prática utilizando aplicações ASP.NET Core 7 com o RabbitMQ.

Se curtiu, compartilhe com colegas de equipe em sua empresa!