Skip to content

Commit

Permalink
Allow implicit conversion of PredicateBuilder to delegates (#1332)
Browse files Browse the repository at this point in the history
  • Loading branch information
martintmk authored Jun 21, 2023
1 parent 2890e5a commit 12b66c2
Show file tree
Hide file tree
Showing 10 changed files with 328 additions and 203 deletions.
Original file line number Diff line number Diff line change
@@ -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 |
46 changes: 46 additions & 0 deletions bench/Polly.Core.Benchmarks/PredicateBenchmark.cs
Original file line number Diff line number Diff line change
@@ -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<HttpResponseMessage, RetryPredicateArguments> _args = new(
ResilienceContext.Get(),
Outcome.FromResult(new HttpResponseMessage(HttpStatusCode.OK)),
new RetryPredicateArguments(0));

private readonly RetryStrategyOptions<HttpResponseMessage> _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<HttpResponseMessage> _builder = new()
{
ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
.HandleResult(r => r.StatusCode == HttpStatusCode.InternalServerError)
.Handle<HttpRequestException>()
.Handle<InvalidOperationException>(e => false)
};

[Benchmark(Baseline = true)]
public ValueTask<bool> Predicate_SwitchExpression()
{
return _delegate.ShouldHandle(_args);
}

[Benchmark]
public ValueTask<bool> Predicate_PredicateBuilder()
{
return _builder.ShouldHandle(_args);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,37 +8,6 @@ namespace Polly;
/// </summary>
public static class FallbackResilienceStrategyBuilderExtensions
{
/// <summary>
/// Adds a fallback resilience strategy for a specific <typeparamref name="TResult"/> type to the builder.
/// </summary>
/// <typeparam name="TResult">The result type.</typeparam>
/// <param name="builder">The resilience strategy builder.</param>
/// <param name="shouldHandle">An action to configure the fallback predicate.</param>
/// <param name="fallbackAction">The fallback action to be executed.</param>
/// <returns>The builder instance with the fallback strategy added.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/> or <paramref name="shouldHandle"/> or <paramref name="fallbackAction"/> is <see langword="null"/>.</exception>
public static ResilienceStrategyBuilder<TResult> AddFallback<TResult>(
this ResilienceStrategyBuilder<TResult> builder,
Action<PredicateBuilder<TResult>> shouldHandle,
Func<OutcomeArguments<TResult, FallbackPredicateArguments>, ValueTask<Outcome<TResult>>> fallbackAction)
{
Guard.NotNull(builder);
Guard.NotNull(shouldHandle);
Guard.NotNull(fallbackAction);

var options = new FallbackStrategyOptions<TResult>
{
FallbackAction = fallbackAction,
};

var predicateBuilder = new PredicateBuilder<TResult>();
shouldHandle(predicateBuilder);

options.ShouldHandle = predicateBuilder.CreatePredicate<FallbackPredicateArguments>();

return builder.AddFallback(options);
}

/// <summary>
/// Adds a fallback resilience strategy with the provided options to the builder.
/// </summary>
Expand Down
56 changes: 56 additions & 0 deletions src/Polly.Core/PredicateBuilder.Operators.cs
Original file line number Diff line number Diff line change
@@ -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<TResult>
{
/// <summary>
/// The operator that converts <paramref name="builder"/> to <see cref="RetryStrategyOptions{TResult}.ShouldHandle"/> delegate.
/// </summary>
/// <param name="builder">The builder instance.</param>
[EditorBrowsable(EditorBrowsableState.Never)]
public static implicit operator Func<OutcomeArguments<TResult, RetryPredicateArguments>, ValueTask<bool>>(PredicateBuilder<TResult> builder)
{
Guard.NotNull(builder);
return builder.Build<RetryPredicateArguments>();
}

/// <summary>
/// The operator that converts <paramref name="builder"/> to <see cref="RetryStrategyOptions{TResult}.ShouldHandle"/> delegate.
/// </summary>
/// <param name="builder">The builder instance.</param>
[EditorBrowsable(EditorBrowsableState.Never)]
public static implicit operator Func<OutcomeArguments<TResult, HedgingPredicateArguments>, ValueTask<bool>>(PredicateBuilder<TResult> builder)
{
Guard.NotNull(builder);
return builder.Build<HedgingPredicateArguments>();
}

/// <summary>
/// The operator that converts <paramref name="builder"/> to <see cref="RetryStrategyOptions{TResult}.ShouldHandle"/> delegate.
/// </summary>
/// <param name="builder">The builder instance.</param>
[EditorBrowsable(EditorBrowsableState.Never)]
public static implicit operator Func<OutcomeArguments<TResult, FallbackPredicateArguments>, ValueTask<bool>>(PredicateBuilder<TResult> builder)
{
Guard.NotNull(builder);
return builder.Build<FallbackPredicateArguments>();
}

/// <summary>
/// The operator that converts <paramref name="builder"/> to <see cref="RetryStrategyOptions{TResult}.ShouldHandle"/> delegate.
/// </summary>
/// <param name="builder">The builder instance.</param>
[EditorBrowsable(EditorBrowsableState.Never)]
public static implicit operator Func<OutcomeArguments<TResult, CircuitBreakerPredicateArguments>, ValueTask<bool>>(PredicateBuilder<TResult> builder)
{
Guard.NotNull(builder);
return builder.Build<CircuitBreakerPredicateArguments>();
}
}
140 changes: 140 additions & 0 deletions src/Polly.Core/PredicateBuilder.TResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
namespace Polly;

/// <summary>
/// Defines a builder for creating predicates for <typeparamref name="TResult"/> and <see cref="Exception"/> combinations.
/// </summary>
/// <typeparam name="TResult">The type of the result.</typeparam>
public partial class PredicateBuilder<TResult>
{
private readonly List<Predicate<Outcome<TResult>>> _predicates = new();

/// <summary>
/// Adds a predicate for handling exceptions of the specified type.
/// </summary>
/// <typeparam name="TException">The type of the exception to handle.</typeparam>
/// <returns>The same instance of the <see cref="PredicateBuilder{TResult}"/> for chaining.</returns>
public PredicateBuilder<TResult> Handle<TException>()
where TException : Exception
{
return Handle<TException>(static _ => true);
}

/// <summary>
/// Adds a predicate for handling exceptions of the specified type.
/// </summary>
/// <typeparam name="TException">The type of the exception to handle.</typeparam>
/// <param name="predicate">The predicate function to use for handling the exception.</param>
/// <returns>The same instance of the <see cref="PredicateBuilder{TResult}"/> for chaining.</returns>
/// <exception cref="ArgumentNullException">Thrown when the <paramref name="predicate"/> is <see langword="null"/>.</exception>
public PredicateBuilder<TResult> Handle<TException>(Func<TException, bool> predicate)
where TException : Exception
{
Guard.NotNull(predicate);

return Add(outcome => outcome.Exception is TException exception && predicate(exception));
}

/// <summary>
/// Adds a predicate for handling inner exceptions of the specified type.
/// </summary>
/// <typeparam name="TException">The type of the inner exception to handle.</typeparam>
/// <returns>The same instance of the <see cref="PredicateBuilder{TResult}"/> for chaining.</returns>
public PredicateBuilder<TResult> HandleInner<TException>()
where TException : Exception
{
return HandleInner<TException>(static _ => true);
}

/// <summary>
/// Adds a predicate for handling inner exceptions of the specified type.
/// </summary>
/// <typeparam name="TException">The type of the inner exception to handle.</typeparam>
/// <param name="predicate">The predicate function to use for handling the inner exception.</param>
/// <returns>The same instance of the <see cref="PredicateBuilder{TResult}"/> for chaining.</returns>
/// <exception cref="ArgumentNullException">Thrown when the <paramref name="predicate"/> is <see langword="null"/>.</exception>
public PredicateBuilder<TResult> HandleInner<TException>(Func<TException, bool> predicate)
where TException : Exception
{
Guard.NotNull(predicate);

return Add(outcome => outcome.Exception?.InnerException is TException innerException && predicate(innerException));
}

/// <summary>
/// Adds a predicate for handling results.
/// </summary>
/// <param name="predicate">The predicate function to use for handling the result.</param>
/// <returns>The same instance of the <see cref="PredicateBuilder{TResult}"/> for chaining.</returns>
public PredicateBuilder<TResult> HandleResult(Func<TResult, bool> predicate)
=> Add(outcome => outcome.TryGetResult(out var result) && predicate(result!));

/// <summary>
/// Adds a predicate for handling results with a specific value.
/// </summary>
/// <param name="result">The result value to handle.</param>
/// <param name="comparer">The comparer to use for comparing results. If null, the default comparer is used.</param>
/// <returns>The same instance of the <see cref="PredicateBuilder{TResult}"/> for chaining.</returns>
public PredicateBuilder<TResult> HandleResult(TResult result, IEqualityComparer<TResult>? comparer = null)
{
comparer ??= EqualityComparer<TResult>.Default;

return HandleResult(r => comparer.Equals(r, result));
}

/// <summary>
/// Builds the predicate.
/// </summary>
/// <returns>An instance of predicate delegate.</returns>
/// <exception cref="InvalidOperationException">Thrown when no predicates were configured using this builder.</exception>
/// <remarks>
/// The returned predicate will return <see langword="true"/> if any of the configured predicates return <see langword="true"/>.
/// 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.
/// </remarks>
public Predicate<Outcome<TResult>> 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()),
};

/// <summary>
/// Builds the predicate for delegates that use <see cref="OutcomeArguments{TResult, TArgs}"/> and return <see cref="ValueTask{TResult}"/> of <see cref="bool"/>.
/// </summary>
/// <typeparam name="TArgs">The type of arguments used by the delegate.</typeparam>
/// <returns>An instance of predicate delegate.</returns>
/// <exception cref="InvalidOperationException">Thrown when no predicates were configured using this builder.</exception>
/// <remarks>
/// The returned predicate will return <see langword="true"/> if any of the configured predicates return <see langword="true"/>.
/// 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.
/// </remarks>
public Func<OutcomeArguments<TResult, TArgs>, ValueTask<bool>> Build<TArgs>()
{
var predicate = Build();

return args => new ValueTask<bool>(predicate(args.Outcome));
}

private static Predicate<Outcome<TResult>> CreatePredicate(Predicate<Outcome<TResult>>[] predicates)
{
return outcome =>
{
foreach (var predicate in predicates)
{
if (predicate(outcome))
{
return true;
}
}
return false;
};
}

private PredicateBuilder<TResult> Add(Predicate<Outcome<TResult>> predicate)
{
_predicates.Add(predicate);
return this;
}
}
Loading

0 comments on commit 12b66c2

Please sign in to comment.