< Summary - AsyncResponse (Release / net8.0+net10.0 / unit+integration)

Information
Class: AsyncResponse.Transports.Redis.RedisTransportOptionsValidator
Assembly: AsyncResponse.Transports.Redis
File(s): /_/src/Transports/AsyncResponse.Transports.Redis/RedisTransportOptionsValidator.cs
Line coverage
92%
Covered lines: 48
Uncovered lines: 4
Coverable lines: 52
Total lines: 102
Line coverage: 92.3%
Branch coverage
96%
Covered branches: 25
Total branches: 26
Branch coverage: 96.1%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
Required(...)100%22100%
PositiveOrNull(...)100%44100%
ValidateCommon(...)95%202091.3%

File(s)

/_/src/Transports/AsyncResponse.Transports.Redis/RedisTransportOptionsValidator.cs

#LineLine coverage
 1namespace AsyncResponse.Transports.Redis;
 2
 3internal static class RedisTransportOptionsValidator
 4{
 5    /// <summary>Validates the supplied options.</summary>
 6    public static string Required(string? value, string name)
 121167        => !string.IsNullOrWhiteSpace(value)
 121168            ? value
 121169            : throw new InvalidOperationException($"{nameof(RedisAsyncResponseTransportOptions)}.{name} must be configur
 10
 11    /// <summary>Validates the supplied options.</summary>
 12    public static void PositiveOrNull(long? value, string name)
 13    {
 356614        if (value is <= 0)
 415            throw new InvalidOperationException($"{nameof(RedisAsyncResponseTransportOptions)}.{name} must be positive w
 356216    }
 17
 18    /// <summary>Validates the supplied options.</summary>
 19    public static void ValidateCommon(RedisAsyncResponseTransportOptions options)
 20    {
 181421        _ = Required(options.KeyPrefix, nameof(options.KeyPrefix));
 180822        _ = Required(options.WorkerConsumerGroup, nameof(options.WorkerConsumerGroup));
 180623        _ = Required(options.ResponseConsumerGroup, nameof(options.ResponseConsumerGroup));
 180424        _ = Required(options.CorrelationIdField, nameof(options.CorrelationIdField));
 180225        _ = Required(options.PayloadField, nameof(options.PayloadField));
 180026        _ = Required(options.DefaultReplyTargetName, nameof(options.DefaultReplyTargetName));
 27
 28        // Worker and response subscribers must never share one stream: a Redis stream is not
 29        // partitioned between consumer groups — every entry is visible to every group — so a
 30        // shared stream would feed worker jobs to the response ingress and responses to the
 31        // worker dispatcher. Compare the resolved names so an explicit value colliding with the
 32        // other role's derived default is caught too.
 179833        var schema = new RedisTransportKeySchema(options);
 179834        if (StringComparer.Ordinal.Equals(schema.WorkerStream.ToString(), schema.ResponseStream.ToString()))
 35        {
 036            throw new InvalidOperationException(
 037                $"{nameof(RedisAsyncResponseTransportOptions)}.{nameof(options.WorkerStream)} and " +
 038                $"{nameof(options.ResponseStream)} must resolve to distinct streams so worker and response " +
 039                "subscribers do not consume each other's messages.");
 40        }
 41
 42        // Parity with the Kafka/NATS validators: the dead-letter stream must not be a live one.
 43        // Streams fan out to every consumer group, so a dead-letter XADD into the worker stream is
 44        // read back as a brand-new entry with Attempt=1 — fail, dead-letter, re-read, an unbounded
 45        // loop re-running the handler's side effects; aimed at the response stream, poison worker
 46        // envelopes complete live waiters. Compare the resolved names so an explicit value
 47        // colliding with a derived default is caught too.
 179848        var deadLetterStream = schema.DeadLetterStream.ToString();
 179849        if (StringComparer.Ordinal.Equals(deadLetterStream, schema.WorkerStream.ToString())
 179850            || StringComparer.Ordinal.Equals(deadLetterStream, schema.ResponseStream.ToString()))
 51        {
 652            throw new InvalidOperationException(
 653                $"{nameof(RedisAsyncResponseTransportOptions)}.{nameof(options.DeadLetterStream)} must resolve to a stre
 654                $"distinct from {nameof(options.WorkerStream)} and {nameof(options.ResponseStream)} " +
 655                $"(it resolves to '{deadLetterStream}') so dead-lettered messages park instead of re-entering live consu
 56        }
 57
 58        // The worker publish dedup marker must share the worker stream's cluster slot (a
 59        // MULTI/EXEC couples them). The schema reuses the stream's own hash tag when it carries a
 60        // well-formed one; a name whose braces do NOT form one has no marker key that can land in
 61        // its slot, so reject it here instead of failing every publish with CROSSSLOT.
 179262        var workerStream = schema.WorkerStream.ToString();
 179263        if (workerStream.AsSpan().IndexOfAny('{', '}') >= 0
 179264            && string.Equals(RedisTransportKeySchema.HashTagOf(workerStream), workerStream, StringComparison.Ordinal))
 65        {
 466            throw new InvalidOperationException(
 467                $"{nameof(RedisAsyncResponseTransportOptions)}.{nameof(options.WorkerStream)} (resolved to '{workerStrea
 468                "braces that do not form one well-formed Redis hash tag ('{tag}' with a non-empty tag). The idempotent p
 469                "must share the stream's cluster slot, which is only possible when the name has no braces or exactly one
 70        }
 71
 72        // OperationTimeout arms a CancellationTokenSource per command; the retry delays feed
 73        // Task.Delay — all timer-armed, so all carry the .NET timer ceiling.
 178874        AsyncResponseChannelOptions.EnsureTimerBacked(options.OperationTimeout, nameof(RedisAsyncResponseTransportOption
 178475        AsyncResponseChannelOptions.EnsureTimerBacked(options.PublishRetryBaseDelay, nameof(RedisAsyncResponseTransportO
 178476        AsyncResponseChannelOptions.EnsureTimerBacked(options.PublishRetryMaxDelay, nameof(RedisAsyncResponseTransportOp
 178477        AsyncResponseChannelOptions.EnsureTimerBacked(options.SubscriberRetryBaseDelay, nameof(RedisAsyncResponseTranspo
 178478        AsyncResponseChannelOptions.EnsureTimerBacked(options.SubscriberRetryMaxDelay, nameof(RedisAsyncResponseTranspor
 178479        PositiveOrNull(options.StreamMaxLength, nameof(options.StreamMaxLength));
 178280        PositiveOrNull(options.DeadLetterStreamMaxLength, nameof(options.DeadLetterStreamMaxLength));
 81
 178082        if (options.PublishMaxAttempts <= 0)
 283            throw new InvalidOperationException($"{nameof(RedisAsyncResponseTransportOptions)}.{nameof(options.PublishMa
 84
 177885        if (options.PublishRetryBaseDelay > options.PublishRetryMaxDelay)
 86        {
 287            throw new InvalidOperationException(
 288                $"{nameof(RedisAsyncResponseTransportOptions)}.{nameof(options.PublishRetryBaseDelay)} cannot exceed " +
 289                $"{nameof(RedisAsyncResponseTransportOptions)}.{nameof(options.PublishRetryMaxDelay)}.");
 90        }
 91
 177692        if (options.SubscriberRetryBaseDelay > options.SubscriberRetryMaxDelay)
 93        {
 294            throw new InvalidOperationException(
 295                $"{nameof(RedisAsyncResponseTransportOptions)}.{nameof(options.SubscriberRetryBaseDelay)} cannot exceed 
 296                $"{nameof(RedisAsyncResponseTransportOptions)}.{nameof(options.SubscriberRetryMaxDelay)}.");
 97        }
 98
 177499        if (options.HostShutdownTimeout is { } hostShutdownTimeout && hostShutdownTimeout <= TimeSpan.Zero)
 2100            throw new InvalidOperationException($"{nameof(RedisAsyncResponseTransportOptions)}.{nameof(options.HostShutd
 1772101    }
 102}