# 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 ```bash dotnet add package Svrnty.CQRS.Events.RabbitMQ ``` ### 2. Configure RabbitMQ Provider ```csharp 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: ```csharp // 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 { public async Task 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 ```csharp // Service B: Consuming Service using Svrnty.CQRS.Events.Abstractions; public class UserEventConsumer : BackgroundService { private readonly IExternalEventDeliveryProvider _rabbitMq; private readonly ILogger _logger; public UserEventConsumer( IExternalEventDeliveryProvider rabbitMq, ILogger 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 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 ```csharp 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 ```csharp 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 ```csharp 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 ```csharp 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 ```csharp 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 ```csharp 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. ```csharp 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. ```csharp // 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:** ```json { "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 ```csharp private async Task HandleEventAsync( ICorrelatedEvent @event, IDictionary 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: ```csharp options.DeadLetterExchange = "dlx.events"; ``` Monitor the DLQ for failed messages: ```bash # 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. ```csharp // Good: Single instance registered in DI services.AddRabbitMQEventDelivery(connectionString); // Bad: Don't create multiple instances manually ``` ### 2. Configure Prefetch Balance throughput vs memory usage: ```csharp 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: ```csharp options.EnablePublisherConfirms = true; options.PublisherConfirmTimeout = TimeSpan.FromSeconds(5); ``` ### 4. Set Message TTL Prevent queue buildup with old messages: ```csharp options.MessageTTL = TimeSpan.FromDays(7); ``` ### 5. Monitor Queue Lengths ```csharp options.MaxQueueLength = 100000; // Prevent unbounded growth ``` ### 6. Use Durable Queues and Exchanges For production: ```csharp options.DurableExchanges = true; options.DurableQueues = true; options.PersistentMessages = true; ``` ### 7. Configure Dead Letter Exchange Always configure DLQ for production: ```csharp options.DeadLetterExchange = "dlx.events"; ``` ## Monitoring ### Health Checks ```csharp var provider = serviceProvider.GetRequiredService(); 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 ```yaml 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 ```bash docker-compose up -d rabbitmq ``` ### Stop RabbitMQ ```bash 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 ```csharp // Publisher sets custom routing key var metadata = new Dictionary { { "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): ```csharp // Set priority in metadata var metadata = new Dictionary { { "priority", "5" } // 0-9, higher = more important }; ``` ### Manual Topology Management If you prefer to manage topology externally: ```csharp options.AutoDeclareTopology = false; ``` Then create exchanges and queues manually via RabbitMQ Management UI or CLI. ## Migration Guide ### From Direct RabbitMQ Usage **Before:** ```csharp 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:** ```csharp // 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