From 7c559540a18f8ce2087cb4945cb04e2ccd5cc4ca Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Tue, 9 Jun 2026 11:25:16 -0500 Subject: [PATCH 1/3] fix: correct disposal ordering to prevent ObjectDisposedException during shutdown Call SignalDispose() before disposing resources to cancel background tasks before the CancellationTokenSource is disposed. Move base.Dispose() to the end to ensure the CTS stays alive while resources are being cleaned up. Fixes same class of bug as FoundatioFx/Foundatio.Redis#165. --- .../Queues/AzureServiceBusQueue.cs | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/Foundatio.AzureServiceBus/Queues/AzureServiceBusQueue.cs b/src/Foundatio.AzureServiceBus/Queues/AzureServiceBusQueue.cs index 227fe9d..97662fe 100644 --- a/src/Foundatio.AzureServiceBus/Queues/AzureServiceBusQueue.cs +++ b/src/Foundatio.AzureServiceBus/Queues/AzureServiceBusQueue.cs @@ -622,7 +622,13 @@ private CreateQueueOptions CreateQueueOptions() public override void Dispose() { - base.Dispose(); + if (IsDisposed) + { + _logger.LogTrace("Queue {QueueName} ({QueueId}) dispose was already called", _options.Name, QueueId); + return; + } + + SignalDispose(); if (_queueSender is not null) { @@ -640,11 +646,19 @@ public override void Dispose() { _client.Value.DisposeAsync().AsTask().GetAwaiter().GetResult(); } + + base.Dispose(); } public async ValueTask DisposeAsync() { - base.Dispose(); + if (IsDisposed) + { + _logger.LogTrace("Queue {QueueName} ({QueueId}) async dispose was already called", _options.Name, QueueId); + return; + } + + SignalDispose(); if (_queueSender is not null) { @@ -662,6 +676,8 @@ public async ValueTask DisposeAsync() { await _client.Value.DisposeAsync().AnyContext(); } + + base.Dispose(); } private async Task DeadLetterMessageAsync(AzureServiceBusQueueEntry entry) From 0ea016fdd84869aa114b02a3bd68219d4e9d6e68 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Tue, 9 Jun 2026 12:02:48 -0500 Subject: [PATCH 2/3] refactor: use atomic if (!SignalDispose()) guard pattern Collapse separate IsDisposed check + SignalDispose() call into single atomic operation that eliminates the TOCTOU gap. --- .../Queues/AzureServiceBusQueue.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/Foundatio.AzureServiceBus/Queues/AzureServiceBusQueue.cs b/src/Foundatio.AzureServiceBus/Queues/AzureServiceBusQueue.cs index 97662fe..fddbc12 100644 --- a/src/Foundatio.AzureServiceBus/Queues/AzureServiceBusQueue.cs +++ b/src/Foundatio.AzureServiceBus/Queues/AzureServiceBusQueue.cs @@ -622,14 +622,12 @@ private CreateQueueOptions CreateQueueOptions() public override void Dispose() { - if (IsDisposed) + if (!SignalDispose()) { _logger.LogTrace("Queue {QueueName} ({QueueId}) dispose was already called", _options.Name, QueueId); return; } - SignalDispose(); - if (_queueSender is not null) { _queueSender.DisposeAsync().AsTask().GetAwaiter().GetResult(); @@ -652,14 +650,12 @@ public override void Dispose() public async ValueTask DisposeAsync() { - if (IsDisposed) + if (!SignalDispose()) { _logger.LogTrace("Queue {QueueName} ({QueueId}) async dispose was already called", _options.Name, QueueId); return; } - SignalDispose(); - if (_queueSender is not null) { await _queueSender.DisposeAsync().AnyContext(); From e3b21d424e139183310091fb9045ee01ae4cf5e3 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Tue, 9 Jun 2026 15:16:24 -0500 Subject: [PATCH 3/3] bump deps --- tests/Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index d9b7080..6a7db9d 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -7,7 +7,7 @@ $(NoWarn);CS1591;NU1701 - +