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 identifiercorrelation-id: Workflow correlation IDtimestamp: 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
- Connection Status:
IsHealthy() - Active Consumers:
GetActiveConsumerCount() - Queue Length: Monitor via RabbitMQ Management UI
- Message Rate: Publish/Consume rates
- 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:
- Check connection string format
- Verify RabbitMQ is running:
docker psorrabbitmqctl status - Check network connectivity:
telnet localhost 5672 - Review firewall rules
Messages Not Delivered
Symptom: Publisher succeeds but consumer doesn't receive messages
Solutions:
- Check exchange exists:
rabbitmqadmin list exchanges - Check queue exists and is bound:
rabbitmqadmin list bindings - Verify routing keys match
- Check consumer is connected:
rabbitmqadmin list consumers
Type Resolution Errors
Symptom: Could not resolve event type
Solutions:
- Ensure event classes have same namespace in both services
- Check
assembly-qualified-nameheader matches actual type - Verify event assemblies are loaded
High Memory Usage
Symptom: Consumer process uses excessive memory
Solutions:
- Lower prefetch count:
options.PrefetchCount = 10; - Add message TTL:
options.MessageTTL = TimeSpan.FromHours(24); - 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