Note: This document captures user requirements and needs. Technical design decisions and implementation details should be documented in an Architecture Decision Record (ADR) in
docs/adr/.
Linked Issue: #4042
As a developer using Brighter, I would like to register a single non-generic ResiliencePipeline and reuse it across multiple command handlers, so that I don't have to register duplicate pipeline builders for every command type.
Currently, Brighter's resilience pipeline resolution has two bugs that prevent pipeline reuse:
-
Async handler ignores
UseTypePipelineflag:ResilienceExceptionPolicyHandlerAsync<TRequest>.InitializeFromAttributeParams()always callsGetPipeline<TRequest>(key)(the generic/typed lookup) regardless of theUseTypePipelineattribute setting. It never readsinitializerList[1]and has no non-generic_pipelinefield. This means async handlers always require a per-command-type generic pipeline registration. -
No fallback from generic to non-generic lookup: When
UseTypePipeline = trueis set on the sync handler (ResilienceExceptionPolicyHandler), it callsGetPipeline<TRequest>(key)which throwsKeyNotFoundExceptionif only a non-generic pipeline was registered with that key. There is no fallback to check for a non-generic pipeline.
The reporter's scenario: they register a non-generic ResiliencePipeline (e.g., a timeout pipeline named "MyNonGenericTimeoutPipeline") and reference it from a handler via [UseResiliencePipeline("MyNonGenericTimeoutPipeline", 1)]. The async handler always uses the generic lookup path, causing KeyNotFoundException.
Fix the resilience pipeline handlers so that:
- Both sync and async handlers respect the
UseTypePipelineflag - When
UseTypePipeline = false(the default), useGetPipeline(key)to get a non-generic pipeline that can be shared across command types - When
UseTypePipeline = true, useGetPipeline<TRequest>(key)to get a type-specific pipeline - Optionally: when
UseTypePipeline = trueand the generic lookup fails, fall back to the non-generic lookup (graceful degradation)
- FR1:
ResilienceExceptionPolicyHandlerAsyncmust respect theUseTypePipelineattribute parameter, matching the behavior of the sync handler - FR2: When
UseTypePipeline = false(default), both sync and async handlers must look up a non-genericResiliencePipelineviaGetPipeline(key) - FR3: When
UseTypePipeline = true, both sync and async handlers must look up a genericResiliencePipeline<TRequest>viaGetPipeline<TRequest>(key) - FR4: A non-generic
ResiliencePipelineregistered once must be usable across multiple different command handler types without re-registration
- No breaking changes to existing API surface
- No performance regression in pipeline resolution
- Existing tests for typed pipelines must continue to pass
- The
UseResiliencePipelineAttributeandUseResiliencePipelineAsyncAttributealready passUseTypePipelineviaInitializerParams()— no attribute changes needed - Polly's
ResiliencePipelineRegistry<string>supports bothGetPipeline(key)andGetPipeline<T>(key)natively - The sync handler (
ResilienceExceptionPolicyHandler) already handles both paths correctly; it serves as the reference implementation
- Changes to the
UseResiliencePipelineAttributeorUseResiliencePipelineAsyncAttributeclasses - Adding new attribute types
- Changes to Polly's registry behavior
- Automatic fallback from generic to non-generic lookup (nice-to-have, not required)
- AC1: A handler using
[UseResiliencePipeline("key", 1)](defaultUseTypePipeline = false) works with a non-genericResiliencePipelineregistered viaregistry.TryAddBuilder("key", ...)— for both sync and async handlers - AC2: A handler using
[UseResiliencePipeline("key", 1, UseTypePipeline = true)]works with a genericResiliencePipeline<TRequest>registered viaregistry.TryAddBuilder<TRequest>("key", ...)— for both sync and async handlers - AC3: The same non-generic pipeline key can be used by handlers for different command types without
KeyNotFoundException - AC4: All existing resilience pipeline tests continue to pass
- AC5: New tests cover the async handler's non-generic pipeline path
- Reporter's reproduction repo: https://github.com/ravriel/BrighterResilience
- Brighter version affected: 10.3.0
- The sync handler at
ResilienceExceptionPolicyHandler.cs:56-79is the correct reference implementation - The async handler at
ResilienceExceptionPolicyHandlerAsync.cs:59-70is the primary bug location