| id | 0053-fix-resilience-pipeline-reuse | |||
|---|---|---|---|---|
| title | Fix Resilience Pipeline Reuse Across Command Handlers | |||
| status | Accepted | |||
| author |
|
|||
| created | 2026-03-11 | |||
| summary | Fixes `ResilienceExceptionPolicyHandlerAsync<TRequest>` which ignored the `UseTypePipeline` flag and always called `GetPipeline<TRequest>()`, preventing non-generic shared `ResiliencePipeline` instances from being reused across multiple command handler types. | |||
| tags |
|
Date: 2026-03-11
Accepted
Parent Requirement: specs/0024-resilience_pipeline_bug/requirements.md
Scope: This ADR addresses the bug where non-generic ResiliencePipeline instances cannot be shared across multiple command handler types (#4042).
Brighter's resilience pipeline support uses Polly's ResiliencePipelineRegistry<string> to look up pipelines by key. The UseResiliencePipelineAttribute has a UseTypePipeline property that controls whether to look up a non-generic ResiliencePipeline (shared across all command types) or a generic ResiliencePipeline<TRequest> (scoped per command type).
The sync handler (ResilienceExceptionPolicyHandler<TRequest>) correctly implements both paths:
UseTypePipeline = false → GetPipeline(key) → ResiliencePipeline (shared)
UseTypePipeline = true → GetPipeline<TRequest>(key) → ResiliencePipeline<TRequest> (typed)
The async handler (ResilienceExceptionPolicyHandlerAsync<TRequest>) has two defects:
- It does not read the
UseTypePipelineparameter frominitializerList[1] - It only has a
ResiliencePipeline<TRequest> _pipelinefield — no non-generic field - It always calls
GetPipeline<TRequest>(key), forcing per-command-type registration
This means any handler using the async path with a non-generic pipeline throws KeyNotFoundException.
Align ResilienceExceptionPolicyHandlerAsync<TRequest> with the sync handler's existing implementation pattern.
The ResilienceExceptionPolicyHandlerAsync<TRequest> class has two responsibilities:
- Knowing: Which pipeline to use (resolved during
InitializeFromAttributeParams) - Doing: Wrapping the downstream handler call in the resolved pipeline (during
HandleAsync)
The bug is entirely in the "knowing" responsibility — the handler resolves the wrong pipeline type. The "doing" responsibility needs a minor adjustment to execute the correct pipeline based on which was resolved.
Changes to ResilienceExceptionPolicyHandlerAsync<TRequest> (src/Paramore.Brighter/Policies/Handlers/ResilienceExceptionPolicyHandlerAsync.cs):
- Add a non-generic field:
private ResiliencePipeline _pipeline = ResiliencePipeline.Empty; - Rename existing field for clarity:
private ResiliencePipeline<TRequest> _typePipeline = ResiliencePipeline<TRequest>.Empty; - In
InitializeFromAttributeParams:- Read
UseTypePipelinefrominitializerList[1] - Branch on the flag: call
GetPipeline(key)orGetPipeline<TRequest>(key)
- Read
- In
HandleAsync:- Check
_pipeline != ResiliencePipeline.Emptyfirst (non-generic path) - Fall through to
_typePipelinefor the typed path
- Check
The resulting code mirrors ResilienceExceptionPolicyHandler<TRequest> line-for-line in its initialization logic, just using ExecuteAsync instead of Execute in the handle method.
- No fallback logic: If
UseTypePipeline = trueand the generic pipeline isn't found, we let Polly throwKeyNotFoundException. This is explicit and debuggable. Fallback would mask misconfiguration. - No attribute changes: Both
UseResiliencePipelineAttributeandUseResiliencePipelineAsyncAttributealready passUseTypePipelineviaInitializerParams(). The attributes are correct; only the async handler is broken. - No new types: This is a bug fix in an existing type. No new classes, interfaces, or abstractions are needed.
- Non-generic resilience pipelines can be shared across handlers of different command types
- Async and sync handlers have consistent behavior
- No breaking changes — existing code using
UseTypePipeline = truewith properly registered generic pipelines continues to work - The fix is minimal and focused
- None significant. The change is purely corrective.
- Risk: Existing users who worked around the bug by registering generic pipelines per command type could be affected if they switch to non-generic.
- Mitigation: No behavioral change for
UseTypePipeline = true— only the default (false) path is fixed. Existing workarounds continue to work.
- Mitigation: No behavioral change for
When UseTypePipeline = true and GetPipeline<TRequest>(key) fails, fall back to GetPipeline(key). Rejected because:
- Masks configuration errors
- The
UseTypePipelineflag already gives users explicit control - Adds complexity for a scenario that isn't the reported bug
Simplify by only supporting non-generic pipelines. Rejected because:
- Breaking change for users who rely on typed pipelines
- Typed pipelines have legitimate use cases (e.g., per-command circuit breakers)
- Requirements: specs/0024-resilience_pipeline_bug/requirements.md
- Issue: #4042
- Sync handler (reference implementation):
src/Paramore.Brighter/Policies/Handlers/ResilienceExceptionPolicyHandler.cs - Async handler (bug location):
src/Paramore.Brighter/Policies/Handlers/ResilienceExceptionPolicyHandlerAsync.cs