diff --git a/bench/BenchmarkDotNet.Artifacts/results/Polly.Core.Benchmarks.PredicateBenchmark-report-github.md b/bench/BenchmarkDotNet.Artifacts/results/Polly.Core.Benchmarks.PredicateBenchmark-report-github.md new file mode 100644 index 00000000000..6f3b3726649 --- /dev/null +++ b/bench/BenchmarkDotNet.Artifacts/results/Polly.Core.Benchmarks.PredicateBenchmark-report-github.md @@ -0,0 +1,15 @@ +``` ini + +BenchmarkDotNet=v0.13.5, OS=Windows 11 (10.0.22621.1848/22H2/2022Update/SunValley2), VM=Hyper-V +Intel Xeon Platinum 8370C CPU 2.80GHz, 1 CPU, 16 logical and 8 physical cores +.NET SDK=7.0.304 + [Host] : .NET 7.0.7 (7.0.723.27404), X64 RyuJIT AVX2 + +Job=MediumRun Toolchain=InProcessEmitToolchain IterationCount=15 +LaunchCount=2 WarmupCount=10 + +``` +| Method | Mean | Error | StdDev | Ratio | RatioSD | Allocated | Alloc Ratio | +|--------------------------- |---------:|---------:|---------:|------:|--------:|----------:|------------:| +| Predicate_SwitchExpression | 17.17 ns | 0.028 ns | 0.041 ns | 1.00 | 0.00 | - | NA | +| Predicate_PredicateBuilder | 29.64 ns | 0.859 ns | 1.232 ns | 1.73 | 0.07 | - | NA | diff --git a/bench/Polly.Core.Benchmarks/PredicateBenchmark.cs b/bench/Polly.Core.Benchmarks/PredicateBenchmark.cs new file mode 100644 index 00000000000..f4f884bd9f2 --- /dev/null +++ b/bench/Polly.Core.Benchmarks/PredicateBenchmark.cs @@ -0,0 +1,46 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; + +namespace Polly.Core.Benchmarks; + +public class PredicateBenchmark +{ + private readonly OutcomeArguments _args = new( + ResilienceContext.Get(), + Outcome.FromResult(new HttpResponseMessage(HttpStatusCode.OK)), + new RetryPredicateArguments(0)); + + private readonly RetryStrategyOptions _delegate = new() + { + ShouldHandle = args => args switch + { + { Result: { StatusCode: HttpStatusCode.InternalServerError } } => PredicateResult.True, + { Exception: HttpRequestException } => PredicateResult.True, + { Exception: IOException } => PredicateResult.True, + { Exception: InvalidOperationException } => PredicateResult.False, + _ => PredicateResult.False, + } + }; + + private readonly RetryStrategyOptions _builder = new() + { + ShouldHandle = new PredicateBuilder() + .HandleResult(r => r.StatusCode == HttpStatusCode.InternalServerError) + .Handle() + .Handle(e => false) + }; + + [Benchmark(Baseline = true)] + public ValueTask Predicate_SwitchExpression() + { + return _delegate.ShouldHandle(_args); + } + + [Benchmark] + public ValueTask Predicate_PredicateBuilder() + { + return _builder.ShouldHandle(_args); + } +} diff --git a/src/Polly.Core/Fallback/FallbackResilienceStrategyBuilderExtensions.cs b/src/Polly.Core/Fallback/FallbackResilienceStrategyBuilderExtensions.cs index 5a7327db278..89ee15dbddc 100644 --- a/src/Polly.Core/Fallback/FallbackResilienceStrategyBuilderExtensions.cs +++ b/src/Polly.Core/Fallback/FallbackResilienceStrategyBuilderExtensions.cs @@ -8,37 +8,6 @@ namespace Polly; /// public static class FallbackResilienceStrategyBuilderExtensions { - /// - /// Adds a fallback resilience strategy for a specific type to the builder. - /// - /// The result type. - /// The resilience strategy builder. - /// An action to configure the fallback predicate. - /// The fallback action to be executed. - /// The builder instance with the fallback strategy added. - /// Thrown when or or is . - public static ResilienceStrategyBuilder AddFallback( - this ResilienceStrategyBuilder builder, - Action> shouldHandle, - Func, ValueTask>> fallbackAction) - { - Guard.NotNull(builder); - Guard.NotNull(shouldHandle); - Guard.NotNull(fallbackAction); - - var options = new FallbackStrategyOptions - { - FallbackAction = fallbackAction, - }; - - var predicateBuilder = new PredicateBuilder(); - shouldHandle(predicateBuilder); - - options.ShouldHandle = predicateBuilder.CreatePredicate(); - - return builder.AddFallback(options); - } - /// /// Adds a fallback resilience strategy with the provided options to the builder. /// diff --git a/src/Polly.Core/PredicateBuilder.Operators.cs b/src/Polly.Core/PredicateBuilder.Operators.cs new file mode 100644 index 00000000000..6743dafbb73 --- /dev/null +++ b/src/Polly.Core/PredicateBuilder.Operators.cs @@ -0,0 +1,56 @@ +using System.ComponentModel; +using Polly.CircuitBreaker; +using Polly.Fallback; +using Polly.Hedging; +using Polly.Retry; + +namespace Polly; + +#pragma warning disable CA2225 // Operator overloads have named alternates + +public partial class PredicateBuilder +{ + /// + /// The operator that converts to delegate. + /// + /// The builder instance. + [EditorBrowsable(EditorBrowsableState.Never)] + public static implicit operator Func, ValueTask>(PredicateBuilder builder) + { + Guard.NotNull(builder); + return builder.Build(); + } + + /// + /// The operator that converts to delegate. + /// + /// The builder instance. + [EditorBrowsable(EditorBrowsableState.Never)] + public static implicit operator Func, ValueTask>(PredicateBuilder builder) + { + Guard.NotNull(builder); + return builder.Build(); + } + + /// + /// The operator that converts to delegate. + /// + /// The builder instance. + [EditorBrowsable(EditorBrowsableState.Never)] + public static implicit operator Func, ValueTask>(PredicateBuilder builder) + { + Guard.NotNull(builder); + return builder.Build(); + } + + /// + /// The operator that converts to delegate. + /// + /// The builder instance. + [EditorBrowsable(EditorBrowsableState.Never)] + public static implicit operator Func, ValueTask>(PredicateBuilder builder) + { + Guard.NotNull(builder); + return builder.Build(); + } +} diff --git a/src/Polly.Core/PredicateBuilder.TResult.cs b/src/Polly.Core/PredicateBuilder.TResult.cs new file mode 100644 index 00000000000..02e2750b29c --- /dev/null +++ b/src/Polly.Core/PredicateBuilder.TResult.cs @@ -0,0 +1,140 @@ +namespace Polly; + +/// +/// Defines a builder for creating predicates for and combinations. +/// +/// The type of the result. +public partial class PredicateBuilder +{ + private readonly List>> _predicates = new(); + + /// + /// Adds a predicate for handling exceptions of the specified type. + /// + /// The type of the exception to handle. + /// The same instance of the for chaining. + public PredicateBuilder Handle() + where TException : Exception + { + return Handle(static _ => true); + } + + /// + /// Adds a predicate for handling exceptions of the specified type. + /// + /// The type of the exception to handle. + /// The predicate function to use for handling the exception. + /// The same instance of the for chaining. + /// Thrown when the is . + public PredicateBuilder Handle(Func predicate) + where TException : Exception + { + Guard.NotNull(predicate); + + return Add(outcome => outcome.Exception is TException exception && predicate(exception)); + } + + /// + /// Adds a predicate for handling inner exceptions of the specified type. + /// + /// The type of the inner exception to handle. + /// The same instance of the for chaining. + public PredicateBuilder HandleInner() + where TException : Exception + { + return HandleInner(static _ => true); + } + + /// + /// Adds a predicate for handling inner exceptions of the specified type. + /// + /// The type of the inner exception to handle. + /// The predicate function to use for handling the inner exception. + /// The same instance of the for chaining. + /// Thrown when the is . + public PredicateBuilder HandleInner(Func predicate) + where TException : Exception + { + Guard.NotNull(predicate); + + return Add(outcome => outcome.Exception?.InnerException is TException innerException && predicate(innerException)); + } + + /// + /// Adds a predicate for handling results. + /// + /// The predicate function to use for handling the result. + /// The same instance of the for chaining. + public PredicateBuilder HandleResult(Func predicate) + => Add(outcome => outcome.TryGetResult(out var result) && predicate(result!)); + + /// + /// Adds a predicate for handling results with a specific value. + /// + /// The result value to handle. + /// The comparer to use for comparing results. If null, the default comparer is used. + /// The same instance of the for chaining. + public PredicateBuilder HandleResult(TResult result, IEqualityComparer? comparer = null) + { + comparer ??= EqualityComparer.Default; + + return HandleResult(r => comparer.Equals(r, result)); + } + + /// + /// Builds the predicate. + /// + /// An instance of predicate delegate. + /// Thrown when no predicates were configured using this builder. + /// + /// The returned predicate will return if any of the configured predicates return . + /// Please be aware of the performance penalty if you register too many predicates with this builder. In such case, it's better to create your own predicate + /// manually as a delegate. + /// + public Predicate> Build() => _predicates.Count switch + { + 0 => throw new InvalidOperationException("No predicates were configured. There must be at least one predicate added."), + 1 => _predicates[0], + _ => CreatePredicate(_predicates.ToArray()), + }; + + /// + /// Builds the predicate for delegates that use and return of . + /// + /// The type of arguments used by the delegate. + /// An instance of predicate delegate. + /// Thrown when no predicates were configured using this builder. + /// + /// The returned predicate will return if any of the configured predicates return . + /// Please be aware of the performance penalty if you register too many predicates with this builder. In such case, it's better to create your own predicate + /// manually as a delegate. + /// + public Func, ValueTask> Build() + { + var predicate = Build(); + + return args => new ValueTask(predicate(args.Outcome)); + } + + private static Predicate> CreatePredicate(Predicate>[] predicates) + { + return outcome => + { + foreach (var predicate in predicates) + { + if (predicate(outcome)) + { + return true; + } + } + + return false; + }; + } + + private PredicateBuilder Add(Predicate> predicate) + { + _predicates.Add(predicate); + return this; + } +} diff --git a/src/Polly.Core/PredicateBuilder.cs b/src/Polly.Core/PredicateBuilder.cs index f99470c0f60..29169e4ef88 100644 --- a/src/Polly.Core/PredicateBuilder.cs +++ b/src/Polly.Core/PredicateBuilder.cs @@ -1,121 +1,8 @@ -using System.ComponentModel.DataAnnotations; - -namespace Polly; +namespace Polly; /// -/// Defines a builder for creating predicates for and combinations. +/// Defines a builder for creating exception predicates. /// -/// The type of the result. -public sealed class PredicateBuilder +public sealed class PredicateBuilder : PredicateBuilder { - private readonly List>> _predicates = new(); - - internal PredicateBuilder() - { - } - - /// - /// Adds a predicate for handling exceptions of the specified type. - /// - /// The type of the exception to handle. - /// The same instance of the for chaining. - public PredicateBuilder Handle() - where TException : Exception - { - return Handle(static _ => true); - } - - /// - /// Adds a predicate for handling exceptions of the specified type. - /// - /// The type of the exception to handle. - /// The predicate function to use for handling the exception. - /// The same instance of the for chaining. - /// Thrown when the is . - public PredicateBuilder Handle(Func predicate) - where TException : Exception - { - Guard.NotNull(predicate); - - return Add(outcome => outcome.Exception is TException exception && predicate(exception)); - } - - /// - /// Adds a predicate for handling inner exceptions of the specified type. - /// - /// The type of the inner exception to handle. - /// The same instance of the for chaining. - public PredicateBuilder HandleInner() - where TException : Exception - { - return HandleInner(static _ => true); - } - - /// - /// Adds a predicate for handling inner exceptions of the specified type. - /// - /// The type of the inner exception to handle. - /// The predicate function to use for handling the inner exception. - /// The same instance of the for chaining. - /// Thrown when the is . - public PredicateBuilder HandleInner(Func predicate) - where TException : Exception - { - Guard.NotNull(predicate); - - return Add(outcome => outcome.Exception?.InnerException is TException innerException && predicate(innerException)); - } - - /// - /// Adds a predicate for handling results. - /// - /// The predicate function to use for handling the result. - /// The same instance of the for chaining. - public PredicateBuilder HandleResult(Func predicate) - => Add(outcome => outcome.TryGetResult(out var result) && predicate(result!)); - - /// - /// Adds a predicate for handling results with a specific value. - /// - /// The result value to handle. - /// The comparer to use for comparing results. If null, the default comparer is used. - /// The same instance of the for chaining. - public PredicateBuilder HandleResult(TResult result, IEqualityComparer? comparer = null) - { - comparer ??= EqualityComparer.Default; - - return HandleResult(r => comparer.Equals(r, result)); - } - - internal Func, ValueTask> CreatePredicate() => _predicates.Count switch - { - 0 => throw new ValidationException("No predicates were configured. There must be at least one predicate added."), - 1 => CreatePredicate(_predicates[0]), - _ => CreatePredicate(_predicates.ToArray()), - }; - - private static Func, ValueTask> CreatePredicate(Predicate> predicate) - => args => new ValueTask(predicate(args.Outcome)); - - private static Func, ValueTask> CreatePredicate(Predicate>[] predicates) - { - return args => - { - foreach (var predicate in predicates) - { - if (predicate(args.Outcome)) - { - return new ValueTask(true); - } - } - - return new ValueTask(false); - }; - } - - private PredicateBuilder Add(Predicate> predicate) - { - _predicates.Add(predicate); - return this; - } } diff --git a/src/Polly.Core/Retry/RetryResilienceStrategyBuilderExtensions.cs b/src/Polly.Core/Retry/RetryResilienceStrategyBuilderExtensions.cs index 7902e775ca5..f8a3590fc15 100644 --- a/src/Polly.Core/Retry/RetryResilienceStrategyBuilderExtensions.cs +++ b/src/Polly.Core/Retry/RetryResilienceStrategyBuilderExtensions.cs @@ -8,40 +8,6 @@ namespace Polly; /// public static class RetryResilienceStrategyBuilderExtensions { - /// - /// Adds a retry strategy to the builder. - /// - /// The type of result the retry strategy handles. - /// The builder instance. - /// A predicate that defines the retry conditions. - /// The backoff type to use for the retry strategy. - /// The number of retries to attempt before giving up. - /// The base delay between retries. - /// The builder instance with the retry strategy added. - /// Thrown when or is . - /// Thrown when the options constructed from the arguments are invalid. - public static ResilienceStrategyBuilder AddRetry( - this ResilienceStrategyBuilder builder, - Action> shouldRetry, - RetryBackoffType backoffType, - int retryCount, - TimeSpan baseDelay) - { - Guard.NotNull(builder); - Guard.NotNull(shouldRetry); - - var options = new RetryStrategyOptions - { - BackoffType = backoffType, - RetryCount = retryCount, - BaseDelay = baseDelay - }; - - ConfigureShouldRetry(shouldRetry, options); - - return builder.AddRetry(options); - } - /// /// Adds a retry strategy to the builder. /// @@ -87,11 +53,4 @@ private static TBuilder AddRetryCore(this TBuilder builder, R RandomUtil.Instance), options); } - - private static void ConfigureShouldRetry(Action> shouldRetry, RetryStrategyOptions options) - { - var predicate = new PredicateBuilder(); - shouldRetry(predicate); - options.ShouldHandle = predicate.CreatePredicate(); - } } diff --git a/test/Polly.Core.Tests/Fallback/FallbackResilienceStrategyBuilderExtensionsTests.cs b/test/Polly.Core.Tests/Fallback/FallbackResilienceStrategyBuilderExtensionsTests.cs index fcabf63d220..d78615785f4 100644 --- a/test/Polly.Core.Tests/Fallback/FallbackResilienceStrategyBuilderExtensionsTests.cs +++ b/test/Polly.Core.Tests/Fallback/FallbackResilienceStrategyBuilderExtensionsTests.cs @@ -14,11 +14,7 @@ public class FallbackResilienceStrategyBuilderExtensionsTests FallbackAction = _ => Outcome.FromResultAsTask(0), ShouldHandle = _ => PredicateResult.False, }); - }, - builder => - { - builder.AddFallback(handle => handle.HandleResult(1), _ => Outcome.FromResultAsTask(0)); - }, + } }; [MemberData(nameof(FallbackOverloadsGeneric))] diff --git a/test/Polly.Core.Tests/PredicateBuilderTests.cs b/test/Polly.Core.Tests/PredicateBuilderTests.cs index 26d81010ba1..afb573dd1a0 100644 --- a/test/Polly.Core.Tests/PredicateBuilderTests.cs +++ b/test/Polly.Core.Tests/PredicateBuilderTests.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +using Polly.CircuitBreaker; +using Polly.Fallback; +using Polly.Hedging; +using Polly.Retry; namespace Polly.Core.Tests; @@ -26,15 +29,22 @@ public class PredicateBuilderTests { builder => builder.HandleInner(e => e.Message == "x"), Outcome.FromException(new InvalidOperationException("dummy", new FormatException("m") )), false }, }; + [Fact] + public void Ctor_Ok() + { + new PredicateBuilder().Should().NotBeNull(); + new PredicateBuilder().Should().NotBeNull(); + } + [MemberData(nameof(HandleResultData))] [Theory] - public async Task HandleResult_Ok(Action> configure, Outcome value, bool handled) + public void HandleResult_Ok(Action> configure, Outcome value, bool handled) { var predicate = new PredicateBuilder(); configure(predicate); - var result = await predicate.CreatePredicate()(new OutcomeArguments(ResilienceContext.Get(), value, string.Empty)); + var result = predicate.Build()(value); result.Should().Be(handled); } @@ -42,9 +52,61 @@ public async Task HandleResult_Ok(Action> configure, Ou public void CreatePredicate_NotConfigured_Throws() { var predicate = new PredicateBuilder() - .Invoking(b => b.CreatePredicate()) + .Invoking(b => b.Build()) .Should() - .Throw() + .Throw() .WithMessage("No predicates were configured. There must be at least one predicate added."); } + + [Fact] + public async Task Operator_RetryStrategyOptions_Ok() + { + var options = new RetryStrategyOptions + { + ShouldHandle = new PredicateBuilder().HandleResult("error") + }; + + var handled = await options.ShouldHandle(new(ResilienceContext.Get(), Outcome.FromResult("error"), new RetryPredicateArguments(0))); + + handled.Should().BeTrue(); + } + + [Fact] + public async Task Operator_FallbackStrategyOptions_Ok() + { + var options = new FallbackStrategyOptions + { + ShouldHandle = new PredicateBuilder().HandleResult("error") + }; + + var handled = await options.ShouldHandle(new(ResilienceContext.Get(), Outcome.FromResult("error"), new FallbackPredicateArguments())); + + handled.Should().BeTrue(); + } + + [Fact] + public async Task Operator_HedgingStrategyOptions_Ok() + { + var options = new HedgingStrategyOptions + { + ShouldHandle = new PredicateBuilder().HandleResult("error") + }; + + var handled = await options.ShouldHandle(new(ResilienceContext.Get(), Outcome.FromResult("error"), new HedgingPredicateArguments())); + + handled.Should().BeTrue(); + } + + [Fact] + public async Task Operator_AdvancedCircuitBreakerStrategyOptions_Ok() + { + var options = new AdvancedCircuitBreakerStrategyOptions + { + ShouldHandle = new PredicateBuilder().HandleResult("error") + }; + + var handled = await options.ShouldHandle(new(ResilienceContext.Get(), Outcome.FromResult("error"), new CircuitBreakerPredicateArguments())); + + handled.Should().BeTrue(); + } } diff --git a/test/Polly.Core.Tests/Retry/RetryResilienceStrategyBuilderExtensionsTests.cs b/test/Polly.Core.Tests/Retry/RetryResilienceStrategyBuilderExtensionsTests.cs index 221324e0fee..7750a3c33ac 100644 --- a/test/Polly.Core.Tests/Retry/RetryResilienceStrategyBuilderExtensionsTests.cs +++ b/test/Polly.Core.Tests/Retry/RetryResilienceStrategyBuilderExtensionsTests.cs @@ -25,11 +25,6 @@ public class RetryResilienceStrategyBuilderExtensionsTests public static readonly TheoryData>> OverloadsDataGeneric = new() { - builder => - { - builder.AddRetry(retry => retry.HandleResult(10), RetryBackoffType.Linear, 2, TimeSpan.FromSeconds(1)); - AssertStrategy(builder, RetryBackoffType.Linear, 2, TimeSpan.FromSeconds(1)); - }, builder => { builder.AddRetry(new RetryStrategyOptions