diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index de1fc606..b7d130ae 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -10,7 +10,7 @@
- When modifying a public member, make sure to update the XML Documentation and the corresponding docs in the docs folder and the README file if applicable.
- When modifying a public member, make sure to check the Public.API files for the affected assembly and update them if necessary.
- Do not ignore CS1591 warnings; analyze and add missing XML comments instead.
-- Do not add comments inside methods; for complex logic, consider extracting it into a separate method with a descriptive name instead of adding comments.
+- Comments inside method bodies must explain *why*, not *what*: a why-comment states a constraint, invariant, or rationale the code cannot express on its own (e.g. concurrency/ordering requirements, the reason for a workaround, a justification for otherwise-surprising deliberate behavior). Narration/what-comments — restating what the next line does, section-header comments, play-by-play commentary — are not allowed. For complex logic, prefer extracting it into a separate method with a descriptive name over adding a comment.
## Testing Guidelines
- Prefer using the Vulthil.xUnit testing framework for tests.
diff --git a/src/Vulthil.Messaging/Partitioning/PartitionSpec.cs b/src/Vulthil.Messaging/PartitionSpec.cs
similarity index 100%
rename from src/Vulthil.Messaging/Partitioning/PartitionSpec.cs
rename to src/Vulthil.Messaging/PartitionSpec.cs
diff --git a/src/Vulthil.Messaging/Partitioning/Partitioner.cs b/src/Vulthil.Messaging/Partitioner.cs
similarity index 100%
rename from src/Vulthil.Messaging/Partitioning/Partitioner.cs
rename to src/Vulthil.Messaging/Partitioner.cs
diff --git a/src/Vulthil.Messaging/Queues/DeadLetterDefinition.cs b/src/Vulthil.Messaging/Queues/DeadLetterDefinition.cs
new file mode 100644
index 00000000..edc26de5
--- /dev/null
+++ b/src/Vulthil.Messaging/Queues/DeadLetterDefinition.cs
@@ -0,0 +1,20 @@
+namespace Vulthil.Messaging.Queues;
+
+///
+/// Describes the dead letter queue and exchange configuration.
+///
+public sealed record DeadLetterDefinition
+{
+ ///
+ /// Gets or sets the dead letter queue name, or to use the default.
+ ///
+ public string? QueueName { get; set; }
+ ///
+ /// Gets or sets the dead letter exchange name, or to use the default.
+ ///
+ public string? ExchangeName { get; set; }
+ ///
+ /// Gets or sets a value indicating whether dead letter routing is enabled.
+ ///
+ public bool Enabled { get; set; }
+}
diff --git a/src/Vulthil.Messaging/Queues/IQueueConfigurator.cs b/src/Vulthil.Messaging/Queues/IQueueConfigurator.cs
index ed38b720..f52561dc 100644
--- a/src/Vulthil.Messaging/Queues/IQueueConfigurator.cs
+++ b/src/Vulthil.Messaging/Queues/IQueueConfigurator.cs
@@ -1,5 +1,4 @@
using System.Reflection;
-using System.Security.Cryptography;
using Vulthil.Messaging.Abstractions.Consumers;
namespace Vulthil.Messaging.Queues;
@@ -79,263 +78,3 @@ public interface IQueueConfigurator
///
IQueueConfigurator UseSingleActiveConsumer();
}
-
-///
-/// Defines a retry policy including intervals, jitter, and exception filtering.
-///
-public sealed record RetryPolicyDefinition
-{
- ///
- /// Gets or sets the maximum number of retry attempts.
- ///
- public int MaxRetryCount { get; set; }
- ///
- /// Gets or sets the jitter factor (0.0–1.0) applied to retry intervals.
- ///
- public double JitterFactor { get; set; }
- ///
- /// Gets or sets a value indicating whether retries run in-memory — the consumer is re-invoked in-process
- /// while the delivery is held — rather than via delayed re-delivery through the retry queue. In-memory
- /// retries preserve message order (a later message cannot overtake the one being retried), so they are
- /// used automatically for partitioned queues. Defaults to (delayed re-delivery).
- ///
- public bool InMemory { get; set; }
- ///
- /// Gets the delay intervals between successive retry attempts.
- ///
- public ICollection Intervals { get; } = [];
- ///
- /// Gets the fully-qualified type names of exceptions that should be excluded from retry attempts.
- /// Names outside the core library must be assembly-qualified to resolve; a name that cannot be
- /// resolved is skipped (the RabbitMQ transport logs a startup warning for each such name).
- ///
- public ICollection IgnoreExceptions { get; } = [];
-
- private readonly object _ignoredTypesGate = new();
- private readonly HashSet _ignoredTypes = [];
- private HashSet? _resolvedIgnoredTypes;
-
- internal void AddIgnoredType(Type type) => _ignoredTypes.Add(type);
- ///
- /// Gets the resolved CLR exception types that should be excluded from retry attempts. The set is built
- /// once, on first access, from the fluently ignored types and the resolvable names in
- /// ; the resolution is thread-safe and later mutations of either
- /// collection do not change the result.
- ///
- public HashSet GetIgnoredExceptionTypes()
- {
- lock (_ignoredTypesGate)
- {
- return _resolvedIgnoredTypes ??= ResolveIgnoredExceptionTypes();
- }
- }
-
- private HashSet ResolveIgnoredExceptionTypes()
- {
- var resolved = new HashSet(_ignoredTypes);
- foreach (var name in IgnoreExceptions)
- {
- if (Type.GetType(name, false) is { } type)
- {
- resolved.Add(type);
- }
- }
-
- return resolved;
- }
-
- ///
- /// Calculates the delay for the specified retry attempt, applying jitter when configured.
- /// Attempts beyond the last configured interval reuse that interval (capped back-off).
- ///
- /// The zero-based attempt index.
- /// The delay before the next retry.
- public TimeSpan GetDelay(int attempt)
- {
- if (Intervals.Count == 0)
- {
- return TimeSpan.Zero;
- }
-
- var interval = GetIntervalForAttempt(attempt);
- if (JitterFactor <= 0.0)
- {
- return interval;
- }
-
- var jitterMultiplier = RandomNumberGeneratorExtensions.GetDouble() * 2 * JitterFactor - JitterFactor;
- var finalDelay = interval.TotalMilliseconds * (1 + jitterMultiplier);
- return TimeSpan.FromMilliseconds(Math.Max(0, finalDelay));
- }
-
- private TimeSpan GetIntervalForAttempt(int attempt)
- {
- var index = Math.Min(attempt, Intervals.Count - 1);
- return Intervals is IList list ? list[index] : Intervals.ElementAt(index);
- }
-}
-
-internal static class RandomNumberGeneratorExtensions
-{
- ///
- /// Generates a cryptographically random in the range [0, 1). Retry jitter does
- /// not need cryptographic strength, but the repository enforces CA5394 (no ) as an
- /// error, so the secure generator is used here as well.
- ///
- public static double GetDouble()
- {
- var bytes = RandomNumberGenerator.GetBytes(8);
- var ul = BitConverter.ToUInt64(bytes, 0) / (1 << 11);
- return ul / (double)(1UL << 53);
- }
-}
-
-///
-/// Describes the dead letter queue and exchange configuration.
-///
-public sealed record DeadLetterDefinition
-{
- ///
- /// Gets or sets the dead letter queue name, or to use the default.
- ///
- public string? QueueName { get; set; }
- ///
- /// Gets or sets the dead letter exchange name, or to use the default.
- ///
- public string? ExchangeName { get; set; }
- ///
- /// Gets or sets a value indicating whether dead letter routing is enabled.
- ///
- public bool Enabled { get; set; }
-}
-
-///
-/// Fluent builder for constructing a .
-///
-public sealed class RetryPolicyConfigurator
-{
- private readonly HashSet _ignoredExceptions = [];
- private List _intervals = [];
- ///
- /// Gets the maximum number of retry attempts configured via one of the interval methods.
- ///
- public int RetryLimit { get; private set; }
- private double _jitterFactor;
- private bool _inMemory;
- ///
- /// Gets the configured delay intervals between successive retry attempts.
- ///
- public IReadOnlyList Intervals => _intervals;
- ///
- /// Gets the exception types that should be excluded from retry attempts and immediately surfaced.
- ///
- public IReadOnlySet IgnoredExceptions => _ignoredExceptions;
-
- ///
- /// Configures immediate retries with zero delay.
- ///
- /// The number of retries.
- public void Immediate(int retryCount)
- {
- RetryLimit = retryCount;
- _intervals = [.. Enumerable.Repeat(TimeSpan.Zero, retryCount)];
- }
-
- ///
- /// Configures explicit retry intervals.
- ///
- /// The delay between each retry attempt.
- public void SetIntervals(params TimeSpan[] intervals)
- {
- RetryLimit = intervals.Length;
- _intervals = [.. intervals];
- }
- ///
- /// Adds random jitter to retry intervals.
- ///
- /// The jitter factor between 0.0 and 1.0.
- /// Thrown when is outside the valid range.
- public void UseJitter(double factor)
- {
- if (factor is < 0 or > 1.0)
- {
- throw new ArgumentOutOfRangeException(nameof(factor), "Jitter must be between 0.0 and 1.0");
- }
-
- _jitterFactor = factor;
- }
- ///
- /// Configures exponential back-off retry intervals.
- ///
- /// The number of retries.
- /// The delay for the first retry.
- /// The maximum delay cap.
- /// The multiplier applied to each successive interval. Default is 2.0.
- public void Exponential(
- int retryCount,
- TimeSpan initialInterval,
- TimeSpan maxInterval,
- double multiplier = 2.0)
- {
- RetryLimit = retryCount;
- var intervals = new List();
- var currentInterval = initialInterval;
-
- for (int i = 0; i < retryCount; i++)
- {
- intervals.Add(currentInterval);
-
- // Calculate next interval
- double nextTicks = currentInterval.Ticks * multiplier;
- currentInterval = TimeSpan.FromTicks((long)nextTicks);
-
- // Cap it at the max interval
- if (currentInterval > maxInterval)
- {
- currentInterval = maxInterval;
- }
- }
-
- _intervals = [.. intervals];
- }
-
- ///
- /// Adds an exception type to the ignore list; matching exceptions will not trigger retries.
- ///
- /// The exception type to ignore.
- public void Ignore() where TException : Exception => _ignoredExceptions.Add(typeof(TException));
-
- ///
- /// Configures retries to run in-memory: the consumer is re-invoked in-process while the delivery is held,
- /// instead of being re-delivered later through the retry queue. This preserves message order, so it is the
- /// behavior used automatically for partitioned queues.
- ///
- public void InMemory() => _inMemory = true;
-
- ///
- /// Builds the from the current configuration.
- ///
- /// The constructed retry policy definition.
- public RetryPolicyDefinition Build()
- {
- var definition = new RetryPolicyDefinition
- {
- MaxRetryCount = RetryLimit,
- JitterFactor = _jitterFactor,
- InMemory = _inMemory
- };
-
- foreach (var interval in _intervals)
- {
- definition.Intervals.Add(interval);
- }
-
- foreach (var type in _ignoredExceptions)
- {
- definition.AddIgnoredType(type);
- definition.IgnoreExceptions.Add(type.AssemblyQualifiedName!);
- }
-
- return definition;
- }
-}
diff --git a/src/Vulthil.Messaging/Queues/QueueConfigurator.cs b/src/Vulthil.Messaging/Queues/QueueConfigurator.cs
index 45a4092e..f1bf013d 100644
--- a/src/Vulthil.Messaging/Queues/QueueConfigurator.cs
+++ b/src/Vulthil.Messaging/Queues/QueueConfigurator.cs
@@ -75,7 +75,6 @@ public IQueueConfigurator AddRequestConsumer(Action
internal void Build()
{
- // 1. Auto-subscribe any concrete TMessage from consumer registrations not yet subscribed.
- // Routing keys are a Subscription-level concern, so auto-subscribed
- // subscriptions get a null routing key (broker uses an empty pattern). For direct/topic exchanges that
- // require a specific pattern, the caller must explicitly q.Subscribe("pattern") first.
+ // Routing keys are a Subscription-level concern, so auto-subscribed subscriptions get a null routing
+ // key (broker uses an empty pattern). For direct/topic exchanges that require a specific pattern, the
+ // caller must explicitly q.Subscribe("pattern") first.
var concreteConsumerMessageTypes = _queueDefinition.Registrations
.Select(r => r.MessageType)
.Where(m => m.Type is { IsAbstract: false, IsInterface: false });
@@ -181,7 +179,7 @@ internal void Build()
_queueDefinition.AddSubscription(new Subscription(messageType));
}
- // 2. Request consumers cannot be polymorphic — the response type is fixed and can't be selected
+ // Request consumers cannot be polymorphic — the response type is fixed and can't be selected
// by the incoming concrete type.
foreach (var rpc in _queueDefinition.Registrations.OfType())
{
@@ -194,7 +192,6 @@ internal void Build()
}
}
- // 3. Every consumer must have at least one matching concrete subscription.
var orphanConsumer = _queueDefinition.Registrations.FirstOrDefault(
r => !_queueDefinition.Subscriptions.Any(s => r.MessageType.Type.IsAssignableFrom(s.MessageType.Type)));
if (orphanConsumer is not null)
@@ -205,7 +202,6 @@ internal void Build()
"Call q.Subscribe() or q.SubscribeAll(assembly) for at least one implementer.");
}
- // 4. Every subscription must have at least one matching consumer.
var orphanSubscription = _queueDefinition.Subscriptions.FirstOrDefault(
s => !_queueDefinition.Registrations.Any(r => r.MessageType.Type.IsAssignableFrom(s.MessageType.Type)));
if (orphanSubscription is not null)
diff --git a/src/Vulthil.Messaging/Queues/RandomNumberGeneratorExtensions.cs b/src/Vulthil.Messaging/Queues/RandomNumberGeneratorExtensions.cs
new file mode 100644
index 00000000..d27793ae
--- /dev/null
+++ b/src/Vulthil.Messaging/Queues/RandomNumberGeneratorExtensions.cs
@@ -0,0 +1,18 @@
+using System.Security.Cryptography;
+
+namespace Vulthil.Messaging.Queues;
+
+internal static class RandomNumberGeneratorExtensions
+{
+ ///
+ /// Generates a cryptographically random in the range [0, 1). Retry jitter does
+ /// not need cryptographic strength, but the repository enforces CA5394 (no ) as an
+ /// error, so the secure generator is used here as well.
+ ///
+ public static double GetDouble()
+ {
+ var bytes = RandomNumberGenerator.GetBytes(8);
+ var ul = BitConverter.ToUInt64(bytes, 0) / (1 << 11);
+ return ul / (double)(1UL << 53);
+ }
+}
diff --git a/src/Vulthil.Messaging/Queues/RetryPolicyConfigurator.cs b/src/Vulthil.Messaging/Queues/RetryPolicyConfigurator.cs
new file mode 100644
index 00000000..9bd0f694
--- /dev/null
+++ b/src/Vulthil.Messaging/Queues/RetryPolicyConfigurator.cs
@@ -0,0 +1,130 @@
+namespace Vulthil.Messaging.Queues;
+
+///
+/// Fluent builder for constructing a .
+///
+public sealed class RetryPolicyConfigurator
+{
+ private readonly HashSet _ignoredExceptions = [];
+ private List _intervals = [];
+ ///
+ /// Gets the maximum number of retry attempts configured via one of the interval methods.
+ ///
+ public int RetryLimit { get; private set; }
+ private double _jitterFactor;
+ private bool _inMemory;
+ ///
+ /// Gets the configured delay intervals between successive retry attempts.
+ ///
+ public IReadOnlyList Intervals => _intervals;
+ ///
+ /// Gets the exception types that should be excluded from retry attempts and immediately surfaced.
+ ///
+ public IReadOnlySet IgnoredExceptions => _ignoredExceptions;
+
+ ///
+ /// Configures immediate retries with zero delay.
+ ///
+ /// The number of retries.
+ public void Immediate(int retryCount)
+ {
+ RetryLimit = retryCount;
+ _intervals = [.. Enumerable.Repeat(TimeSpan.Zero, retryCount)];
+ }
+
+ ///
+ /// Configures explicit retry intervals.
+ ///
+ /// The delay between each retry attempt.
+ public void SetIntervals(params TimeSpan[] intervals)
+ {
+ RetryLimit = intervals.Length;
+ _intervals = [.. intervals];
+ }
+ ///
+ /// Adds random jitter to retry intervals.
+ ///
+ /// The jitter factor between 0.0 and 1.0.
+ /// Thrown when is outside the valid range.
+ public void UseJitter(double factor)
+ {
+ if (factor is < 0 or > 1.0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(factor), "Jitter must be between 0.0 and 1.0");
+ }
+
+ _jitterFactor = factor;
+ }
+ ///
+ /// Configures exponential back-off retry intervals.
+ ///
+ /// The number of retries.
+ /// The delay for the first retry.
+ /// The maximum delay cap.
+ /// The multiplier applied to each successive interval. Default is 2.0.
+ public void Exponential(
+ int retryCount,
+ TimeSpan initialInterval,
+ TimeSpan maxInterval,
+ double multiplier = 2.0)
+ {
+ RetryLimit = retryCount;
+ var intervals = new List();
+ var currentInterval = initialInterval;
+
+ for (int i = 0; i < retryCount; i++)
+ {
+ intervals.Add(currentInterval);
+
+ double nextTicks = currentInterval.Ticks * multiplier;
+ currentInterval = TimeSpan.FromTicks((long)nextTicks);
+
+ if (currentInterval > maxInterval)
+ {
+ currentInterval = maxInterval;
+ }
+ }
+
+ _intervals = [.. intervals];
+ }
+
+ ///
+ /// Adds an exception type to the ignore list; matching exceptions will not trigger retries.
+ ///
+ /// The exception type to ignore.
+ public void Ignore() where TException : Exception => _ignoredExceptions.Add(typeof(TException));
+
+ ///
+ /// Configures retries to run in-memory: the consumer is re-invoked in-process while the delivery is held,
+ /// instead of being re-delivered later through the retry queue. This preserves message order, so it is the
+ /// behavior used automatically for partitioned queues.
+ ///
+ public void InMemory() => _inMemory = true;
+
+ ///
+ /// Builds the from the current configuration.
+ ///
+ /// The constructed retry policy definition.
+ public RetryPolicyDefinition Build()
+ {
+ var definition = new RetryPolicyDefinition
+ {
+ MaxRetryCount = RetryLimit,
+ JitterFactor = _jitterFactor,
+ InMemory = _inMemory
+ };
+
+ foreach (var interval in _intervals)
+ {
+ definition.Intervals.Add(interval);
+ }
+
+ foreach (var type in _ignoredExceptions)
+ {
+ definition.AddIgnoredType(type);
+ definition.IgnoreExceptions.Add(type.AssemblyQualifiedName!);
+ }
+
+ return definition;
+ }
+}
diff --git a/src/Vulthil.Messaging/Queues/RetryPolicyDefinition.cs b/src/Vulthil.Messaging/Queues/RetryPolicyDefinition.cs
new file mode 100644
index 00000000..76bd5022
--- /dev/null
+++ b/src/Vulthil.Messaging/Queues/RetryPolicyDefinition.cs
@@ -0,0 +1,96 @@
+namespace Vulthil.Messaging.Queues;
+
+///
+/// Defines a retry policy including intervals, jitter, and exception filtering.
+///
+public sealed record RetryPolicyDefinition
+{
+ ///
+ /// Gets or sets the maximum number of retry attempts.
+ ///
+ public int MaxRetryCount { get; set; }
+ ///
+ /// Gets or sets the jitter factor (0.0–1.0) applied to retry intervals.
+ ///
+ public double JitterFactor { get; set; }
+ ///
+ /// Gets or sets a value indicating whether retries run in-memory — the consumer is re-invoked in-process
+ /// while the delivery is held — rather than via delayed re-delivery through the retry queue. In-memory
+ /// retries preserve message order (a later message cannot overtake the one being retried), so they are
+ /// used automatically for partitioned queues. Defaults to (delayed re-delivery).
+ ///
+ public bool InMemory { get; set; }
+ ///
+ /// Gets the delay intervals between successive retry attempts.
+ ///
+ public ICollection Intervals { get; } = [];
+ ///
+ /// Gets the fully-qualified type names of exceptions that should be excluded from retry attempts.
+ /// Names outside the core library must be assembly-qualified to resolve; a name that cannot be
+ /// resolved is skipped (the RabbitMQ transport logs a startup warning for each such name).
+ ///
+ public ICollection IgnoreExceptions { get; } = [];
+
+ private readonly object _ignoredTypesGate = new();
+ private readonly HashSet _ignoredTypes = [];
+ private HashSet? _resolvedIgnoredTypes;
+
+ internal void AddIgnoredType(Type type) => _ignoredTypes.Add(type);
+ ///
+ /// Gets the resolved CLR exception types that should be excluded from retry attempts. The set is built
+ /// once, on first access, from the fluently ignored types and the resolvable names in
+ /// ; the resolution is thread-safe and later mutations of either
+ /// collection do not change the result.
+ ///
+ public HashSet GetIgnoredExceptionTypes()
+ {
+ lock (_ignoredTypesGate)
+ {
+ return _resolvedIgnoredTypes ??= ResolveIgnoredExceptionTypes();
+ }
+ }
+
+ private HashSet ResolveIgnoredExceptionTypes()
+ {
+ var resolved = new HashSet(_ignoredTypes);
+ foreach (var name in IgnoreExceptions)
+ {
+ if (Type.GetType(name, false) is { } type)
+ {
+ resolved.Add(type);
+ }
+ }
+
+ return resolved;
+ }
+
+ ///
+ /// Calculates the delay for the specified retry attempt, applying jitter when configured.
+ /// Attempts beyond the last configured interval reuse that interval (capped back-off).
+ ///
+ /// The zero-based attempt index.
+ /// The delay before the next retry.
+ public TimeSpan GetDelay(int attempt)
+ {
+ if (Intervals.Count == 0)
+ {
+ return TimeSpan.Zero;
+ }
+
+ var interval = GetIntervalForAttempt(attempt);
+ if (JitterFactor <= 0.0)
+ {
+ return interval;
+ }
+
+ var jitterMultiplier = RandomNumberGeneratorExtensions.GetDouble() * 2 * JitterFactor - JitterFactor;
+ var finalDelay = interval.TotalMilliseconds * (1 + jitterMultiplier);
+ return TimeSpan.FromMilliseconds(Math.Max(0, finalDelay));
+ }
+
+ private TimeSpan GetIntervalForAttempt(int attempt)
+ {
+ var index = Math.Min(attempt, Intervals.Count - 1);
+ return Intervals is IList list ? list[index] : Intervals.ElementAt(index);
+ }
+}
diff --git a/src/Vulthil.SharedKernel.Infrastructure.Cosmos/DependencyInjectionExtensions.cs b/src/Vulthil.SharedKernel.Infrastructure.Cosmos/DependencyInjectionExtensions.cs
index 357941e6..ab856a89 100644
--- a/src/Vulthil.SharedKernel.Infrastructure.Cosmos/DependencyInjectionExtensions.cs
+++ b/src/Vulthil.SharedKernel.Infrastructure.Cosmos/DependencyInjectionExtensions.cs
@@ -25,6 +25,8 @@ public static IDatabaseInfrastructureConfigurator UseCosmosDb? configureDbContextOptions = null)
where TDbContext : DbContext, ISaveOutboxMessages
{
+ ArgumentNullException.ThrowIfNull(configurator);
+
configurator.OnConfigured(c =>
{
if (c is not DatabaseInfrastructureConfigurator { OutboxStoreCustomized: true })
diff --git a/src/Vulthil.SharedKernel.Infrastructure.Relational/OutboxCommitInterceptor.cs b/src/Vulthil.SharedKernel.Infrastructure.Relational/OutboxProcessing/OutboxCommitInterceptor.cs
similarity index 100%
rename from src/Vulthil.SharedKernel.Infrastructure.Relational/OutboxCommitInterceptor.cs
rename to src/Vulthil.SharedKernel.Infrastructure.Relational/OutboxProcessing/OutboxCommitInterceptor.cs
diff --git a/src/Vulthil.SharedKernel.Infrastructure.Relational/RelationalOutboxStore.cs b/src/Vulthil.SharedKernel.Infrastructure.Relational/OutboxProcessing/RelationalOutboxStore.cs
similarity index 100%
rename from src/Vulthil.SharedKernel.Infrastructure.Relational/RelationalOutboxStore.cs
rename to src/Vulthil.SharedKernel.Infrastructure.Relational/OutboxProcessing/RelationalOutboxStore.cs
diff --git a/tests/Vulthil.IntegrationTests/ProviderOutboxRegistrationTests.cs b/tests/Vulthil.IntegrationTests/ProviderOutboxRegistrationTests.cs
index 672bf0fb..e8593640 100644
--- a/tests/Vulthil.IntegrationTests/ProviderOutboxRegistrationTests.cs
+++ b/tests/Vulthil.IntegrationTests/ProviderOutboxRegistrationTests.cs
@@ -170,6 +170,19 @@ public void UseCosmosDbPreservesAUserStoreSelectedAfterIt()
OutboxStoreDescriptor(builder).ImplementationType.ShouldBe(typeof(CustomOutboxStore));
}
+ [Fact]
+ public void UseCosmosDbThrowsWhenConfiguratorIsNull()
+ {
+ // Arrange
+ IDatabaseInfrastructureConfigurator configurator = null!;
+
+ // Act
+ var act = () => configurator.UseCosmosDb(ConnectionStringKey);
+
+ // Assert
+ Should.Throw(act);
+ }
+
private static HostApplicationBuilder NewBuilder(string connectionString)
{
var builder = Host.CreateEmptyApplicationBuilder(new HostApplicationBuilderSettings());