Fix RabbitMQ Consumer Not Working in ASP.NET Core with BackgroundService

Summary

The consumer was never triggered because the AddConsumer method was registered as a delegate and never invoked.
builder.Services.AddSingleton(new RMQConfig().AddConsumer); creates an instance of RMQConfig and stores the method group (Action) in the DI container, but the ASP.NET Core runtime never calls that delegate during startup, so the consumer never starts listening.


Root Cause

  • Method never invokedAddConsumer is async void and only attached to DI as a delegate, not executed.
  • Scope leakage – Opening connections inside async void with using disposes them immediately after the method ends, preventing long‑lived consumers.
  • No proper registration – Missing implementation of a background service to keep the consumer alive.

Why This Happens in Real Systems

  • Developers often copy/paste sample code into a class and presume the framework will run it.
  • async void is traditionally used for event handlers; using it for initialization leads to silent failures.
  • Lifetimes of connections are misunderstood; using disposes them too early.
  • Background work is usually forgotten when service registration is minimal.

Real-World Impact

  • Lost messages: Producers publish, consumers never receive.
  • Silent failures: Breakpoints never hit, no diagnostics.
  • Resource inefficiency: Unused connections are created then discarded.
  • Operational risk: Systems appear to work until a critical trade is missed.

Example or Code (if necessary and relevant)

// Correct consumer registration as a hosted service
public class RabbitMqConsumerService : BackgroundService
{
    private readonly IConnection _connection;
    private readonly IModel _channel;
    private readonly string _queueName = "hello";

    public RabbitMqConsumerService()
    {
        var factory = new ConnectionFactory { HostName = "localhost" };
        _connection = factory.CreateConnection();
        _channel = _connection.CreateModel();

        _channel.QueueDeclare(queue: _queueName, durable: false, exclusive: false, autoDelete: false, arguments: null);
    }

    protected override Task ExecuteAsync(CancellationToken stoppingToken)
    {
        var consumer = new AsyncEventingBasicConsumer(_channel);
        consumer.ReceivedAsync += async (model, ea) =>
        {
            var body = ea.Body.ToArray();
            var message = Encoding.UTF8.GetString(body);
            Console.WriteLine($" [x] Received {message}");
            await Task.CompletedTask;
        };

        _channel.BasicConsume(queue: _queueName, autoAck: true, consumer: consumer);
        return Task.CompletedTask;
    }

    public override void Dispose()
    {
        _channel?.Dispose();
        _connection?.Dispose();
        base.Dispose();
    }
}

In Program.cs:

builder.Services.AddHostedService();

Now the consumer runs for the lifetime of the application and receives messages.


How Senior Engineers Fix It

  • Use a hosted background service (BackgroundService or IHostedService) for long‑running tasks.
  • Avoid async void – use Task or Task<T> for async operations.
  • Register the service in DI so the framework calls it during startup.
  • Keep connections alive – do not dispose them inside the initialization method.
  • Add diagnostics – log connection status and message receipt.

Why Juniors Miss It

  • Misunderstanding of DI vs. execution flow: registering a delegate does not execute it.
  • Familiarity with async void from event handlers leads to incorrect usage.
  • Lack of awareness that using blocks dispose objects immediately, killing the consumer.
  • Neglecting to register background services, assuming static code runs automatically.

Leave a Comment