Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 53 additions & 15 deletions src/Polly.Core/PredicateBuilder.TResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ public PredicateBuilder<TResult> Handle<TException>(Func<TException, bool> predi
/// </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>
/// <remarks>
/// This method will also handle any exception found for <see cref="Exception.InnerException"/> of
/// an <see cref="Exception"/>, or at any level of nesting within an <see cref="AggregateException"/>.
/// </remarks>
public PredicateBuilder<TResult> HandleInner<TException>()
where TException : Exception => HandleInner<TException>(static _ => true);

Expand All @@ -46,12 +50,46 @@ public PredicateBuilder<TResult> HandleInner<TException>()
/// <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>
/// <remarks>
/// This method will also handle any exception found for <see cref="Exception.InnerException"/> of
/// an <see cref="Exception"/>, or at any level of nesting within an <see cref="AggregateException"/>.
/// </remarks>
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));
return Add(outcome => HandleInner(outcome.Exception, predicate));

static bool HandleInner(Exception? exception, Func<TException, bool> predicate)
{
if (exception is AggregateException aggregate)
{
foreach (var innerException in aggregate.Flatten().InnerExceptions)
{
if (HandleNested(predicate, innerException))
{
return true;
}
}
}

return HandleNested(predicate, exception);

static bool HandleNested(Func<TException, bool> predicate, Exception? current)
{
if (current is null)
{
return false;
}
else if (current is TException exceptionOfT)
{
return predicate(exceptionOfT);
}

return HandleNested(predicate, current.InnerException);
}
}
}

/// <summary>
Expand Down Expand Up @@ -89,7 +127,7 @@ public PredicateBuilder<TResult> HandleResult(TResult result, IEqualityComparer<
{
0 => throw new InvalidOperationException("No predicates were configured. There must be at least one predicate added."),
1 => _predicates[0],
_ => CreatePredicate(_predicates.ToArray()),
_ => CreatePredicate([.. _predicates]),
};

internal Func<TArgs, ValueTask<bool>> Build<TArgs>()
Expand All @@ -100,19 +138,19 @@ internal Func<TArgs, ValueTask<bool>> Build<TArgs>()
return args => new ValueTask<bool>(predicate(args.Outcome));
}

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

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

return false;
};

private PredicateBuilder<TResult> Add(Predicate<Outcome<TResult>> predicate)
{
Expand Down
73 changes: 51 additions & 22 deletions test/Polly.Core.Tests/PredicateBuilderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,47 @@ public class PredicateBuilderTests
{
public static TheoryData<Action<PredicateBuilder<string>>, Outcome<string>, bool> HandleResultData = new()
{
{ builder => builder.HandleResult("val"), Outcome.FromResult("val"), true },
{ builder => builder.HandleResult("val"), Outcome.FromResult("val2"), false },
{ builder => builder.HandleResult("val"), Outcome.FromException<string>(new InvalidOperationException()), false },
{ builder => builder.HandleResult("val", StringComparer.OrdinalIgnoreCase) ,Outcome.FromResult("VAL"), true },
{ builder => builder.HandleResult(r => r == "val"), Outcome.FromResult("val"), true },
{ builder => builder.HandleResult(r => r == "val2"), Outcome.FromResult("val"), false },
{ builder => builder.Handle<InvalidOperationException>(), Outcome.FromException<string>(new InvalidOperationException()), true },
{ builder => builder.Handle<InvalidOperationException>(), Outcome.FromException<string>(new FormatException()), false },
{ builder => builder.Handle<InvalidOperationException>(e => false), Outcome.FromException<string>(new InvalidOperationException()), false },
{ builder => builder.HandleInner<InvalidOperationException>(e => false), Outcome.FromException<string>(new InvalidOperationException()), false },
{ builder => builder.HandleInner<InvalidOperationException>(), Outcome.FromResult("value"), false },
{ builder => builder.Handle<InvalidOperationException>(), Outcome.FromResult("value"), false },
{ builder => builder.Handle<InvalidOperationException>().HandleResult("value"), Outcome.FromResult("value"), true },
{ builder => builder.Handle<InvalidOperationException>().HandleResult("value"), Outcome.FromResult("value2"), false },
{ builder => builder.HandleInner<FormatException>(), Outcome.FromException<string>(new InvalidOperationException("dummy", new FormatException() )), true },
{ builder => builder.HandleInner<ArgumentNullException>(e => false), Outcome.FromException<string>(new InvalidOperationException("dummy", new FormatException() )), false },
{ builder => builder.HandleInner<FormatException>(e => e.Message == "m"), Outcome.FromException<string>(new InvalidOperationException("dummy", new FormatException("m") )), true },
{ builder => builder.HandleInner<FormatException>(e => e.Message == "x"), Outcome.FromException<string>(new InvalidOperationException("dummy", new FormatException("m") )), false },
{ builder => builder.HandleResult("val"), CreateOutcome("val"), true },
{ builder => builder.HandleResult("val"), CreateOutcome("val2"), false },
{ builder => builder.HandleResult("val"), CreateOutcome(new InvalidOperationException()), false },
{ builder => builder.HandleResult("val", StringComparer.OrdinalIgnoreCase), CreateOutcome("VAL"), true },
{ builder => builder.HandleResult(r => r == "val"), CreateOutcome("val"), true },
{ builder => builder.HandleResult(r => r == "val2"), CreateOutcome("val"), false },
{ builder => builder.Handle<InvalidOperationException>(), CreateOutcome(new InvalidOperationException()), true },
{ builder => builder.Handle<InvalidOperationException>(), CreateOutcome(new FormatException()), false },
{ builder => builder.Handle<InvalidOperationException>(e => false), CreateOutcome(new InvalidOperationException()), false },
{ builder => builder.HandleInner<InvalidOperationException>(e => false), CreateOutcome(new InvalidOperationException()), false },
{ builder => builder.HandleInner<InvalidOperationException>(), CreateOutcome("value"), false },
{ builder => builder.Handle<InvalidOperationException>(), CreateOutcome("value"), false },
{ builder => builder.Handle<InvalidOperationException>().HandleResult("value"), CreateOutcome("value"), true },
{ builder => builder.Handle<InvalidOperationException>().HandleResult("value"), CreateOutcome("value2"), false },
{ builder => builder.HandleInner<FormatException>(), CreateOutcome(new InvalidOperationException("dummy", new FormatException() )), true },
{ builder => builder.HandleInner<ArgumentNullException>(e => false), CreateOutcome(new InvalidOperationException("dummy", new FormatException() )), false },
{ builder => builder.HandleInner<FormatException>(e => e.Message == "m"), CreateOutcome(new InvalidOperationException("dummy", new FormatException("m") )), true },
{ builder => builder.HandleInner<FormatException>(e => e.Message == "x"), CreateOutcome(new InvalidOperationException("dummy", new FormatException("m") )), false },
#pragma warning disable CA2201
//// See https://github.com/App-vNext/Polly/issues/2161
{ builder => builder.HandleInner<InvalidOperationException>(), CreateOutcome(new InvalidOperationException("1")), true },
{ builder => builder.HandleInner<InvalidOperationException>(), CreateOutcome(new Exception("1", new InvalidOperationException("2"))), true },
{ builder => builder.HandleInner<InvalidOperationException>(), CreateOutcome(new FormatException("1", new InvalidOperationException("2"))), true },
{ builder => builder.HandleInner<InvalidOperationException>(), CreateOutcome(new Exception("1", new Exception("2", new InvalidOperationException("3")))), true },
{ builder => builder.HandleInner<InvalidOperationException>(), CreateOutcome(new AggregateException("1", new Exception("2a"), new InvalidOperationException("2b"))), true },
{ builder => builder.HandleInner<InvalidOperationException>(), CreateOutcome(new AggregateException("1", new Exception("2", new InvalidOperationException("3")))), true },
{ builder => builder.HandleInner<InvalidOperationException>(), CreateOutcome(new AggregateException("1", new FormatException("2", new NotSupportedException("3")))), false },
{ builder => builder.HandleInner<InvalidOperationException>(), CreateOutcome(new AggregateException("1")), false },
{ builder => builder.HandleInner<InvalidOperationException>(ex => ex.Message is "3"), CreateOutcome(new AggregateException("1", new FormatException("2", new NotSupportedException("3")))), false },
{ builder => builder.HandleInner<InvalidOperationException>(ex => ex.Message is "unreachable"), CreateOutcome(new AggregateException("1", new FormatException("2", new NotSupportedException("3")))), false },
{ builder => builder.HandleInner<InvalidOperationException>(ex => ex.Message is "1"), CreateOutcome(new InvalidOperationException("1")), true },
{ builder => builder.HandleInner<InvalidOperationException>(ex => ex.Message is "2"), CreateOutcome(new Exception("1", new InvalidOperationException("2"))), true },
{ builder => builder.HandleInner<InvalidOperationException>(ex => ex.Message is "3"), CreateOutcome(new Exception("1", new Exception("2", new InvalidOperationException("3")))), true },
{ builder => builder.HandleInner<InvalidOperationException>(ex => ex.Message is "2b"), CreateOutcome(new AggregateException("1", new Exception("2a"), new InvalidOperationException("2b"))), true },
{ builder => builder.HandleInner<InvalidOperationException>(ex => ex.Message is "3"), CreateOutcome(new AggregateException("1", new Exception("2", new InvalidOperationException("3")))), true },
{ builder => builder.HandleInner<InvalidOperationException>(ex => ex.Message is "unreachable"), CreateOutcome(new InvalidOperationException("1")), false },
{ builder => builder.HandleInner<InvalidOperationException>(ex => ex.Message is "unreachable"), CreateOutcome(new Exception("1", new InvalidOperationException("2"))), false },
{ builder => builder.HandleInner<InvalidOperationException>(ex => ex.Message is "unreachable"), CreateOutcome(new Exception("1", new Exception("2", new InvalidOperationException("3")))), false },
{ builder => builder.HandleInner<InvalidOperationException>(ex => ex.Message is "unreachable"), CreateOutcome(new AggregateException("1", new Exception("2a"), new InvalidOperationException("2b"))), false },
{ builder => builder.HandleInner<InvalidOperationException>(ex => ex.Message is "unreachable"), CreateOutcome(new AggregateException("1", new Exception("2", new InvalidOperationException("3")))), false },
#pragma warning restore CA2201
};

[Fact]
Expand Down Expand Up @@ -66,7 +89,7 @@ public async Task Operator_RetryStrategyOptions_Ok()
ShouldHandle = new PredicateBuilder<string>().HandleResult("error")
};

var handled = await options.ShouldHandle(new RetryPredicateArguments<string>(ResilienceContextPool.Shared.Get(), Outcome.FromResult("error"), 0));
var handled = await options.ShouldHandle(new RetryPredicateArguments<string>(ResilienceContextPool.Shared.Get(), CreateOutcome("error"), 0));

handled.Should().BeTrue();
}
Expand All @@ -79,7 +102,7 @@ public async Task Operator_FallbackStrategyOptions_Ok()
ShouldHandle = new PredicateBuilder<string>().HandleResult("error")
};

var handled = await options.ShouldHandle(new(ResilienceContextPool.Shared.Get(), Outcome.FromResult("error")));
var handled = await options.ShouldHandle(new(ResilienceContextPool.Shared.Get(), CreateOutcome("error")));

handled.Should().BeTrue();
}
Expand All @@ -92,7 +115,7 @@ public async Task Operator_HedgingStrategyOptions_Ok()
ShouldHandle = new PredicateBuilder<string>().HandleResult("error")
};

var handled = await options.ShouldHandle(new(ResilienceContextPool.Shared.Get(), Outcome.FromResult("error")));
var handled = await options.ShouldHandle(new(ResilienceContextPool.Shared.Get(), CreateOutcome("error")));

handled.Should().BeTrue();
}
Expand All @@ -105,8 +128,14 @@ public async Task Operator_AdvancedCircuitBreakerStrategyOptions_Ok()
ShouldHandle = new PredicateBuilder<string>().HandleResult("error")
};

var handled = await options.ShouldHandle(new(ResilienceContextPool.Shared.Get(), Outcome.FromResult("error")));
var handled = await options.ShouldHandle(new(ResilienceContextPool.Shared.Get(), CreateOutcome("error")));

handled.Should().BeTrue();
}

private static Outcome<string> CreateOutcome(Exception exception)
=> Outcome.FromException<string>(exception);

private static Outcome<string> CreateOutcome(string result)
=> Outcome.FromResult(result);
}