diff --git a/bench/Polly.Core.Benchmarks/BridgeBenchmark.cs b/bench/Polly.Core.Benchmarks/BridgeBenchmark.cs index 473a0175168..afb19561cc9 100644 --- a/bench/Polly.Core.Benchmarks/BridgeBenchmark.cs +++ b/bench/Polly.Core.Benchmarks/BridgeBenchmark.cs @@ -9,7 +9,7 @@ public class BridgeBenchmark public void Setup() { _policy = Policy.NoOpAsync(); - _policyWrapped = NullResiliencePipeline.Instance.AsAsyncPolicy(); + _policyWrapped = ResiliencePipeline.Null.AsAsyncPolicy(); } [Benchmark(Baseline = true)] diff --git a/bench/Polly.Core.Benchmarks/ResiliencePipelineBenchmark.cs b/bench/Polly.Core.Benchmarks/ResiliencePipelineBenchmark.cs index c3b67fd06ca..b154af10223 100644 --- a/bench/Polly.Core.Benchmarks/ResiliencePipelineBenchmark.cs +++ b/bench/Polly.Core.Benchmarks/ResiliencePipelineBenchmark.cs @@ -10,7 +10,7 @@ public class ResiliencePipelineBenchmark public async ValueTask ExecuteOutcomeAsync() { var context = ResilienceContextPool.Shared.Get(); - await NullResiliencePipeline.Instance.ExecuteOutcomeAsync((_, _) => Outcome.FromResultAsTask("dummy"), context, "state").ConfigureAwait(false); + await ResiliencePipeline.Null.ExecuteOutcomeAsync((_, _) => Outcome.FromResultAsTask("dummy"), context, "state").ConfigureAwait(false); ResilienceContextPool.Shared.Return(context); } @@ -18,40 +18,40 @@ public async ValueTask ExecuteOutcomeAsync() public async ValueTask ExecuteAsync_ResilienceContextAndState() { var context = ResilienceContextPool.Shared.Get(); - await NullResiliencePipeline.Instance.ExecuteAsync((_, _) => new ValueTask("dummy"), context, "state").ConfigureAwait(false); + await ResiliencePipeline.Null.ExecuteAsync((_, _) => new ValueTask("dummy"), context, "state").ConfigureAwait(false); ResilienceContextPool.Shared.Return(context); } [Benchmark] public async ValueTask ExecuteAsync_CancellationToken() { - await NullResiliencePipeline.Instance.ExecuteAsync(_ => new ValueTask("dummy"), CancellationToken.None).ConfigureAwait(false); + await ResiliencePipeline.Null.ExecuteAsync(_ => new ValueTask("dummy"), CancellationToken.None).ConfigureAwait(false); } [Benchmark] public async ValueTask ExecuteAsync_GenericStrategy_CancellationToken() { - await NullResiliencePipeline.Instance.ExecuteAsync(_ => new ValueTask("dummy"), CancellationToken.None).ConfigureAwait(false); + await ResiliencePipeline.Null.ExecuteAsync(_ => new ValueTask("dummy"), CancellationToken.None).ConfigureAwait(false); } [Benchmark] public void Execute_ResilienceContextAndState() { var context = ResilienceContextPool.Shared.Get(); - NullResiliencePipeline.Instance.Execute((_, _) => "dummy", context, "state"); + ResiliencePipeline.Null.Execute((_, _) => "dummy", context, "state"); ResilienceContextPool.Shared.Return(context); } [Benchmark] public void Execute_CancellationToken() { - NullResiliencePipeline.Instance.Execute(_ => "dummy", CancellationToken.None); + ResiliencePipeline.Null.Execute(_ => "dummy", CancellationToken.None); } [Benchmark] public void Execute_GenericStrategy_CancellationToken() { - NullResiliencePipeline.Instance.Execute(_ => "dummy", CancellationToken.None); + ResiliencePipeline.Null.Execute(_ => "dummy", CancellationToken.None); } public class NonGenericStrategy diff --git a/src/Polly.Core/NullResiliencePipeline.TResult.cs b/src/Polly.Core/NullResiliencePipeline.TResult.cs deleted file mode 100644 index a77c8e017dd..00000000000 --- a/src/Polly.Core/NullResiliencePipeline.TResult.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Polly; - -/// -/// A resilience pipeline that executes an user-provided callback without any additional logic. -/// -/// The type of result this pipeline handles. -public sealed class NullResiliencePipeline : ResiliencePipeline -{ - /// - /// Gets the singleton instance of the . - /// - public static readonly NullResiliencePipeline Instance = new(); - - private NullResiliencePipeline() - : base(NullResiliencePipeline.Instance) - { - } -} diff --git a/src/Polly.Core/NullResiliencePipeline.cs b/src/Polly.Core/NullResiliencePipeline.cs deleted file mode 100644 index de66fbbe517..00000000000 --- a/src/Polly.Core/NullResiliencePipeline.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace Polly; - -/// -/// A resilience pipeline that executes an user-provided callback without any additional logic. -/// -public sealed class NullResiliencePipeline : ResiliencePipeline -{ - /// - /// Gets the singleton instance of the . - /// - public static readonly NullResiliencePipeline Instance = new(); - - private NullResiliencePipeline() - { - } - - internal override ValueTask> ExecuteCore( - Func>> callback, - ResilienceContext context, - TState state) - { - Guard.NotNull(callback); - Guard.NotNull(context); - - context.AssertInitialized(); - - return callback(context, state); - } -} diff --git a/src/Polly.Core/PublicAPI.Unshipped.txt b/src/Polly.Core/PublicAPI.Unshipped.txt index 616816d64f1..21122acf546 100644 --- a/src/Polly.Core/PublicAPI.Unshipped.txt +++ b/src/Polly.Core/PublicAPI.Unshipped.txt @@ -131,8 +131,6 @@ Polly.Hedging.OnHedgingArguments.OnHedgingArguments() -> void Polly.Hedging.OnHedgingArguments.OnHedgingArguments(int attemptNumber, bool hasOutcome, System.TimeSpan duration) -> void Polly.HedgingResiliencePipelineBuilderExtensions Polly.LegacySupport -Polly.NullResiliencePipeline -Polly.NullResiliencePipeline Polly.Outcome Polly.Outcome Polly.Outcome.EnsureSuccess() -> void @@ -408,7 +406,7 @@ static Polly.RetryResiliencePipelineBuilderExtensions.AddRetry(this Polly.Resili static Polly.RetryResiliencePipelineBuilderExtensions.AddRetry(this Polly.ResiliencePipelineBuilder! builder, Polly.Retry.RetryStrategyOptions! options) -> Polly.ResiliencePipelineBuilder! static Polly.TimeoutResiliencePipelineBuilderExtensions.AddTimeout(this TBuilder! builder, Polly.Timeout.TimeoutStrategyOptions! options) -> TBuilder! static Polly.TimeoutResiliencePipelineBuilderExtensions.AddTimeout(this TBuilder! builder, System.TimeSpan timeout) -> TBuilder! -static readonly Polly.NullResiliencePipeline.Instance -> Polly.NullResiliencePipeline! -static readonly Polly.NullResiliencePipeline.Instance -> Polly.NullResiliencePipeline! +static readonly Polly.ResiliencePipeline.Null -> Polly.ResiliencePipeline! +static readonly Polly.ResiliencePipeline.Null -> Polly.ResiliencePipeline! virtual Polly.Registry.ResiliencePipelineProvider.GetPipeline(TKey key) -> Polly.ResiliencePipeline! virtual Polly.Registry.ResiliencePipelineProvider.GetPipeline(TKey key) -> Polly.ResiliencePipeline! diff --git a/src/Polly.Core/Registry/ResiliencePipelineRegistry.TResult.cs b/src/Polly.Core/Registry/ResiliencePipelineRegistry.TResult.cs index 48c5ade2fa1..1fe4849965d 100644 --- a/src/Polly.Core/Registry/ResiliencePipelineRegistry.TResult.cs +++ b/src/Polly.Core/Registry/ResiliencePipelineRegistry.TResult.cs @@ -56,11 +56,11 @@ public ResiliencePipeline GetOrAdd(TKey key, Action { - return new ResiliencePipeline(CreatePipeline(factory.instance._activator, factory.context, factory.configure)); + return new ResiliencePipeline(CreatePipelineComponent(factory.instance._activator, factory.context, factory.configure)); }, (instance: this, context, configure)); #else - return _strategies.GetOrAdd(key, _ => new ResiliencePipeline(CreatePipeline(_activator, context, configure))); + return _strategies.GetOrAdd(key, _ => new ResiliencePipeline(CreatePipelineComponent(_activator, context, configure))); #endif } diff --git a/src/Polly.Core/Registry/ResiliencePipelineRegistry.cs b/src/Polly.Core/Registry/ResiliencePipelineRegistry.cs index 3512dcb6765..6b4fe3de875 100644 --- a/src/Polly.Core/Registry/ResiliencePipelineRegistry.cs +++ b/src/Polly.Core/Registry/ResiliencePipelineRegistry.cs @@ -161,11 +161,11 @@ public ResiliencePipeline GetOrAddPipeline(TKey key, Action { - return CreatePipeline(factory.instance._activator, factory.context, factory.configure); + return new ResiliencePipeline(CreatePipelineComponent(factory.instance._activator, factory.context, factory.configure)); }, (instance: this, context, configure)); #else - return _pipelines.GetOrAdd(key, _ => CreatePipeline(_activator, context, configure)); + return _pipelines.GetOrAdd(key, _ => new ResiliencePipeline(CreatePipelineComponent(_activator, context, configure))); #endif } @@ -264,7 +264,7 @@ public bool TryAddBuilder(TKey key, Action public void ClearPipelines() => GetGenericRegistry().Clear(); - private static ResiliencePipeline CreatePipeline( + private static PipelineComponent CreatePipelineComponent( Func activator, ConfigureBuilderContext context, Action> configure) @@ -281,7 +281,7 @@ private static ResiliencePipeline CreatePipeline( }; var builder = factory(); - var pipeline = builder.BuildPipeline(); + var pipeline = builder.BuildPipelineComponent(); var telemetry = new ResilienceStrategyTelemetry( new ResilienceTelemetrySource(context.BuilderName, context.BuilderInstanceName, null), builder.TelemetryListener); @@ -291,7 +291,7 @@ private static ResiliencePipeline CreatePipeline( return pipeline; } - return new ReloadableResiliencePipeline(pipeline, context.ReloadTokenProducer(), () => factory().BuildPipeline(), telemetry); + return PipelineComponent.CreateReloadable(pipeline, context.ReloadTokenProducer(), () => factory().BuildPipelineComponent(), telemetry); } private GenericRegistry GetGenericRegistry() diff --git a/src/Polly.Core/ResiliencePipeline.Async.cs b/src/Polly.Core/ResiliencePipeline.Async.cs index 308d214c2c4..00ce9bc9577 100644 --- a/src/Polly.Core/ResiliencePipeline.Async.cs +++ b/src/Polly.Core/ResiliencePipeline.Async.cs @@ -3,7 +3,7 @@ namespace Polly; #pragma warning disable CA1031 // Do not catch general exception types #pragma warning disable RS0027 // API with optional parameter(s) should have the most parameters amongst its public overloads -public abstract partial class ResiliencePipeline +public partial class ResiliencePipeline { /// /// Executes the specified callback. diff --git a/src/Polly.Core/ResiliencePipeline.AsyncT.cs b/src/Polly.Core/ResiliencePipeline.AsyncT.cs index ce22820a711..603f0741ba3 100644 --- a/src/Polly.Core/ResiliencePipeline.AsyncT.cs +++ b/src/Polly.Core/ResiliencePipeline.AsyncT.cs @@ -3,7 +3,7 @@ namespace Polly; #pragma warning disable CA1031 // Do not catch general exception types #pragma warning disable RS0027 // API with optional parameter(s) should have the most parameters amongst its public overloads -public abstract partial class ResiliencePipeline +public partial class ResiliencePipeline { /// /// Executes the specified outcome-based callback. diff --git a/src/Polly.Core/ResiliencePipeline.Sync.cs b/src/Polly.Core/ResiliencePipeline.Sync.cs index 57f3ffdc114..ce14a6f430c 100644 --- a/src/Polly.Core/ResiliencePipeline.Sync.cs +++ b/src/Polly.Core/ResiliencePipeline.Sync.cs @@ -3,7 +3,7 @@ namespace Polly; #pragma warning disable CA1031 // Do not catch general exception types #pragma warning disable RS0027 // API with optional parameter(s) should have the most parameters amongst its public overloads -public abstract partial class ResiliencePipeline +public partial class ResiliencePipeline { /// /// Executes the specified callback. diff --git a/src/Polly.Core/ResiliencePipeline.SyncT.cs b/src/Polly.Core/ResiliencePipeline.SyncT.cs index 18c61e0231b..a3f64aafb91 100644 --- a/src/Polly.Core/ResiliencePipeline.SyncT.cs +++ b/src/Polly.Core/ResiliencePipeline.SyncT.cs @@ -3,7 +3,7 @@ namespace Polly; #pragma warning disable CA1031 // Do not catch general exception types #pragma warning disable RS0027 // API with optional parameter(s) should have the most parameters amongst its public overloads -public abstract partial class ResiliencePipeline +public partial class ResiliencePipeline { /// /// Executes the specified callback. diff --git a/src/Polly.Core/ResiliencePipeline.cs b/src/Polly.Core/ResiliencePipeline.cs index 079040de805..ba5c597f5de 100644 --- a/src/Polly.Core/ResiliencePipeline.cs +++ b/src/Polly.Core/ResiliencePipeline.cs @@ -7,20 +7,23 @@ namespace Polly; /// Resilience pipeline supports various types of callbacks and provides a unified way to execute them. /// This includes overloads for synchronous and asynchronous callbacks, generic and non-generic callbacks. /// -public partial class ResiliencePipeline +public sealed partial class ResiliencePipeline { - internal static ResilienceContextPool Pool => ResilienceContextPool.Shared; + /// + /// Resilience pipeline that executes the user-provided callback without any additional logic. + /// + public static readonly ResiliencePipeline Null = new(PipelineComponent.Null); - internal ResilienceStrategyOptions? Options { get; set; } + internal ResiliencePipeline(PipelineComponent component) => Component = component; - internal ResiliencePipeline() - { - } + internal static ResilienceContextPool Pool => ResilienceContextPool.Shared; + + internal PipelineComponent Component { get; } - internal abstract ValueTask> ExecuteCore( + internal ValueTask> ExecuteCore( Func>> callback, ResilienceContext context, - TState state); + TState state) => Component.ExecuteCore(callback, context, state); private Outcome ExecuteCoreSync( Func> callback, diff --git a/src/Polly.Core/ResiliencePipelineBuilder.TResult.cs b/src/Polly.Core/ResiliencePipelineBuilder.TResult.cs index 6741b4f0e3f..b6ac86cfa49 100644 --- a/src/Polly.Core/ResiliencePipelineBuilder.TResult.cs +++ b/src/Polly.Core/ResiliencePipelineBuilder.TResult.cs @@ -30,5 +30,5 @@ internal ResiliencePipelineBuilder(ResiliencePipelineBuilderBase other) /// /// An instance of . /// Thrown when this builder has invalid configuration. - public ResiliencePipeline Build() => new(BuildPipeline()); + public ResiliencePipeline Build() => new(BuildPipelineComponent()); } diff --git a/src/Polly.Core/ResiliencePipelineBuilder.cs b/src/Polly.Core/ResiliencePipelineBuilder.cs index 30ca4a25ea6..2772fe30a9f 100644 --- a/src/Polly.Core/ResiliencePipelineBuilder.cs +++ b/src/Polly.Core/ResiliencePipelineBuilder.cs @@ -17,5 +17,5 @@ public sealed class ResiliencePipelineBuilder : ResiliencePipelineBuilderBase /// /// An instance of . /// Thrown when this builder has invalid configuration. - public ResiliencePipeline Build() => BuildPipeline(); + public ResiliencePipeline Build() => new(BuildPipelineComponent()); } diff --git a/src/Polly.Core/ResiliencePipelineBuilderBase.cs b/src/Polly.Core/ResiliencePipelineBuilderBase.cs index 7267b891c8f..4da5db5bf18 100644 --- a/src/Polly.Core/ResiliencePipelineBuilderBase.cs +++ b/src/Polly.Core/ResiliencePipelineBuilderBase.cs @@ -88,7 +88,7 @@ private protected ResiliencePipelineBuilderBase(ResiliencePipelineBuilderBase ot internal Action Validator { get; private protected set; } = ValidationHelper.ValidateObject; [RequiresUnreferencedCode(Constants.OptionsValidation)] - internal void AddStrategyCore(Func factory, ResilienceStrategyOptions options) + internal void AddStrategyCore(Func factory, ResilienceStrategyOptions options) { Guard.NotNull(factory); Guard.NotNull(options); @@ -103,28 +103,30 @@ internal void AddStrategyCore(Func f _entries.Add(new Entry(factory, options)); } - internal ResiliencePipeline BuildPipeline() + internal PipelineComponent BuildPipelineComponent() { Validator(new(this, $"The '{nameof(ResiliencePipelineBuilder)}' configuration is invalid.")); _used = true; - var strategies = _entries.Select(CreateResiliencePipeline).ToList(); + var components = _entries.Select(CreateComponent).ToList(); - if (strategies.Count == 0) + if (components.Count == 0) { - return NullResiliencePipeline.Instance; + return PipelineComponent.Null; } var source = new ResilienceTelemetrySource(Name, InstanceName, null); - return CompositeResiliencePipeline.Create( - strategies, - new ResilienceStrategyTelemetry(source, TelemetryListener), - TimeProvider); + if (components.Distinct().Count() != components.Count) + { + throw new InvalidOperationException("The resilience pipeline must contain unique resilience strategies."); + } + + return PipelineComponent.CreateComposite(components, new ResilienceStrategyTelemetry(source, TelemetryListener), TimeProvider); } - private ResiliencePipeline CreateResiliencePipeline(Entry entry) + private PipelineComponent CreateComponent(Entry entry) { var source = new ResilienceTelemetrySource(Name, InstanceName, entry.Options.Name); var context = new StrategyBuilderContext(new ResilienceStrategyTelemetry(source, TelemetryListener), TimeProvider); @@ -134,5 +136,5 @@ private ResiliencePipeline CreateResiliencePipeline(Entry entry) return strategy; } - private sealed record Entry(Func Factory, ResilienceStrategyOptions Options); + private sealed record Entry(Func Factory, ResilienceStrategyOptions Options); } diff --git a/src/Polly.Core/ResiliencePipelineBuilderExtensions.cs b/src/Polly.Core/ResiliencePipelineBuilderExtensions.cs index 17923e51f6e..04ce0540a44 100644 --- a/src/Polly.Core/ResiliencePipelineBuilderExtensions.cs +++ b/src/Polly.Core/ResiliencePipelineBuilderExtensions.cs @@ -27,7 +27,7 @@ public static TBuilder AddPipeline(this TBuilder builder, ResiliencePi Guard.NotNull(builder); Guard.NotNull(pipeline); - builder.AddStrategyCore(_ => pipeline, EmptyOptions.Instance); + builder.AddStrategyCore(_ => PipelineComponent.FromPipeline(pipeline), EmptyOptions.Instance); return builder; } @@ -40,12 +40,17 @@ public static TBuilder AddPipeline(this TBuilder builder, ResiliencePi /// The same builder instance. /// Thrown when is null. /// Thrown when this builder was already used to create a strategy. The builder cannot be modified after it has been used. + [UnconditionalSuppressMessage( + "Trimming", + "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", + Justification = "The EmptyOptions have nothing to validate.")] public static ResiliencePipelineBuilder AddPipeline(this ResiliencePipelineBuilder builder, ResiliencePipeline pipeline) { Guard.NotNull(builder); Guard.NotNull(pipeline); - return builder.AddPipeline(pipeline.Strategy); + builder.AddStrategyCore(_ => PipelineComponent.FromPipeline(pipeline), EmptyOptions.Instance); + return builder; } /// @@ -67,7 +72,7 @@ public static TBuilder AddStrategy(this TBuilder builder, Func new ResiliencePipelineBridge(factory(context)), options); + builder.AddStrategyCore(context => PipelineComponent.FromStrategy(factory(context)), options); return builder; } @@ -90,7 +95,7 @@ public static ResiliencePipelineBuilder AddStrategy( Guard.NotNull(factory); Guard.NotNull(options); - builder.AddStrategyCore(context => new ResiliencePipelineBridge(factory(context)), options); + builder.AddStrategyCore(context => PipelineComponent.FromStrategy(factory(context)), options); return builder; } @@ -114,7 +119,7 @@ public static ResiliencePipelineBuilder AddStrategy( Guard.NotNull(factory); Guard.NotNull(options); - builder.AddStrategyCore(context => new ResiliencePipelineBridge(factory(context)), options); + builder.AddStrategyCore(context => PipelineComponent.FromStrategy(factory(context)), options); return builder; } diff --git a/src/Polly.Core/ResiliencePipelineT.Async.cs b/src/Polly.Core/ResiliencePipelineT.Async.cs index fe9cc6fe090..897c603b874 100644 --- a/src/Polly.Core/ResiliencePipelineT.Async.cs +++ b/src/Polly.Core/ResiliencePipelineT.Async.cs @@ -23,7 +23,7 @@ public ValueTask ExecuteAsync( Guard.NotNull(callback); Guard.NotNull(context); - return Strategy.ExecuteAsync(callback, context, state); + return Pipeline.ExecuteAsync(callback, context, state); } /// @@ -42,7 +42,7 @@ public ValueTask ExecuteAsync( Guard.NotNull(callback); Guard.NotNull(context); - return Strategy.ExecuteAsync(callback, context); + return Pipeline.ExecuteAsync(callback, context); } /// @@ -63,7 +63,7 @@ public ValueTask ExecuteAsync( { Guard.NotNull(callback); - return Strategy.ExecuteAsync(callback, state, cancellationToken); + return Pipeline.ExecuteAsync(callback, state, cancellationToken); } /// @@ -81,7 +81,7 @@ public ValueTask ExecuteAsync( { Guard.NotNull(callback); - return Strategy.ExecuteAsync(callback, cancellationToken); + return Pipeline.ExecuteAsync(callback, cancellationToken); } /// @@ -107,6 +107,6 @@ public ValueTask> ExecuteOutcomeAsync( Guard.NotNull(callback); Guard.NotNull(context); - return Strategy.ExecuteOutcomeAsync(callback, context, state); + return Pipeline.ExecuteOutcomeAsync(callback, context, state); } } diff --git a/src/Polly.Core/ResiliencePipelineT.Sync.cs b/src/Polly.Core/ResiliencePipelineT.Sync.cs index be2baeadc05..2874630813a 100644 --- a/src/Polly.Core/ResiliencePipelineT.Sync.cs +++ b/src/Polly.Core/ResiliencePipelineT.Sync.cs @@ -23,7 +23,7 @@ public TResult Execute( Guard.NotNull(callback); Guard.NotNull(context); - return Strategy.Execute(callback, context, state); + return Pipeline.Execute(callback, context, state); } /// @@ -42,7 +42,7 @@ public TResult Execute( Guard.NotNull(callback); Guard.NotNull(context); - return Strategy.Execute(callback, context); + return Pipeline.Execute(callback, context); } /// @@ -60,7 +60,7 @@ public TResult Execute( { Guard.NotNull(callback); - return Strategy.Execute(callback, cancellationToken); + return Pipeline.Execute(callback, cancellationToken); } /// @@ -75,7 +75,7 @@ public TResult Execute(Func callback) { Guard.NotNull(callback); - return Strategy.Execute(callback); + return Pipeline.Execute(callback); } /// @@ -92,7 +92,7 @@ public TResult Execute(Func callback, TState s { Guard.NotNull(callback); - return Strategy.Execute(callback, state); + return Pipeline.Execute(callback, state); } /// @@ -113,6 +113,6 @@ public TResult Execute( { Guard.NotNull(callback); - return Strategy.Execute(callback, state, cancellationToken); + return Pipeline.Execute(callback, state, cancellationToken); } } diff --git a/src/Polly.Core/ResiliencePipelineT.cs b/src/Polly.Core/ResiliencePipelineT.cs index 8b8268df882..a8cc314ced8 100644 --- a/src/Polly.Core/ResiliencePipelineT.cs +++ b/src/Polly.Core/ResiliencePipelineT.cs @@ -5,12 +5,20 @@ namespace Polly; /// /// The type of result this pipeline supports. /// -/// Resilience ppeline supports various types of callbacks of result type +/// Resilience pipeline supports various types of callbacks of result type /// and provides a unified way to execute them. This includes overloads for synchronous and asynchronous callbacks. /// -public partial class ResiliencePipeline +public sealed partial class ResiliencePipeline { - internal ResiliencePipeline(ResiliencePipeline strategy) => Strategy = strategy; + /// + /// Resilience pipeline that executes the user-provided callback without any additional logic. + /// + public static readonly ResiliencePipeline Null = new(PipelineComponent.Null); + + internal ResiliencePipeline(PipelineComponent component) => Pipeline = new ResiliencePipeline(component); + + internal PipelineComponent Component => Pipeline.Component; + + private ResiliencePipeline Pipeline { get; } - internal ResiliencePipeline Strategy { get; } } diff --git a/src/Polly.Core/Utils/CompositeResiliencePipeline.DebuggerProxy.cs b/src/Polly.Core/Utils/CompositeResiliencePipeline.DebuggerProxy.cs deleted file mode 100644 index 57cbde31478..00000000000 --- a/src/Polly.Core/Utils/CompositeResiliencePipeline.DebuggerProxy.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Polly.Utils; - -internal partial class CompositeResiliencePipeline -{ - internal sealed class DebuggerProxy - { - private readonly CompositeResiliencePipeline _pipeline; - - public DebuggerProxy(CompositeResiliencePipeline pipeline) => _pipeline = pipeline; - - [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] - public IEnumerable Strategies => _pipeline.Strategies; - } -} diff --git a/src/Polly.Core/Utils/CompositeResiliencePipeline.cs b/src/Polly.Core/Utils/CompositeResiliencePipeline.cs deleted file mode 100644 index cfd1219eb8a..00000000000 --- a/src/Polly.Core/Utils/CompositeResiliencePipeline.cs +++ /dev/null @@ -1,129 +0,0 @@ -using Polly.Telemetry; - -namespace Polly.Utils; - -#pragma warning disable S2302 // "nameof" should be used - -/// -/// A combination of multiple resilience strategies. -/// -[DebuggerDisplay("CompositeResiliencePipeline, Strategies = {Strategies.Count}")] -[DebuggerTypeProxy(typeof(DebuggerProxy))] -internal sealed partial class CompositeResiliencePipeline : ResiliencePipeline -{ - private readonly ResiliencePipeline _firstStrategy; - private readonly ResilienceStrategyTelemetry _telemetry; - private readonly TimeProvider _timeProvider; - - public static CompositeResiliencePipeline Create(IReadOnlyList strategies, ResilienceStrategyTelemetry telemetry, TimeProvider timeProvider) - { - Guard.NotNull(strategies); - - if (strategies.Count == 0) - { - throw new InvalidOperationException("The composite resilience strategy must contain at least one resilience strategy."); - } - - if (strategies.Distinct().Count() != strategies.Count) - { - throw new InvalidOperationException("The composite resilience strategy must contain unique resilience strategies."); - } - - if (strategies.Count == 1) - { - return new CompositeResiliencePipeline(strategies[0], strategies, telemetry, timeProvider); - } - - // convert all strategies to delegating ones (except the last one as it's not required) - var delegatingStrategies = strategies - .Take(strategies.Count - 1) - .Select(strategy => new DelegatingResiliencePipeline(strategy)) - .ToList(); - -#if NET6_0_OR_GREATER - // link the last one - delegatingStrategies[^1].Next = strategies[^1]; -#else - delegatingStrategies[delegatingStrategies.Count - 1].Next = strategies[strategies.Count - 1]; -#endif - - // link the remaining ones - for (var i = 0; i < delegatingStrategies.Count - 1; i++) - { - delegatingStrategies[i].Next = delegatingStrategies[i + 1]; - } - - return new CompositeResiliencePipeline(delegatingStrategies[0], strategies, telemetry, timeProvider); - } - - private CompositeResiliencePipeline(ResiliencePipeline first, IReadOnlyList strategies, ResilienceStrategyTelemetry telemetry, TimeProvider timeProvider) - { - Strategies = strategies; - - _telemetry = telemetry; - _timeProvider = timeProvider; - _firstStrategy = first; - } - - public IReadOnlyList Strategies { get; } - - internal override async ValueTask> ExecuteCore( - Func>> callback, - ResilienceContext context, - TState state) - { - var timeStamp = _timeProvider.GetTimestamp(); - _telemetry.Report(new ResilienceEvent(ResilienceEventSeverity.Debug, TelemetryUtil.PipelineExecuting), context, default(PipelineExecutingArguments)); - - Outcome outcome; - - if (context.CancellationToken.IsCancellationRequested) - { - outcome = Outcome.FromException(new OperationCanceledException(context.CancellationToken).TrySetStackTrace()); - } - else - { - outcome = await _firstStrategy.ExecuteCore(callback, context, state).ConfigureAwait(context.ContinueOnCapturedContext); - } - - _telemetry.Report( - new ResilienceEvent(ResilienceEventSeverity.Information, TelemetryUtil.PipelineExecuted), - new OutcomeArguments( - context, - outcome, - new PipelineExecutedArguments(_timeProvider.GetElapsedTime(timeStamp)))); - - return outcome; - } - - /// - /// A resilience strategy that delegates the execution to the next strategy in the chain. - /// - private sealed class DelegatingResiliencePipeline : ResiliencePipeline - { - private readonly ResiliencePipeline _pipeline; - - public DelegatingResiliencePipeline(ResiliencePipeline strategy) => _pipeline = strategy; - - public ResiliencePipeline? Next { get; set; } - - internal override ValueTask> ExecuteCore( - Func>> callback, - ResilienceContext context, - TState state) - { - return _pipeline.ExecuteCore( - static (context, state) => - { - if (context.CancellationToken.IsCancellationRequested) - { - return Outcome.FromExceptionAsTask(new OperationCanceledException(context.CancellationToken).TrySetStackTrace()); - } - - return state.Next!.ExecuteCore(state.callback, context, state.state); - }, - context, - (Next, callback, state)); - } - } -} diff --git a/src/Polly.Core/Utils/PipelineComponent.Bridge.cs b/src/Polly.Core/Utils/PipelineComponent.Bridge.cs new file mode 100644 index 00000000000..f7fed2d91de --- /dev/null +++ b/src/Polly.Core/Utils/PipelineComponent.Bridge.cs @@ -0,0 +1,52 @@ +namespace Polly.Utils; + +internal abstract partial class PipelineComponent +{ + [DebuggerDisplay("{Strategy}")] + internal sealed class BridgeComponent : PipelineComponent + { + public BridgeComponent(ResilienceStrategy strategy) => Strategy = strategy; + + public ResilienceStrategy Strategy { get; } + + 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); + } + } + } + + [DebuggerDisplay("{Strategy}")] + internal sealed class BridgeComponent : PipelineComponent + { + public BridgeComponent(ResilienceStrategy strategy) => Strategy = strategy; + + public ResilienceStrategy Strategy { get; } + + internal override ValueTask> ExecuteCore( + Func>> callback, + ResilienceContext context, + TState state) => Strategy.ExecuteCore(callback, context, state); + } +} diff --git a/src/Polly.Core/Utils/PipelineComponent.Composite.cs b/src/Polly.Core/Utils/PipelineComponent.Composite.cs new file mode 100644 index 00000000000..1539595c1a5 --- /dev/null +++ b/src/Polly.Core/Utils/PipelineComponent.Composite.cs @@ -0,0 +1,132 @@ +using Polly.Telemetry; + +namespace Polly.Utils; + +internal abstract partial class PipelineComponent +{ + /// + /// A combination of multiple components. + /// + [DebuggerDisplay("Pipeline, Strategies = {Strategies.Count}")] + [DebuggerTypeProxy(typeof(CompositeDebuggerProxy))] + internal sealed class CompositeComponent : PipelineComponent + { + private readonly PipelineComponent _firstComponent; + private readonly ResilienceStrategyTelemetry _telemetry; + private readonly TimeProvider _timeProvider; + + private CompositeComponent( + PipelineComponent first, + IReadOnlyList components, + ResilienceStrategyTelemetry telemetry, + TimeProvider timeProvider) + { + Components = components; + + _telemetry = telemetry; + _timeProvider = timeProvider; + _firstComponent = first; + } + + public static PipelineComponent Create( + IReadOnlyList components, + ResilienceStrategyTelemetry telemetry, + TimeProvider timeProvider) + { + if (components.Count == 1) + { + return new CompositeComponent(components[0], components, telemetry, timeProvider); + } + + // convert all components to delegating ones (except the last one as it's not required) + var delegatingComponents = components + .Take(components.Count - 1) + .Select(strategy => new DelegatingComponent(strategy)) + .ToList(); + +#if NET6_0_OR_GREATER + // link the last one + delegatingComponents[^1].Next = components[^1]; +#else + delegatingComponents[delegatingComponents.Count - 1].Next = components[components.Count - 1]; +#endif + + // link the remaining ones + for (var i = 0; i < delegatingComponents.Count - 1; i++) + { + delegatingComponents[i].Next = delegatingComponents[i + 1]; + } + + return new CompositeComponent(delegatingComponents[0], components, telemetry, timeProvider); + } + + public IReadOnlyList Components { get; } + + internal override async ValueTask> ExecuteCore( + Func>> callback, + ResilienceContext context, + TState state) + { + var timeStamp = _timeProvider.GetTimestamp(); + _telemetry.Report(new ResilienceEvent(ResilienceEventSeverity.Debug, TelemetryUtil.PipelineExecuting), context, default(PipelineExecutingArguments)); + + Outcome outcome; + + if (context.CancellationToken.IsCancellationRequested) + { + outcome = Outcome.FromException(new OperationCanceledException(context.CancellationToken).TrySetStackTrace()); + } + else + { + outcome = await _firstComponent.ExecuteCore(callback, context, state).ConfigureAwait(context.ContinueOnCapturedContext); + } + + _telemetry.Report( + new ResilienceEvent(ResilienceEventSeverity.Information, TelemetryUtil.PipelineExecuted), + new OutcomeArguments(context, outcome, new PipelineExecutedArguments(_timeProvider.GetElapsedTime(timeStamp)))); + + return outcome; + } + } + + /// + /// A component that delegates the execution to the next component in the chain. + /// + private sealed class DelegatingComponent : PipelineComponent + { + private readonly PipelineComponent _component; + + public DelegatingComponent(PipelineComponent component) => _component = component; + + public PipelineComponent? Next { get; set; } + + internal override ValueTask> ExecuteCore( + Func>> callback, + ResilienceContext context, + TState state) + { + return _component.ExecuteCore( + static (context, state) => + { + if (context.CancellationToken.IsCancellationRequested) + { + return Outcome.FromExceptionAsTask(new OperationCanceledException(context.CancellationToken).TrySetStackTrace()); + } + + return state.Next!.ExecuteCore(state.callback, context, state.state); + }, + context, + (Next, callback, state)); + } + } + + internal sealed class CompositeDebuggerProxy + { + private readonly CompositeComponent _pipeline; + + public CompositeDebuggerProxy(CompositeComponent pipeline) => _pipeline = pipeline; + + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] + public IEnumerable Strategies => _pipeline.Components; + } +} diff --git a/src/Polly.Core/Utils/PipelineComponent.Reloadale.cs b/src/Polly.Core/Utils/PipelineComponent.Reloadale.cs new file mode 100644 index 00000000000..e117f3938ee --- /dev/null +++ b/src/Polly.Core/Utils/PipelineComponent.Reloadale.cs @@ -0,0 +1,78 @@ +using Polly.Telemetry; + +namespace Polly.Utils; + +internal abstract partial class PipelineComponent +{ + internal sealed class ReloadableComponent : PipelineComponent + { + public const string ReloadFailedEvent = "ReloadFailed"; + + public const string OnReloadEvent = "OnReload"; + + private readonly Func _onReload; + private readonly Func _factory; + private readonly ResilienceStrategyTelemetry _telemetry; + private CancellationTokenRegistration _registration; + + public ReloadableComponent( + PipelineComponent initialComponent, + Func onReload, + Func factory, + ResilienceStrategyTelemetry telemetry) + { + Component = initialComponent; + + _onReload = onReload; + _factory = factory; + _telemetry = telemetry; + + RegisterOnReload(default); + } + + public PipelineComponent Component { get; private set; } + + internal override ValueTask> ExecuteCore( + Func>> callback, + ResilienceContext context, + TState state) + { + return Component.ExecuteCore(callback, context, state); + } + + private void RegisterOnReload(CancellationToken previousToken) + { + var token = _onReload(); + if (token == previousToken) + { + return; + } + + _registration = token.Register(() => + { + var context = ResilienceContextPool.Shared.Get().Initialize(isSynchronous: true); + +#pragma warning disable CA1031 // Do not catch general exception types + try + { + _telemetry.Report(new(ResilienceEventSeverity.Information, OnReloadEvent), context, new OnReloadArguments()); + Component = _factory(); + } + catch (Exception e) + { + var args = new OutcomeArguments(context, Outcome.FromException(e), new ReloadFailedArguments(e)); + _telemetry.Report(new(ResilienceEventSeverity.Error, ReloadFailedEvent), args); + ResilienceContextPool.Shared.Return(context); + } +#pragma warning restore CA1031 // Do not catch general exception types + + _registration.Dispose(); + RegisterOnReload(token); + }); + } + + internal readonly record struct ReloadFailedArguments(Exception Exception); + + internal readonly record struct OnReloadArguments(); + } +} diff --git a/src/Polly.Core/Utils/PipelineComponent.cs b/src/Polly.Core/Utils/PipelineComponent.cs new file mode 100644 index 00000000000..fa76c7f73d9 --- /dev/null +++ b/src/Polly.Core/Utils/PipelineComponent.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Polly.Telemetry; + +namespace Polly.Utils; + +/// +/// Represents a single component of a resilience pipeline. +/// +/// +/// The component of the pipeline can be either a strategy, a generic strategy or a whole pipeline. +/// +internal abstract partial class PipelineComponent +{ + public static PipelineComponent Null { get; } = new NullComponent(); + + internal ResilienceStrategyOptions? Options { get; set; } + + public static PipelineComponent FromPipeline(ResiliencePipeline pipeline) => pipeline.Component; + + public static PipelineComponent FromPipeline(ResiliencePipeline pipeline) => pipeline.Component; + + public static PipelineComponent FromStrategy(ResilienceStrategy strategy) => new BridgeComponent(strategy); + + public static PipelineComponent FromStrategy(ResilienceStrategy strategy) => new BridgeComponent(strategy); + + public static PipelineComponent CreateComposite( + IReadOnlyList components, + ResilienceStrategyTelemetry telemetry, + TimeProvider timeProvider) => CompositeComponent.Create(components, telemetry, timeProvider); + + public static PipelineComponent CreateReloadable( + PipelineComponent initialComponent, + Func onReload, + Func factory, + ResilienceStrategyTelemetry telemetry) => new ReloadableComponent(initialComponent, onReload, factory, telemetry); + + internal abstract ValueTask> ExecuteCore( + Func>> callback, + ResilienceContext context, + TState state); + + internal class NullComponent : PipelineComponent + { + internal override ValueTask> ExecuteCore(Func>> callback, ResilienceContext context, TState state) + => callback(context, state); + } +} diff --git a/src/Polly.Core/Utils/ReloadableResiliencePipeline.cs b/src/Polly.Core/Utils/ReloadableResiliencePipeline.cs deleted file mode 100644 index 7bdebdcb462..00000000000 --- a/src/Polly.Core/Utils/ReloadableResiliencePipeline.cs +++ /dev/null @@ -1,75 +0,0 @@ -using Polly.Telemetry; - -namespace Polly.Utils; - -internal sealed class ReloadableResiliencePipeline : ResiliencePipeline -{ - public const string ReloadFailedEvent = "ReloadFailed"; - - public const string OnReloadEvent = "OnReload"; - - private readonly Func _onReload; - private readonly Func _resiliencePipelineFactory; - private readonly ResilienceStrategyTelemetry _telemetry; - private CancellationTokenRegistration _registration; - - public ReloadableResiliencePipeline( - ResiliencePipeline initialPipeline, - Func onReload, - Func resiliencePipelineFactory, - ResilienceStrategyTelemetry telemetry) - { - Pipeline = initialPipeline; - - _onReload = onReload; - _resiliencePipelineFactory = resiliencePipelineFactory; - _telemetry = telemetry; - - RegisterOnReload(default); - } - - public ResiliencePipeline Pipeline { get; private set; } - - internal override ValueTask> ExecuteCore( - Func>> callback, - ResilienceContext context, - TState state) - { - return Pipeline.ExecuteCore(callback, context, state); - } - - private void RegisterOnReload(CancellationToken previousToken) - { - var token = _onReload(); - if (token == previousToken) - { - return; - } - - _registration = token.Register(() => - { - var context = ResilienceContextPool.Shared.Get().Initialize(isSynchronous: true); - -#pragma warning disable CA1031 // Do not catch general exception types - try - { - _telemetry.Report(new(ResilienceEventSeverity.Information, OnReloadEvent), context, new OnReloadArguments()); - Pipeline = _resiliencePipelineFactory(); - } - catch (Exception e) - { - var args = new OutcomeArguments(context, Outcome.FromException(e), new ReloadFailedArguments(e)); - _telemetry.Report(new(ResilienceEventSeverity.Error, ReloadFailedEvent), args); - ResilienceContextPool.Shared.Return(context); - } -#pragma warning restore CA1031 // Do not catch general exception types - - _registration.Dispose(); - RegisterOnReload(token); - }); - } - - internal readonly record struct ReloadFailedArguments(Exception Exception); - - internal readonly record struct OnReloadArguments(); -} diff --git a/src/Polly.Core/Utils/ResiliencePipelineBridge.TResult.cs b/src/Polly.Core/Utils/ResiliencePipelineBridge.TResult.cs deleted file mode 100644 index 75581f215e8..00000000000 --- a/src/Polly.Core/Utils/ResiliencePipelineBridge.TResult.cs +++ /dev/null @@ -1,36 +0,0 @@ -namespace Polly.Utils; - -[DebuggerDisplay("{Strategy}")] -internal sealed class ResiliencePipelineBridge : ResiliencePipeline -{ - public ResiliencePipelineBridge(ResilienceStrategy strategy) => Strategy = strategy; - - public ResilienceStrategy Strategy { get; } - - 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/ResiliencePipelineBridge.cs b/src/Polly.Core/Utils/ResiliencePipelineBridge.cs deleted file mode 100644 index eeeaf4af235..00000000000 --- a/src/Polly.Core/Utils/ResiliencePipelineBridge.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Polly.Utils; - -[DebuggerDisplay("{Strategy}")] -internal sealed class ResiliencePipelineBridge : ResiliencePipeline -{ - public ResiliencePipelineBridge(ResilienceStrategy strategy) => Strategy = strategy; - - public ResilienceStrategy Strategy { get; } - - internal override ValueTask> ExecuteCore( - Func>> callback, - ResilienceContext context, - TState state) => Strategy.ExecuteCore(callback, context, state); -} diff --git a/src/Polly.Core/Utils/SynchronousExecutionHelper.cs b/src/Polly.Core/Utils/SynchronousExecutionHelper.cs deleted file mode 100644 index 4c3641f1dd1..00000000000 --- a/src/Polly.Core/Utils/SynchronousExecutionHelper.cs +++ /dev/null @@ -1,41 +0,0 @@ -namespace Polly.Utils; - -#pragma warning disable S5034 // "ValueTask" should be consumed correctly -#pragma warning disable SA1515 // Single-line comment should be preceded by blank line - -/// -/// Helper methods to execute value tasks synchronously. -/// -internal static class SynchronousExecutionHelper -{ - public static void GetResult(this ValueTask task) - { - Debug.Assert( - task.IsCompleted, - "The value task should be already completed at this point. If not, it's an indication that the strategy does not respect the ResilienceContext.IsSynchronous value."); - - // Stryker disable once boolean : no means to test this - if (task.IsCompleted) - { - _ = task.Result; - return; - } - - task.Preserve().GetAwaiter().GetResult(); - } - - public static TResult GetResult(this ValueTask task) - { - Debug.Assert( - task.IsCompleted, - "The value task should be already completed at this point. If not, it's an indication that the strategy does not respect the ResilienceContext.IsSynchronous value."); - - // Stryker disable once boolean : no means to test this - if (task.IsCompleted) - { - return task.Result; - } - - return task.Preserve().GetAwaiter().GetResult(); - } -} diff --git a/src/Polly.Core/Utils/TaskHelper.cs b/src/Polly.Core/Utils/TaskHelper.cs index f5891616ef7..e3b970ccc32 100644 --- a/src/Polly.Core/Utils/TaskHelper.cs +++ b/src/Polly.Core/Utils/TaskHelper.cs @@ -1,7 +1,40 @@ namespace Polly.Utils; +#pragma warning disable S5034 // "ValueTask" should be consumed correctly + internal static class TaskHelper { + public static void GetResult(this ValueTask task) + { + Debug.Assert( + task.IsCompleted, + "The value task should be already completed at this point. If not, it's an indication that the strategy does not respect the ResilienceContext.IsSynchronous value."); + + // Stryker disable once boolean : no means to test this + if (task.IsCompleted) + { + _ = task.Result; + return; + } + + task.Preserve().GetAwaiter().GetResult(); + } + + public static TResult GetResult(this ValueTask task) + { + Debug.Assert( + task.IsCompleted, + "The value task should be already completed at this point. If not, it's an indication that the strategy does not respect the ResilienceContext.IsSynchronous value."); + + // Stryker disable once boolean : no means to test this + if (task.IsCompleted) + { + return task.Result; + } + + return task.Preserve().GetAwaiter().GetResult(); + } + public static ValueTask> ConvertValueTask(ValueTask> valueTask, ResilienceContext resilienceContext) { if (valueTask.IsCompletedSuccessfully) diff --git a/src/Polly.Core/Utils/ValidationContextExtensions.cs b/src/Polly.Core/Utils/ValidationContextExtensions.cs deleted file mode 100644 index 3fee8f585cd..00000000000 --- a/src/Polly.Core/Utils/ValidationContextExtensions.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -internal static class ValidationContextExtensions -{ - public static string[]? GetMemberName(this ValidationContext? validationContext) - { -#pragma warning disable S1168 // Empty arrays and collections should be returned instead of null - return validationContext?.MemberName is { } memberName - ? new[] { memberName } - : null; -#pragma warning restore S1168 // Empty arrays and collections should be returned instead of null - } - - public static string GetDisplayName(this ValidationContext? validationContext) - { - return validationContext?.DisplayName ?? string.Empty; - } -} diff --git a/src/Polly.Core/Utils/ValidationHelper.cs b/src/Polly.Core/Utils/ValidationHelper.cs index 7598a50664b..6e22635bb08 100644 --- a/src/Polly.Core/Utils/ValidationHelper.cs +++ b/src/Polly.Core/Utils/ValidationHelper.cs @@ -7,6 +7,20 @@ namespace Polly.Utils; [ExcludeFromCodeCoverage] internal static class ValidationHelper { + public static string[]? GetMemberName(this ValidationContext? validationContext) + { +#pragma warning disable S1168 // Empty arrays and collections should be returned instead of null + return validationContext?.MemberName is { } memberName + ? new[] { memberName } + : null; +#pragma warning restore S1168 // Empty arrays and collections should be returned instead of null + } + + public static string GetDisplayName(this ValidationContext? validationContext) + { + return validationContext?.DisplayName ?? string.Empty; + } + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(TimeSpan))] [UnconditionalSuppressMessage( "Trimming", diff --git a/src/Polly.Testing/ResiliencePipelineExtensions.cs b/src/Polly.Testing/ResiliencePipelineExtensions.cs index 0da1f12931a..f3a625ab17e 100644 --- a/src/Polly.Testing/ResiliencePipelineExtensions.cs +++ b/src/Polly.Testing/ResiliencePipelineExtensions.cs @@ -18,7 +18,7 @@ public static ResiliencePipelineDescriptor GetPipelineDescriptor(this R { Guard.NotNull(pipeline); - return GetPipelineDescriptorCore(pipeline.Strategy); + return GetPipelineDescriptorCore(pipeline.Component); } /// @@ -31,55 +31,55 @@ public static ResiliencePipelineDescriptor GetPipelineDescriptor(this Resilience { Guard.NotNull(pipeline); - return GetPipelineDescriptorCore(pipeline); + return GetPipelineDescriptorCore(pipeline.Component); } - private static ResiliencePipelineDescriptor GetPipelineDescriptorCore(ResiliencePipeline strategy) + private static ResiliencePipelineDescriptor GetPipelineDescriptorCore(PipelineComponent component) { - var strategies = new List(); - strategy.ExpandStrategies(strategies); + var components = new List(); + component.ExpandComponents(components); - var innerStrategies = strategies.Select(s => new ResilienceStrategyDescriptor(s.Options, GetStrategyInstance(s))).ToList(); + var descriptors = components.Select(s => new ResilienceStrategyDescriptor(s.Options, GetStrategyInstance(s))).ToList(); return new ResiliencePipelineDescriptor( - innerStrategies.Where(s => !ShouldSkip(s.StrategyInstance)).ToList().AsReadOnly(), - isReloadable: innerStrategies.Exists(s => s.StrategyInstance is ReloadableResiliencePipeline)); + descriptors.Where(s => !ShouldSkip(s.StrategyInstance)).ToList().AsReadOnly(), + isReloadable: components.Exists(s => s is PipelineComponent.ReloadableComponent)); } - private static object GetStrategyInstance(ResiliencePipeline strategy) + private static object GetStrategyInstance(PipelineComponent component) { - if (strategy is ResiliencePipelineBridge reactiveBridge) + if (component is PipelineComponent.BridgeComponent reactiveBridge) { return reactiveBridge.Strategy; } - if (strategy is ResiliencePipelineBridge nonReactiveBridge) + if (component is PipelineComponent.BridgeComponent nonReactiveBridge) { return nonReactiveBridge.Strategy; } - return strategy; + return component; } - private static bool ShouldSkip(object instance) => instance is ReloadableResiliencePipeline; + private static bool ShouldSkip(object instance) => instance is PipelineComponent.ReloadableComponent; - private static void ExpandStrategies(this ResiliencePipeline strategy, List strategies) + private static void ExpandComponents(this PipelineComponent component, List components) { - if (strategy is CompositeResiliencePipeline pipeline) + if (component is PipelineComponent.CompositeComponent pipeline) { - foreach (var inner in pipeline.Strategies) + foreach (var inner in pipeline.Components) { - inner.ExpandStrategies(strategies); + inner.ExpandComponents(components); } } - else if (strategy is ReloadableResiliencePipeline reloadable) + else if (component is PipelineComponent.ReloadableComponent reloadable) { - strategies.Add(reloadable); - ExpandStrategies(reloadable.Pipeline, strategies); + components.Add(reloadable); + ExpandComponents(reloadable.Component, components); } else { - strategies.Add(strategy); + components.Add(component); } } } diff --git a/test/Polly.Core.Tests/CircuitBreaker/CircuitBreakerResilienceStrategyTests.cs b/test/Polly.Core.Tests/CircuitBreaker/CircuitBreakerResilienceStrategyTests.cs index aad93accf62..8169de07377 100644 --- a/test/Polly.Core.Tests/CircuitBreaker/CircuitBreakerResilienceStrategyTests.cs +++ b/test/Polly.Core.Tests/CircuitBreaker/CircuitBreakerResilienceStrategyTests.cs @@ -2,7 +2,6 @@ using NSubstitute; using Polly.CircuitBreaker; using Polly.Telemetry; -using Polly.Utils; namespace Polly.Core.Tests.CircuitBreaker; @@ -116,7 +115,7 @@ public void Execute_UnhandledException_NoCalls() _options.ShouldHandle = args => new ValueTask(args.Exception is InvalidOperationException); var strategy = Create(); - strategy.Invoking(s => s.Execute(_ => throw new ArgumentException())).Should().Throw(); + strategy.Invoking(s => s.Execute(_ => throw new ArgumentException())).Should().Throw(); _behavior.DidNotReceiveWithAnyArgs().OnActionFailure(default, out Arg.Any()); _behavior.DidNotReceiveWithAnyArgs().OnActionSuccess(default); @@ -135,6 +134,6 @@ public void Execute_Ok() _behavior.Received(1).OnActionSuccess(CircuitState.Closed); } - private ResiliencePipelineBridge Create() - => new(new CircuitBreakerResilienceStrategy(_options.ShouldHandle!, _controller, _options.StateProvider, _options.ManualControl)); + private ResiliencePipeline Create() + => new CircuitBreakerResilienceStrategy(_options.ShouldHandle!, _controller, _options.StateProvider, _options.ManualControl).AsPipeline(); } diff --git a/test/Polly.Core.Tests/Fallback/FallbackResilienceStrategyTests.cs b/test/Polly.Core.Tests/Fallback/FallbackResilienceStrategyTests.cs index 5e76eb77e41..e13ed1b834e 100644 --- a/test/Polly.Core.Tests/Fallback/FallbackResilienceStrategyTests.cs +++ b/test/Polly.Core.Tests/Fallback/FallbackResilienceStrategyTests.cs @@ -1,6 +1,5 @@ using Polly.Fallback; using Polly.Telemetry; -using Polly.Utils; namespace Polly.Core.Tests.Fallback; @@ -90,8 +89,6 @@ private void SetHandler( _handler = FallbackHelper.CreateHandler(shouldHandle, fallback); } - private ResiliencePipelineBridge Create() => new(new FallbackResilienceStrategy( - _handler!, - _options.OnFallback, - _telemetry)); + private ResiliencePipeline Create() + => new FallbackResilienceStrategy(_handler!, _options.OnFallback, _telemetry).AsPipeline(); } diff --git a/test/Polly.Core.Tests/GenericResiliencePipelineBuilderTests.cs b/test/Polly.Core.Tests/GenericResiliencePipelineBuilderTests.cs index 0a228f908bf..79928ad1cbb 100644 --- a/test/Polly.Core.Tests/GenericResiliencePipelineBuilderTests.cs +++ b/test/Polly.Core.Tests/GenericResiliencePipelineBuilderTests.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.Time.Testing; +using NSubstitute; using Polly.Utils; namespace Polly.Core.Tests; @@ -43,14 +44,14 @@ public void Build_Ok() // assert strategy.Should().NotBeNull(); - strategy.Strategy.Should().BeOfType().Subject.Strategies.Should().HaveCount(2); + strategy.Component.Should().BeOfType().Subject.Components.Should().HaveCount(2); } [Fact] public void AddGenericStrategy_Ok() { // arrange - var testStrategy = new ResiliencePipeline(new TestResilienceStrategy().AsPipeline()); + var testStrategy = Substitute.For>().AsPipeline(); _builder.AddPipeline(testStrategy); // act @@ -58,6 +59,6 @@ public void AddGenericStrategy_Ok() // assert strategy.Should().NotBeNull(); - ((CompositeResiliencePipeline)strategy.Strategy).Strategies[0].Should().Be(testStrategy.Strategy); + ((PipelineComponent.CompositeComponent)strategy.Component).Components[0].Should().Be(testStrategy.Component); } } diff --git a/test/Polly.Core.Tests/Hedging/HedgingResilienceStrategyTests.cs b/test/Polly.Core.Tests/Hedging/HedgingResilienceStrategyTests.cs index ba96439bae6..7bb90dd248a 100644 --- a/test/Polly.Core.Tests/Hedging/HedgingResilienceStrategyTests.cs +++ b/test/Polly.Core.Tests/Hedging/HedgingResilienceStrategyTests.cs @@ -1,7 +1,7 @@ using Polly.Hedging; using Polly.Hedging.Utils; using Polly.Telemetry; -using Polly.Utils; +using Polly.Testing; using Xunit.Abstractions; namespace Polly.Core.Tests.Hedging; @@ -47,7 +47,7 @@ public void Dispose() public void Ctor_EnsureDefaults() { ConfigureHedging(); - var strategy = (HedgingResilienceStrategy)Create().Strategy; + var strategy = (HedgingResilienceStrategy)Create().GetPipelineDescriptor().FirstStrategy.StrategyInstance; strategy.MaxHedgedAttempts.Should().Be(_options.MaxHedgedAttempts); strategy.HedgingDelay.Should().Be(_options.HedgingDelay); @@ -60,12 +60,12 @@ public async Task Execute_CancellationRequested_Throws() { ConfigureHedging(); - var strategy = Create(); + var strategy = (HedgingResilienceStrategy)Create().GetPipelineDescriptor().FirstStrategy.StrategyInstance; _cts.Cancel(); var context = ResilienceContextPool.Shared.Get(); context.CancellationToken = _cts.Token; - var outcome = await strategy.ExecuteOutcomeAsync((_, _) => Outcome.FromResultAsTask("dummy"), context, "state"); + var outcome = await strategy.ExecuteCore((_, _) => Outcome.FromResultAsTask("dummy"), context, "state"); outcome.Exception.Should().BeOfType(); outcome.Exception!.StackTrace.Should().Contain("Execute_CancellationRequested_Throws"); } @@ -128,7 +128,7 @@ public async Task GetHedgingDelayAsync_GeneratorSet_EnsureCorrectGeneratedValue( { _options.HedgingDelayGenerator = args => new ValueTask(TimeSpan.FromSeconds(seconds)); - var strategy = (HedgingResilienceStrategy)Create().Strategy; + var strategy = (HedgingResilienceStrategy)Create().GetPipelineDescriptor().FirstStrategy.StrategyInstance; var result = await strategy.GetHedgingDelayAsync(ResilienceContextPool.Shared.Get(), 0); @@ -140,7 +140,7 @@ public async Task GetHedgingDelayAsync_NoGeneratorSet_EnsureCorrectValue() { _options.HedgingDelay = TimeSpan.FromMilliseconds(123); - var strategy = (HedgingResilienceStrategy)Create().Strategy; + var strategy = (HedgingResilienceStrategy)Create().GetPipelineDescriptor().FirstStrategy.StrategyInstance; var result = await strategy.GetHedgingDelayAsync(ResilienceContextPool.Shared.Get(), 0); @@ -330,10 +330,10 @@ public async Task ExecuteAsync_EnsureDiscardedResultDisposed() }; }); - var strategy = new ResiliencePipelineBridge(Create(handler, null)); + var pipeline = Create(handler, null).AsPipeline(); // act - var resultTask = strategy.ExecuteAsync(async token => + var resultTask = pipeline.ExecuteAsync(async token => { #pragma warning disable CA2016 // Forward the 'CancellationToken' parameter to methods await _timeProvider.Delay(LongDelay); @@ -665,7 +665,7 @@ public async void ExecuteAsync_ZeroHedgingDelay_EnsureAllTasksSpawnedAtOnce() _options.HedgingDelay = TimeSpan.Zero; // act - var task = Create().ExecuteAsync(async c => (await Execute(c)).Result, default); + var task = Create().ExecuteAsync(async c => (await Execute(c)).Result!, default); // assert Assert.True(allExecutionsReached.WaitOne(AssertTimeout)); @@ -691,7 +691,7 @@ public void ExecuteAsync_InfiniteHedgingDelay_EnsureNoConcurrentExecutions() bool executing = false; int executions = 0; using var allExecutions = new ManualResetEvent(true); - ConfigureHedging(context => Execute(context.CancellationToken)); + ConfigureHedging(async context => Outcome.FromResult(await Execute(context.CancellationToken))); // act var pending = Create().ExecuteAsync(Execute, _cts.Token); @@ -699,11 +699,11 @@ public void ExecuteAsync_InfiniteHedgingDelay_EnsureNoConcurrentExecutions() // assert Assert.True(allExecutions.WaitOne(AssertTimeout)); - async ValueTask> Execute(CancellationToken token) + async ValueTask Execute(CancellationToken token) { if (executing) { - return Outcome.FromException(new InvalidOperationException("Concurrent execution detected!")); + throw new InvalidOperationException("Concurrent execution detected!"); } executing = true; @@ -716,7 +716,7 @@ async ValueTask> Execute(CancellationToken token) await _timeProvider.Delay(LongDelay, token); - return Outcome.FromResult("dummy"); + return "dummy"; } finally { @@ -912,8 +912,8 @@ public async Task ExecuteAsync_EnsureOnHedgingTelemetry() var strategy = Create(); await strategy.ExecuteAsync((_, _) => new ValueTask(Failure), context, "state"); - context.ResilienceEvents.Should().HaveCount(_options.MaxHedgedAttempts + 1); - context.ResilienceEvents.Select(v => v.EventName).Distinct().Should().HaveCount(2); + context.ResilienceEvents.Should().HaveCount(_options.MaxHedgedAttempts + 3); + context.ResilienceEvents.Select(v => v.EventName).Distinct().Should().HaveCount(4); } private void ConfigureHedging() @@ -954,7 +954,7 @@ private void ConfigureHedging(TimeSpan delay) => ConfigureHedging(args => async return Outcome.FromResult("secondary"); }); - private ResiliencePipelineBridge Create() => new(Create(_handler!, _options.OnHedging)); + private ResiliencePipeline Create() => Create(_handler!, _options.OnHedging).AsPipeline(); private HedgingResilienceStrategy Create( HedgingHandler handler, diff --git a/test/Polly.Core.Tests/NullResiliencePipelineTests.cs b/test/Polly.Core.Tests/NullResiliencePipelineTests.cs deleted file mode 100644 index 874b9eb6d47..00000000000 --- a/test/Polly.Core.Tests/NullResiliencePipelineTests.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace Polly.Core.Tests; - -public class NullResiliencePipelineTests -{ - [Fact] - public void Instance_ShouldNotBeNull() - { - NullResiliencePipeline.Instance.Should().NotBeNull(); - NullResiliencePipeline.Instance.Should().NotBeNull(); - - } - - [Fact] - public void Execute_Ok() - { - bool executed = false; - NullResiliencePipeline.Instance.Execute(_ => executed = true); - executed.Should().BeTrue(); - - NullResiliencePipeline.Instance.Execute(_ => "res").Should().Be("res"); - } -} diff --git a/test/Polly.Core.Tests/Registry/ResiliencePipelineProviderTests.cs b/test/Polly.Core.Tests/Registry/ResiliencePipelineProviderTests.cs index d33c27bdfbd..5adcc497df5 100644 --- a/test/Polly.Core.Tests/Registry/ResiliencePipelineProviderTests.cs +++ b/test/Polly.Core.Tests/Registry/ResiliencePipelineProviderTests.cs @@ -37,7 +37,7 @@ public void Get_Exist_Ok() [Fact] public void Get_GenericExist_Ok() { - var provider = new Provider { GenericStrategy = new TestResiliencePipeline() }; + var provider = new Provider { GenericStrategy = ResiliencePipeline.Null }; provider.GetPipeline("exists").Should().Be(provider.GenericStrategy); } diff --git a/test/Polly.Core.Tests/Registry/ResiliencePipelineRegistryTests.cs b/test/Polly.Core.Tests/Registry/ResiliencePipelineRegistryTests.cs index 1e50e7e7473..cde4c8fe910 100644 --- a/test/Polly.Core.Tests/Registry/ResiliencePipelineRegistryTests.cs +++ b/test/Polly.Core.Tests/Registry/ResiliencePipelineRegistryTests.cs @@ -3,6 +3,7 @@ using Polly.Retry; using Polly.Testing; using Polly.Timeout; +using Polly.Utils; namespace Polly.Core.Tests.Registry; @@ -63,9 +64,9 @@ public void ClearPipelines_Generic_Ok() registry.TryAddBuilder("C", (b, _) => b.AddStrategy(new TestResilienceStrategy())); - registry.TryAddPipeline("A", new TestResiliencePipeline()); - registry.TryAddPipeline("B", new TestResiliencePipeline()); - registry.TryAddPipeline("C", new TestResiliencePipeline()); + registry.TryAddPipeline("A", new ResiliencePipeline(PipelineComponent.Null)); + registry.TryAddPipeline("B", new ResiliencePipeline(PipelineComponent.Null)); + registry.TryAddPipeline("C", new ResiliencePipeline(PipelineComponent.Null)); registry.ClearPipelines(); @@ -94,8 +95,8 @@ public void Remove_Generic_Ok() { var registry = new ResiliencePipelineRegistry(); - registry.TryAddPipeline("A", new TestResiliencePipeline()); - registry.TryAddPipeline("B", new TestResiliencePipeline()); + registry.TryAddPipeline("A", new ResiliencePipeline(PipelineComponent.Null)); + registry.TryAddPipeline("B", new ResiliencePipeline(PipelineComponent.Null)); registry.RemovePipeline("A").Should().BeTrue(); registry.RemovePipeline("A").Should().BeFalse(); @@ -316,7 +317,7 @@ public void TryGet_ExplicitPipelineAdded_Ok() [Fact] public void TryGet_GenericExplicitPipelineAdded_Ok() { - var expectedPipeline = new TestResiliencePipeline(); + var expectedPipeline = new ResiliencePipeline(PipelineComponent.Null); var registry = CreateRegistry(); var key = StrategyId.Create("A", "Instance"); registry.TryAddPipeline(key, expectedPipeline).Should().BeTrue(); @@ -343,12 +344,12 @@ public void TryAdd_Twice_SecondNotAdded() [Fact] public void TryAdd_GenericTwice_SecondNotAdded() { - var expectedPipeline = new TestResiliencePipeline(); + var expectedPipeline = new ResiliencePipeline(PipelineComponent.Null); var registry = CreateRegistry(); var key = StrategyId.Create("A", "Instance"); registry.TryAddPipeline(key, expectedPipeline).Should().BeTrue(); - registry.TryAddPipeline(key, new TestResiliencePipeline()).Should().BeFalse(); + registry.TryAddPipeline(key, new ResiliencePipeline(PipelineComponent.Null)).Should().BeFalse(); registry.TryGetPipeline(key, out var strategy).Should().BeTrue(); strategy.Should().BeSameAs(expectedPipeline); diff --git a/test/Polly.Core.Tests/ResiliencePipelineBuilderTests.cs b/test/Polly.Core.Tests/ResiliencePipelineBuilderTests.cs index f7ae4cef685..1fbdf987220 100644 --- a/test/Polly.Core.Tests/ResiliencePipelineBuilderTests.cs +++ b/test/Polly.Core.Tests/ResiliencePipelineBuilderTests.cs @@ -89,10 +89,11 @@ public void AddPipeline_Multiple_Ok() // act var strategy = builder.Build(); strategy + .Component .Should() - .BeOfType() + .BeOfType() .Subject - .Strategies.Should().HaveCount(3); + .Components.Should().HaveCount(3); // assert strategy.Execute(_ => executions.Add(4)); @@ -107,13 +108,13 @@ public void AddPipeline_Duplicate_Throws() // arrange var executions = new List(); var builder = new ResiliencePipelineBuilder() - .AddPipeline(NullResiliencePipeline.Instance) - .AddPipeline(NullResiliencePipeline.Instance); + .AddPipeline(ResiliencePipeline.Null) + .AddPipeline(ResiliencePipeline.Null); builder.Invoking(b => b.Build()) .Should() .Throw() - .WithMessage("The composite resilience strategy must contain unique resilience strategies."); + .WithMessage("The resilience pipeline must contain unique resilience strategies."); } [Fact] @@ -173,7 +174,7 @@ public void AddPipeline_MultipleNonDelegating_Ok() } [Fact] - public void Build_Empty_ReturnsNullResiliencePipeline() => new ResiliencePipelineBuilder().Build().Should().BeSameAs(NullResiliencePipeline.Instance); + public void Build_Empty_ReturnsNullResiliencePipeline() => new ResiliencePipelineBuilder().Build().Component.Should().BeSameAs(PipelineComponent.Null); [Fact] public void AddPipeline_AfterUsed_Throws() @@ -183,7 +184,7 @@ public void AddPipeline_AfterUsed_Throws() builder.Build(); builder - .Invoking(b => b.AddPipeline(NullResiliencePipeline.Instance)) + .Invoking(b => b.AddPipeline(ResiliencePipeline.Null)) .Should() .Throw() .WithMessage("Cannot add any more resilience strategies to the builder after it has been used to build a pipeline once."); @@ -194,7 +195,7 @@ public void Build_InvalidBuilderOptions_Throw() { var builder = new InvalidResiliencePipelineBuilder(); - builder.Invoking(b => b.BuildPipeline()) + builder.Invoking(b => b.BuildPipelineComponent()) .Should() .Throw() .WithMessage( diff --git a/test/Polly.Core.Tests/ResiliencePipelineTTests.Async.cs b/test/Polly.Core.Tests/ResiliencePipelineTTests.Async.cs index 2a6a08d6849..78ee73b24fe 100644 --- a/test/Polly.Core.Tests/ResiliencePipelineTTests.Async.cs +++ b/test/Polly.Core.Tests/ResiliencePipelineTTests.Async.cs @@ -1,3 +1,5 @@ +using Polly.Utils; + namespace Polly.Core.Tests; #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously @@ -63,7 +65,7 @@ public partial class ResiliencePipelineTests [Theory] public async Task ExecuteAsync_GenericStrategy_Ok(Func, ValueTask> execute) { - var strategy = new ResiliencePipeline(new TestResilienceStrategy + var pipeline = new ResiliencePipeline(PipelineComponent.FromStrategy(new TestResilienceStrategy { Before = (c, _) => { @@ -71,15 +73,15 @@ public async Task ExecuteAsync_GenericStrategy_Ok(Func.Instance.ExecuteOutcomeAsync((context, state) => + var result = await ResiliencePipeline.Null.ExecuteOutcomeAsync((context, state) => { state.Should().Be("state"); context.IsSynchronous.Should().BeFalse(); diff --git a/test/Polly.Core.Tests/ResiliencePipelineTTests.Sync.cs b/test/Polly.Core.Tests/ResiliencePipelineTTests.Sync.cs index 20cfa9ac3a6..c8f95c513bf 100644 --- a/test/Polly.Core.Tests/ResiliencePipelineTTests.Sync.cs +++ b/test/Polly.Core.Tests/ResiliencePipelineTTests.Sync.cs @@ -1,3 +1,5 @@ +using Polly.Utils; + namespace Polly.Core.Tests; public partial class ResiliencePipelineTests @@ -77,15 +79,15 @@ public partial class ResiliencePipelineTests [Theory] public void Execute_GenericStrategy_Ok(Action> execute) { - var strategy = new ResiliencePipeline(new TestResilienceStrategy + var pipeline = new ResiliencePipeline(PipelineComponent.FromStrategy(new TestResilienceStrategy { Before = (c, _) => { c.IsSynchronous.Should().BeTrue(); c.ResultType.Should().Be(typeof(string)); }, - }.AsPipeline()); + })); - execute(strategy); + execute(pipeline); } } diff --git a/test/Polly.Core.Tests/ResiliencePipelineTests.AsyncT.cs b/test/Polly.Core.Tests/ResiliencePipelineTests.AsyncT.cs index 71a7c06a896..c79459f8837 100644 --- a/test/Polly.Core.Tests/ResiliencePipelineTests.AsyncT.cs +++ b/test/Polly.Core.Tests/ResiliencePipelineTests.AsyncT.cs @@ -117,7 +117,7 @@ static async ValueTask AssertStackTrace(Func + var result = await ResiliencePipeline.Null.ExecuteOutcomeAsync((context, state) => { state.Should().Be("state"); context.IsSynchronous.Should().BeFalse(); diff --git a/test/Polly.Core.Tests/ResiliencePipelineTests.cs b/test/Polly.Core.Tests/ResiliencePipelineTests.cs index 55af7923078..ad463e0b907 100644 --- a/test/Polly.Core.Tests/ResiliencePipelineTests.cs +++ b/test/Polly.Core.Tests/ResiliencePipelineTests.cs @@ -1,3 +1,4 @@ +using NSubstitute; using Polly.Utils; namespace Polly.Core.Tests; @@ -6,16 +7,23 @@ public partial class ResiliencePipelineTests { public static readonly CancellationToken CancellationToken = new CancellationTokenSource().Token; + [Fact] + public void Null_Ok() + { + ResiliencePipeline.Null.Should().NotBeNull(); + ResiliencePipeline.Null.Should().NotBeNull(); + } + [Fact] public void DebuggerProxy_Ok() { - var pipeline = CompositeResiliencePipeline.Create(new[] + var pipeline = (PipelineComponent.CompositeComponent)PipelineComponent.CreateComposite(new[] { - new TestResilienceStrategy().AsPipeline(), - new TestResilienceStrategy().AsPipeline() + Substitute.For(), + Substitute.For(), }, null!, null!); - new CompositeResiliencePipeline.DebuggerProxy(pipeline).Strategies.Should().HaveCount(2); + new PipelineComponent.CompositeDebuggerProxy(pipeline).Strategies.Should().HaveCount(2); } public class ExecuteParameters : ExecuteParameters diff --git a/test/Polly.Core.Tests/Retry/RetryResilienceStrategyTests.cs b/test/Polly.Core.Tests/Retry/RetryResilienceStrategyTests.cs index 5b30c655ed8..9d6a2ef1ae9 100644 --- a/test/Polly.Core.Tests/Retry/RetryResilienceStrategyTests.cs +++ b/test/Polly.Core.Tests/Retry/RetryResilienceStrategyTests.cs @@ -2,7 +2,6 @@ using Microsoft.Extensions.Time.Testing; using Polly.Retry; using Polly.Telemetry; -using Polly.Utils; namespace Polly.Core.Tests.Retry; @@ -154,7 +153,7 @@ public void RetryDelayGenerator_Respected() return new ValueTask(delay); }; - CreateSut(TimeProvider.System).Execute(_ => { }); + CreateSut(TimeProvider.System).Execute(_ => "dummy"); retries.Should().Be(3); generatedValues.Should().Be(3); @@ -185,7 +184,7 @@ public async Task RetryDelayGenerator_ZeroDelay_NoTimeProviderCalls() }; var sut = CreateSut(provider); - await sut.ExecuteAsync(_ => default); + await sut.ExecuteAsync(_ => new ValueTask("dummy")); retries.Should().Be(3); generatedValues.Should().Be(3); @@ -336,7 +335,7 @@ public void RetryDelayGenerator_EnsureCorrectArguments() private void SetupNoDelay() => _options.RetryDelayGenerator = _ => new ValueTask(TimeSpan.Zero); - private async ValueTask ExecuteAndAdvance(ResiliencePipelineBridge sut) + private async ValueTask ExecuteAndAdvance(ResiliencePipeline sut) { var executing = sut.ExecuteAsync(_ => new ValueTask(0)).AsTask(); @@ -348,6 +347,6 @@ private async ValueTask ExecuteAndAdvance(ResiliencePipelineBridge return await executing; } - private ResiliencePipelineBridge CreateSut(TimeProvider? timeProvider = null) => - new(new RetryResilienceStrategy(_options, timeProvider ?? _timeProvider, _telemetry)); + private ResiliencePipeline CreateSut(TimeProvider? timeProvider = null) => + new RetryResilienceStrategy(_options, timeProvider ?? _timeProvider, _telemetry).AsPipeline(); } diff --git a/test/Polly.Core.Tests/Utils/CompositeResiliencePipelineTests.cs b/test/Polly.Core.Tests/Utils/CompositeResiliencePipelineTests.cs deleted file mode 100644 index 3a1cf7ee701..00000000000 --- a/test/Polly.Core.Tests/Utils/CompositeResiliencePipelineTests.cs +++ /dev/null @@ -1,162 +0,0 @@ -using Microsoft.Extensions.Time.Testing; -using NSubstitute; -using Polly.Telemetry; -using Polly.Utils; - -namespace Polly.Core.Tests.Utils; - -public class CompositeResiliencePipelineTests -{ - private readonly ResilienceStrategyTelemetry _telemetry; - private Action>? _onTelemetry; - - public CompositeResiliencePipelineTests() - => _telemetry = TestUtilities.CreateResilienceTelemetry(args => _onTelemetry?.Invoke(args)); - - [Fact] - public void Create_ArgValidation() - { - Assert.Throws(() => CompositeResiliencePipeline.Create(null!, null!, null!)); - Assert.Throws(() => CompositeResiliencePipeline.Create(Array.Empty(), null!, null!)); - Assert.Throws(() => CompositeResiliencePipeline.Create(new ResiliencePipeline[] - { - NullResiliencePipeline.Instance, - NullResiliencePipeline.Instance - }, null!, null!)); - } - - [Fact] - public void Create_EnsureOriginalStrategiesPreserved() - { - var strategies = new[] - { - new TestResilienceStrategy().AsPipeline(), - new Strategy().AsPipeline(), - new TestResilienceStrategy().AsPipeline(), - }; - - var pipeline = CreateSut(strategies); - - for (var i = 0; i < strategies.Length; i++) - { - pipeline.Strategies[i].Should().BeSameAs(strategies[i]); - } - - pipeline.Strategies.SequenceEqual(strategies).Should().BeTrue(); - } - - [Fact] - public async Task Create_EnsureExceptionsNotWrapped() - { - var strategies = new[] - { - new Strategy().AsPipeline(), - new Strategy().AsPipeline(), - }; - - var pipeline = CreateSut(strategies); - await pipeline - .Invoking(p => p.ExecuteCore((_, _) => Outcome.FromResultAsTask(10), ResilienceContextPool.Shared.Get(), "state").AsTask()) - .Should() - .ThrowAsync(); - } - - [Fact] - public void Create_EnsurePipelineReusableAcrossDifferentPipelines() - { - var strategies = new[] - { - new TestResilienceStrategy().AsPipeline(), - new Strategy().AsPipeline(), - new TestResilienceStrategy().AsPipeline(), - }; - - var pipeline = CreateSut(strategies); - - CreateSut(new ResiliencePipeline[] { NullResiliencePipeline.Instance, pipeline }); - - this.Invoking(_ => CreateSut(new ResiliencePipeline[] { NullResiliencePipeline.Instance, pipeline })) - .Should() - .NotThrow(); - } - - [Fact] - public async Task Create_Cancelled_EnsureNoExecution() - { - using var cancellation = new CancellationTokenSource(); - cancellation.Cancel(); - var strategies = new[] - { - new TestResilienceStrategy().AsPipeline(), - new TestResilienceStrategy().AsPipeline(), - }; - - var pipeline = CreateSut(strategies, new FakeTimeProvider()); - var context = ResilienceContextPool.Shared.Get(); - context.CancellationToken = cancellation.Token; - - var result = await pipeline.ExecuteOutcomeAsync((_, _) => Outcome.FromResultAsTask("result"), context, "state"); - result.Exception.Should().BeOfType(); - } - - [Fact] - public async Task Create_CancelledLater_EnsureNoExecution() - { - var executed = false; - using var cancellation = new CancellationTokenSource(); - var strategies = new[] - { - new TestResilienceStrategy { Before = (_, _) => { executed = true; cancellation.Cancel(); } }.AsPipeline(), - new TestResilienceStrategy().AsPipeline(), - }; - var pipeline = CreateSut(strategies, new FakeTimeProvider()); - var context = ResilienceContextPool.Shared.Get(); - context.CancellationToken = cancellation.Token; - - var result = await pipeline.ExecuteOutcomeAsync((_, _) => Outcome.FromResultAsTask("result"), context, "state"); - result.Exception.Should().BeOfType(); - executed.Should().BeTrue(); - } - - [Fact] - public void ExecuptePipeline_EnsureTelemetryArgumentsReported() - { - var items = new List(); - var timeProvider = new FakeTimeProvider(); - - _onTelemetry = args => - { - if (args.Arguments is PipelineExecutedArguments executed) - { - executed.Duration.Should().Be(TimeSpan.FromHours(1)); - } - - items.Add(args.Arguments); - }; - - var pipeline = CreateSut(new[] { new TestResilienceStrategy().AsPipeline() }, timeProvider); - pipeline.Execute(() => { timeProvider.Advance(TimeSpan.FromHours(1)); }); - - items.Should().HaveCount(2); - items[0].Should().BeOfType(); - items[1].Should().BeOfType(); - } - - private CompositeResiliencePipeline CreateSut(ResiliencePipeline[] strategies, TimeProvider? timeProvider = null) - { - return CompositeResiliencePipeline.Create(strategies, _telemetry, timeProvider ?? Substitute.For()); - } - - private class Strategy : ResilienceStrategy - { - protected internal override async ValueTask> ExecuteCore( - Func>> callback, - ResilienceContext context, - TState state) - { - await callback(context, state); - - throw new NotSupportedException(); - } - } -} diff --git a/test/Polly.Core.Tests/Utils/ResiliencePipelineBridgeTests.cs b/test/Polly.Core.Tests/Utils/PipelineComponents/BridgePipelineComponentTests.cs similarity index 64% rename from test/Polly.Core.Tests/Utils/ResiliencePipelineBridgeTests.cs rename to test/Polly.Core.Tests/Utils/PipelineComponents/BridgePipelineComponentTests.cs index ce26c409201..bf128ba2bd2 100644 --- a/test/Polly.Core.Tests/Utils/ResiliencePipelineBridgeTests.cs +++ b/test/Polly.Core.Tests/Utils/PipelineComponents/BridgePipelineComponentTests.cs @@ -1,8 +1,8 @@ using Polly.Utils; -namespace Polly.Core.Tests.Utils; +namespace Polly.Core.Tests.Utils.PipelineComponents; -public class ResiliencePipelineBridgeTests +public class BridgePipelineComponentTests { [Fact] public void Ctor_Ok() @@ -15,15 +15,15 @@ public void Execute_NonGeneric_Ok() { var values = new List(); - var strategy = new ResiliencePipelineBridge(new Strategy(outcome => + var pipeline = new ResiliencePipeline(PipelineComponent.FromStrategy(new Strategy(outcome => { values.Add(outcome.Result); - })); + }))); - strategy.Execute(args => "dummy"); - strategy.Execute(args => 0); - strategy.Execute(args => null); - strategy.Execute(args => true); + pipeline.Execute(args => "dummy"); + pipeline.Execute(args => 0); + pipeline.Execute(args => null); + pipeline.Execute(args => true); values[0].Should().Be("dummy"); values[1].Should().Be(0); @@ -36,12 +36,12 @@ public void Execute_Generic_Ok() { var values = new List(); - var strategy = new ResiliencePipelineBridge(new Strategy(outcome => + var pipeline = new ResiliencePipeline(PipelineComponent.FromStrategy(new Strategy(outcome => { values.Add(outcome.Result); - })); + }))); - strategy.Execute(args => "dummy"); + pipeline.Execute(args => "dummy"); values.Should().HaveCount(1); values[0].Should().Be("dummy"); @@ -51,13 +51,14 @@ public void Execute_Generic_Ok() public void Pipeline_TypeCheck_Ok() { var called = false; - var strategy = new ResiliencePipelineBridge(new Strategy(o => + + var pipeline = new ResiliencePipeline(PipelineComponent.FromStrategy(new Strategy(outcome => { - o.Result.Should().Be(-1); + outcome.Result.Should().Be(-1); called = true; - })); + }))); - strategy.Execute(() => -1); + pipeline.Execute(() => -1); called.Should().BeTrue(); } diff --git a/test/Polly.Core.Tests/Utils/PipelineComponents/CompositePipelineComponentTests.cs b/test/Polly.Core.Tests/Utils/PipelineComponents/CompositePipelineComponentTests.cs new file mode 100644 index 00000000000..367a1a47ed6 --- /dev/null +++ b/test/Polly.Core.Tests/Utils/PipelineComponents/CompositePipelineComponentTests.cs @@ -0,0 +1,130 @@ +using Microsoft.Extensions.Time.Testing; +using NSubstitute; +using Polly.Telemetry; +using Polly.Utils; + +namespace Polly.Core.Tests.Utils.PipelineComponents; + +public class CompositePipelineComponentTests +{ + private readonly ResilienceStrategyTelemetry _telemetry; + private readonly FakeTelemetryListener _listener; + + public CompositePipelineComponentTests() + { + _listener = new FakeTelemetryListener(); + _telemetry = TestUtilities.CreateResilienceTelemetry(_listener); + } + + [Fact] + public void Create_EnsureOriginalStrategiesPreserved() + { + var pipelines = new[] + { + Substitute.For(), + Substitute.For(), + Substitute.For(), + }; + + var pipeline = CreateSut(pipelines); + + for (var i = 0; i < pipelines.Length; i++) + { + pipeline.Components[i].Should().BeSameAs(pipelines[i]); + } + + pipeline.Components.SequenceEqual(pipelines).Should().BeTrue(); + } + + [Fact] + public async Task Create_EnsureExceptionsNotWrapped() + { + var components = new[] + { + PipelineComponent.FromStrategy(new TestResilienceStrategy { Before = (_, _) => throw new NotSupportedException() }), + PipelineComponent.FromStrategy(new TestResilienceStrategy { Before = (_, _) => throw new NotSupportedException() }), + }; + + var pipeline = CreateSut(components); + await pipeline + .Invoking(p => p.ExecuteCore((_, _) => Outcome.FromResultAsTask(10), ResilienceContextPool.Shared.Get(), "state").AsTask()) + .Should() + .ThrowAsync(); + } + + [Fact] + public void Create_EnsurePipelineReusableAcrossDifferentPipelines() + { + var components = new[] + { + PipelineComponent.FromStrategy(new TestResilienceStrategy()), + Substitute.For(), + PipelineComponent.FromStrategy(new TestResilienceStrategy()), + + }; + + var pipeline = CreateSut(components); + + CreateSut(new PipelineComponent[] { PipelineComponent.Null, pipeline }); + + this.Invoking(_ => CreateSut(new PipelineComponent[] { PipelineComponent.Null, pipeline })) + .Should() + .NotThrow(); + } + + [Fact] + public async Task Create_Cancelled_EnsureNoExecution() + { + using var cancellation = new CancellationTokenSource(); + cancellation.Cancel(); + var strategies = new[] + { + PipelineComponent.FromStrategy(new TestResilienceStrategy()), + PipelineComponent.FromStrategy(new TestResilienceStrategy()), + }; + + var pipeline = new ResiliencePipeline(CreateSut(strategies, new FakeTimeProvider())); + var context = ResilienceContextPool.Shared.Get(); + context.CancellationToken = cancellation.Token; + + var result = await pipeline.ExecuteOutcomeAsync((_, _) => Outcome.FromResultAsTask("result"), context, "state"); + result.Exception.Should().BeOfType(); + } + + [Fact] + public async Task Create_CancelledLater_EnsureNoExecution() + { + var executed = false; + using var cancellation = new CancellationTokenSource(); + var strategies = new[] + { + PipelineComponent.FromStrategy( new TestResilienceStrategy { Before = (_, _) => { executed = true; cancellation.Cancel(); } }), + PipelineComponent.FromStrategy(new TestResilienceStrategy()), + }; + var pipeline = new ResiliencePipeline(CreateSut(strategies, new FakeTimeProvider())); + var context = ResilienceContextPool.Shared.Get(); + context.CancellationToken = cancellation.Token; + + var result = await pipeline.ExecuteOutcomeAsync((_, _) => Outcome.FromResultAsTask("result"), context, "state"); + result.Exception.Should().BeOfType(); + executed.Should().BeTrue(); + } + + [Fact] + public void ExecutePipeline_EnsureTelemetryArgumentsReported() + { + var timeProvider = new FakeTimeProvider(); + + var pipeline = new ResiliencePipeline(CreateSut(new[] { Substitute.For() }, timeProvider)); + pipeline.Execute(() => { timeProvider.Advance(TimeSpan.FromHours(1)); }); + + _listener.Events.Should().HaveCount(2); + _listener.GetArgs().Should().HaveCount(1); + _listener.GetArgs().Should().HaveCount(1); + } + + private PipelineComponent.CompositeComponent CreateSut(PipelineComponent[] components, TimeProvider? timeProvider = null) + { + return (PipelineComponent.CompositeComponent)PipelineComponent.CreateComposite(components, _telemetry, timeProvider ?? Substitute.For()); + } +} diff --git a/test/Polly.Core.Tests/Utils/ReloadableResiliencePipelineTests.cs b/test/Polly.Core.Tests/Utils/PipelineComponents/PipelineComponentTests.cs similarity index 57% rename from test/Polly.Core.Tests/Utils/ReloadableResiliencePipelineTests.cs rename to test/Polly.Core.Tests/Utils/PipelineComponents/PipelineComponentTests.cs index 4870007c7f0..829f185422b 100644 --- a/test/Polly.Core.Tests/Utils/ReloadableResiliencePipelineTests.cs +++ b/test/Polly.Core.Tests/Utils/PipelineComponents/PipelineComponentTests.cs @@ -1,15 +1,16 @@ +using NSubstitute; using Polly.Telemetry; using Polly.Utils; -namespace Polly.Core.Tests.Utils; +namespace Polly.Core.Tests.Utils.PipelineComponents; -public class ReloadableResiliencePipelineTests : IDisposable +public class PipelineComponentTests : IDisposable { private readonly List> _events = new(); private readonly ResilienceStrategyTelemetry _telemetry; private CancellationTokenSource _cancellationTokenSource; - public ReloadableResiliencePipelineTests() + public PipelineComponentTests() { _telemetry = TestUtilities.CreateResilienceTelemetry(args => { @@ -24,19 +25,19 @@ public ReloadableResiliencePipelineTests() [Fact] public void Ctor_Ok() { - var strategy = new TestResilienceStrategy().AsPipeline(); - var sut = CreateSut(strategy); + var component = Substitute.For(); + var sut = CreateSut(component); - sut.Pipeline.Should().Be(strategy); + sut.Component.Should().Be(component); - ReloadableResiliencePipeline.ReloadFailedEvent.Should().Be("ReloadFailed"); + PipelineComponent.ReloadableComponent.ReloadFailedEvent.Should().Be("ReloadFailed"); } [Fact] public void ChangeTriggered_StrategyReloaded() { - var strategy = new TestResilienceStrategy().AsPipeline(); - var sut = CreateSut(strategy); + var component = Substitute.For(); + var sut = CreateSut(component); for (var i = 0; i < 10; i++) { @@ -44,7 +45,7 @@ public void ChangeTriggered_StrategyReloaded() _cancellationTokenSource = new CancellationTokenSource(); src.Cancel(); - sut.Pipeline.Should().NotBe(strategy); + sut.Component.Should().NotBe(component); } _events.Where(e => e.Event.EventName == "ReloadFailed").Should().HaveCount(0); @@ -54,34 +55,33 @@ public void ChangeTriggered_StrategyReloaded() [Fact] public void ChangeTriggered_FactoryError_LastStrategyUsedAndErrorReported() { - var strategy = new TestResilienceStrategy().AsPipeline(); - var sut = CreateSut(strategy, () => throw new InvalidOperationException()); + var component = Substitute.For(); + var sut = CreateSut(component, () => throw new InvalidOperationException()); _cancellationTokenSource.Cancel(); - sut.Pipeline.Should().Be(strategy); + sut.Component.Should().Be(component); _events.Should().HaveCount(2); _events[0] .Arguments .Should() - .BeOfType(); + .BeOfType(); var args = _events[1] .Arguments .Should() - .BeOfType() + .BeOfType() .Subject; args.Exception.Should().BeOfType(); } - private ReloadableResiliencePipeline CreateSut(ResiliencePipeline? initial = null, Func? factory = null) + private PipelineComponent.ReloadableComponent CreateSut(PipelineComponent? initial = null, Func? factory = null) { - factory ??= () => new TestResilienceStrategy().AsPipeline(); + factory ??= () => PipelineComponent.Null; - return new( - initial ?? new TestResilienceStrategy().AsPipeline(), + return (PipelineComponent.ReloadableComponent)PipelineComponent.CreateReloadable(initial ?? PipelineComponent.Null, () => _cancellationTokenSource.Token, factory, _telemetry); diff --git a/test/Polly.Core.Tests/Utils/StrategyHelperTests.cs b/test/Polly.Core.Tests/Utils/StrategyHelperTests.cs index 59fb929e3e7..84888b73dc0 100644 --- a/test/Polly.Core.Tests/Utils/StrategyHelperTests.cs +++ b/test/Polly.Core.Tests/Utils/StrategyHelperTests.cs @@ -4,6 +4,20 @@ namespace Polly.Core.Tests.Utils; public class StrategyHelperTests { + [Fact] + public async Task ExecuteCallbackSafeAsync_Cancelled_EnsureOperationCanceledException() + { + using var token = new CancellationTokenSource(); + token.Cancel(); + + var outcome = await StrategyHelper.ExecuteCallbackSafeAsync( + (_, _) => throw new InvalidOperationException(), + ResilienceContextPool.Shared.Get(token.Token), + "dummy"); + + outcome.Exception.Should().BeOfType(); + } + [InlineData(true)] [InlineData(false)] [Theory] diff --git a/test/Polly.Core.Tests/Utils/SynchronousExecutionHelperTests.cs b/test/Polly.Core.Tests/Utils/TaskHelperTests.cs similarity index 56% rename from test/Polly.Core.Tests/Utils/SynchronousExecutionHelperTests.cs rename to test/Polly.Core.Tests/Utils/TaskHelperTests.cs index 17444bd25da..a4ccfe5552c 100644 --- a/test/Polly.Core.Tests/Utils/SynchronousExecutionHelperTests.cs +++ b/test/Polly.Core.Tests/Utils/TaskHelperTests.cs @@ -2,17 +2,17 @@ namespace Polly.Core.Tests.Utils; -public class SynchronousExecutionHelperTests +public class TaskHelperTests { - public SynchronousExecutionHelperTests() => Trace.Listeners.Clear(); + public TaskHelperTests() => Trace.Listeners.Clear(); [Fact] public void GetResult_ValueTaskT_Ok() { - var result = SynchronousExecutionHelper.GetResult(new ValueTask(42)); + var result = TaskHelper.GetResult(new ValueTask(42)); result.Should().Be(42); - result = SynchronousExecutionHelper.GetResult(GetValue()); + result = TaskHelper.GetResult(GetValue()); result.Should().Be(42); @@ -26,9 +26,9 @@ static async ValueTask GetValue() [Fact] public void GetResult_ValueTask_Ok() { - SynchronousExecutionHelper.GetResult(default); + TaskHelper.GetResult(default); - this.Invoking(_ => SynchronousExecutionHelper.GetResult(GetValue())).Should().NotThrow(); + this.Invoking(_ => TaskHelper.GetResult(GetValue())).Should().NotThrow(); static async ValueTask GetValue() { diff --git a/test/Polly.Core.Tests/Utils/ValidationContextExtensionsTests.cs b/test/Polly.Core.Tests/Utils/ValidationHelperTests.cs similarity index 94% rename from test/Polly.Core.Tests/Utils/ValidationContextExtensionsTests.cs rename to test/Polly.Core.Tests/Utils/ValidationHelperTests.cs index 82fecffadc1..5228289db98 100644 --- a/test/Polly.Core.Tests/Utils/ValidationContextExtensionsTests.cs +++ b/test/Polly.Core.Tests/Utils/ValidationHelperTests.cs @@ -1,8 +1,9 @@ using System.ComponentModel.DataAnnotations; +using Polly.Utils; namespace Polly.Core.Tests.Utils; -public class ValidationContextExtensionsTests +public class ValidationHelperTests { [Fact] public void GetMemberName_Ok() diff --git a/test/Polly.TestUtils/ResilienceStrategyExtensions.cs b/test/Polly.TestUtils/ResilienceStrategyExtensions.cs index 8e172137be9..88df7547da2 100644 --- a/test/Polly.TestUtils/ResilienceStrategyExtensions.cs +++ b/test/Polly.TestUtils/ResilienceStrategyExtensions.cs @@ -1,14 +1,17 @@ -using Polly.Utils; - -namespace Polly.TestUtils; +namespace Polly.TestUtils; public static class ResilienceStrategyExtensions { - public static ResiliencePipeline AsPipeline(this ResilienceStrategy strategy) => new ResiliencePipelineBridge(strategy); + public static ResiliencePipeline AsPipeline(this ResilienceStrategy strategy) + => new ResiliencePipelineBuilder().AddStrategy(strategy).Build(); + + public static ResiliencePipeline AsPipeline(this ResilienceStrategy strategy) + => new ResiliencePipelineBuilder().AddStrategy(strategy).Build(); public static TBuilder AddStrategy(this TBuilder builder, ResilienceStrategy strategy) where TBuilder : ResiliencePipelineBuilderBase - { - return builder.AddPipeline(strategy.AsPipeline()); - } + => builder.AddStrategy(_ => strategy, new TestResilienceStrategyOptions()); + + public static ResiliencePipelineBuilder AddStrategy(this ResiliencePipelineBuilder builder, ResilienceStrategy strategy) + => builder.AddStrategy(_ => strategy, new TestResilienceStrategyOptions()); } diff --git a/test/Polly.TestUtils/TestResiliencePipeline.TResult.cs b/test/Polly.TestUtils/TestResiliencePipeline.TResult.cs deleted file mode 100644 index eac95244cd2..00000000000 --- a/test/Polly.TestUtils/TestResiliencePipeline.TResult.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Polly.TestUtils; - -public class TestResiliencePipeline : ResiliencePipeline -{ - public TestResiliencePipeline() - : base(new TestResilienceStrategy().AsPipeline()) - { - } -}