Skip to content

Commit 389506b

Browse files
Add QueueException and guard against reusing behavior instances
Add QueueException following the established pattern (CacheException, StorageException, MessageBusException) to provide a consistent exception type for queue operations. Add a guard in QueueBehaviorBase.Attach that throws QueueException if the behavior is already attached to a queue, preventing subtle bugs from duplicate event handler subscriptions or stale queue references. Include documentation updates in docs/guide/queues.md with behavior attachment rules and QueueException usage, plus two new tests following naming standards. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent d1a1981 commit 389506b

File tree

4 files changed

+83
-0
lines changed

4 files changed

+83
-0
lines changed

docs/guide/queues.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -511,8 +511,25 @@ await queue.EnqueueAsync(new OrderWorkItem { OrderId = 456 }); // ✅ Enqueued
511511
3. If not found, the identifier is cached with the specified TTL
512512
4. On dequeue, the identifier is removed from the cache (allowing re-submission)
513513

514+
### Behavior Attachment Rules
515+
516+
Each behavior instance can only be attached to a single queue. Attempting to attach the same behavior instance to multiple queues or attaching it twice to the same queue throws a `QueueException`. This prevents subtle bugs where event handlers could fire against the wrong queue reference.
517+
518+
```csharp
519+
// ✅ Correct: separate instances for each queue
520+
queue1.AttachBehavior(new LoggingQueueBehavior<WorkItem>(logger));
521+
queue2.AttachBehavior(new LoggingQueueBehavior<WorkItem>(logger));
522+
523+
// ❌ Throws QueueException: same instance attached twice
524+
var behavior = new LoggingQueueBehavior<WorkItem>(logger);
525+
queue1.AttachBehavior(behavior);
526+
queue2.AttachBehavior(behavior); // throws QueueException
527+
```
528+
514529
### Attaching Multiple Behaviors
515530

531+
You can attach multiple different behavior instances to a single queue:
532+
516533
```csharp
517534
var queue = new InMemoryQueue<WorkItem>(o => o
518535
.Behaviors(
@@ -751,6 +768,26 @@ services.AddSingleton<IQueue<EmailWorkItem>>(sp =>
751768
new InMemoryQueue<EmailWorkItem>(o => o.Name = "emails"));
752769
```
753770

771+
## Queue Exceptions
772+
773+
Queue operations throw `QueueException` for queue-specific error conditions. This provides a consistent, predictable exception type across all queue implementations (in-memory, Redis, Azure, AWS, etc.).
774+
775+
```csharp
776+
using Foundatio.Queues;
777+
778+
try
779+
{
780+
// Attempting to reuse a behavior instance throws QueueException
781+
var behavior = new LoggingQueueBehavior<WorkItem>(logger);
782+
queue1.AttachBehavior(behavior);
783+
queue2.AttachBehavior(behavior); // throws QueueException
784+
}
785+
catch (QueueException ex)
786+
{
787+
logger.LogError(ex, "Queue operation failed: {Message}", ex.Message);
788+
}
789+
```
790+
754791
## Best Practices
755792

756793
### 1. Proper Resource Disposal

src/Foundatio/Queues/QueueBehaviour.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ public abstract class QueueBehaviorBase<T> : IQueueBehavior<T>, IDisposable wher
1616

1717
public virtual void Attach(IQueue<T> queue)
1818
{
19+
ArgumentNullException.ThrowIfNull(queue);
20+
21+
if (_queue is not null)
22+
throw new QueueException("This behavior is already attached to a queue. Create a separate behavior instance for each queue.");
23+
1924
_queue = queue;
2025

2126
_disposables.Add(_queue.Enqueuing.AddHandler(OnEnqueuing));
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using System;
2+
3+
namespace Foundatio.Queues;
4+
5+
/// <summary>
6+
/// Exception thrown for queue operation errors.
7+
/// </summary>
8+
public class QueueException : Exception
9+
{
10+
public QueueException(string message) : base(message)
11+
{
12+
}
13+
14+
public QueueException(string message, Exception innerException) : base(message, innerException)
15+
{
16+
}
17+
}

tests/Foundatio.Tests/Queue/InMemoryQueueTests.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,30 @@ public async Task DeleteQueueAsync_WithAttachedBehavior_InvokesBehaviorOnQueueDe
330330
Assert.True(behavior.QueueDeletedCalled);
331331
}
332332

333+
[Fact]
334+
public void AttachBehavior_WhenAlreadyAttached_ThrowsQueueException()
335+
{
336+
// Arrange
337+
using var q1 = new InMemoryQueue<SimpleWorkItem>(o => o.LoggerFactory(Log));
338+
using var q2 = new InMemoryQueue<SimpleWorkItem>(o => o.LoggerFactory(Log));
339+
var behavior = new QueueDeletedTestBehavior<SimpleWorkItem>();
340+
q1.AttachBehavior(behavior);
341+
342+
// Act & Assert
343+
var ex = Assert.Throws<QueueException>(() => q2.AttachBehavior(behavior));
344+
Assert.Contains("already attached", ex.Message);
345+
}
346+
347+
[Fact]
348+
public void AttachBehavior_WithNullQueue_ThrowsArgumentNullException()
349+
{
350+
// Arrange
351+
var behavior = new QueueDeletedTestBehavior<SimpleWorkItem>();
352+
353+
// Act & Assert
354+
Assert.Throws<ArgumentNullException>(() => behavior.Attach(null));
355+
}
356+
333357
private class QueueDeletedTestBehavior<T> : QueueBehaviorBase<T> where T : class
334358
{
335359
public bool QueueDeletedCalled { get; private set; }

0 commit comments

Comments
 (0)