using System;
using Svrnty.CQRS.Events.RabbitMQ.Configuration;
using Svrnty.CQRS.Events.Abstractions.Subscriptions;
using Svrnty.CQRS.Events.Abstractions.Configuration;
using Svrnty.CQRS.Events.Abstractions.EventStore;
using System.Collections.Generic;
using Microsoft.Extensions.Logging;
using RabbitMQ.Client;
using Svrnty.CQRS.Events.Abstractions;
namespace Svrnty.CQRS.Events.RabbitMQ.Configuration;
///
/// Manages RabbitMQ topology (exchanges, queues, bindings).
///
///
/// This class is responsible for creating and configuring RabbitMQ topology
/// based on the stream and subscription configuration.
///
public sealed class RabbitMQTopologyManager
{
private readonly RabbitMQConfiguration _config;
private readonly ILogger _logger;
public RabbitMQTopologyManager(
RabbitMQConfiguration config,
ILogger logger)
{
_config = config ?? throw new ArgumentNullException(nameof(config));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
///
/// Gets the exchange name for a stream.
///
/// The stream name.
/// The full exchange name.
public string GetExchangeName(string streamName)
{
if (string.IsNullOrWhiteSpace(streamName))
throw new ArgumentException("Stream name cannot be null or whitespace.", nameof(streamName));
return string.IsNullOrWhiteSpace(_config.ExchangePrefix)
? streamName
: $"{_config.ExchangePrefix}.{streamName}";
}
///
/// Gets the queue name for a subscription.
///
/// The subscription ID.
/// The consumer ID (for broadcast mode).
/// The subscription mode.
/// The queue name.
public string GetQueueName(string subscriptionId, string? consumerId, SubscriptionMode mode)
{
if (string.IsNullOrWhiteSpace(subscriptionId))
throw new ArgumentException("Subscription ID cannot be null or whitespace.", nameof(subscriptionId));
var queueName = mode switch
{
SubscriptionMode.Broadcast when !string.IsNullOrWhiteSpace(consumerId) =>
// Each consumer gets its own queue
$"{subscriptionId}.{consumerId}",
SubscriptionMode.Exclusive or SubscriptionMode.ConsumerGroup =>
// All consumers share one queue
subscriptionId,
_ => subscriptionId
};
return string.IsNullOrWhiteSpace(_config.ExchangePrefix)
? queueName
: $"{_config.ExchangePrefix}.{queueName}";
}
///
/// Gets the routing key for an event based on the configured strategy.
///
/// The stream name.
/// The event.
/// The routing key.
public string GetRoutingKey(string streamName, ICorrelatedEvent @event)
{
if (string.IsNullOrWhiteSpace(streamName))
throw new ArgumentException("Stream name cannot be null or whitespace.", nameof(streamName));
if (@event == null)
throw new ArgumentNullException(nameof(@event));
return _config.DefaultRoutingKeyStrategy switch
{
"EventType" => @event.GetType().Name,
"StreamName" => streamName,
"Wildcard" => "#",
_ => @event.GetType().Name
};
}
///
/// Declares an exchange for a stream.
///
/// The RabbitMQ channel.
/// The stream name.
public void DeclareExchange(IChannel channel, string streamName)
{
if (channel == null)
throw new ArgumentNullException(nameof(channel));
if (string.IsNullOrWhiteSpace(streamName))
throw new ArgumentException("Stream name cannot be null or whitespace.", nameof(streamName));
if (!_config.AutoDeclareTopology)
{
_logger.LogDebug("Auto-declare topology is disabled, skipping exchange declaration for {StreamName}", streamName);
return;
}
var exchangeName = GetExchangeName(streamName);
try
{
channel.ExchangeDeclareAsync(
exchange: exchangeName,
type: _config.DefaultExchangeType,
durable: _config.DurableExchanges,
autoDelete: false,
arguments: null).GetAwaiter().GetResult();
_logger.LogInformation(
"Declared exchange {ExchangeName} (type: {ExchangeType}, durable: {Durable})",
exchangeName,
_config.DefaultExchangeType,
_config.DurableExchanges);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to declare exchange {ExchangeName}", exchangeName);
throw;
}
}
///
/// Declares a queue for a subscription.
///
/// The RabbitMQ channel.
/// The subscription ID.
/// The consumer ID (for broadcast mode).
/// The subscription mode.
/// The declared queue name.
public string DeclareQueue(
IChannel channel,
string subscriptionId,
string? consumerId,
SubscriptionMode mode)
{
if (channel == null)
throw new ArgumentNullException(nameof(channel));
if (string.IsNullOrWhiteSpace(subscriptionId))
throw new ArgumentException("Subscription ID cannot be null or whitespace.", nameof(subscriptionId));
if (!_config.AutoDeclareTopology)
{
var queueName = GetQueueName(subscriptionId, consumerId, mode);
_logger.LogDebug("Auto-declare topology is disabled, skipping queue declaration for {QueueName}", queueName);
return queueName;
}
var queue = GetQueueName(subscriptionId, consumerId, mode);
var arguments = new Dictionary();
// Add dead letter exchange if configured
if (!string.IsNullOrWhiteSpace(_config.DeadLetterExchange))
{
arguments["x-dead-letter-exchange"] = _config.DeadLetterExchange;
}
// Add message TTL if configured
if (_config.MessageTTL.HasValue)
{
arguments["x-message-ttl"] = (int)_config.MessageTTL.Value.TotalMilliseconds;
}
// Add max queue length if configured
if (_config.MaxQueueLength.HasValue)
{
arguments["x-max-length"] = _config.MaxQueueLength.Value;
}
try
{
channel.QueueDeclareAsync(
queue: queue,
durable: _config.DurableQueues,
exclusive: false,
autoDelete: mode == SubscriptionMode.Broadcast, // Auto-delete broadcast queues
arguments: arguments.Count > 0 ? arguments : null).GetAwaiter().GetResult();
_logger.LogInformation(
"Declared queue {QueueName} (durable: {Durable}, mode: {Mode})",
queue,
_config.DurableQueues,
mode);
return queue;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to declare queue {QueueName}", queue);
throw;
}
}
///
/// Binds a queue to an exchange with routing keys.
///
/// The RabbitMQ channel.
/// The stream name.
/// The queue name.
/// The routing keys for binding.
public void BindQueue(
IChannel channel,
string streamName,
string queueName,
IEnumerable routingKeys)
{
if (channel == null)
throw new ArgumentNullException(nameof(channel));
if (string.IsNullOrWhiteSpace(streamName))
throw new ArgumentException("Stream name cannot be null or whitespace.", nameof(streamName));
if (string.IsNullOrWhiteSpace(queueName))
throw new ArgumentException("Queue name cannot be null or whitespace.", nameof(queueName));
if (routingKeys == null)
throw new ArgumentNullException(nameof(routingKeys));
if (!_config.AutoDeclareTopology)
{
_logger.LogDebug("Auto-declare topology is disabled, skipping queue binding for {QueueName}", queueName);
return;
}
var exchangeName = GetExchangeName(streamName);
foreach (var routingKey in routingKeys)
{
try
{
channel.QueueBindAsync(
queue: queueName,
exchange: exchangeName,
routingKey: routingKey,
arguments: null).GetAwaiter().GetResult();
_logger.LogInformation(
"Bound queue {QueueName} to exchange {ExchangeName} with routing key {RoutingKey}",
queueName,
exchangeName,
routingKey);
}
catch (Exception ex)
{
_logger.LogError(
ex,
"Failed to bind queue {QueueName} to exchange {ExchangeName} with routing key {RoutingKey}",
queueName,
exchangeName,
routingKey);
throw;
}
}
}
///
/// Declares the dead letter exchange if configured.
///
/// The RabbitMQ channel.
public void DeclareDeadLetterExchange(IChannel channel)
{
if (channel == null)
throw new ArgumentNullException(nameof(channel));
if (string.IsNullOrWhiteSpace(_config.DeadLetterExchange))
return;
if (!_config.AutoDeclareTopology)
{
_logger.LogDebug("Auto-declare topology is disabled, skipping dead letter exchange declaration");
return;
}
try
{
channel.ExchangeDeclareAsync(
exchange: _config.DeadLetterExchange,
type: "topic",
durable: true,
autoDelete: false,
arguments: null).GetAwaiter().GetResult();
_logger.LogInformation("Declared dead letter exchange {ExchangeName}", _config.DeadLetterExchange);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to declare dead letter exchange {ExchangeName}", _config.DeadLetterExchange);
throw;
}
}
}