dotnet-cqrs/RABBITMQ-GUIDE.md

15 KiB

RabbitMQ Cross-Service Event Streaming Guide

Phase 4 Feature: Cross-service event streaming via RabbitMQ

Overview

The Svrnty.CQRS.Events.RabbitMQ package provides automatic cross-service event streaming using RabbitMQ as the message broker. Events published by one service can be consumed by other services with zero RabbitMQ knowledge required from developers.

Features

  • Automatic Topology Management - Exchanges, queues, and bindings created automatically
  • Connection Resilience - Automatic reconnection and recovery
  • Publisher Confirms - Reliable message delivery with acknowledgments
  • Consumer Acknowledgments - Manual or automatic ack/nack support
  • Dead Letter Queue - Failed messages automatically routed to DLQ
  • Message Persistence - Messages survive broker restarts
  • Zero Developer Friction - Just configure streams, framework handles RabbitMQ

Quick Start

1. Install Package

dotnet add package Svrnty.CQRS.Events.RabbitMQ

2. Configure RabbitMQ Provider

using Svrnty.CQRS.Events.RabbitMQ;

var builder = WebApplication.CreateBuilder(args);

// Register RabbitMQ event delivery
builder.Services.AddRabbitMQEventDelivery(options =>
{
    options.ConnectionString = "amqp://guest:guest@localhost:5672/";
    options.ExchangePrefix = "myapp";  // Optional: prefix for all exchanges
    options.DefaultExchangeType = "topic";
    options.EnablePublisherConfirms = true;
    options.AutoDeclareTopology = true;  // Auto-create exchanges/queues
});

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

3. Publish Events Externally

Events published from workflows are automatically sent to RabbitMQ when configured:

// Service A: Publishing Service
public class UserCreatedEvent : ICorrelatedEvent
{
    public string EventId { get; set; } = Guid.NewGuid().ToString();
    public string? CorrelationId { get; set; }
    public int UserId { get; set; }
    public string Email { get; set; } = string.Empty;
    public DateTimeOffset CreatedAt { get; set; }
}

public class CreateUserCommandHandler : ICommandHandlerWithWorkflow<CreateUserCommand, int, UserWorkflow>
{
    public async Task<int> HandleAsync(
        CreateUserCommand command,
        UserWorkflow workflow,
        CancellationToken ct)
    {
        // Create user in database
        var userId = await _repository.CreateUserAsync(command.Email);

        // Emit event - this will be published to RabbitMQ
        workflow.Emit(new UserCreatedEvent
        {
            UserId = userId,
            Email = command.Email,
            CreatedAt = DateTimeOffset.UtcNow
        });

        return userId;
    }
}

4. Subscribe to External Events

// Service B: Consuming Service
using Svrnty.CQRS.Events.Abstractions;

public class UserEventConsumer : BackgroundService
{
    private readonly IExternalEventDeliveryProvider _rabbitMq;
    private readonly ILogger<UserEventConsumer> _logger;

    public UserEventConsumer(
        IExternalEventDeliveryProvider rabbitMq,
        ILogger<UserEventConsumer> logger)
    {
        _rabbitMq = rabbitMq;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await _rabbitMq.SubscribeExternalAsync(
            streamName: "user-events",
            subscriptionId: "email-service",
            consumerId: "worker-1",
            eventHandler: HandleEventAsync,
            cancellationToken: stoppingToken);
    }

    private async Task HandleEventAsync(
        ICorrelatedEvent @event,
        IDictionary<string, string> metadata,
        CancellationToken ct)
    {
        switch (@event)
        {
            case UserCreatedEvent userCreated:
                _logger.LogInformation("Sending welcome email to {Email}", userCreated.Email);
                await SendWelcomeEmailAsync(userCreated.Email, ct);
                break;
        }
    }

    private async Task SendWelcomeEmailAsync(string email, CancellationToken ct)
    {
        // Send email logic
        await Task.Delay(100, ct); // Simulate email sending
    }
}

Configuration Reference

Connection Settings

options.ConnectionString = "amqp://username:password@hostname:port/virtualhost";
// Examples:
// - Local: "amqp://guest:guest@localhost:5672/"
// - Remote: "amqp://user:pass@rabbitmq.example.com:5672/production"
// - SSL: "amqps://user:pass@rabbitmq.example.com:5671/"

options.HeartbeatInterval = TimeSpan.FromSeconds(60);
options.AutoRecovery = true;
options.RecoveryInterval = TimeSpan.FromSeconds(10);

Exchange Configuration

options.ExchangePrefix = "myapp";  // Prefix for all exchanges
options.DefaultExchangeType = "topic";  // topic, fanout, direct, headers
options.DurableExchanges = true;  // Survive broker restart
options.AutoDeclareTopology = true;  // Auto-create exchanges

Queue Configuration

options.DurableQueues = true;  // Survive broker restart
options.PrefetchCount = 10;  // Number of unacked messages per consumer
options.MessageTTL = TimeSpan.FromDays(7);  // Message expiration (optional)
options.MaxQueueLength = 10000;  // Max queue size (optional)

Routing Configuration

options.DefaultRoutingKeyStrategy = "EventType";  // EventType, StreamName, Wildcard
// EventType: Routes by event class name (UserCreatedEvent)
// StreamName: Routes by stream name (user-events)
// Wildcard: Routes to all consumers (#)

Reliability Configuration

options.PersistentMessages = true;  // Messages survive broker restart
options.EnablePublisherConfirms = true;  // Wait for broker acknowledgment
options.PublisherConfirmTimeout = TimeSpan.FromSeconds(5);

options.MaxPublishRetries = 3;
options.PublishRetryDelay = TimeSpan.FromSeconds(1);

options.MaxConnectionRetries = 5;
options.ConnectionRetryDelay = TimeSpan.FromSeconds(5);

Dead Letter Queue

options.DeadLetterExchange = "dlx.events";  // Dead letter exchange name
// Failed messages are automatically routed to this exchange

Subscription Modes

Broadcast Mode

Each consumer gets a copy of every event.

await rabbitMq.SubscribeExternalAsync(
    streamName: "user-events",
    subscriptionId: "analytics",
    consumerId: "analytics-worker-1",  // Each worker gets own queue
    eventHandler: HandleEventAsync,
    cancellationToken: stoppingToken);

RabbitMQ Topology:

  • Queue: myapp.analytics.analytics-worker-1 (auto-delete)
  • Binding: All events routed to this queue

Consumer Group Mode

Events load-balanced across multiple consumers.

// Consumer 1
await rabbitMq.SubscribeExternalAsync(
    streamName: "user-events",
    subscriptionId: "email-service",
    consumerId: "worker-1",
    eventHandler: HandleEventAsync,
    cancellationToken: stoppingToken);

// Consumer 2
await rabbitMq.SubscribeExternalAsync(
    streamName: "user-events",
    subscriptionId: "email-service",
    consumerId: "worker-2",
    eventHandler: HandleEventAsync,
    cancellationToken: stoppingToken);

RabbitMQ Topology:

  • Queue: myapp.email-service (shared by all workers)
  • Binding: Events distributed round-robin

Message Format

Events are serialized to JSON with metadata in message headers:

Headers:

  • event-type: Event class name (e.g., "UserCreatedEvent")
  • event-id: Unique event identifier
  • correlation-id: Workflow correlation ID
  • timestamp: Event occurrence time (ISO 8601)
  • assembly-qualified-name: Full type name for deserialization

Body:

{
  "eventId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "correlationId": "workflow-12345",
  "userId": 42,
  "email": "user@example.com",
  "createdAt": "2025-12-10T10:30:00Z"
}

Topology Naming Conventions

Exchange Names

Format: {ExchangePrefix}.{StreamName}

Examples:

  • Stream: user-events, Prefix: myapp → Exchange: myapp.user-events
  • Stream: orders, Prefix: `` → Exchange: orders

Queue Names

Broadcast Mode: Format: {ExchangePrefix}.{SubscriptionId}.{ConsumerId}

Example: myapp.analytics.worker-1

Consumer Group / Exclusive Mode: Format: {ExchangePrefix}.{SubscriptionId}

Example: myapp.email-service

Routing Keys

Determined by DefaultRoutingKeyStrategy:

  • EventType: UserCreatedEvent, OrderPlacedEvent
  • StreamName: user-events, order-events
  • Wildcard: # (matches all)

Error Handling

Automatic Retry

private async Task HandleEventAsync(
    ICorrelatedEvent @event,
    IDictionary<string, string> metadata,
    CancellationToken ct)
{
    try
    {
        // Process event
        await ProcessEventAsync(@event);
        // Auto-ACK on success (if using default behavior)
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Failed to process event {EventId}", @event.EventId);
        // Auto-NACK with requeue on exception
        throw;
    }
}

Dead Letter Queue

Events that fail after max retries are sent to the dead letter exchange:

options.DeadLetterExchange = "dlx.events";

Monitor the DLQ for failed messages:

# List messages in DLQ
rabbitmqadmin get queue=dlx.events count=10

Production Best Practices

1. Use Connection Pooling

RabbitMQ provider automatically manages connections. Don't create multiple instances.

// Good: Single instance registered in DI
services.AddRabbitMQEventDelivery(connectionString);

// Bad: Don't create multiple instances manually

2. Configure Prefetch

Balance throughput vs memory usage:

options.PrefetchCount = 10;  // Low: Better for heavy processing
options.PrefetchCount = 100; // High: Better for lightweight processing

3. Enable Publisher Confirms

For critical events, always enable confirms:

options.EnablePublisherConfirms = true;
options.PublisherConfirmTimeout = TimeSpan.FromSeconds(5);

4. Set Message TTL

Prevent queue buildup with old messages:

options.MessageTTL = TimeSpan.FromDays(7);

5. Monitor Queue Lengths

options.MaxQueueLength = 100000;  // Prevent unbounded growth

6. Use Durable Queues and Exchanges

For production:

options.DurableExchanges = true;
options.DurableQueues = true;
options.PersistentMessages = true;

7. Configure Dead Letter Exchange

Always configure DLQ for production:

options.DeadLetterExchange = "dlx.events";

Monitoring

Health Checks

var provider = serviceProvider.GetRequiredService<IExternalEventDeliveryProvider>();

if (provider.IsHealthy())
{
    Console.WriteLine($"RabbitMQ is healthy. Active consumers: {provider.GetActiveConsumerCount()}");
}
else
{
    Console.WriteLine("RabbitMQ connection is down!");
}

Metrics to Monitor

  1. Connection Status: IsHealthy()
  2. Active Consumers: GetActiveConsumerCount()
  3. Queue Length: Monitor via RabbitMQ Management UI
  4. Message Rate: Publish/Consume rates
  5. Error Rate: Failed messages / DLQ depth

RabbitMQ Management UI

Access at http://localhost:15672 (default credentials: guest/guest)

Monitor:

  • Exchanges and their message rates
  • Queues and their depths
  • Connections and channels
  • Consumer status

Troubleshooting

Connection Failures

Symptom: Failed to connect to RabbitMQ

Solutions:

  1. Check connection string format
  2. Verify RabbitMQ is running: docker ps or rabbitmqctl status
  3. Check network connectivity: telnet localhost 5672
  4. Review firewall rules

Messages Not Delivered

Symptom: Publisher succeeds but consumer doesn't receive messages

Solutions:

  1. Check exchange exists: rabbitmqadmin list exchanges
  2. Check queue exists and is bound: rabbitmqadmin list bindings
  3. Verify routing keys match
  4. Check consumer is connected: rabbitmqadmin list consumers

Type Resolution Errors

Symptom: Could not resolve event type

Solutions:

  1. Ensure event classes have same namespace in both services
  2. Check assembly-qualified-name header matches actual type
  3. Verify event assemblies are loaded

High Memory Usage

Symptom: Consumer process uses excessive memory

Solutions:

  1. Lower prefetch count: options.PrefetchCount = 10;
  2. Add message TTL: options.MessageTTL = TimeSpan.FromHours(24);
  3. Implement backpressure in event handlers

Docker Setup

docker-compose.yml

version: '3.8'

services:
  rabbitmq:
    image: rabbitmq:3-management-alpine
    container_name: rabbitmq
    ports:
      - "5672:5672"    # AMQP
      - "15672:15672"  # Management UI
    environment:
      RABBITMQ_DEFAULT_USER: guest
      RABBITMQ_DEFAULT_PASS: guest
    volumes:
      - rabbitmq_data:/var/lib/rabbitmq
    healthcheck:
      test: rabbitmq-diagnostics -q ping
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  rabbitmq_data:

Start RabbitMQ

docker-compose up -d rabbitmq

Stop RabbitMQ

docker-compose down

Example: Cross-Service Communication

See CROSS-SERVICE-EXAMPLE.md for a complete example with two microservices communicating via RabbitMQ.

Advanced Topics

Custom Routing Keys

// Publisher sets custom routing key
var metadata = new Dictionary<string, string>
{
    { "routing-key", "user.created.premium" }
};

await rabbitMq.PublishExternalAsync(
    streamName: "user-events",
    @event: userCreatedEvent,
    metadata: metadata);

Message Priority

RabbitMQ supports message priority (requires queue declaration with priority support):

// Set priority in metadata
var metadata = new Dictionary<string, string>
{
    { "priority", "5" }  // 0-9, higher = more important
};

Manual Topology Management

If you prefer to manage topology externally:

options.AutoDeclareTopology = false;

Then create exchanges and queues manually via RabbitMQ Management UI or CLI.

Migration Guide

From Direct RabbitMQ Usage

Before:

var factory = new ConnectionFactory { Uri = new Uri("amqp://localhost") };
using var connection = await factory.CreateConnectionAsync();
using var channel = await connection.CreateChannelAsync();

await channel.ExchangeDeclareAsync("user-events", "topic", durable: true);
await channel.QueueDeclareAsync("email-service", durable: true);
await channel.QueueBindAsync("email-service", "user-events", "#");

// Complex publish logic...

After:

// Just configuration
services.AddRabbitMQEventDelivery("amqp://localhost");

// Events automatically published
workflow.Emit(new UserCreatedEvent { ... });

Summary

The RabbitMQ integration provides enterprise-grade cross-service event streaming with minimal configuration. The framework handles all RabbitMQ complexity, allowing developers to focus on business logic.

Key Benefits:

  • Zero RabbitMQ knowledge required
  • Production-ready out of the box
  • Automatic topology management
  • Built-in resilience and reliability
  • Comprehensive monitoring and logging

For questions or issues, see the main repository: https://git.openharbor.io/svrnty/dotnet-cqrs