Skip to content

feat(sqs): transport-wide DefaultDeadLetterQueueName with per-listener override precedence#2714

Merged
jeremydmiller merged 1 commit into
mainfrom
issue-2653-sqs-default-dlq-name
May 10, 2026
Merged

feat(sqs): transport-wide DefaultDeadLetterQueueName with per-listener override precedence#2714
jeremydmiller merged 1 commit into
mainfrom
issue-2653-sqs-default-dlq-name

Conversation

@jeremydmiller
Copy link
Copy Markdown
Member

Closes #2653.

Adds AmazonSqsTransportConfiguration.DefaultDeadLetterQueueName(...) so a host can rename the SQS DLQ used by every auto-provisioned listener in one call instead of duplicating ConfigureDeadLetterQueue("…") across every ListenToSqsQueue(...). Multi-environment AWS accounts that share a region collide on the historical wolverine-dead-letter-queue default; conventional routing / auto-provisioning makes per-listener configuration impractical.

opts.UseAmazonSqsTransport()
    .DefaultDeadLetterQueueName("my-service-dlq")
    .AutoProvision();

opts.ListenToSqsQueue("orders");      // inherits "my-service-dlq"
opts.ListenToSqsQueue("payments")
    .ConfigureDeadLetterQueue("payments-errors");   // wins for this listener
opts.ListenToSqsQueue("notifications")
    .DisableDeadLetterQueueing();                    // wins for this listener

Resolution rule (per listener)

  1. ConfigureDeadLetterQueue("name") on the listener wins.
  2. DisableDeadLetterQueueing() on the listener wins (no DLQ for that one).
  3. Otherwise the transport-wide DefaultDeadLetterQueueName(...) is used.
  4. If none of the above is set, the historical default wolverine-dead-letter-queue applies.

DisableAllNativeDeadLetterQueues() remains the global kill-switch and trumps both. Default fallback when nothing is set is the historical wolverine-dead-letter-queueno behaviour change for hosts that don't opt in.

Implementation

AmazonSqsQueue.DeadLetterQueueName is no longer an auto-property default-initialized to the constant; it's a getter that resolves through the parent transport's new DefaultDeadLetterQueueName property when the queue hasn't been explicitly configured. The setter records an "explicitly set" flag, so the existing per-listener API surface (ConfigureDeadLetterQueue / DisableDeadLetterQueueing) keeps working unchanged — both paths go through that setter and mark the queue as having an explicit override (including null for "disabled"), which then beats the transport default at read time.

The transport stores its default in AmazonSqsTransport.DefaultDeadLetterQueueName, initialised in the ctor to the historical DeadLetterQueueName constant. The new DefaultDeadLetterQueueName(string) configuration method validates the input (suggests DisableAllNativeDeadLetterQueues() for the disable case) and runs SanitizeSqsName so authors can write acme.payments.dlq and get the same acme-payments-dlq canonical form per-listener config produces.

System queues (response / control queues, when EnableSystemQueues() is on) explicitly opt out of dead-lettering during transport bootstrap by setting DeadLetterQueueName = null directly. Under the new resolver that null-set marks the system queue as explicitly disabled — so the transport-wide default doesn't accidentally enroll system queues into a DLQ on hosts that opt into DefaultDeadLetterQueueName.

Test plan — every permutation

default_dead_letter_queue_name.cs (10 tests, 10/10 green on net9.0 against LocalStack):

  • Fallback — neither transport nor per-endpoint set ⇒ historical wolverine-dead-letter-queue
  • Transport default — applied to every unconfigured listener
  • Per-endpoint name override wins over transport default (mixed listeners)
  • Per-endpoint disable wins over transport default
  • Global disable (DisableAllNativeDeadLetterQueues()) trumps the transport default
  • Sanitization — dots and other illegal SQS characters in the transport name normalize identically to per-listener config
  • Provisioning — only the resolved DLQ is provisioned; the historical default name isn't enrolled when nobody references it
  • Mixed — inherit, name-override, and disabled coexist on the same transport (each gets the right DLQ)
  • Argument validationnull / empty / whitespace throw with a hint at DisableAllNativeDeadLetterQueues()
  • System queues — explicit no-DLQ setting on response/control queues is preserved under a transport-wide default

Docs

New "Customizing the Default Dead Letter Queue Name" section in docs/guide/messaging/transports/sqs/deadletterqueues.md covering the resolution order, sanitization, and the system-queue exemption. vitepress build clean.

🤖 Generated with Claude Code

…r override precedence

Closes #2653.

Adds `AmazonSqsTransportConfiguration.DefaultDeadLetterQueueName(...)` so a
host can rename the SQS DLQ used by every auto-provisioned listener in one
call instead of duplicating `ConfigureDeadLetterQueue("…")` across every
`ListenToSqsQueue(...)` line. Required when multi-environment AWS accounts
share a region and the historical `wolverine-dead-letter-queue` collides
across environments, or when conventional routing / auto-provisioning makes
per-listener configuration impractical.

```csharp
opts.UseAmazonSqsTransport()
    .DefaultDeadLetterQueueName("my-service-dlq")
    .AutoProvision();

opts.ListenToSqsQueue("orders");      // inherits "my-service-dlq"
opts.ListenToSqsQueue("payments")
    .ConfigureDeadLetterQueue("payments-errors");   // wins for this listener
opts.ListenToSqsQueue("notifications")
    .DisableDeadLetterQueueing();                    // wins for this listener
```

## Resolution rule

Per-listener `ConfigureDeadLetterQueue("name")` or
`DisableDeadLetterQueueing()` always win over the transport-wide default.
`DisableAllNativeDeadLetterQueues()` remains the global kill-switch and
trumps both. Default fallback when nothing is set is the historical
`wolverine-dead-letter-queue` — no behaviour change for hosts that don't
opt in.

## Implementation

`AmazonSqsQueue.DeadLetterQueueName` is no longer an auto-property
default-initialized to the constant; it's a getter that resolves through
the parent transport's new `DefaultDeadLetterQueueName` property when the
queue hasn't been explicitly configured. The setter records an
"explicitly set" flag, so the existing per-listener API surface
(`ConfigureDeadLetterQueue` / `DisableDeadLetterQueueing`) keeps working
unchanged — both paths go through that setter and mark the queue as
having an explicit override (including `null` for "disabled"), which then
beats the transport default at read time.

The transport stores its default in
`AmazonSqsTransport.DefaultDeadLetterQueueName`, initialised in the ctor
to the historical `DeadLetterQueueName` constant. The new
`DefaultDeadLetterQueueName(string)` configuration method validates the
input is non-null/non-whitespace (suggests
`DisableAllNativeDeadLetterQueues()` for the disable case) and runs
`SanitizeSqsName` so authors can write `acme.payments.dlq` and get the
same `acme-payments-dlq` canonical form per-listener config produces.

System queues (response / control queues, when `EnableSystemQueues()` is
on) explicitly opt out of dead-lettering during transport bootstrap by
setting `DeadLetterQueueName = null` directly. Under the new resolver
that null-set marks the system queue as explicitly disabled — so the
transport-wide default doesn't accidentally enroll system queues into a
DLQ on hosts that opt into `DefaultDeadLetterQueueName`.

## Test coverage

10 new tests in `default_dead_letter_queue_name.cs` covering every
permutation of the resolution rule plus argument validation:

- Built-in fallback when nothing is configured
- Transport default applied to every unconfigured listener
- Per-endpoint override wins over transport default (mixed listeners)
- Per-endpoint disable wins over transport default
- Global `DisableAllNativeDeadLetterQueues()` trumps the transport default
- Sanitization of the transport-wide name (dots → hyphens) matches per-listener
- Auto-provisioning: only the resolved DLQ name is provisioned, the historical default isn't
- Mixed: inherit + override + disabled all coexist on the same transport
- Argument validation throws for null / empty / whitespace
- System queues keep their explicit no-DLQ setting under a transport default

10/10 passing on net9.0 against LocalStack.

Docs: new "Customizing the Default Dead Letter Queue Name" section in
`docs/guide/messaging/transports/sqs/deadletterqueues.md` covering the
resolution order, sanitization behaviour, and the system-queue exemption.
`vitepress build` clean.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Global custom dead letter queue name for Amazon SQS transport

1 participant