Tracking Audit Logs with RabbitMQ: Publishing and Consuming Events in .NET

Introduction Imagine a receptionist at a hotel manually logging every guest check-in in a notebook. Now, what if instead, the receptionist just sent a digital message to a system that automatically logs the activity and processes it without any delay while the receptionist attends to the next guest? That’s exactly how event-driven logging works! Instead of a system slowing down while recording user actions, RabbitMQ helps us publish events and process them asynchronously without affecting the main application. In this article, we'll implement a Simple User Registration Logger using RabbitMQ in a .NET Web API. Our focus will be on registering users, publishing registration events, and consuming them in a background service for logging. What We’ll Build A .NET Web API that: Registers users and stores them in memory. Publishes a user registration event to RabbitMQ. Consumes this event asynchronously and logs the message. Prerequisites Before we start, ensure you have the following: .NET SDK 8.0 or later RabbitMQ installed and running (Refer to the previous article for setup.) Basic knowledge of .NET Web API development Step 1: Setting Up the .NET Web API Create a new .NET Web API project: dotnet new webapi -n UserActivityLogger cd UserActivityLogger Install RabbitMQ client package: dotnet add package RabbitMQ.Client --version 6.8.1 Add RabbitMQ settings to appsettings.json: { ..., "RabbitMQ": { "HostName": "localhost", "Port": 5672, "UserName": "guest", "Password": "guest", "QueueName": "user_registration_queue" } } Step 2: Defining the User Model and In-Memory Storage A user model represents the structure of user data. Here’s our User.cs model: public class User { public string Username { get; set; } = string.Empty; public string Email { get; set; } = string.Empty; } To temporarily store user data, we create an in-memory storage class: public static class UserStore { public static List Users = new(); } This allows us to simulate a database without additional setup. Step 3: Creating the RabbitMQ Settings Class Before integrating RabbitMQ, we define a configuration class for its settings: namespace UserActivityLogger.Settings { public class RabbitMQSettings { public string HostName { get; set; } = string.Empty; public int Port { get; set; } = 5672; public string UserName { get; set; } = "guest"; public string Password { get; set; } = "guest"; public string QueueName { get; set; } = "user_registration_queue"; } } Now, we bind these settings in Program.cs: var builder = WebApplication.CreateBuilder(args); builder.Services.Configure(builder.Configuration.GetSection("RabbitMQ")); This ensures the RabbitMQSettings class dynamically loads configuration values from appsettings.json, making it easy to manage connection details. Step 4: Creating the RabbitMQ Service Create a Services folder and a RabbitMQService.cs file in the folder to handle publishing messages: The RabbitMQService is responsible for establishing a connection with RabbitMQ, ensuring messages are published correctly. using RabbitMQ.Client; using System.Text; using Microsoft.Extensions.Configuration; using UserActivityLogger.Settings; using Microsoft.Extensions.Options; namespace UserActivityLogger.Service { public class RabbitMQService { private readonly IConnection _connection; private readonly IModel _channel; private readonly RabbitMQSettings _settings; public RabbitMQService(IOptions settings) { _settings = settings.Value; var factory = new ConnectionFactory() { HostName = _settings.HostName, Port = _settings.Port, UserName = _settings.UserName, Password = _settings.Password, AutomaticRecoveryEnabled = true // Automatically reconnects if RabbitMQ restarts }; _connection = factory.CreateConnection(); _channel = _connection.CreateModel(); _channel.QueueDeclare( _settings.QueueName, durable: true, // The queue will persist even if RabbitMQ restarts. exclusive: false, // Allows multiple consumers to listen to this queue. autoDelete: false // Prevents the queue from being deleted when no consumers are active. ); } public void PublishMessage(string message) { var body = Encoding.UTF8.GetBytes(message); // Publish a message to the specified queue _channel.BasicPublish(exchange: "", routingKey: _settings.QueueName, basicProperties: null, body: body); } } } The AutomaticRecoveryEnabled property in the ConnectionFactory ensures that the service can a

Feb 9, 2025 - 18:50
 0
Tracking Audit Logs with RabbitMQ: Publishing and Consuming Events in .NET

Introduction

Imagine a receptionist at a hotel manually logging every guest check-in in a notebook. Now, what if instead, the receptionist just sent a digital message to a system that automatically logs the activity and processes it without any delay while the receptionist attends to the next guest? That’s exactly how event-driven logging works! Instead of a system slowing down while recording user actions, RabbitMQ helps us publish events and process them asynchronously without affecting the main application.

In this article, we'll implement a Simple User Registration Logger using RabbitMQ in a .NET Web API. Our focus will be on registering users, publishing registration events, and consuming them in a background service for logging.

What We’ll Build

A .NET Web API that:

  1. Registers users and stores them in memory.
  2. Publishes a user registration event to RabbitMQ.
  3. Consumes this event asynchronously and logs the message.

Prerequisites

Before we start, ensure you have the following:

  • .NET SDK 8.0 or later
  • RabbitMQ installed and running (Refer to the previous article for setup.)
  • Basic knowledge of .NET Web API development

Step 1: Setting Up the .NET Web API

Create a new .NET Web API project:

 dotnet new webapi -n UserActivityLogger
 cd UserActivityLogger

Install RabbitMQ client package:

 dotnet add package RabbitMQ.Client --version 6.8.1

Add RabbitMQ settings to appsettings.json:

 {
  ...,
  "RabbitMQ": {
    "HostName": "localhost",
    "Port": 5672,
    "UserName": "guest",
    "Password": "guest",
    "QueueName": "user_registration_queue"
  }
}

Step 2: Defining the User Model and In-Memory Storage

A user model represents the structure of user data. Here’s our User.cs model:

public class User
{
    public string Username { get; set; } = string.Empty;
    public string Email { get; set; } = string.Empty;
}

To temporarily store user data, we create an in-memory storage class:

public static class UserStore
{
    public static List<User> Users = new();
}

This allows us to simulate a database without additional setup.

Step 3: Creating the RabbitMQ Settings Class

Before integrating RabbitMQ, we define a configuration class for its settings:

namespace UserActivityLogger.Settings
{
    public class RabbitMQSettings
    {
        public string HostName { get; set; } = string.Empty;
        public int Port { get; set; } = 5672;
        public string UserName { get; set; } = "guest";
        public string Password { get; set; } = "guest";
        public string QueueName { get; set; } = "user_registration_queue";
    }
}

Now, we bind these settings in Program.cs:

var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<RabbitMQSettings>(builder.Configuration.GetSection("RabbitMQ"));

This ensures the RabbitMQSettings class dynamically loads configuration values from appsettings.json, making it easy to manage connection details.

Step 4: Creating the RabbitMQ Service

Create a Services folder and a RabbitMQService.cs file in the folder to handle publishing messages:

The RabbitMQService is responsible for establishing a connection with RabbitMQ, ensuring messages are published correctly.

using RabbitMQ.Client;
using System.Text;
using Microsoft.Extensions.Configuration;
using UserActivityLogger.Settings;
using Microsoft.Extensions.Options;

namespace UserActivityLogger.Service
{
    public class RabbitMQService
    {
        private readonly IConnection _connection;
        private readonly IModel _channel;
        private readonly RabbitMQSettings _settings;

        public RabbitMQService(IOptions<RabbitMQSettings> settings)
        {
            _settings = settings.Value;
            var factory = new ConnectionFactory()
            {
                HostName = _settings.HostName,
                Port = _settings.Port,
                UserName = _settings.UserName,
                Password = _settings.Password,
                AutomaticRecoveryEnabled = true // Automatically reconnects if RabbitMQ restarts
            };
            _connection = factory.CreateConnection();
            _channel = _connection.CreateModel();

            _channel.QueueDeclare(
                _settings.QueueName,
                durable: true, // The queue will persist even if RabbitMQ restarts.
                exclusive: false, // Allows multiple consumers to listen to this queue.
                autoDelete: false // Prevents the queue from being deleted when no consumers are active.
            );
        }

        public void PublishMessage(string message)
        {
            var body = Encoding.UTF8.GetBytes(message);
            // Publish a message to the specified queue
            _channel.BasicPublish(exchange: "", routingKey: _settings.QueueName, basicProperties: null, body: body);
        }
    }
}

The AutomaticRecoveryEnabled property in the ConnectionFactory ensures that the service can automatically reconnect to RabbitMQ if the connection is lost, preventing downtime.

The IOptions injects RabbitMQ settings from appsettings.json at runtime.

Step 5: Creating the Consumer Service

Create a UserRegistrationConsumerService.cs file in the Services folder. The UserRegistrationConsumerService is responsible for listening to RabbitMQ and processing messages as they arrive. It runs in the background. This service ensures that user registration events are handled asynchronously.

using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using System.Text;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using UserActivityLogger.Settings;

namespace UserActivityLogger.Service
{
    public class UserRegistrationConsumerService : BackgroundService
    {
        private readonly IConnection _connection;
        private readonly IModel _channel;
        private readonly RabbitMQSettings _settings;

        public UserRegistrationConsumerService(IOptions<RabbitMQSettings> settings)
        {
            _settings = settings.Value;

            var factory = new ConnectionFactory()
            {
                HostName = _settings.HostName,
                Port = _settings.Port,
                UserName = _settings.UserName,
                Password = _settings.Password,
                AutomaticRecoveryEnabled = true
            };
            _connection = factory.CreateConnection();
            _channel = _connection.CreateModel();

            // Declares a queue to ensure it exists before consuming messages
            _channel.QueueDeclare(_settings.QueueName, durable: true, exclusive: false, autoDelete: false);
        }

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

            // Event triggered when a message is received
            consumer.Received += (model, ea) =>
            {
                var body = ea.Body.ToArray();
                var message = Encoding.UTF8.GetString(body);
                Console.WriteLine($"User registration event received: {message}");
            };

            // Starts consuming messages from the queue
            _channel.BasicConsume(queue: _settings.QueueName, autoAck: true, consumer: consumer);

            return Task.CompletedTask;
        }
    }
}

Step 6: Creating the User Controller

Create UserController.cs to handle user registration and event publishing:

using Microsoft.AspNetCore.Mvc;
using UserActivityLogger.Model;
using UserActivityLogger.Service;

namespace UserActivityLogger.Controller
{
    [ApiController]
    [Route("api/user")]
    public class UserController : ControllerBase
    {
        private readonly RabbitMQService _rabbitMQService;

        public UserController(RabbitMQService rabbitMQService)
        {
            _rabbitMQService = rabbitMQService;
        }

        [HttpPost("register")]
        public IActionResult RegisterUser([FromBody] User user)
        {
            UserStore.Users.Add(user);
            _rabbitMQService.PublishMessage($"User {user.Username} registered with email {user.Email}");
            return Ok("User registered successfully");
        }
    }
}

Step 7: Registering Services in Program.cs

Modify Program.cs to register services:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSingleton<RabbitMQService>();
builder.Services.AddHostedService<UserRegistrationConsumerService>();

var app = builder.Build();
app.UseAuthorization();
app.MapControllers();
app.Run();

Step 8: Testing the Implementation

  1. Start RabbitMQ (if it’s not running):

     rabbitmq-server
    
  2. Run the API:

     dotnet run
    
  3. Send a user registration request:

     curl -X POST http://localhost:5000/api/user/register -H "Content-Type: application/json" -d '{"username": "johndoe", "email": "john@example.com"}'
    
  4. Check the consumer output, you should see:

     User registration event received: User johndoe registered with email john@example.com
    

    If logs do not appear in the console, you can use the RabbitMQ Management Plugin to verify whether messages are being published and consumed correctly.
    1️. Enable the Management Plugin (If Not Already Enabled)

     rabbitmq-plugins enable rabbitmq_management
    

    After enabling, restart RabbitMQ if necessary.

    2️. Access the RabbitMQ Management UI
    Open a web browser and go to:http://localhost:15672/
    Log in with:

     Username: guest
     Password: guest
    

    3️. Check If Messages Are Being Queued
    Navigate to the Queues tab. Look for the queue user_registration_queue. If messages are present but not being consumed, the consumer might not be processing them correctly.

    4️. Manually Fetch Messages From the Queue
    To manually check for messages waiting in the queue, run:

     rabbitmqadmin get queue=user_registration_queue
    

    This will display messages that are currently in the queue. If the queue is empty, it means the messages are consumed properly.

Next Steps

  • Store registered users in a real database.
  • Implement retry mechanisms for message failures.
  • Scale consumers for higher loads.

Conclusion

By using RabbitMQ, we’ve built an event-driven User Registration Logger that publishes and consumes events asynchronously. This pattern improves scalability and keeps applications responsive. In the next article, we’ll explore how to persist logs in a database and handle errors gracefully.

GitHub Repository

Find the complete source code here.