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

Information
Class: AsyncResponse.Transports.AzureServiceBus.AzureServiceBusSubscriberOptions
Assembly: AsyncResponse.Transports.AzureServiceBus
File(s): /home/runner/work/AsyncResponse/AsyncResponse/src/Transports/AsyncResponse.Transports.AzureServiceBus/AzureServiceBusAsyncResponseOptions.cs
Line coverage
100%
Covered lines: 13
Uncovered lines: 0
Coverable lines: 13
Total lines: 228
Line coverage: 100%
Branch coverage
100%
Covered branches: 6
Total branches: 6
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor()100%11100%
UseAckAfterEnqueue(...)100%66100%

File(s)

/home/runner/work/AsyncResponse/AsyncResponse/src/Transports/AsyncResponse.Transports.AzureServiceBus/AzureServiceBusAsyncResponseOptions.cs

#LineLine coverage
 1namespace AsyncResponse.Transports.AzureServiceBus;
 2
 3/// <summary>Options for the Azure Service Bus AsyncResponse transport.</summary>
 4public sealed class AzureServiceBusAsyncResponseOptions
 5{
 6    /// <summary>The transport name reported to reply targets and startup validation.</summary>
 7    public const string TransportName = "AzureServiceBus";
 8
 9    /// <summary>
 10    /// Service Bus namespace connection string. Required unless an <see cref="Azure.Messaging.ServiceBus.ServiceBusClie
 11    /// singleton is already registered in the application's service container.
 12    /// </summary>
 13    public string? ConnectionString { get; set; }
 14
 15    /// <summary>Service Bus queue used by <see cref="AzureServiceBusWorkerTransport"/> to publish worker jobs.</summary
 16    public string WorkerQueue { get; set; } = "asyncresponse-worker";
 17
 18    /// <summary>Worker queue handling options.</summary>
 19    public AzureServiceBusSubscriberOptions WorkerSubscriber { get; } = new();
 20
 21    /// <summary>Service Bus queue consumed by the hosted response-ingress subscriber.</summary>
 22    public string ResponseQueue { get; set; } = "asyncresponse-response";
 23
 24    /// <summary>Response queue handling options.</summary>
 25    public AzureServiceBusSubscriberOptions ResponseSubscriber { get; } = new();
 26
 27    /// <summary>The logical reply target name used by <c>WithReplyTarget()</c>. Default: <c>default</c>.</summary>
 28    public string DefaultReplyTargetName { get; set; } = "default";
 29
 30    /// <summary>
 31    /// Named reply targets exposed to Core through <see cref="IAsyncResponseReplyTargetProvider"/>.
 32    /// When empty, <see cref="ResponseQueue"/> becomes the default reply target.
 33    /// </summary>
 34    public Dictionary<string, AzureServiceBusReplyTargetOptions> ReplyTargets { get; } = new(StringComparer.Ordinal);
 35
 36    /// <summary>
 37    /// Application property used to carry the AsyncResponse correlation id. The transport also sets the
 38    /// Service Bus system <c>CorrelationId</c> property for interoperability.
 39    /// </summary>
 40    public string CorrelationIdProperty { get; set; } = "correlationId";
 41
 42    /// <summary>
 43    /// JSON paths inspected when a response message does not carry the correlation id in the Service
 44    /// Bus system property or <see cref="CorrelationIdProperty"/>. Paths are case-insensitive and
 45    /// support nested JSON strings.
 46    /// </summary>
 47    public string[] CorrelationIdJsonPaths { get; set; } =
 48    [
 49        "CorrelationId",
 50        "CustomParameters",
 51        "CustomParameters.CorrelationId",
 52        "PubSubParams.CustomParameters",
 53        "PubSubParams.CustomParameters.CorrelationId",
 54        "DagJsonParameters.CorrelationId"
 55    ];
 56
 57    /// <summary>Maximum messages requested from Service Bus in one receive call.</summary>
 58    public int MaxMessagesPerReceive { get; set; } = 16;
 59
 60    /// <summary>Maximum time a subscriber receive call waits for messages before polling again.</summary>
 61    public TimeSpan ReceiveWaitTime { get; set; } = TimeSpan.FromSeconds(1);
 62
 63    /// <summary>Maximum attempts for Service Bus send operations. Set to 1 to disable transport-level retries.</summary
 64    public int PublishMaxAttempts { get; set; } = 3;
 65
 66    /// <summary>Initial delay before retrying a failed send operation.</summary>
 67    public TimeSpan PublishRetryBaseDelay { get; set; } = TimeSpan.FromMilliseconds(50);
 68
 69    /// <summary>Maximum delay between send retry attempts.</summary>
 70    public TimeSpan PublishRetryMaxDelay { get; set; } = TimeSpan.FromSeconds(1);
 71
 72    /// <summary>Initial delay after a subscriber loop failure.</summary>
 73    public TimeSpan SubscriberRetryBaseDelay { get; set; } = TimeSpan.FromMilliseconds(250);
 74
 75    /// <summary>Maximum delay after repeated subscriber loop failures.</summary>
 76    public TimeSpan SubscriberRetryMaxDelay { get; set; } = TimeSpan.FromSeconds(5);
 77
 78    /// <summary>
 79    /// Bounds receiver/sender close and the lock-renewal task join while a hosted subscriber
 80    /// stops. These complete in milliseconds when healthy; when they do not, the work is abandoned
 81    /// anyway, so keep this short — it counts against the host's shutdown budget. Default: <c>5s</c>.
 82    /// </summary>
 83    public TimeSpan ShutdownTimeout { get; set; } = TimeSpan.FromSeconds(5);
 84
 85    /// <summary>
 86    /// The hosting shutdown budget that must contain Service Bus receiver/sender shutdown plus
 87    /// <see cref="AzureServiceBusSubscriberOptions.BackgroundDrainTimeout"/> when a subscriber uses
 88    /// <see cref="AzureServiceBusAckMode.AckAfterEnqueue"/>.
 89    /// </summary>
 90    public TimeSpan? HostShutdownTimeout { get; set; } = TimeSpan.FromSeconds(30);
 91
 92    /// <summary>Adds or replaces a named Azure Service Bus reply target.</summary>
 93    public AzureServiceBusAsyncResponseOptions AddReplyTarget(string name, string queue)
 94    {
 95        ArgumentException.ThrowIfNullOrWhiteSpace(name);
 96        ArgumentException.ThrowIfNullOrWhiteSpace(queue);
 97
 98        ReplyTargets[name] = new AzureServiceBusReplyTargetOptions { Queue = queue };
 99        return this;
 100    }
 101}
 102
 103/// <summary>Options for one named Azure Service Bus async-response reply target.</summary>
 104public sealed class AzureServiceBusReplyTargetOptions
 105{
 106    /// <summary>Queue remote systems should publish responses to.</summary>
 107    public string? Queue { get; set; }
 108
 109    /// <summary>Additional values copied to the transport-neutral reply target.</summary>
 110    public Dictionary<string, string> Properties { get; } = new(StringComparer.Ordinal);
 111}
 112
 113/// <summary>Controls when a Service Bus message is acknowledged relative to handling.</summary>
 114public enum AzureServiceBusAckMode
 115{
 116    /// <summary>
 117    /// Complete the message only after the AsyncResponse handler completes successfully. Handler
 118    /// failures abandon the message until <see cref="AzureServiceBusSubscriberOptions.MaxDeliveryAttempts"/>
 119    /// is reached, then move it to the Service Bus dead-letter subqueue.
 120    /// </summary>
 121    AckAfterHandlerCompletes = 0,
 122
 123    /// <summary>
 124    /// Complete the message immediately after it is accepted into a bounded in-process background
 125    /// queue. Handler failures are logged and reported through <see cref="AzureServiceBusSubscriberOptions.OnBackground
 126    /// because the original Service Bus lock has already been completed.
 127    /// </summary>
 128    AckAfterEnqueue = 1
 129}
 130
 131/// <summary>Describes a handler failure that happened after a Service Bus message was already completed.</summary>
 132public sealed class AzureServiceBusBackgroundFailureContext
 133{
 134    internal AzureServiceBusBackgroundFailureContext(
 135        string queue,
 136        string subscriberRole,
 137        long sequenceNumber,
 138        string messageId,
 139        string? correlationId,
 140        Exception exception)
 141    {
 142        Queue = queue;
 143        SubscriberRole = subscriberRole;
 144        SequenceNumber = sequenceNumber;
 145        MessageId = messageId;
 146        CorrelationId = correlationId;
 147        Exception = exception;
 148    }
 149
 150    /// <summary>The Service Bus queue the message came from.</summary>
 151    public string Queue { get; }
 152
 153    /// <summary>The logical subscriber role, such as <c>Worker</c> or <c>ResponseIngress</c>.</summary>
 154    public string SubscriberRole { get; }
 155
 156    /// <summary>The Service Bus sequence number for the already-completed message.</summary>
 157    public long SequenceNumber { get; }
 158
 159    /// <summary>The Service Bus message id.</summary>
 160    public string MessageId { get; }
 161
 162    /// <summary>The AsyncResponse correlation id, when one was available.</summary>
 163    public string? CorrelationId { get; }
 164
 165    /// <summary>The exception thrown by the background handler.</summary>
 166    public Exception Exception { get; }
 167}
 168
 169/// <summary>Per-queue Azure Service Bus subscriber behavior.</summary>
 170public sealed class AzureServiceBusSubscriberOptions
 171{
 172    /// <summary>Controls when a message is completed. Defaults to <see cref="AzureServiceBusAckMode.AckAfterHandlerComp
 173    public AzureServiceBusAckMode AckMode { get; set; } = AzureServiceBusAckMode.AckAfterHandlerCompletes;
 174
 175    /// <summary>
 176    /// Maximum delivery attempts before a failing message is dead-lettered. <c>0</c> means unlimited
 177    /// application-level retries and leaves any broker-level MaxDeliveryCount policy to Service Bus.
 178    /// Default: <c>5</c>.
 179    /// </summary>
 3180    public int MaxDeliveryAttempts { get; set; } = 5;
 181
 182    /// <summary>Number of messages the receiver prefetches locally. Default: <c>0</c>.</summary>
 183    public int PrefetchCount { get; set; }
 184
 185    /// <summary>
 186    /// Heartbeat cadence for renewing the peek locks of received-but-unsettled messages while a
 187    /// <see cref="AzureServiceBusAckMode.AckAfterHandlerCompletes"/> batch is worked through
 188    /// serially. Each beat calls <c>RenewMessageLockAsync</c> for every message that has not been
 189    /// settled yet (including the one currently in the handler), so a slow handler does not let the
 190    /// locks of later batch messages expire and cause systematic duplicate processing. Renewal
 191    /// failures are logged and processing continues — the message simply redelivers, preserving
 192    /// at-least-once semantics. Set to <c>null</c> to disable renewal. Ignored in
 193    /// <see cref="AzureServiceBusAckMode.AckAfterEnqueue"/> (messages are already completed).
 194    /// Default: <c>30 seconds</c>.
 195    /// </summary>
 3196    public TimeSpan? LockRenewalInterval { get; set; } = TimeSpan.FromSeconds(30);
 197
 198    /// <summary>Number of background workers used by <see cref="AzureServiceBusAckMode.AckAfterEnqueue"/>.</summary>
 199    public int BackgroundWorkerCount { get; set; }
 200
 201    /// <summary>Maximum number of completed messages waiting in the background queue.</summary>
 202    public int BackgroundQueueCapacity { get; set; }
 203
 204    /// <summary>Maximum time to wait for queued/running background handlers while stopping.</summary>
 3205    public TimeSpan BackgroundDrainTimeout { get; set; } = TimeSpan.FromSeconds(20);
 206
 207    /// <summary>Optional callback invoked when a background handler fails after the message was already completed.</sum
 208    public Func<AzureServiceBusBackgroundFailureContext, ValueTask>? OnBackgroundFailure { get; set; }
 209
 210    /// <summary>Explicitly opts this subscriber into complete-after-enqueue behavior.</summary>
 211    public AzureServiceBusSubscriberOptions UseAckAfterEnqueue(
 212        int backgroundWorkerCount,
 213        int backgroundQueueCapacity,
 214        TimeSpan? backgroundDrainTimeout = null)
 215    {
 3216        ArgumentOutOfRangeException.ThrowIfNegativeOrZero(backgroundWorkerCount);
 3217        ArgumentOutOfRangeException.ThrowIfNegativeOrZero(backgroundQueueCapacity);
 3218        if (backgroundDrainTimeout is { } timeout && timeout <= TimeSpan.Zero)
 3219            throw new ArgumentOutOfRangeException(nameof(backgroundDrainTimeout), timeout, "Drain timeout must be positi
 220
 3221        AckMode = AzureServiceBusAckMode.AckAfterEnqueue;
 3222        BackgroundWorkerCount = backgroundWorkerCount;
 3223        BackgroundQueueCapacity = backgroundQueueCapacity;
 3224        if (backgroundDrainTimeout is not null)
 3225            BackgroundDrainTimeout = backgroundDrainTimeout.Value;
 3226        return this;
 227    }
 228}