Integrating RabbitMQ in ASP.NET Core Web API

RabbitMQ Integration in ASP.NET Core Web API

In this post, I will explain how to integrate RabbitMQ in the ASP.NET Core Web API application. Please read our previous article, which discusses the steps for RabbitMQ integration.

Create a New Shared Project

First, create a solution folder named Messaging. Inside the Messaging solution folder, create a new Class Library project:

  • Project name: Messaging.Common
  • Purpose: Provide reusable RabbitMQ utilities (connection, publisher, base consumer).
  • Add this project to your Solution where OrderService, PaymentService, etc. already exist.
Creating Folders:

Inside Messaging.Common class library project, please create the following folders:

  • Connection: Contains code for creating and managing RabbitMQ connections and channels. Connections to RabbitMQ are expensive, so they are managed centrally instead of being created everywhere.
  • Publishing: Holds classes related to sending (publishing) messages to RabbitMQ. Keeps publishing logic (serialization, message properties, persistence, correlation IDs) separate from consuming logic.
  • Consuming: Contains reusable base classes for message consumers. Every service consumes data differently, but common operations include ACK/NACK handling, deserialization, and error handling.
  • Extensions: Contains extension methods to integrate RabbitMQ with ASP.NET Core’s Dependency Injection (DI) system. Therefore, each service can register with RabbitMQ, such as a builder.Services.AddRabbitMq(“localhost”, “user”, “password”, “vhost”);
  • Models: Defines shared base classes for event DTOs. Handle consistency across all services (all events have EventId, Timestamp, CorrelationId).
  • Options: Holds configuration classes (e.g., RabbitMqOptions) that map RabbitMQ settings from appsettings.json.
  • Topology: Contains classes that declare and bind exchanges, queues, and DLQs to ensure RabbitMQ infrastructure exists at startup.
  • Events: We will create classes to hold event data, which will be shared across microservices.
Required Packages

Please execute the following command in Package Manager Console and also select the Messaging.Common.

  • Install-Package RabbitMQ.Client -Version 6.5.0
  • Install-Package Microsoft.Extensions.DependencyInjection.Abstractions
RabbitMQ.Client (v6.5.0)
  • The official .NET client library for RabbitMQ.
  • It provides all the classes and interfaces (ConnectionFactory, IConnection, IModel, AsyncEventingBasicConsumer, etc.) to connect, publish, and consume messages.
  • Without it, you cannot talk to RabbitMQ at all.
Microsoft.Extensions.DependencyInjection.Abstractions
  • The base dependency injection (DI) abstractions used in ASP.NET Core.
  • Why we need it:
    • Allows us to write ServiceCollectionExtensions.cs to register RabbitMQ-related services in DI.
    • Ensures we can do builder.Services.AddRabbitMq(…) in each microservice.
Based Events Metadata

Create a class file named EventBase.cs within the Models folder, and then copy and paste the following code. All event DTOs (e.g., OrderPlacedEvent) should inherit from EventBase. Every service consuming this event can see when it was created and trace it using CorrelationId.

namespace Messaging.Common.Models
{
    public abstract class EventBase
    {
        //Unique ID per event
        public Guid EventId { get; private set; } = Guid.NewGuid();

        //When the event was created (useful for logging, debugging, or event ordering)
        public DateTime Timestamp { get; private set; } = DateTime.UtcNow;

        //Lets you trace a request across multiple services (e.g., Order → Payment → Notification).
        public string? CorrelationId { get; set; }
    }
}
Implement Connection Manager

Create a class file named ConnectionManager.cs within the Connection folder, and then copy and paste the following code. This class is responsible for managing RabbitMQ connections in a reusable way.

This service manages RabbitMQ connections and channels. It ensures that services reuse connections instead of creating new ones each time (connection pooling). This allows multiple consumers/publishers to share the same connection.

using RabbitMQ.Client;

namespace Messaging.Common.Connection
{
    public class ConnectionManager
    {
        // Private field: holds the RabbitMQ connection factory (used to create connections).
        private readonly ConnectionFactory _factory;

        // Private field: keeps a reference to the active connection.
        // The question mark (?) means it can be null initially.
        private IConnection? _connection;

        // Constructor: initializes the connection factory with RabbitMQ settings.
        public ConnectionManager(string hostName, string userName, string password, string vhost)
        {
            // Create a new ConnectionFactory instance with the given configuration.
            _factory = new ConnectionFactory
            {
                // The hostname or IP of the RabbitMQ broker (e.g., localhost or a server name).
                HostName = hostName,

                // Username to authenticate with RabbitMQ (e.g., ecommerce_user).
                UserName = userName,

                // Password for the above username.
                Password = password,

                // The virtual host (vhost) in RabbitMQ to connect to (e.g., ecommerce_vhost).
                VirtualHost = vhost,

                // Important: enables async consumers instead of the older sync consumer model. 
                // This is the modern and recommended way in .NET.
                DispatchConsumersAsync = true
            };
        }

        // This method returns an open RabbitMQ connection.
        // If no connection exists or the existing one is closed, it creates a new one.
        public IConnection GetConnection()
        {
            // Check if the _connection is null OR closed
            if (_connection == null || !_connection.IsOpen)
            {
                // Create a new connection using the factory.
                // This is an expensive operation, so we only do it when needed.
                _connection = _factory.CreateConnection();
            }

            // Return the active connection (either existing or newly created).
            return _connection;
        }
    }
}
Implement Publisher

Create a class file named Publisher.cs within the Publishing folder, and then copy and paste the following code. The Publisher class is responsible for sending messages to RabbitMQ.

using RabbitMQ.Client;
using System.Text;
using System.Text.Json;

namespace Messaging.Common.Publishing
{
    public class Publisher
    {
        // The RabbitMQ channel (IModel) used to send messages.
        private readonly IModel _channel;

        // Constructor: requires a RabbitMQ channel (injected from DI).
        public Publisher(IModel channel)
        {
            _channel = channel;
        }

        // Publishes a message to a RabbitMQ exchange with a given routing key.
        // T: Type of the message to publish.
        // exchange: Exchange name (e.g., ecommerce_exchange).
        // routingKey: Routing key used for queue binding (e.g., order.placed).
        // message: The message object (will be serialized to JSON).
        // correlationId: Optional unique ID for tracing.
        public void Publish<T>(string exchange, string routingKey, T message, string? correlationId = null)
        {
            // Serialize the message object into JSON, then encode into UTF-8 byte array.
            var body = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(message));

            // Create message properties.
            var props = _channel.CreateBasicProperties();
            props.Persistent = true; // Makes message persistent (saved to disk).
            props.CorrelationId = correlationId ?? Guid.NewGuid().ToString();

            // Publish the message to RabbitMQ.
            _channel.BasicPublish(
                exchange: exchange,          // Exchange name
                routingKey: routingKey,      // Routing key (routes message to queues)
                basicProperties: props,      // Properties (persistence + correlationId)
                body: body                   // The actual message payload
            );
        }
    }
}
Implement BaseConsumer

Create a class file named BaseConsumer.cs within the Consuming folder, and then copy and paste the following code. The BaseConsumer class provides a reusable foundation for consuming messages from RabbitMQ. It handles deserialization, ACK, and NACK automatically.

using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using System.Text;
using System.Text.Json;

namespace Messaging.Common.Consuming
{
    // T: The type of message being consumed (DTO/Event).
    public abstract class BaseConsumer<T>
    {
        // The RabbitMQ channel (IModel) this consumer will use.
        private readonly IModel _channel;

        // The name of the queue this consumer listens to.
        private readonly string _queue;

        // Constructor: accepts the channel and queue name as inputs.
        protected BaseConsumer(IModel channel, string queue)
        {
            _channel = channel;
            _queue = queue;
        }

        // Starts consuming messages from the configured queue.
        public void Start()
        {
            // Create an async consumer for this channel.
            var consumer = new AsyncEventingBasicConsumer(_channel);

            // consumer is an instance of AsyncEventingBasicConsumer.
            // .Received is an event that RabbitMQ raises whenever a new message arrives in the queue.
            // The += operator means: subscribe to this event with the following handler (a lambda function).
            // So this says: "When a message arrives, run this block of code."

            consumer.Received += async (model, ea) =>
            {
                // This is a lambda expression (an inline function) with two parameters:
                // model → sender (the consumer object, usually ignored).
                // ea → an instance of BasicDeliverEventArgs (the delivery details).
                // The ea object is very important — it contains:
                // ea.Body → the actual message payload(in bytes).
                // ea.BasicProperties → metadata(like CorrelationId, headers, delivery mode).
                // ea.DeliveryTag → a unique number RabbitMQ assigns to this message(used for ACK / NACK).
                // ea.RoutingKey, ea.Exchange, etc.

                try
                {
                    // Convert the message body (byte array) into a UTF-8 string.
                    var body = Encoding.UTF8.GetString(ea.Body.ToArray());

                    // Deserialize the JSON string into the expected type T.
                    var message = JsonSerializer.Deserialize<T>(body);

                    // Call the abstract handler method for actual business logic.
                    await HandleMessage(message!, ea.BasicProperties.CorrelationId);

                    // If no exception occurs → acknowledge the message (mark as processed).
                    _channel.BasicAck(ea.DeliveryTag, multiple: false);
                    // ACK = "Acknowledgement".
                    // Informs RabbitMQ: I successfully processed this message, you can remove it from the queue.
                    // Uses the unique DeliveryTag from ea.
                    // multiple: false → only ACK this single message(not multiple at once).
                    // Without this, RabbitMQ will think the message was not processed and will redeliver it.
                }
                catch (Exception ex)
                {
                    // If something goes wrong → log the error.
                    Console.WriteLine($"[Error] Failed to process message: {ex.Message}");

                    // Negative Acknowledgement → message goes to DLQ (if DLX is configured).
                    _channel.BasicNack(ea.DeliveryTag, multiple: false, requeue: false);

                    // NACK = "Negative Acknowledgement".
                    // Tells RabbitMQ: I could not process this message.
                    // requeue: false → don’t put it back in the same queue → instead send it to the Dead Letter Exchange(DLX) if configured.
                    // If DLX isn’t configured → message is discarded.
                }
            };

            // Begin consuming messages from the queue.
            // Start delivering messages from this queue to my consumer,
            // and I will manually confirm (ACK/NACK) each one.
            _channel.BasicConsume(queue: _queue, autoAck: false, consumer: consumer);
        }

        // Abstract method for handling a message.
        // Must be implemented in derived consumers (e.g., PaymentConsumer, InventoryConsumer).
        protected abstract Task HandleMessage(T message, string correlationId);
    }
}
Dependency Injection Support

We’ll add a DI extension so each service (OrderService, PaymentService, etc.) can just call builder.Services.AddRabbitMq(…). So, create a class file named ServiceCollectionExtensions.cs within the Extensions folder, and then copy and paste the following code.

using Microsoft.Extensions.DependencyInjection;
using Messaging.Common.Connection;

namespace Messaging.Common.Extensions
{
    public static class ServiceCollectionExtensions
    {
        // Extension method for IServiceCollection that registers RabbitMQ connection-related services
        // into the ASP.NET Core Dependency Injection (DI) container.
        public static IServiceCollection AddRabbitMq(
            this IServiceCollection services,  // "this" means we extend IServiceCollection with our own method
            string hostName,                   // RabbitMQ host (e.g., localhost, or server name)
            string userName,                   // RabbitMQ username (e.g., ecommerce_user)
            string password,                   // RabbitMQ password
            string vhost)                      // RabbitMQ virtual host (e.g., ecommerce_vhost)
        {
            // Create our custom ConnectionManager, which handles RabbitMQ connection logic
            var connectionManager = new ConnectionManager(hostName, userName, password, vhost);

            // Get an active connection from RabbitMQ (if not open, it will create one)
            var connection = connectionManager.GetConnection();

            // From the connection, create a channel (IModel) — this is used to declare queues,
            // exchanges, and to publish/consume messages.
            var channel = connection.CreateModel();

            // Register ConnectionManager as a singleton service — one instance will be reused for the whole application lifetime.
            services.AddSingleton(connectionManager);

            // Register the IConnection itself as a singleton service — shared across the app (expensive to create, so reuse).
            services.AddSingleton(connection);

            // Register the IModel (channel) as a singleton — consumers/publishers can inject this directly.
            services.AddSingleton(channel);

            // Return IServiceCollection so we can chain this method in Program.cs (fluent API style).
            return services;
        }
    }
}
OrderPlacedEvent.cs

First, create a folder named Events within the Messaging.Common project. Then, create a class file named OrderPlacedEvent.cs within the Events folder, and copy-paste the following code.

using Messaging.Common.Models;
namespace Messaging.Common.Events
{
    public sealed class OrderPlacedEvent : EventBase
    {
        public Guid OrderId { get; set; }
        public Guid UserId { get; set; }
        public string OrderNumber { get; set; } = null!;
        public string CustomerName { get; set; } = null!; 
        public string CustomerEmail { get; set; } = null!;
        public string PhoneNumber { get; set; } = null!;
        public decimal TotalAmount { get; set; }
        public List<OrderItemLine> Items { get; set; } = new();
    }

    public sealed class OrderItemLine
    {
        public Guid ProductId { get; set; }
        public int Quantity { get; set; }
        public decimal UnitPrice { get; set; }
    }
}
RabbitMqOptions:

Create a folder named Options within the Messaging.Common project. Then, create a class named RabbitMqOptions.cs within the Options folder, and copy-paste the following code. Each service can reuse this same POCO; it simply ignores queue names it doesn’t use.

namespace Messaging.Common.Options
{
    public sealed class RabbitMqOptions
    {
        // Connection
        public string HostName { get; set; } = "localhost";
        public int Port { get; set; } = 5672;
        public string UserName { get; set; } = "ecommerce_user";
        public string Password { get; set; } = "Test@1234";
        public string VirtualHost { get; set; } = "ecommerce_vhost";

        // Exchange(s)
        public string ExchangeName { get; set; } = "ecommerce.topic";

        // Dead-lettering (optional but recommended)
        public string? DlxExchangeName { get; set; } = "ecommerce.dlx";
        public string? DlxQueueName { get; set; } = "ecommerce.dlq";

        // Queues we care about for this feature set
        public string ProductOrderPlacedQueue { get; set; } = "product.order_placed";
        public string NotificationOrderPlacedQueue { get; set; } = "notification.order_placed";
    }
}
RabbitTopology:

The RabbitTopology class is a bootstrapper for RabbitMQ infrastructure. Create a folder named Topology within the Messaging.Common project. Then, create a class named RabbitTopology.cs within the Topology folder, and copy-paste the following code.

using Messaging.Common.Options;
using RabbitMQ.Client;

namespace Messaging.Common.Topology
{
    public static class RabbitTopology
    {
        public static void EnsureAll(IModel channel, RabbitMqOptions rabbitMqOptions)
        {
            // Declare the main exchange (topic-based)
            //    - Durable: survives broker restarts
            //    - AutoDelete: false means it won't disappear when unused
            //    - Type: Topic exchange routes messages based on pattern matching
            channel.ExchangeDeclare(
                exchange: rabbitMqOptions.ExchangeName,
                type: ExchangeType.Topic,
                durable: true,
                autoDelete: false);

            // Declare the Dead Letter Exchange (DLX) if configured
            //    - Used for failed/rejected messages (safety net)
            if (!string.IsNullOrWhiteSpace(rabbitMqOptions.DlxExchangeName))
            {
                channel.ExchangeDeclare(
                    exchange: rabbitMqOptions.DlxExchangeName!,
                    type: ExchangeType.Fanout,   // Fanout: send dead letters to all bound queues
                    durable: true,
                    autoDelete: false);

                // Declare Dead Letter Queue if provided
                if (!string.IsNullOrWhiteSpace(rabbitMqOptions.DlxQueueName))
                {
                    channel.QueueDeclare(
                        queue: rabbitMqOptions.DlxQueueName!,
                        durable: true,      // survive broker restarts
                        exclusive: false,   // can be consumed by multiple consumers
                        autoDelete: false,  // not auto-deleted when last consumer disconnects
                        arguments: null);

                    // Bind DLQ to DLX (routingKey irrelevant for fanout exchange)
                    channel.QueueBind(rabbitMqOptions.DlxQueueName, rabbitMqOptions.DlxExchangeName!, routingKey: "");
                }
            }

            // Common queue arguments (applied to business queues)
            //    - Add DLX binding if one exists, so rejected messages are routed safely
            var args = new Dictionary<string, object>();
            if (!string.IsNullOrWhiteSpace(rabbitMqOptions.DlxExchangeName))
                args["x-dead-letter-exchange"] = rabbitMqOptions.DlxExchangeName;
                //args["x-message-ttl"] = 10000;
                //args["x-max-length"] = 100;

            // Declare ProductService queue (listens for order.placed events)
            channel.QueueDeclare(
                queue: rabbitMqOptions.ProductOrderPlacedQueue,
                durable: true,
                exclusive: false,
                autoDelete: false,
                arguments: args); // attach DLX args if available

            // Declare NotificationService queue (listens for order.placed events)
            channel.QueueDeclare(
                queue: rabbitMqOptions.NotificationOrderPlacedQueue,
                durable: true,
                exclusive: false,
                autoDelete: false,
                arguments: args); // attach DLX args if available

            // Bind queues to the main exchange with the routing key "order.placed"
            //    - Any publisher sending to exchange "ecommerce.topic" with routingKey "order.placed"
            //      will be delivered to both queues (Product & Notification)
            channel.QueueBind(
                queue: rabbitMqOptions.ProductOrderPlacedQueue,
                exchange: rabbitMqOptions.ExchangeName,
                routingKey: "order.placed");

            channel.QueueBind(
                queue: rabbitMqOptions.NotificationOrderPlacedQueue,
                exchange: rabbitMqOptions.ExchangeName,
                routingKey: "order.placed");
        }
    }
}
Purpose of RabbitTopology

When your microservices start up, this class ensures that:

  1. The main exchange (e.g., ecommerce.topic) exists.
  2. Optional dead-letter exchange (DLX) and dead-letter queue (DLQ) exist.
  3. All required queues (product.order_placed.q, notification.order_placed.q) are declared.
  4. The queues are bound to the exchange with the right routing key (order.placed).

This way, you don’t rely on manual setup in the RabbitMQ Management UI; every service can declare the topology it needs at startup.

In this post, I discuss how to integrate RabbitMQ in an ASP.NET Core Web API application. In the next post, I will discuss End-to-end Order Placed Communication using RabbitMQ.

Leave a Reply

Your email address will not be published. Required fields are marked *