using System; using Svrnty.CQRS.Events.RabbitMQ.Configuration; using System.Collections.Generic; namespace Svrnty.CQRS.Events.RabbitMQ.Configuration; /// /// Configuration for RabbitMQ event delivery provider. /// public sealed class RabbitMQConfiguration { /// /// Gets or sets the RabbitMQ connection string. /// /// /// Format: amqp://username:password@hostname:port/virtualhost /// Example: amqp://guest:guest@localhost:5672/ /// public string ConnectionString { get; set; } = "amqp://guest:guest@localhost:5672/"; /// /// Gets or sets the exchange name prefix. /// /// /// The actual exchange name will be: {ExchangePrefix}.{stream-name} /// Example: "myapp" results in "myapp.user-events" /// Default: Empty string (no prefix) /// public string ExchangePrefix { get; set; } = string.Empty; /// /// Gets or sets the default exchange type. /// /// /// Supported values: "topic", "fanout", "direct", "headers" /// Default: "topic" (recommended for event streaming) /// public string DefaultExchangeType { get; set; } = "topic"; /// /// Gets or sets the default routing key strategy. /// /// /// /// EventTypeRoute by event type name (e.g., "UserCreatedEvent") /// StreamNameRoute by stream name (e.g., "user-events") /// WildcardRoute to all consumers (use "#" routing key for topic exchange) /// /// Default: "EventType" /// public string DefaultRoutingKeyStrategy { get; set; } = "EventType"; /// /// Gets or sets whether to automatically declare exchanges and queues. /// /// /// Default: true (recommended for development) /// Set to false in production if topology is managed externally. /// public bool AutoDeclareTopology { get; set; } = true; /// /// Gets or sets whether exchanges should be durable (survive broker restart). /// /// /// Default: true (recommended for production) /// public bool DurableExchanges { get; set; } = true; /// /// Gets or sets whether queues should be durable. /// /// /// Default: true (recommended for production) /// public bool DurableQueues { get; set; } = true; /// /// Gets or sets whether messages should be persistent. /// /// /// Default: true (messages survive broker restart) /// Set to false for fire-and-forget scenarios. /// public bool PersistentMessages { get; set; } = true; /// /// Gets or sets the prefetch count for consumers. /// /// /// Number of unacknowledged messages each consumer can receive. /// Higher values = better throughput, more memory usage. /// Default: 10 /// public ushort PrefetchCount { get; set; } = 10; /// /// Gets or sets the maximum number of connection retry attempts. /// /// /// Default: 5 /// Set to 0 to disable retries. /// public int MaxConnectionRetries { get; set; } = 5; /// /// Gets or sets the delay between connection retry attempts. /// /// /// Default: 5 seconds /// Exponential backoff is applied. /// public TimeSpan ConnectionRetryDelay { get; set; } = TimeSpan.FromSeconds(5); /// /// Gets or sets the maximum number of publish retry attempts. /// /// /// Default: 3 /// Set to 0 to disable retries. /// public int MaxPublishRetries { get; set; } = 3; /// /// Gets or sets the delay between publish retry attempts. /// /// /// Default: 1 second /// public TimeSpan PublishRetryDelay { get; set; } = TimeSpan.FromSeconds(1); /// /// Gets or sets whether to enable publisher confirms. /// /// /// When enabled, RabbitMQ confirms message receipt. /// Slower but more reliable. /// Default: true (recommended for production) /// public bool EnablePublisherConfirms { get; set; } = true; /// /// Gets or sets the timeout for publisher confirms. /// /// /// Default: 5 seconds /// public TimeSpan PublisherConfirmTimeout { get; set; } = TimeSpan.FromSeconds(5); /// /// Gets or sets the heartbeat interval for connection health checks. /// /// /// Default: 60 seconds /// RabbitMQ will close connections that don't send data within 2x heartbeat. /// public TimeSpan HeartbeatInterval { get; set; } = TimeSpan.FromSeconds(60); /// /// Gets or sets whether to automatically recover from connection failures. /// /// /// Default: true (recommended) /// public bool AutoRecovery { get; set; } = true; /// /// Gets or sets the interval between recovery attempts. /// /// /// Default: 10 seconds /// public TimeSpan RecoveryInterval { get; set; } = TimeSpan.FromSeconds(10); /// /// Gets or sets additional connection properties. /// /// /// These are sent to RabbitMQ during connection handshake. /// Useful for debugging and monitoring. /// public Dictionary ConnectionProperties { get; set; } = new() { { "connection_name", "Svrnty.CQRS.Events" }, { "product", "Svrnty.CQRS" } }; /// /// Gets or sets the dead letter exchange name for failed messages. /// /// /// If null, no dead letter exchange is configured. /// Example: "dlx.events" /// public string? DeadLetterExchange { get; set; } /// /// Gets or sets the time-to-live for messages in queues. /// /// /// If null, messages don't expire. /// Example: TimeSpan.FromDays(7) /// public TimeSpan? MessageTTL { get; set; } /// /// Gets or sets the maximum queue length. /// /// /// If null, no limit. /// Oldest messages are dropped when limit is reached. /// public int? MaxQueueLength { get; set; } /// /// Validates the configuration. /// /// Thrown if configuration is invalid. public void Validate() { if (string.IsNullOrWhiteSpace(ConnectionString)) throw new InvalidOperationException("ConnectionString cannot be null or whitespace."); if (!Uri.TryCreate(ConnectionString, UriKind.Absolute, out var uri) || uri.Scheme != "amqp" && uri.Scheme != "amqps") throw new InvalidOperationException("ConnectionString must be a valid AMQP URI (amqp:// or amqps://)."); var validExchangeTypes = new[] { "topic", "fanout", "direct", "headers" }; if (!validExchangeTypes.Contains(DefaultExchangeType.ToLowerInvariant())) throw new InvalidOperationException($"DefaultExchangeType must be one of: {string.Join(", ", validExchangeTypes)}"); var validRoutingStrategies = new[] { "EventType", "StreamName", "Wildcard" }; if (!validRoutingStrategies.Contains(DefaultRoutingKeyStrategy)) throw new InvalidOperationException($"DefaultRoutingKeyStrategy must be one of: {string.Join(", ", validRoutingStrategies)}"); if (PrefetchCount == 0) throw new InvalidOperationException("PrefetchCount must be greater than 0."); if (MaxConnectionRetries < 0) throw new InvalidOperationException("MaxConnectionRetries cannot be negative."); if (MaxPublishRetries < 0) throw new InvalidOperationException("MaxPublishRetries cannot be negative."); if (ConnectionRetryDelay <= TimeSpan.Zero) throw new InvalidOperationException("ConnectionRetryDelay must be positive."); if (PublishRetryDelay <= TimeSpan.Zero) throw new InvalidOperationException("PublishRetryDelay must be positive."); if (PublisherConfirmTimeout <= TimeSpan.Zero) throw new InvalidOperationException("PublisherConfirmTimeout must be positive."); if (HeartbeatInterval <= TimeSpan.Zero) throw new InvalidOperationException("HeartbeatInterval must be positive."); if (RecoveryInterval <= TimeSpan.Zero) throw new InvalidOperationException("RecoveryInterval must be positive."); if (MessageTTL.HasValue && MessageTTL.Value <= TimeSpan.Zero) throw new InvalidOperationException("MessageTTL must be positive if specified."); if (MaxQueueLength.HasValue && MaxQueueLength.Value <= 0) throw new InvalidOperationException("MaxQueueLength must be positive if specified."); } }