From 4153d2a4947df4fcd7d262d9bb0dceb6612fce53 Mon Sep 17 00:00:00 2001 From: martintmk <103487740+martintmk@users.noreply.github.com> Date: Tue, 8 Aug 2023 11:41:39 +0200 Subject: [PATCH] Make the `ReactiveResilienceStrategy` type-safe (#1462) --- ...eakerCompositeStrategyBuilderExtensions.cs | 43 ++++++-------- .../CircuitBreakerResilienceStrategy.cs | 2 +- .../CompositeStrategyBuilderExtensions.cs | 47 +++++++++++++++ ...lbackCompositeStrategyBuilderExtensions.cs | 45 +++++++------- .../Fallback/FallbackResilienceStrategy.cs | 2 +- ...dgingCompositeStrategyBuilderExtensions.cs | 58 ++++++++++--------- .../Hedging/HedgingResilienceStrategy.cs | 2 +- src/Polly.Core/PublicAPI.Unshipped.txt | 9 +-- src/Polly.Core/ReactiveResilienceStrategy.cs | 37 +++--------- src/Polly.Core/ResilienceStrategy.cs | 35 +---------- ...RetryCompositeStrategyBuilderExtensions.cs | 34 +++++------ .../Retry/RetryResilienceStrategy.cs | 2 +- .../Utils/ReactiveResilienceStrategyBridge.cs | 35 +++++++++++ src/Polly.Core/Utils/StrategyHelper.cs | 44 ++++++++++++++ .../ResilienceStrategyExtensions.cs | 19 +++++- ...uitBreakerCompositeStrategyBuilderTests.cs | 11 +++- .../CircuitBreakerResilienceStrategyTests.cs | 3 +- .../CompositeStrategyBuilderTests.cs | 10 +++- ...CompositeStrategyBuilderExtensionsTests.cs | 7 ++- .../FallbackResilienceStrategyTests.cs | 5 +- ...CompositeStrategyBuilderExtensionsTests.cs | 13 ++++- .../Hedging/HedgingResilienceStrategyTests.cs | 11 ++-- ...CompositeStrategyBuilderExtensionsTests.cs | 5 +- .../Retry/RetryResilienceStrategyTests.cs | 9 +-- ... ReactiveResilienceStrategyBridgeTests.cs} | 20 ++++--- .../ResilienceStrategyExtensionsTests.cs | 44 +++++++++++++- 26 files changed, 354 insertions(+), 198 deletions(-) create mode 100644 src/Polly.Core/Utils/ReactiveResilienceStrategyBridge.cs create mode 100644 src/Polly.Core/Utils/StrategyHelper.cs rename test/Polly.Core.Tests/Utils/{ReactiveResilienceStrategyTests.cs => ReactiveResilienceStrategyBridgeTests.cs} (69%) diff --git a/src/Polly.Core/CircuitBreaker/CircuitBreakerCompositeStrategyBuilderExtensions.cs b/src/Polly.Core/CircuitBreaker/CircuitBreakerCompositeStrategyBuilderExtensions.cs index a7f02bcac3c..281e7845cd8 100644 --- a/src/Polly.Core/CircuitBreaker/CircuitBreakerCompositeStrategyBuilderExtensions.cs +++ b/src/Polly.Core/CircuitBreaker/CircuitBreakerCompositeStrategyBuilderExtensions.cs @@ -24,12 +24,17 @@ public static class CircuitBreakerCompositeStrategyBuilderExtensions /// /// Thrown when or is . /// Thrown when are invalid. + [UnconditionalSuppressMessage( + "Trimming", + "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", + Justification = "All options members preserved.")] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(CircuitBreakerStrategyOptions))] public static CompositeStrategyBuilder AddCircuitBreaker(this CompositeStrategyBuilder builder, CircuitBreakerStrategyOptions options) { Guard.NotNull(builder); Guard.NotNull(options); - return builder.AddCircuitBreakerCore(options); + return builder.AddStrategy(context => CreateStrategy(context, options), options); } /// @@ -47,39 +52,29 @@ public static CompositeStrategyBuilder AddCircuitBreaker(this CompositeStrategyB /// /// Thrown when or is . /// Thrown when are invalid. - public static CompositeStrategyBuilder AddCircuitBreaker(this CompositeStrategyBuilder builder, CircuitBreakerStrategyOptions options) - { - Guard.NotNull(builder); - Guard.NotNull(options); - - return builder.AddCircuitBreakerCore(options); - } - [UnconditionalSuppressMessage( "Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "All options members preserved.")] - private static TBuilder AddCircuitBreakerCore(this TBuilder builder, CircuitBreakerStrategyOptions options) - where TBuilder : CompositeStrategyBuilderBase + public static CompositeStrategyBuilder AddCircuitBreaker<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TResult>( + this CompositeStrategyBuilder builder, + CircuitBreakerStrategyOptions options) { - return builder.AddStrategy( - context => - { - var behavior = new AdvancedCircuitBehavior( - options.FailureRatio, - options.MinimumThroughput, - HealthMetrics.Create(options.SamplingDuration, context.TimeProvider)); + Guard.NotNull(builder); + Guard.NotNull(options); - return CreateStrategy>(context, options, behavior); - }, - options); + return builder.AddStrategy(context => CreateStrategy(context, options), options); } - internal static CircuitBreakerResilienceStrategy CreateStrategy( + internal static CircuitBreakerResilienceStrategy CreateStrategy( StrategyBuilderContext context, - CircuitBreakerStrategyOptions options, - CircuitBehavior behavior) + CircuitBreakerStrategyOptions options) { + var behavior = new AdvancedCircuitBehavior( + options.FailureRatio, + options.MinimumThroughput, + HealthMetrics.Create(options.SamplingDuration, context.TimeProvider)); + var controller = new CircuitStateController( options.BreakDuration, options.OnOpened, diff --git a/src/Polly.Core/CircuitBreaker/CircuitBreakerResilienceStrategy.cs b/src/Polly.Core/CircuitBreaker/CircuitBreakerResilienceStrategy.cs index 716931863c9..f564c86aa01 100644 --- a/src/Polly.Core/CircuitBreaker/CircuitBreakerResilienceStrategy.cs +++ b/src/Polly.Core/CircuitBreaker/CircuitBreakerResilienceStrategy.cs @@ -21,7 +21,7 @@ public CircuitBreakerResilienceStrategy( _controller.Dispose); } - protected override async ValueTask> ExecuteCore(Func>> callback, ResilienceContext context, TState state) + protected internal override async ValueTask> ExecuteCore(Func>> callback, ResilienceContext context, TState state) { if (await _controller.OnActionPreExecuteAsync(context).ConfigureAwait(context.ContinueOnCapturedContext) is Outcome outcome) { diff --git a/src/Polly.Core/CompositeStrategyBuilderExtensions.cs b/src/Polly.Core/CompositeStrategyBuilderExtensions.cs index 0986ae01bda..a55625d9c6f 100644 --- a/src/Polly.Core/CompositeStrategyBuilderExtensions.cs +++ b/src/Polly.Core/CompositeStrategyBuilderExtensions.cs @@ -70,6 +70,53 @@ public static TBuilder AddStrategy(this TBuilder builder, Func + /// Adds a reactive strategy to the builder. + /// + /// The builder instance. + /// The factory that creates a resilience strategy. + /// The options associated with the strategy. If none are provided the default instance of is created. + /// The same builder instance. + /// Thrown when , or is . + /// Thrown when this builder was already used to create a strategy. The builder cannot be modified after it has been used. + /// Thrown when is invalid. + [RequiresUnreferencedCode(Constants.OptionsValidation)] + public static CompositeStrategyBuilder AddStrategy( + this CompositeStrategyBuilder builder, Func> factory, + ResilienceStrategyOptions options) + { + Guard.NotNull(builder); + Guard.NotNull(factory); + Guard.NotNull(options); + + builder.AddStrategyCore(context => new ReactiveResilienceStrategyBridge(factory(context)), options); + return builder; + } + + /// + /// Adds a reactive strategy to the builder. + /// + /// The type of the result. + /// The builder instance. + /// The factory that creates a resilience strategy. + /// The options associated with the strategy. If none are provided the default instance of is created. + /// The same builder instance. + /// Thrown when , or is . + /// Thrown when this builder was already used to create a strategy. The builder cannot be modified after it has been used. + /// Thrown when is invalid. + [RequiresUnreferencedCode(Constants.OptionsValidation)] + public static CompositeStrategyBuilder AddStrategy( + this CompositeStrategyBuilder builder, Func> factory, + ResilienceStrategyOptions options) + { + Guard.NotNull(builder); + Guard.NotNull(factory); + Guard.NotNull(options); + + builder.AddStrategyCore(context => new ReactiveResilienceStrategyBridge(factory(context)), options); + return builder; + } + internal sealed class EmptyOptions : ResilienceStrategyOptions { public static readonly EmptyOptions Instance = new(); diff --git a/src/Polly.Core/Fallback/FallbackCompositeStrategyBuilderExtensions.cs b/src/Polly.Core/Fallback/FallbackCompositeStrategyBuilderExtensions.cs index 93f6dec04be..9404b51467f 100644 --- a/src/Polly.Core/Fallback/FallbackCompositeStrategyBuilderExtensions.cs +++ b/src/Polly.Core/Fallback/FallbackCompositeStrategyBuilderExtensions.cs @@ -18,13 +18,18 @@ public static class FallbackCompositeStrategyBuilderExtensions /// The builder instance with the fallback strategy added. /// Thrown when or is . /// Thrown when are invalid. - public static CompositeStrategyBuilder AddFallback(this CompositeStrategyBuilder builder, FallbackStrategyOptions options) + [UnconditionalSuppressMessage( + "Trimming", + "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", + Justification = "All options members preserved.")] + public static CompositeStrategyBuilder AddFallback<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TResult>( + this CompositeStrategyBuilder builder, + FallbackStrategyOptions options) { Guard.NotNull(builder); Guard.NotNull(options); - builder.AddFallbackCore>(options); - return builder; + return builder.AddStrategy(context => CreateFallback(context, options), options); } /// @@ -35,34 +40,30 @@ public static CompositeStrategyBuilder AddFallback(this Compos /// The builder instance with the fallback strategy added. /// Thrown when or is . /// Thrown when are invalid. + [UnconditionalSuppressMessage( + "Trimming", + "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", + Justification = "All options members preserved.")] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(FallbackStrategyOptions))] internal static CompositeStrategyBuilder AddFallback(this CompositeStrategyBuilder builder, FallbackStrategyOptions options) { Guard.NotNull(builder); Guard.NotNull(options); - builder.AddFallbackCore(options); - return builder; + return builder.AddStrategy(context => CreateFallback(context, options), options); } - [UnconditionalSuppressMessage( - "Trimming", - "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", - Justification = "All options members preserved.")] - internal static void AddFallbackCore( - this CompositeStrategyBuilderBase builder, + private static ReactiveResilienceStrategy CreateFallback( + StrategyBuilderContext context, FallbackStrategyOptions options) { - builder.AddStrategy(context => - { - var handler = new FallbackHandler( - options.ShouldHandle!, - options.FallbackAction!); + var handler = new FallbackHandler( + options.ShouldHandle!, + options.FallbackAction!); - return new FallbackResilienceStrategy( - handler, - options.OnFallback, - context.Telemetry); - }, - options); + return new FallbackResilienceStrategy( + handler, + options.OnFallback, + context.Telemetry); } } diff --git a/src/Polly.Core/Fallback/FallbackResilienceStrategy.cs b/src/Polly.Core/Fallback/FallbackResilienceStrategy.cs index 7c153dc20ce..951d47c20e8 100644 --- a/src/Polly.Core/Fallback/FallbackResilienceStrategy.cs +++ b/src/Polly.Core/Fallback/FallbackResilienceStrategy.cs @@ -17,7 +17,7 @@ public FallbackResilienceStrategy(FallbackHandler handler, Func> ExecuteCore(Func>> callback, ResilienceContext context, TState state) + protected internal override async ValueTask> ExecuteCore(Func>> callback, ResilienceContext context, TState state) { var outcome = await ExecuteCallbackSafeAsync(callback, context, state).ConfigureAwait(context.ContinueOnCapturedContext); var handleFallbackArgs = new OutcomeArguments(context, outcome, default); diff --git a/src/Polly.Core/Hedging/HedgingCompositeStrategyBuilderExtensions.cs b/src/Polly.Core/Hedging/HedgingCompositeStrategyBuilderExtensions.cs index 63a06f674e9..179dfc63757 100644 --- a/src/Polly.Core/Hedging/HedgingCompositeStrategyBuilderExtensions.cs +++ b/src/Polly.Core/Hedging/HedgingCompositeStrategyBuilderExtensions.cs @@ -19,13 +19,18 @@ public static class HedgingCompositeStrategyBuilderExtensions /// The builder instance with the hedging strategy added. /// Thrown when or is . /// Thrown when are invalid. - public static CompositeStrategyBuilder AddHedging(this CompositeStrategyBuilder builder, HedgingStrategyOptions options) + [UnconditionalSuppressMessage( + "Trimming", + "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", + Justification = "All options members preserved.")] + public static CompositeStrategyBuilder AddHedging<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TResult>( + this CompositeStrategyBuilder builder, + HedgingStrategyOptions options) { Guard.NotNull(builder); Guard.NotNull(options); - builder.AddHedgingCore>(options); - return builder; + return builder.AddStrategy(context => CreateHedgingStrategy(context, options, isGeneric: true), options); } /// @@ -36,39 +41,36 @@ public static CompositeStrategyBuilder AddHedging(this Composi /// The builder instance with the hedging strategy added. /// Thrown when or is . /// Thrown when are invalid. + [UnconditionalSuppressMessage( + "Trimming", + "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", + Justification = "All options members preserved.")] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(HedgingStrategyOptions))] internal static CompositeStrategyBuilder AddHedging(this CompositeStrategyBuilder builder, HedgingStrategyOptions options) { Guard.NotNull(builder); Guard.NotNull(options); - builder.AddHedgingCore(options); - return builder; + return builder.AddStrategy(context => CreateHedgingStrategy(context, options, isGeneric: false), options); } - [UnconditionalSuppressMessage( - "Trimming", - "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", - Justification = "All options members preserved.")] - internal static void AddHedgingCore( - this CompositeStrategyBuilderBase builder, - HedgingStrategyOptions options) + private static HedgingResilienceStrategy CreateHedgingStrategy( + StrategyBuilderContext context, + HedgingStrategyOptions options, + bool isGeneric) { - builder.AddStrategy(context => - { - var handler = new HedgingHandler( - options.ShouldHandle!, - options.HedgingActionGenerator, - IsGeneric: builder is not CompositeStrategyBuilder); + var handler = new HedgingHandler( + options.ShouldHandle!, + options.HedgingActionGenerator, + IsGeneric: isGeneric); - return new HedgingResilienceStrategy( - options.HedgingDelay, - options.MaxHedgedAttempts, - handler, - options.OnHedging, - options.HedgingDelayGenerator, - context.TimeProvider, - context.Telemetry); - }, - options); + return new HedgingResilienceStrategy( + options.HedgingDelay, + options.MaxHedgedAttempts, + handler, + options.OnHedging, + options.HedgingDelayGenerator, + context.TimeProvider, + context.Telemetry); } } diff --git a/src/Polly.Core/Hedging/HedgingResilienceStrategy.cs b/src/Polly.Core/Hedging/HedgingResilienceStrategy.cs index 62f0f7c5efc..f8c9cc90efe 100644 --- a/src/Polly.Core/Hedging/HedgingResilienceStrategy.cs +++ b/src/Polly.Core/Hedging/HedgingResilienceStrategy.cs @@ -41,7 +41,7 @@ public HedgingResilienceStrategy( public Func, ValueTask>? OnHedging { get; } [ExcludeFromCodeCoverage] // coverlet issue - protected override async ValueTask> ExecuteCore( + protected internal override async ValueTask> ExecuteCore( Func>> callback, ResilienceContext context, TState state) diff --git a/src/Polly.Core/PublicAPI.Unshipped.txt b/src/Polly.Core/PublicAPI.Unshipped.txt index 89510909755..69b8176bc73 100644 --- a/src/Polly.Core/PublicAPI.Unshipped.txt +++ b/src/Polly.Core/PublicAPI.Unshipped.txt @@ -1,4 +1,4 @@ -abstract Polly.ReactiveResilienceStrategy.ExecuteCore(System.Func>>! callback, Polly.ResilienceContext! context, TState state) -> System.Threading.Tasks.ValueTask> +abstract Polly.ReactiveResilienceStrategy.ExecuteCore(System.Func>>! callback, Polly.ResilienceContext! context, TState state) -> System.Threading.Tasks.ValueTask> abstract Polly.Registry.ResilienceStrategyProvider.TryGetStrategy(TKey key, out Polly.ResilienceStrategy? strategy) -> bool abstract Polly.Registry.ResilienceStrategyProvider.TryGetStrategy(TKey key, out Polly.ResilienceStrategy? strategy) -> bool abstract Polly.ResilienceContextPool.Get(string? operationKey, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> Polly.ResilienceContext! @@ -11,7 +11,6 @@ override Polly.ResiliencePropertyKey.Equals(object? obj) -> bool override Polly.ResiliencePropertyKey.GetHashCode() -> int override Polly.ResiliencePropertyKey.ToString() -> string! override Polly.Telemetry.ResilienceEvent.ToString() -> string! -override sealed Polly.ReactiveResilienceStrategy.ExecuteCore(System.Func>>! callback, Polly.ResilienceContext! context, TState state) -> System.Threading.Tasks.ValueTask> Polly.CircuitBreaker.BrokenCircuitException Polly.CircuitBreaker.BrokenCircuitException.BrokenCircuitException() -> void Polly.CircuitBreaker.BrokenCircuitException.BrokenCircuitException(string! message) -> void @@ -179,8 +178,8 @@ Polly.PredicateBuilder.HandleResult(System.Func! predica Polly.PredicateBuilder.HandleResult(TResult result, System.Collections.Generic.IEqualityComparer? comparer = null) -> Polly.PredicateBuilder! Polly.PredicateBuilder.PredicateBuilder() -> void Polly.PredicateResult -Polly.ReactiveResilienceStrategy -Polly.ReactiveResilienceStrategy.ReactiveResilienceStrategy() -> void +Polly.ReactiveResilienceStrategy +Polly.ReactiveResilienceStrategy.ReactiveResilienceStrategy() -> void Polly.Registry.ConfigureBuilderContext Polly.Registry.ConfigureBuilderContext.BuilderInstanceName.get -> string? Polly.Registry.ConfigureBuilderContext.BuilderName.get -> string! @@ -388,9 +387,11 @@ Polly.TimeoutCompositeStrategyBuilderExtensions Polly.Utils.LegacySupport static Polly.CircuitBreakerCompositeStrategyBuilderExtensions.AddCircuitBreaker(this Polly.CompositeStrategyBuilder! builder, Polly.CircuitBreaker.CircuitBreakerStrategyOptions! options) -> Polly.CompositeStrategyBuilder! static Polly.CircuitBreakerCompositeStrategyBuilderExtensions.AddCircuitBreaker(this Polly.CompositeStrategyBuilder! builder, Polly.CircuitBreaker.CircuitBreakerStrategyOptions! options) -> Polly.CompositeStrategyBuilder! +static Polly.CompositeStrategyBuilderExtensions.AddStrategy(this Polly.CompositeStrategyBuilder! builder, System.Func!>! factory, Polly.ResilienceStrategyOptions! options) -> Polly.CompositeStrategyBuilder! static Polly.CompositeStrategyBuilderExtensions.AddStrategy(this TBuilder! builder, Polly.ResilienceStrategy! strategy) -> TBuilder! static Polly.CompositeStrategyBuilderExtensions.AddStrategy(this TBuilder! builder, System.Func! factory, Polly.ResilienceStrategyOptions! options) -> TBuilder! static Polly.CompositeStrategyBuilderExtensions.AddStrategy(this Polly.CompositeStrategyBuilder! builder, Polly.ResilienceStrategy! strategy) -> Polly.CompositeStrategyBuilder! +static Polly.CompositeStrategyBuilderExtensions.AddStrategy(this Polly.CompositeStrategyBuilder! builder, System.Func!>! factory, Polly.ResilienceStrategyOptions! options) -> Polly.CompositeStrategyBuilder! static Polly.FallbackCompositeStrategyBuilderExtensions.AddFallback(this Polly.CompositeStrategyBuilder! builder, Polly.Fallback.FallbackStrategyOptions! options) -> Polly.CompositeStrategyBuilder! static Polly.HedgingCompositeStrategyBuilderExtensions.AddHedging(this Polly.CompositeStrategyBuilder! builder, Polly.Hedging.HedgingStrategyOptions! options) -> Polly.CompositeStrategyBuilder! static Polly.Outcome.FromException(System.Exception! exception) -> Polly.Outcome diff --git a/src/Polly.Core/ReactiveResilienceStrategy.cs b/src/Polly.Core/ReactiveResilienceStrategy.cs index c8d8c51a3e9..fe768b1e576 100644 --- a/src/Polly.Core/ReactiveResilienceStrategy.cs +++ b/src/Polly.Core/ReactiveResilienceStrategy.cs @@ -4,11 +4,11 @@ /// This base strategy class is used to simplify the implementation of generic (reactive) /// strategies by limiting the number of generic types the execute method receives. /// -/// The type of result this strategy handles. +/// The type of result this strategy handles. /// /// For strategies that handle all result types the generic parameter must be of type . /// -public abstract class ReactiveResilienceStrategy : ResilienceStrategy +public abstract class ReactiveResilienceStrategy { /// /// An implementation of resilience strategy that executes the specified . @@ -33,36 +33,13 @@ public abstract class ReactiveResilienceStrategy : ResilienceStrategy /// Similarly, do not throw exceptions from your strategy implementation. Instead, return an exception instance as . /// /// - protected abstract ValueTask> ExecuteCore( - Func>> callback, + protected internal abstract ValueTask> ExecuteCore( + Func>> callback, ResilienceContext context, TState state); - /// - protected internal sealed override ValueTask> ExecuteCore( - Func>> callback, + internal static ValueTask> ExecuteCallbackSafeAsync( + Func>> callback, ResilienceContext context, - TState state) - { - // Check if we can cast directly, thus saving some cycles and improving the performance - if (callback is Func>> casted) - { - return TaskHelper.ConvertValueTask( - ExecuteCore(casted, context, state), - context); - } - else - { - var valueTask = ExecuteCore( - static async (context, state) => - { - var outcome = await state.callback(context, state.state).ConfigureAwait(context.ContinueOnCapturedContext); - return outcome.AsOutcome(); - }, - context, - (callback, state)); - - return TaskHelper.ConvertValueTask(valueTask, context); - } - } + TState state) => StrategyHelper.ExecuteCallbackSafeAsync(callback, context, state); } diff --git a/src/Polly.Core/ResilienceStrategy.cs b/src/Polly.Core/ResilienceStrategy.cs index 6556c3efd30..24c439b715d 100644 --- a/src/Polly.Core/ResilienceStrategy.cs +++ b/src/Polly.Core/ResilienceStrategy.cs @@ -63,38 +63,5 @@ private Outcome ExecuteCoreSync( internal static ValueTask> ExecuteCallbackSafeAsync( Func>> callback, ResilienceContext context, - TState state) - { - if (context.CancellationToken.IsCancellationRequested) - { - return new ValueTask>(Outcome.FromException(new OperationCanceledException(context.CancellationToken))); - } - - try - { - var callbackTask = callback(context, state); - if (callbackTask.IsCompleted) - { - return new ValueTask>(callbackTask.GetResult()); - } - - return AwaitTask(callbackTask, context.ContinueOnCapturedContext); - } - catch (Exception e) - { - return new ValueTask>(Outcome.FromException(e)); - } - - static async ValueTask> AwaitTask(ValueTask> task, bool continueOnCapturedContext) - { - try - { - return await task.ConfigureAwait(continueOnCapturedContext); - } - catch (Exception e) - { - return Outcome.FromException(e); - } - } - } + TState state) => StrategyHelper.ExecuteCallbackSafeAsync(callback, context, state); } diff --git a/src/Polly.Core/Retry/RetryCompositeStrategyBuilderExtensions.cs b/src/Polly.Core/Retry/RetryCompositeStrategyBuilderExtensions.cs index 95309303488..716204d1bb3 100644 --- a/src/Polly.Core/Retry/RetryCompositeStrategyBuilderExtensions.cs +++ b/src/Polly.Core/Retry/RetryCompositeStrategyBuilderExtensions.cs @@ -17,13 +17,19 @@ public static class RetryCompositeStrategyBuilderExtensions /// The builder instance with the retry strategy added. /// Thrown when or is . /// Thrown when are invalid. + [UnconditionalSuppressMessage( + "Trimming", + "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", + Justification = "All options members preserved.")] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(RetryStrategyOptions))] public static CompositeStrategyBuilder AddRetry(this CompositeStrategyBuilder builder, RetryStrategyOptions options) { Guard.NotNull(builder); Guard.NotNull(options); - builder.AddRetryCore(options); - return builder; + return builder.AddStrategy( + context => new RetryResilienceStrategy(options, context.TimeProvider, context.Telemetry, context.Randomizer), + options); } /// @@ -35,29 +41,19 @@ public static CompositeStrategyBuilder AddRetry(this CompositeStrategyBuilder bu /// The builder instance with the retry strategy added. /// Thrown when or is . /// Thrown when are invalid. - public static CompositeStrategyBuilder AddRetry(this CompositeStrategyBuilder builder, RetryStrategyOptions options) - { - Guard.NotNull(builder); - Guard.NotNull(options); - - builder.AddRetryCore>(options); - return builder; - } - [UnconditionalSuppressMessage( "Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "All options members preserved.")] - private static void AddRetryCore( - this CompositeStrategyBuilderBase builder, + public static CompositeStrategyBuilder AddRetry<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TResult>( + this CompositeStrategyBuilder builder, RetryStrategyOptions options) { - builder.AddStrategy(context => - new RetryResilienceStrategy( - options, - context.TimeProvider, - context.Telemetry, - context.Randomizer), + Guard.NotNull(builder); + Guard.NotNull(options); + + return builder.AddStrategy( + context => new RetryResilienceStrategy(options, context.TimeProvider, context.Telemetry, context.Randomizer), options); } } diff --git a/src/Polly.Core/Retry/RetryResilienceStrategy.cs b/src/Polly.Core/Retry/RetryResilienceStrategy.cs index 13a7fbcf004..8cb6bbed6ca 100644 --- a/src/Polly.Core/Retry/RetryResilienceStrategy.cs +++ b/src/Polly.Core/Retry/RetryResilienceStrategy.cs @@ -42,7 +42,7 @@ public RetryResilienceStrategy( public Func, ValueTask>? OnRetry { get; } - protected override async ValueTask> ExecuteCore(Func>> callback, ResilienceContext context, TState state) + protected internal override async ValueTask> ExecuteCore(Func>> callback, ResilienceContext context, TState state) { double retryState = 0; diff --git a/src/Polly.Core/Utils/ReactiveResilienceStrategyBridge.cs b/src/Polly.Core/Utils/ReactiveResilienceStrategyBridge.cs new file mode 100644 index 00000000000..ebddbbe1c3f --- /dev/null +++ b/src/Polly.Core/Utils/ReactiveResilienceStrategyBridge.cs @@ -0,0 +1,35 @@ +namespace Polly.Utils; + +internal sealed class ReactiveResilienceStrategyBridge : ResilienceStrategy +{ + public ReactiveResilienceStrategyBridge(ReactiveResilienceStrategy strategy) => Strategy = strategy; + + public ReactiveResilienceStrategy Strategy { get; } + + protected internal override ValueTask> ExecuteCore( + Func>> callback, + ResilienceContext context, + TState state) + { + // Check if we can cast directly, thus saving some cycles and improving the performance + if (callback is Func>> casted) + { + return TaskHelper.ConvertValueTask( + Strategy.ExecuteCore(casted, context, state), + context); + } + else + { + var valueTask = Strategy.ExecuteCore( + static async (context, state) => + { + var outcome = await state.callback(context, state.state).ConfigureAwait(context.ContinueOnCapturedContext); + return outcome.AsOutcome(); + }, + context, + (callback, state)); + + return TaskHelper.ConvertValueTask(valueTask, context); + } + } +} diff --git a/src/Polly.Core/Utils/StrategyHelper.cs b/src/Polly.Core/Utils/StrategyHelper.cs new file mode 100644 index 00000000000..e0f1d9568a0 --- /dev/null +++ b/src/Polly.Core/Utils/StrategyHelper.cs @@ -0,0 +1,44 @@ +namespace Polly.Utils; + +#pragma warning disable CA1031 // Do not catch general exception types + +internal static class StrategyHelper +{ + public static ValueTask> ExecuteCallbackSafeAsync( + Func>> callback, + ResilienceContext context, + TState state) + { + if (context.CancellationToken.IsCancellationRequested) + { + return new ValueTask>(Outcome.FromException(new OperationCanceledException(context.CancellationToken))); + } + + try + { + var callbackTask = callback(context, state); + if (callbackTask.IsCompleted) + { + return new ValueTask>(callbackTask.GetResult()); + } + + return AwaitTask(callbackTask, context.ContinueOnCapturedContext); + } + catch (Exception e) + { + return new ValueTask>(Outcome.FromException(e)); + } + + static async ValueTask> AwaitTask(ValueTask> task, bool continueOnCapturedContext) + { + try + { + return await task.ConfigureAwait(continueOnCapturedContext); + } + catch (Exception e) + { + return Outcome.FromException(e); + } + } + } +} diff --git a/src/Polly.Testing/ResilienceStrategyExtensions.cs b/src/Polly.Testing/ResilienceStrategyExtensions.cs index 9f8f0b3bc89..ccffe1175a3 100644 --- a/src/Polly.Testing/ResilienceStrategyExtensions.cs +++ b/src/Polly.Testing/ResilienceStrategyExtensions.cs @@ -20,7 +20,7 @@ public static InnerStrategiesDescriptor GetInnerStrategies(this Resilie { Guard.NotNull(strategy); - return strategy.Strategy.GetInnerStrategies(); + return GetInnerStrategiesCore(strategy.Strategy); } /// @@ -33,10 +33,15 @@ public static InnerStrategiesDescriptor GetInnerStrategies(this ResilienceStrate { Guard.NotNull(strategy); + return GetInnerStrategiesCore(strategy); + } + + private static InnerStrategiesDescriptor GetInnerStrategiesCore(ResilienceStrategy strategy) + { var strategies = new List(); strategy.ExpandStrategies(strategies); - var innerStrategies = strategies.Select(s => new ResilienceStrategyDescriptor(s.Options, s.GetType())).ToList(); + var innerStrategies = strategies.Select(s => new ResilienceStrategyDescriptor(s.Options, GetStrategyType(s))).ToList(); return new InnerStrategiesDescriptor( innerStrategies.Where(s => !ShouldSkip(s.StrategyType)).ToList().AsReadOnly(), @@ -44,6 +49,16 @@ public static InnerStrategiesDescriptor GetInnerStrategies(this ResilienceStrate isReloadable: innerStrategies.Exists(s => s.StrategyType == typeof(ReloadableResilienceStrategy))); } + private static Type GetStrategyType(ResilienceStrategy strategy) + { + if (strategy is ReactiveResilienceStrategyBridge bridge) + { + return bridge.Strategy.GetType(); + } + + return strategy.GetType(); + } + private static bool ShouldSkip(Type type) => type == typeof(ReloadableResilienceStrategy) || type.FullName == TelemetryResilienceStrategy; private static void ExpandStrategies(this ResilienceStrategy strategy, List strategies) diff --git a/test/Polly.Core.Tests/CircuitBreaker/CircuitBreakerCompositeStrategyBuilderTests.cs b/test/Polly.Core.Tests/CircuitBreaker/CircuitBreakerCompositeStrategyBuilderTests.cs index d79aeba9e92..95628ed9bd4 100644 --- a/test/Polly.Core.Tests/CircuitBreaker/CircuitBreakerCompositeStrategyBuilderTests.cs +++ b/test/Polly.Core.Tests/CircuitBreaker/CircuitBreakerCompositeStrategyBuilderTests.cs @@ -1,6 +1,7 @@ using System.ComponentModel.DataAnnotations; using Microsoft.Extensions.Time.Testing; using Polly.CircuitBreaker; +using Polly.Utils; namespace Polly.Core.Tests.CircuitBreaker; @@ -32,7 +33,10 @@ public void AddCircuitBreaker_Configure(Action builder var strategy = builder.Build(); - strategy.Should().BeOfType>(); + strategy + .Should().BeOfType>().Subject + .Strategy + .Should().BeOfType>(); } [MemberData(nameof(ConfigureDataGeneric))] @@ -45,7 +49,10 @@ public void AddCircuitBreaker_Generic_Configure(Action>(); + strategy + .Should().BeOfType>().Subject + .Strategy + .Should().BeOfType>(); } [Fact] diff --git a/test/Polly.Core.Tests/CircuitBreaker/CircuitBreakerResilienceStrategyTests.cs b/test/Polly.Core.Tests/CircuitBreaker/CircuitBreakerResilienceStrategyTests.cs index f81b2f4dfba..0d2e75accb7 100644 --- a/test/Polly.Core.Tests/CircuitBreaker/CircuitBreakerResilienceStrategyTests.cs +++ b/test/Polly.Core.Tests/CircuitBreaker/CircuitBreakerResilienceStrategyTests.cs @@ -2,6 +2,7 @@ using Moq; using Polly.CircuitBreaker; using Polly.Telemetry; +using Polly.Utils; namespace Polly.Core.Tests.CircuitBreaker; @@ -130,5 +131,5 @@ public void Execute_Ok() Create().Invoking(s => s.Execute(_ => 0)).Should().NotThrow(); } - private CircuitBreakerResilienceStrategy Create() => new(_options.ShouldHandle!, _controller, _options.StateProvider, _options.ManualControl); + private ReactiveResilienceStrategyBridge Create() => new(new CircuitBreakerResilienceStrategy(_options.ShouldHandle!, _controller, _options.StateProvider, _options.ManualControl)); } diff --git a/test/Polly.Core.Tests/CompositeStrategyBuilderTests.cs b/test/Polly.Core.Tests/CompositeStrategyBuilderTests.cs index 255ca5b58e8..3462bab7aa2 100644 --- a/test/Polly.Core.Tests/CompositeStrategyBuilderTests.cs +++ b/test/Polly.Core.Tests/CompositeStrategyBuilderTests.cs @@ -236,7 +236,15 @@ public void AddStrategy_NullFactory_Throws() var builder = new CompositeStrategyBuilder(); builder - .Invoking(b => b.AddStrategy(null!, new TestResilienceStrategyOptions())) + .Invoking(b => b.AddStrategy((Func)null!, new TestResilienceStrategyOptions())) + .Should() + .Throw() + .And.ParamName + .Should() + .Be("factory"); + + builder + .Invoking(b => b.AddStrategy((Func>)null!, new TestResilienceStrategyOptions())) .Should() .Throw() .And.ParamName diff --git a/test/Polly.Core.Tests/Fallback/FallbackCompositeStrategyBuilderExtensionsTests.cs b/test/Polly.Core.Tests/Fallback/FallbackCompositeStrategyBuilderExtensionsTests.cs index 3ad664e5d23..0b86d91e255 100644 --- a/test/Polly.Core.Tests/Fallback/FallbackCompositeStrategyBuilderExtensionsTests.cs +++ b/test/Polly.Core.Tests/Fallback/FallbackCompositeStrategyBuilderExtensionsTests.cs @@ -1,5 +1,6 @@ using System.ComponentModel.DataAnnotations; using Polly.Fallback; +using Polly.Utils; namespace Polly.Core.Tests.Fallback; @@ -23,7 +24,11 @@ public void AddFallback_Generic_Ok(Action> configu { var builder = new CompositeStrategyBuilder(); configure(builder); - builder.Build().Strategy.Should().BeOfType>(); + + builder.Build().Strategy + .Should().BeOfType>().Subject + .Strategy + .Should().BeOfType>(); } [Fact] diff --git a/test/Polly.Core.Tests/Fallback/FallbackResilienceStrategyTests.cs b/test/Polly.Core.Tests/Fallback/FallbackResilienceStrategyTests.cs index ca17f5915c1..9a5091fc030 100644 --- a/test/Polly.Core.Tests/Fallback/FallbackResilienceStrategyTests.cs +++ b/test/Polly.Core.Tests/Fallback/FallbackResilienceStrategyTests.cs @@ -1,5 +1,6 @@ using Polly.Fallback; using Polly.Telemetry; +using Polly.Utils; namespace Polly.Core.Tests.Fallback; @@ -89,8 +90,8 @@ private void SetHandler( _handler = FallbackHelper.CreateHandler(shouldHandle, fallback); } - private FallbackResilienceStrategy Create() => new( + private ReactiveResilienceStrategyBridge Create() => new(new FallbackResilienceStrategy( _handler!, _options.OnFallback, - _telemetry); + _telemetry)); } diff --git a/test/Polly.Core.Tests/Hedging/HedgingCompositeStrategyBuilderExtensionsTests.cs b/test/Polly.Core.Tests/Hedging/HedgingCompositeStrategyBuilderExtensionsTests.cs index cc7224c0032..a4c99ac3644 100644 --- a/test/Polly.Core.Tests/Hedging/HedgingCompositeStrategyBuilderExtensionsTests.cs +++ b/test/Polly.Core.Tests/Hedging/HedgingCompositeStrategyBuilderExtensionsTests.cs @@ -1,5 +1,6 @@ using System.ComponentModel.DataAnnotations; using Polly.Hedging; +using Polly.Utils; namespace Polly.Core.Tests.Hedging; @@ -12,9 +13,12 @@ public class HedgingCompositeStrategyBuilderExtensionsTests public void AddHedging_Ok() { _builder.AddHedging(new HedgingStrategyOptions { ShouldHandle = _ => PredicateResult.True }); - var strategy = _builder.Build().Should().BeOfType>() - .Subject - .HedgingHandler.IsGeneric.Should().BeFalse(); + + _builder.Build() + .Should().BeOfType>().Subject + .Strategy + .Should().BeOfType>() + .Subject.HedgingHandler.IsGeneric.Should().BeFalse(); } [Fact] @@ -25,7 +29,10 @@ public void AddHedging_Generic_Ok() HedgingActionGenerator = args => () => Outcome.FromResultAsTask("dummy"), ShouldHandle = _ => PredicateResult.True }); + _genericBuilder.Build().Strategy + .Should().BeOfType>().Subject + .Strategy .Should().BeOfType>() .Subject.HedgingHandler.IsGeneric.Should().BeTrue(); } diff --git a/test/Polly.Core.Tests/Hedging/HedgingResilienceStrategyTests.cs b/test/Polly.Core.Tests/Hedging/HedgingResilienceStrategyTests.cs index fd2c84ababb..7331e4d2871 100644 --- a/test/Polly.Core.Tests/Hedging/HedgingResilienceStrategyTests.cs +++ b/test/Polly.Core.Tests/Hedging/HedgingResilienceStrategyTests.cs @@ -1,6 +1,7 @@ using Polly.Hedging; using Polly.Hedging.Utils; using Polly.Telemetry; +using Polly.Utils; using Xunit.Abstractions; namespace Polly.Core.Tests.Hedging; @@ -46,7 +47,7 @@ public void Dispose() public void Ctor_EnsureDefaults() { ConfigureHedging(); - var strategy = Create(); + var strategy = (HedgingResilienceStrategy)Create().Strategy; strategy.MaxHedgedAttempts.Should().Be(_options.MaxHedgedAttempts); strategy.HedgingDelay.Should().Be(_options.HedgingDelay); @@ -127,7 +128,7 @@ public async Task GetHedgingDelayAsync_GeneratorSet_EnsureCorrectGeneratedValue( { _options.HedgingDelayGenerator = args => new ValueTask(TimeSpan.FromSeconds(seconds)); - var strategy = Create(); + var strategy = (HedgingResilienceStrategy)Create().Strategy; var result = await strategy.GetHedgingDelayAsync(ResilienceContextPool.Shared.Get(), 0); @@ -139,7 +140,7 @@ public async Task GetHedgingDelayAsync_NoGeneratorSet_EnsureCorrectValue() { _options.HedgingDelay = TimeSpan.FromMilliseconds(123); - var strategy = Create(); + var strategy = (HedgingResilienceStrategy)Create().Strategy; var result = await strategy.GetHedgingDelayAsync(ResilienceContextPool.Shared.Get(), 0); @@ -329,7 +330,7 @@ public async Task ExecuteAsync_EnsureDiscardedResultDisposed() }; }); - var strategy = Create(handler, null); + var strategy = new ReactiveResilienceStrategyBridge(Create(handler, null)); // act var resultTask = strategy.ExecuteAsync(async token => @@ -953,7 +954,7 @@ private void ConfigureHedging(TimeSpan delay) => ConfigureHedging(args => async return Outcome.FromResult("secondary"); }); - private HedgingResilienceStrategy Create() => Create(_handler!, _options.OnHedging); + private ReactiveResilienceStrategyBridge Create() => new(Create(_handler!, _options.OnHedging)); private HedgingResilienceStrategy Create( HedgingHandler handler, diff --git a/test/Polly.Core.Tests/Retry/RetryCompositeStrategyBuilderExtensionsTests.cs b/test/Polly.Core.Tests/Retry/RetryCompositeStrategyBuilderExtensionsTests.cs index fa4cfc8131d..fe1598cee34 100644 --- a/test/Polly.Core.Tests/Retry/RetryCompositeStrategyBuilderExtensionsTests.cs +++ b/test/Polly.Core.Tests/Retry/RetryCompositeStrategyBuilderExtensionsTests.cs @@ -1,5 +1,6 @@ using System.ComponentModel.DataAnnotations; using Polly.Retry; +using Polly.Utils; namespace Polly.Core.Tests.Retry; @@ -70,7 +71,7 @@ public void AddRetry_DefaultOptions_Ok() private static void AssertStrategy(CompositeStrategyBuilder builder, RetryBackoffType type, int retries, TimeSpan delay, Action>? assert = null) { - var strategy = (RetryResilienceStrategy)builder.Build(); + var strategy = (RetryResilienceStrategy)((ReactiveResilienceStrategyBridge)builder.Build()).Strategy; strategy.BackoffType.Should().Be(type); strategy.RetryCount.Should().Be(retries); @@ -81,7 +82,7 @@ private static void AssertStrategy(CompositeStrategyBuilder builder, RetryBackof private static void AssertStrategy(CompositeStrategyBuilder builder, RetryBackoffType type, int retries, TimeSpan delay, Action>? assert = null) { - var strategy = (RetryResilienceStrategy)builder.Build().Strategy; + var strategy = (RetryResilienceStrategy)((ReactiveResilienceStrategyBridge)builder.Build().Strategy).Strategy; strategy.BackoffType.Should().Be(type); strategy.RetryCount.Should().Be(retries); diff --git a/test/Polly.Core.Tests/Retry/RetryResilienceStrategyTests.cs b/test/Polly.Core.Tests/Retry/RetryResilienceStrategyTests.cs index 7b5d4b404db..d9467bc87ef 100644 --- a/test/Polly.Core.Tests/Retry/RetryResilienceStrategyTests.cs +++ b/test/Polly.Core.Tests/Retry/RetryResilienceStrategyTests.cs @@ -3,6 +3,7 @@ using Moq; using Polly.Retry; using Polly.Telemetry; +using Polly.Utils; namespace Polly.Core.Tests.Retry; @@ -338,7 +339,7 @@ public void RetryDelayGenerator_EnsureCorrectArguments() private void SetupNoDelay() => _options.RetryDelayGenerator = _ => new ValueTask(TimeSpan.Zero); - private async ValueTask ExecuteAndAdvance(RetryResilienceStrategy sut) + private async ValueTask ExecuteAndAdvance(ReactiveResilienceStrategyBridge sut) { var executing = sut.ExecuteAsync(_ => new ValueTask(0)).AsTask(); @@ -350,9 +351,9 @@ private async ValueTask ExecuteAndAdvance(RetryResilienceStrategy s return await executing; } - private RetryResilienceStrategy CreateSut(TimeProvider? timeProvider = null) => - new(_options, + private ReactiveResilienceStrategyBridge CreateSut(TimeProvider? timeProvider = null) => + new(new RetryResilienceStrategy(_options, timeProvider ?? _timeProvider, _telemetry, - () => 1.0); + () => 1.0)); } diff --git a/test/Polly.Core.Tests/Utils/ReactiveResilienceStrategyTests.cs b/test/Polly.Core.Tests/Utils/ReactiveResilienceStrategyBridgeTests.cs similarity index 69% rename from test/Polly.Core.Tests/Utils/ReactiveResilienceStrategyTests.cs rename to test/Polly.Core.Tests/Utils/ReactiveResilienceStrategyBridgeTests.cs index 041a59f2e60..5a6a34c2e32 100644 --- a/test/Polly.Core.Tests/Utils/ReactiveResilienceStrategyTests.cs +++ b/test/Polly.Core.Tests/Utils/ReactiveResilienceStrategyBridgeTests.cs @@ -1,6 +1,8 @@ -namespace Polly.Core.Tests.Utils; +using Polly.Utils; -public class ReactiveResilienceStrategyTests +namespace Polly.Core.Tests.Utils; + +public class ReactiveResilienceStrategyBridgeTests { [Fact] public void Ctor_Ok() @@ -13,10 +15,10 @@ public void Execute_NonGeneric_Ok() { var values = new List(); - var strategy = new Strategy(outcome => + var strategy = new ReactiveResilienceStrategyBridge(new Strategy(outcome => { values.Add(outcome.Result); - }); + })); strategy.Execute(args => "dummy"); strategy.Execute(args => 0); @@ -34,10 +36,10 @@ public void Execute_Generic_Ok() { var values = new List(); - var strategy = new Strategy(outcome => + var strategy = new ReactiveResilienceStrategyBridge(new Strategy(outcome => { values.Add(outcome.Result); - }); + })); strategy.Execute(args => "dummy"); @@ -49,11 +51,11 @@ public void Execute_Generic_Ok() public void Pipeline_TypeCheck_Ok() { var called = false; - var strategy = new Strategy(o => + var strategy = new ReactiveResilienceStrategyBridge(new Strategy(o => { o.Result.Should().Be(-1); called = true; - }); + })); strategy.Execute(() => -1); @@ -66,7 +68,7 @@ private class Strategy : ReactiveResilienceStrategy public Strategy(Action> onOutcome) => _onOutcome = onOutcome; - protected override async ValueTask> ExecuteCore(Func>> callback, ResilienceContext context, TState state) + protected internal override async ValueTask> ExecuteCore(Func>> callback, ResilienceContext context, TState state) { var outcome = await callback(context, state); _onOutcome(outcome); diff --git a/test/Polly.Testing.Tests/ResilienceStrategyExtensionsTests.cs b/test/Polly.Testing.Tests/ResilienceStrategyExtensionsTests.cs index cdd589871e3..822c6917a7c 100644 --- a/test/Polly.Testing.Tests/ResilienceStrategyExtensionsTests.cs +++ b/test/Polly.Testing.Tests/ResilienceStrategyExtensionsTests.cs @@ -14,7 +14,7 @@ namespace Polly.Testing.Tests; public class ResilienceStrategyExtensionsTests { [Fact] - public void GetInnerStrategies_Ok() + public void GetInnerStrategies_Generic_Ok() { // arrange var strategy = new CompositeStrategyBuilder() @@ -39,19 +39,61 @@ public void GetInnerStrategies_Ok() descriptor.IsReloadable.Should().BeFalse(); descriptor.Strategies.Should().HaveCount(7); descriptor.Strategies[0].Options.Should().BeOfType>(); + descriptor.Strategies[0].StrategyType.FullName.Should().Contain("Fallback"); descriptor.Strategies[1].Options.Should().BeOfType>(); + descriptor.Strategies[1].StrategyType.FullName.Should().Contain("Retry"); descriptor.Strategies[2].Options.Should().BeOfType>(); + descriptor.Strategies[2].StrategyType.FullName.Should().Contain("CircuitBreaker"); descriptor.Strategies[3].Options.Should().BeOfType(); + descriptor.Strategies[3].StrategyType.FullName.Should().Contain("Timeout"); descriptor.Strategies[3].Options .Should() .BeOfType().Subject.Timeout .Should().Be(TimeSpan.FromSeconds(1)); descriptor.Strategies[4].Options.Should().BeOfType>(); + descriptor.Strategies[4].StrategyType.FullName.Should().Contain("Hedging"); descriptor.Strategies[5].Options.Should().BeOfType(); + descriptor.Strategies[5].StrategyType.FullName.Should().Contain("RateLimiter"); descriptor.Strategies[6].StrategyType.Should().Be(typeof(CustomStrategy)); } + [Fact] + public void GetInnerStrategies_NonGeneric_Ok() + { + // arrange + var strategy = new CompositeStrategyBuilder() + .AddRetry(new()) + .AddCircuitBreaker(new()) + .AddTimeout(TimeSpan.FromSeconds(1)) + .AddConcurrencyLimiter(10) + .AddStrategy(new CustomStrategy()) + .ConfigureTelemetry(NullLoggerFactory.Instance) + .Build(); + + // act + var descriptor = strategy.GetInnerStrategies(); + + // assert + descriptor.HasTelemetry.Should().BeTrue(); + descriptor.IsReloadable.Should().BeFalse(); + descriptor.Strategies.Should().HaveCount(5); + descriptor.Strategies[0].Options.Should().BeOfType(); + descriptor.Strategies[0].StrategyType.FullName.Should().Contain("Retry"); + descriptor.Strategies[1].Options.Should().BeOfType(); + descriptor.Strategies[1].StrategyType.FullName.Should().Contain("CircuitBreaker"); + descriptor.Strategies[2].Options.Should().BeOfType(); + descriptor.Strategies[2].StrategyType.FullName.Should().Contain("Timeout"); + descriptor.Strategies[2].Options + .Should() + .BeOfType().Subject.Timeout + .Should().Be(TimeSpan.FromSeconds(1)); + + descriptor.Strategies[3].Options.Should().BeOfType(); + descriptor.Strategies[3].StrategyType.FullName.Should().Contain("RateLimiter"); + descriptor.Strategies[4].StrategyType.Should().Be(typeof(CustomStrategy)); + } + [Fact] public void GetInnerStrategies_SingleStrategy_Ok() {