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

Information
Class: AsyncResponse.Transports.SqlServer.SqlServerTransportOptionsValidator
Assembly: AsyncResponse.Transports.SqlServer
File(s): /_/src/Transports/AsyncResponse.Transports.SqlServer/SqlServerTransportOptionsValidator.cs
Line coverage
100%
Covered lines: 83
Uncovered lines: 0
Coverable lines: 83
Total lines: 159
Line coverage: 100%
Branch coverage
96%
Covered branches: 52
Total branches: 54
Branch coverage: 96.2%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
ValidateCommon(...)92.85%1414100%
ValidateSubscriber(...)100%22100%
ValidateSubscriber(...)100%1212100%
ValidateQueueName(...)100%66100%
Required(...)100%22100%
ValidateIdentifier(...)100%66100%
IsIdentifier(...)91.66%1212100%

File(s)

/_/src/Transports/AsyncResponse.Transports.SqlServer/SqlServerTransportOptionsValidator.cs

#LineLine coverage
 1namespace AsyncResponse.Transports.SqlServer;
 2
 3internal static class SqlServerTransportOptionsValidator
 4{
 5    public static void ValidateCommon(SqlServerAsyncResponseTransportOptions options)
 6    {
 8947        Required(options.ConnectionString, nameof(options.ConnectionString));
 8928        ValidateIdentifier(options.SchemaName, nameof(options.SchemaName));
 8889        ValidateIdentifier(options.MessageTable, nameof(options.MessageTable));
 88410        Required(options.WorkerQueue, nameof(options.WorkerQueue));
 88411        Required(options.ResponseQueue, nameof(options.ResponseQueue));
 88412        Required(options.DeadLetterQueue, nameof(options.DeadLetterQueue));
 88413        Required(options.CorrelationIdHeader, nameof(options.CorrelationIdHeader));
 88214        Required(options.DefaultReplyTargetName, nameof(options.DefaultReplyTargetName));
 15
 16        // The three logical queues share one table, distinguished only by the queue column, so
 17        // names that the DATABASE considers equal are as dangerous as identical ones. SQL Server
 18        // pads the shorter operand of an `=` comparison with spaces before comparing — even under
 19        // a binary collation — so 'worker' and 'worker ' match each other's rows and both
 20        // subscribers would consume both queues. Ordinal distinctness alone does not see that.
 88221        ValidateQueueName(options.WorkerQueue, nameof(options.WorkerQueue));
 87622        ValidateQueueName(options.ResponseQueue, nameof(options.ResponseQueue));
 87223        ValidateQueueName(options.DeadLetterQueue, nameof(options.DeadLetterQueue));
 24
 86825        if (StringComparer.Ordinal.Equals(options.WorkerQueue, options.ResponseQueue)
 86826            || StringComparer.Ordinal.Equals(options.WorkerQueue, options.DeadLetterQueue)
 86827            || StringComparer.Ordinal.Equals(options.ResponseQueue, options.DeadLetterQueue))
 28        {
 229            throw new InvalidOperationException(
 230                $"{nameof(SqlServerAsyncResponseTransportOptions)}.{nameof(options.WorkerQueue)}, " +
 231                $"{nameof(options.ResponseQueue)}, and {nameof(options.DeadLetterQueue)} must be distinct; they share on
 32        }
 33
 34        // Timer-armed knobs get the .NET timer ceiling (LockTimeout also drives the in-process
 35        // lease-renewal Task.Delay at a third of its value); DeadLetterRetention is a database-side
 36        // "now - retention" prune cutoff and gets the persistence bound instead.
 86637        if (options.DeadLetterRetention is { } deadLetterRetention)
 2438            AsyncResponseChannelOptions.EnsurePersistedTtl(deadLetterRetention, nameof(SqlServerAsyncResponseTransportOp
 39
 86440        AsyncResponseChannelOptions.EnsureTimerBacked(options.LockTimeout, nameof(SqlServerAsyncResponseTransportOptions
 86041        AsyncResponseChannelOptions.EnsureTimerBacked(options.PublishRetryBaseDelay, nameof(SqlServerAsyncResponseTransp
 86042        AsyncResponseChannelOptions.EnsureTimerBacked(options.PublishRetryMaxDelay, nameof(SqlServerAsyncResponseTranspo
 86043        AsyncResponseChannelOptions.EnsureTimerBacked(options.SubscriberRetryBaseDelay, nameof(SqlServerAsyncResponseTra
 86044        AsyncResponseChannelOptions.EnsureTimerBacked(options.SubscriberRetryMaxDelay, nameof(SqlServerAsyncResponseTran
 45
 86046        if (options.PublishMaxAttempts <= 0)
 247            throw new InvalidOperationException($"{nameof(SqlServerAsyncResponseTransportOptions)}.{nameof(options.Publi
 85848        if (options.PublishRetryBaseDelay > options.PublishRetryMaxDelay)
 249            throw new InvalidOperationException($"{nameof(SqlServerAsyncResponseTransportOptions)}.{nameof(options.Publi
 85650        if (options.SubscriberRetryBaseDelay > options.SubscriberRetryMaxDelay)
 251            throw new InvalidOperationException($"{nameof(SqlServerAsyncResponseTransportOptions)}.{nameof(options.Subsc
 85452    }
 53
 54    /// <summary>Validates the supplied subscriber options together with the transport-wide shutdown budget.</summary>
 55    public static void ValidateSubscriber(
 56        SqlServerAsyncResponseTransportOptions transportOptions,
 57        SqlServerSubscriberOptions subscriber,
 58        string role)
 59    {
 86860        ValidateSubscriber(subscriber, role);
 61
 86462        if (subscriber.AckMode is not SqlServerAckMode.AckAfterEnqueue)
 82063            return;
 64
 65        // SQL Server subscribers spend only the background drain at shutdown; the polling loop
 66        // stops with the host token and holds no connection that needs a bounded close.
 4467        ShutdownBudgetValidator.Validate(
 4468            "SQL Server",
 4469            $"{nameof(SqlServerAsyncResponseTransportOptions)}.{nameof(transportOptions.HostShutdownTimeout)}",
 4470            transportOptions.HostShutdownTimeout,
 4471            ($"{nameof(SqlServerSubscriberOptions)}.{nameof(subscriber.BackgroundDrainTimeout)} ({role})", subscriber.Ba
 4072    }
 73
 74    public static void ValidateSubscriber(SqlServerSubscriberOptions subscriber, string role)
 75    {
 88676        if (subscriber.BatchSize <= 0)
 277            throw new InvalidOperationException($"{nameof(SqlServerSubscriberOptions)}.{nameof(subscriber.BatchSize)} ({
 88478        if (subscriber.MaxDeliveryAttempts < 0)
 279            throw new InvalidOperationException($"{nameof(SqlServerSubscriberOptions)}.{nameof(subscriber.MaxDeliveryAtt
 80
 81        // RedeliveryDelay is a database-side "now + delay" visibility stamp (persistence bound);
 82        // EmptyPollDelay arms the idle-poll Task.Delay (timer ceiling).
 88283        AsyncResponseChannelOptions.EnsurePersistedTtl(subscriber.RedeliveryDelay, nameof(SqlServerSubscriberOptions), $
 88084        AsyncResponseChannelOptions.EnsureTimerBacked(subscriber.EmptyPollDelay, nameof(SqlServerSubscriberOptions), $"{
 85
 87886        switch (subscriber.AckMode)
 87        {
 88            case SqlServerAckMode.AckAfterHandlerCompletes:
 82289                return;
 90            case SqlServerAckMode.AckAfterEnqueue:
 5491                if (subscriber.BackgroundWorkerCount <= 0)
 692                    throw new InvalidOperationException($"{nameof(SqlServerSubscriberOptions)}.{nameof(subscriber.Backgr
 4893                if (subscriber.BackgroundQueueCapacity <= 0)
 294                    throw new InvalidOperationException($"{nameof(SqlServerSubscriberOptions)}.{nameof(subscriber.Backgr
 4695                AsyncResponseChannelOptions.EnsureTimerBacked(subscriber.BackgroundDrainTimeout, nameof(SqlServerSubscri
 4696                return;
 97            default:
 298                throw new InvalidOperationException($"{nameof(SqlServerSubscriberOptions)}.{nameof(subscriber.AckMode)} 
 99        }
 100    }
 101
 102    /// <summary>
 103    /// A queue name is a database KEY, not free text: it is stored in <c>nvarchar(200)</c> and
 104    /// matched with <c>=</c>, whose space padding makes trailing blanks invisible to the database
 105    /// while the library still treats the names as distinct. Leading blanks are rejected on the
 106    /// same principle — they survive comparison but make two names indistinguishable in logs.
 107    /// </summary>
 108    public static void ValidateQueueName(string value, string name)
 109    {
 2658110        if (value.Length > MaxQueueNameLength)
 111        {
 6112            throw new InvalidOperationException(
 6113                $"{nameof(SqlServerAsyncResponseTransportOptions)}.{name} is {value.Length} characters; the queue column
 6114                $"nvarchar({MaxQueueNameLength}), so a longer name is truncated or rejected at the first publish.");
 115        }
 116
 2652117        if (value[0] == ' ' || value[^1] == ' ')
 118        {
 18119            throw new InvalidOperationException(
 18120                $"{nameof(SqlServerAsyncResponseTransportOptions)}.{name} '{value}' begins or ends with a space. SQL Ser
 18121                "the shorter operand of an equality comparison, so a trailing space is invisible to the queue lookup — t
 18122                "differing only in trailing spaces would consume each other's messages. Remove the surrounding spaces.")
 123        }
 2634124    }
 125
 126    /// <summary>The queue column's width: <c>queue nvarchar(200)</c> in the transport's DDL.</summary>
 127    public const int MaxQueueNameLength = 200;
 128
 129    public static string Required(string? value, string name)
 5353130        => !string.IsNullOrWhiteSpace(value)
 5353131            ? value
 5353132            : throw new InvalidOperationException($"{nameof(SqlServerAsyncResponseTransportOptions)}.{name} must be conf
 133
 134    public static void ValidateIdentifier(string? value, string name)
 135    {
 1780136        if (string.IsNullOrWhiteSpace(value))
 2137            throw new InvalidOperationException($"{nameof(SqlServerAsyncResponseTransportOptions)}.{name} must be config
 1778138        if (!IsIdentifier(value))
 4139            throw new InvalidOperationException(
 4140                $"{nameof(SqlServerAsyncResponseTransportOptions)}.{name} '{value}' must be a simple SQL Server identifi
 141        // sysname caps identifiers at 128; an over-limit name fails at DDL time with a raw
 142        // "identifier too long" error instead of an actionable configuration error.
 1774143        if (value.Length > 128)
 2144            throw new InvalidOperationException(
 2145                $"{nameof(SqlServerAsyncResponseTransportOptions)}.{name} '{value}' is {value.Length} characters; SQL Se
 1772146    }
 147
 148    private static bool IsIdentifier(string value)
 149    {
 1778150        if (value.Length == 0 || !(char.IsAsciiLetter(value[0]) || value[0] == '_'))
 2151            return false;
 86576152        foreach (var c in value)
 153        {
 41513154            if (!(char.IsAsciiLetterOrDigit(c) || c == '_'))
 2155                return false;
 156        }
 1774157        return true;
 158    }
 159}