A quick word from me
This issue isn't sponsored - I write these deep dives in my free time and keep them free for everyone. If your company sells AI tools, dev tools, courses, or services that .NET developers would actually use, sponsoring an issue is the most direct way to reach them.
Want to reach thousands of .NET developers? Sponsor TheCodeMan âShape the future of .NET tooling by spending just 10 minutes on JetBrainsâ .NET development market research. Fill out the survey and enter the prize draw! Start now
Background
Whether you're building microservices or just want to offload work from your main app, RabbitMQ is one of the best tools to help you get there.
RabbitMQ is a message broker. Think of it like a reliable postal service for your software.
Instead of one system directly calling another (which creates tight coupling), RabbitMQ acts as the middleman:
- One part of your app sends a message.
- Another part receives and processes it when it's ready.
This is called asynchronous communication, and itâs great for performance, reliability, and scalability.
Key Components:
- Producer: The one who sends the messages.
- Consumer: The one who receives the messages.
- Queue: Where the messages wait until they are processed.
- Exchange: The "dispatcher" that directs the messages to the appropriate queues.
- Binding: The rules that connect exchanges with queues.
Setup RabbitMQ (Local with Docker)
You can install RabbitMQ using Docker:
docker run -d --hostname rabbitmq --name rabbitmq \ -p 5672:5672 -p 15672:15672 rabbitmq:3-management
- Visit the dashboard at http://localhost:15672
- Default login: guest / guest
Implementing in .NET
Letâs build a simple project:
- The API sends an "email message" to RabbitMQ.
- A BackgroundService listens and "processes" the message.
Through this example I'm going to explain how it works.
Firstly, you need to add RabbitMQ library to your project:
Install-Package RabbitMQ.Client
Letâs define a C# class to represent an email message:
public class EmailMessage{ public string To { get; set; } = default!; public string Subject { get; set; } = default!; public string Body { get; set; } = default!;}
Publisher
The Publisher is the part of your app that sends messages to RabbitMQ.
Its Job:
- Connect to RabbitMQ
- Create (or ensure) a queue exists
- Serialize the message (e.g., to JSON)
- Send the message into the queue
Real-World Analogy:
Think of it like dropping a letter into a mailbox. Youâre not handling the delivery - just making sure it gets into the system.
public class EmailMessagePublisher{ private const string EmailQueue = "email-queue";Â public async Task Publish(EmailMessage email) { var factory = new ConnectionFactory() { HostName = "localhost" }; using var connection = await factory.CreateConnectionAsync(); using var channel = await connection.CreateChannelAsync();Â // Ensure the queue exists await channel.QueueDeclareAsync(queue: EmailQueue, durable: true, exclusive: false, autoDelete: false, arguments: null);Â // Create a message var message = JsonSerializer.Serialize(email); var body = Encoding.UTF8.GetBytes(message);Â // Publish the message await channel.BasicPublishAsync( exchange: string.Empty, routingKey: EmailQueue, mandatory: true, basicProperties: new BasicProperties { Persistent = true }, body: body); }}
Parameters:
- durable: save to disk so the queue isnât lost on broker restart
- exclusive: can be used by other connections
- autoDelete: donât delete when the last consumer disconnects
Receiver
The Receiver listens to the queue and processes messages as they arrive.
Its Job:
- Connect to RabbitMQ
- Subscribe to the queue
- Wait for messages
- Deserialize and process the messages
Real-World Analogy:
Think of it like a mailroom clerk who monitors the inbox and acts whenever a new letter shows up.
public class EmailMessageConsumer : BackgroundService{ private const string EmailQueue = "email-queue";Â protected override async Task ExecuteAsync(CancellationToken stoppingToken) { var factory = new ConnectionFactory() { HostName = "localhost" }; using var connection = await factory.CreateConnectionAsync(stoppingToken); using var channel = await connection.CreateChannelAsync(cancellationToken: stoppingToken);Â await channel.QueueDeclareAsync(queue: EmailQueue, durable: true, exclusive: false, autoDelete: false, arguments: null, cancellationToken: stoppingToken);Â var consumer = new AsyncEventingBasicConsumer(channel); consumer.ReceivedAsync += async (sender, eventArgs) => { var body = eventArgs.Body.ToArray(); var json = Encoding.UTF8.GetString(body); var email = JsonSerializer.Deserialize<EmailMessage>(json);Â Console.WriteLine($"Sending Email To: {email?.To}, Subject: {email?.Subject}");Â // Simulate sending email... Task.Delay(1000).Wait();Â await ((AsyncEventingBasicConsumer)sender).Channel.BasicAckAsync(eventArgs.DeliveryTag, multiple: false); };Â await channel.BasicConsumeAsync(queue: EmailQueue, autoAck: true, consumer: consumer, cancellationToken: stoppingToken); }}
RabbitMQ Exchange Types Explained
An Exchange in RabbitMQ is like a post office: it decides where to send the message. Each exchange type has a different strategy for routing messages.
Here are the 4 main types:
- Direct Exchange
- Fanout
- Topic
Headers
Direct Exchange (One-to-One Routing)
- A message is routed to queues with the exact same routing key.
- Think of this as sending mail to a specific recipient.
Example Use Case: You want to send emails only to a queue responsible for âwelcomeâ emails.
.NET Setup:
channel.ExchangeDeclare("direct-exchange", ExchangeType.Direct);channel.QueueBind("email-welcome-queue", "direct-exchange", "welcome");
If you publish with routingKey = "welcome", it will go to email-welcome-queue.
- Fanout Exchange (Broadcast to All)
- Messages go to all queues bound to the exchange, ignoring routing keys.
- Itâs a broadcastâlike shouting in a room and everyone hears it.
Example Use Case: You want to send a system-wide notification to all services (email, SMS, push).
.NET Setup:
channel.ExchangeDeclare("fanout-exchange", ExchangeType.Fanout);channel.QueueBind("email-queue", "fanout-exchange", "");channel.QueueBind("sms-queue", "fanout-exchange", "");
Any message sent to "fanout-exchange" goes to both queues.
- Topic Exchange (Wildcard Routing)
- Uses wildcards in the routing key to allow flexible, pattern-based routing.
Wildcards
- matches exactly one word
- # matches zero or more words
Example Use Case: Route logs based on severity and system.
.NET Setup:
channel.ExchangeDeclare("topic-exchange", ExchangeType.Topic);channel.QueueBind("error-queue", "topic-exchange", "log.error.#");channel.QueueBind("auth-queue", "topic-exchange", "log.*.auth");
"log.error.auth" goes to both queues. "log.info.auth" goes to auth-queue. "log.error.database" goes to error-queue.
- Headers Exchange (Route by Metadata)
- Instead of routing keys, it uses message headers for routing.
Example Use Case: Route messages with complex conditions (e.g., "x-type": "invoice" and "region": "EU")
.NET Setup:
channel.ExchangeDeclare("headers-exchange", ExchangeType.Headers);Â var args = new Dictionary<string, object>{ { "x-match", "all" }, // or "any" { "x-type", "invoice" }, { "region", "EU" }};Â channel.QueueBind("invoice-eu-queue", "headers-exchange", string.Empty, args);
Only messages that include both x-type=invoice and region=EU in their headers will be routed.
When to Use Which Exchange?
Direct - Exact one-to-one message routing Fanout - Broadcasting to multiple consumers Topic - Flexible, pattern-based routing (e.g., logs) Headers - Complex routing based on multiple conditions
For alternative messaging solutions, check out Redis Pub/Sub Messaging and NATS Real-Time Messaging.
Wrapping Up
RabbitMQ is a powerful tool for building scalable, decoupled systems - and .NET makes it surprisingly easy to integrate with.
Whether you're building microservices or just want to offload some long-running tasks, RabbitMQ has your back.
That's all from me today.
P.S. Follow me on YouTube.





