Olá! Nas lições anteriores, você aprendeu o padrão de integração de componentes do .NET Aspire, primeiro com um cache Redis e depois com um banco de dados PostgreSQL. Vimos como o Aspire simplifica a orquestração, configuração e observabilidade de dependências externas, seguindo um padrão consistente de declaração no AppHost e consumo nos serviços.
Nesta lição, vamos explorar outro pilar fundamental das arquiteturas distribuídas: a comunicação assíncrona. Nosso objetivo é integrar uma fila de mensagens RabbitMQ em uma aplicação Aspire. Isso permitirá que seus serviços se comuniquem de forma desacoplada, melhorando a escalabilidade, a resiliência e a capacidade de resposta do sistema. Você verá que, mais uma vez, o padrão do Aspire para integrar o RabbitMQ é notavelmente similar ao que você já domina.
Por que Comunicação Assíncrona com RabbitMQ?
Em um sistema distribuído, fazer com que um serviço espere por uma resposta síncrona de outro pode criar gargalos e pontos de falha. Se o serviço B estiver lento ou offline, o serviço A ficará bloqueado.
A comunicação assíncrona, facilitada por um message broker como o RabbitMQ, resolve isso. O serviço A (o produtor) simplesmente publica uma mensagem em uma fila e continua seu trabalho. Em algum momento posterior, o serviço B (o consumidor) pega essa mensagem da fila e a processa.
Isso desacopla os serviços:
- Temporalmente: O produtor e o consumidor não precisam estar online ao mesmo tempo.
- No fluxo de execução: O produtor não bloqueia enquanto espera pelo processamento.
Sua experiência com sistemas de telecomunicações, que frequentemente lidam com protocolos de armazenamento e encaminhamento, oferece um paralelo interessante ao funcionamento de um message broker.
O Padrão de Integração do Aspire para RabbitMQ
Vamos seguir os mesmos três passos que você já conhece.
Passo 1: Configurar o Recurso RabbitMQ no AppHost
Primeiro, adicionamos a capacidade de hospedar o RabbitMQ ao AppHost.
A documentação oficial do Aspire é o ponto de partida ideal. Ela detalha como adicionar o contêiner do RabbitMQ ao AppHost e as opções de configuração disponíveis.
Leia as seções "Hosting integration" e "Add RabbitMQ server resource with management plugin". Observe o uso dos métodos AddRabbitMQ e WithManagementPlugin.
Para começar, adicione o pacote NuGet de hospedagem ao seu projeto AppHost:
dotnet add package Aspire.Hosting.RabbitMQ
Agora, modifique o Program.cs do seu AppHost para declarar o RabbitMQ como um recurso.
// Em AppHost/Program.cs
var builder = DistributedApplication.CreateBuilder(args);
// Adiciona o contêiner RabbitMQ como um recurso nomeado "messaging"
var rabbitmq = builder.AddRabbitMQ("messaging")
// Adiciona a UI de gerenciamento do RabbitMQ, acessível pelo dashboard
.WithManagementPlugin();
// Suponha que temos um serviço de API que produzirá mensagens
var apiService = builder.AddProject<Projects.MyAspireApp_ApiService>("apiservice")
.WithReference(rabbitmq); // Conexão a ser feita no passo 3
// E um serviço de background que consumirá as mensagens
var workerService = builder.AddProject<Projects.MyAspireApp_WorkerService>("workerservice")
.WithReference(rabbitmq); // Conexão a ser feita no passo 3
builder.Build().Run();
Pontos-chave:
AddRabbitMQ("messaging"): Adiciona um contêiner Docker do RabbitMQ. O nome"messaging"é o identificador que usaremos para nos referirmos a ele.WithManagementPlugin(): Semelhante aoWithPgAdmin()ouWithRedisCommander(), este método adiciona a interface de gerenciamento web do RabbitMQ, que será extremamente útil para observarmos o que está acontecendo (quais filas existem, quantas mensagens estão esperando, etc.).
Passo 2: Configurar o Cliente RabbitMQ nos Serviços
Agora, precisamos configurar nossos serviços (ApiService e WorkerService) para se conectarem ao RabbitMQ.
A documentação também explica como configurar o projeto cliente para se conectar ao RabbitMQ.
Leia as seções "Client integration" e "Configuration". Preste atenção em como o método AddRabbitMQClient é usado e como ele registra uma instância de IConnection no contêiner de injeção de dependência (DI).
Em ambos os projetos que precisam se comunicar com o RabbitMQ (ApiService e WorkerService), adicione o pacote cliente:
dotnet add package Aspire.RabbitMQ.Client
Em seguida, no Program.cs de cada um desses serviços, adicione a configuração do cliente:
// Em ApiService/Program.cs e WorkerService/Program.cs
var builder = WebApplication.CreateBuilder(args); // ou Host.CreateApplicationBuilder(args) para um worker
builder.AddServiceDefaults();
// Adiciona a configuração do cliente RabbitMQ
// O nome "messaging" DEVE corresponder ao nome usado em AddRabbitMQ() no AppHost.
builder.AddRabbitMQClient("messaging");
// ... o resto da configuração
var app = builder.Build();
app.Run();
Diferentemente do EF Core, onde o Aspire fornecia um método de alto nível (AddNpgsqlDbContext), aqui o AddRabbitMQClient faz algo mais fundamental:
- Ele lê a connection string injetada pelo Aspire.
- Registra uma implementação da interface
IConnection(do pacoteRabbitMQ.Client) como um singleton no contêiner de DI. - Configura automaticamente health checks e telemetria (tracing).
Isso significa que o Aspire lhe entrega a conexão, e você usa o cliente oficial do RabbitMQ para fazer o resto.
Passo 3: Conectar Tudo com WithReference
Este é o passo final que une tudo, exatamente como você já fez antes. No Program.cs do AppHost, a chamada .WithReference(rabbitmq) informa ao Aspire que os serviços apiService e workerService dependem do recurso rabbitmq. O Aspire então injeta a variável de ambiente ConnectionStrings__messaging em ambos os serviços, que é lida pelo AddRabbitMQClient.
Cloud-native apps with .NET Aspire
Para uma visão geral rápida de como o Aspire simplifica a adição de componentes como o RabbitMQ, este vídeo é excelente. Ele demonstra a facilidade de orquestrar o contêiner e configurar a conexão.
Assista ao trecho de 09:49 a 11:04. Observe como a apresentadora adiciona o pacote NuGet, declara o recurso RabbitMQ no AppHost e o consome no serviço. É uma ótima consolidação visual dos passos que acabamos de discutir.
Na Prática: Implementando um Produtor e um Consumidor
Com a conexão estabelecida, vamos ao que interessa: enviar e receber mensagens. Para isso, vamos injetar a IConnection que o Aspire registrou para nós.
O Produtor (em ApiService)
Vamos criar um endpoint na nossa API que publica uma mensagem.
// Exemplo de um endpoint na sua ApiService
[ApiController]
[Route("[controller]")]
public class OrdersController : ControllerBase
{
private readonly IConnection _rabbitConnection;
public OrdersController(IConnection rabbitConnection)
{
_rabbitConnection = rabbitConnection;
}
[HttpPost]
public IActionResult CreateOrder([FromBody] Order order)
{
using var channel = _rabbitConnection.CreateModel();
// Garante que a fila existe. É idempotente.
channel.QueueDeclare(queue: "order_processing",
durable: true, // A fila sobrevive ao reinício do broker
exclusive: false,
autoDelete: false,
arguments: null);
var messageBody = System.Text.Json.JsonSerializer.Serialize(order);
var body = System.Text.Encoding.UTF8.GetBytes(messageBody);
var properties = channel.CreateBasicProperties();
properties.Persistent = true; // A mensagem sobrevive ao reinício do broker
channel.BasicPublish(exchange: string.Empty, // Usando o exchange padrão
routingKey: "order_processing", // O nome da fila
basicProperties: properties,
body: body);
return Accepted(new { Message = "Pedido recebido e enviado para processamento." });
}
}
Pontos-chave do produtor:
- Injetamos
IConnection. - Criamos um
IModel(um canal), que é o contexto para a maioria das operações de API do RabbitMQ. QueueDeclare: Declara uma fila. Se já existir, nada acontece.durable: trueé importante para que a fila não seja perdida se o RabbitMQ reiniciar.BasicPublish: Publica a mensagem.properties.Persistent = truegarante que a mensagem também seja persistida.
O Consumidor (em WorkerService)
Para o consumidor, a abordagem mais robusta em .NET é usar um IHostedService (serviço de background).
// Exemplo em WorkerService
public class OrderProcessingWorker : BackgroundService
{
private readonly ILogger<OrderProcessingWorker> _logger;
private readonly IConnection _rabbitConnection;
private IModel _channel;
public OrderProcessingWorker(ILogger<OrderProcessingWorker> logger, IConnection rabbitConnection)
{
_logger = logger;
_rabbitConnection = rabbitConnection;
}
protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
_channel = _rabbitConnection.CreateModel();
_channel.QueueDeclare(queue: "order_processing", durable: true, exclusive: false, autoDelete: false, arguments: null);
// Limita a 1 mensagem por vez para este consumidor, para não sobrecarregar
_channel.BasicQos(prefetchSize: 0, prefetchCount: 1, global: false);
var consumer = new EventingBasicConsumer(_channel);
consumer.Received += (model, ea) =>
{
var body = ea.Body.ToArray();
var message = System.Text.Encoding.UTF8.GetString(body);
_logger.LogInformation("--> Mensagem recebida: {Message}", message);
// Processa a mensagem aqui...
// Confirma o recebimento e processamento da mensagem.
// O RabbitMQ pode então removê-la da fila com segurança.
_channel.BasicAck(deliveryTag: ea.DeliveryTag, multiple: false);
};
_channel.BasicConsume(queue: "order_processing",
autoAck: false, // Desabilitamos auto-ack para confirmar manualmente
consumer: consumer);
return Task.CompletedTask;
}
public override void Dispose()
{
_channel?.Close();
base.Dispose();
}
}
Pontos-chave do consumidor:
EventingBasicConsumer: É uma forma de se inscrever para receber mensagens de forma assíncrona.autoAck: false: Este é um detalhe crucial para a confiabilidade. Informamos ao RabbitMQ que confirmaremos manualmente (BasicAck) quando uma mensagem for processada com sucesso. Se o nosso worker falhar no meio do processamento, a mensagem não será perdida; ela permanecerá na fila para ser reprocessada.BasicAck: O "reconhecimento" de que a mensagem foi processada.
Aprofunde-se: Implementação Robusta
Os exemplos acima são simplificados para fins didáticos. Em um cenário de produção, você precisaria lidar com gerenciamento de conexões e canais de forma mais robusta, tratamento de erros, novas tentativas (retries) e dead-letter queues (filas para mensagens que falham consistentemente).
O artigo Robust RabbitMQ implementation for ASP.NET de Mick van der Vegt é um recurso fantástico que detalha como construir uma camada de abstração robusta sobre o RabbitMQ.Client em ASP.NET Core, abordando tópicos como gerenciamento de transações e ciclo de vida de canais. Recomendo a leitura das seções "Publishing messages" e "Consuming messages" para entender um padrão mais avançado.
Verificação e Observabilidade no Dashboard
Execute seu projeto AppHost. O dashboard é onde tudo se junta.
- Recursos: Você verá o contêiner
messagingem execução.

*O dashboard do Aspire mostrando o recurso 'messaging' (RabbitMQ) em execução, com seus endpoints e variáveis de ambiente visíveis.*
2. Management UI: O recurso messaging terá um endpoint. Clique nele para abrir a UI de gerenciamento do RabbitMQ. Vá para a aba "Queues". Depois de executar o produtor, você verá a fila order_processing e poderá inspecionar as mensagens.

*A UI de gerenciamento do RabbitMQ, acessada a partir do dashboard, mostrando a fila 'myAspireRabbit' (no nosso caso, seria 'order_processing').*
3. Logs e Traces:
* Nos logs do WorkerService, você verá a mensagem "--> Mensagem recebida: ...".
* Na aba "Traces" do dashboard, você poderá ver o rastreamento completo: a requisição HTTP chegando no ApiService, a publicação da mensagem para o RabbitMQ e, em uma trace separada, o WorkerService recebendo e processando a mensagem. A telemetria adicionada pelo Aspire conecta tudo isso.
Teste seu entendimento!
No código do consumidor, o que aconteceria se você esquecesse de chamar _channel.BasicAck após processar a mensagem e o autoAck estivesse configurado como false?
Show answer
A mensagem seria processada pelo seu worker, mas do ponto de vista do RabbitMQ, ela nunca foi "confirmada". Ela permaneceria na fila no estado "Unacked" (Não confirmada). Se a conexão do worker com o RabbitMQ fosse fechada (por exemplo, se o worker reiniciasse), o RabbitMQ devolveria a mensagem à fila para ser processada novamente por outro consumidor (ou pelo mesmo, após reiniciar). Isso levaria ao reprocessamento de mensagens, o que pode ou não ser um problema dependendo da sua lógica de negócio (é por isso que a idempotência do consumidor é importante).
Conclusão
Parabéns! Você integrou com sucesso um message broker para comunicação assíncrona, mantendo a experiência de desenvolvimento simplificada que o .NET Aspire proporciona.
Principais Pontos:
- O padrão de integração do Aspire (
Add...no AppHost,Add...Clientno serviço,WithReferencepara conectar) é consistente e se aplica também ao RabbitMQ. - Aspire facilita a orquestração do contêiner RabbitMQ e fornece a
IConnection, mas você ainda usa o clienteRabbitMQ.Clientpara a lógica de negócio (publicar/consumir). - A comunicação assíncrona desacopla seus serviços, aumentando a resiliência e a escalabilidade da sua aplicação.
- O Aspire Dashboard, com acesso integrado à UI de gerenciamento e telemetria ponta a ponta, é inestimável para depurar sistemas baseados em mensagens.
Próxima lição: Até agora, utilizamos componentes com suporte de primeira classe no Aspire (Redis, PostgreSQL, RabbitMQ). Mas e se você precisar de uma dependência que não tem um pacote Aspire.Hosting.* dedicado? Na próxima lição, você aprenderá a adicionar dependências containerizadas arbitrárias a uma aplicação Aspire usando o método AddContainer, o que lhe dará o poder de integrar qualquer serviço que possa ser executado em um contêiner Docker.