Skip to content

Commit

Permalink
Handle nested inner exceptions (#2166)
Browse files Browse the repository at this point in the history
- Replicate behaviour of v7 policies and recursively check all inner exceptions and `AggregateException` to handle inner exceptions.
- Refactor predicate builder test cases to make a bit more readable.
- Use collection expression.
  • Loading branch information
martincostello authored Jun 28, 2024
1 parent 3a78be9 commit 2b44e7a
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 37 deletions.
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);
}

0 comments on commit 2b44e7a

Please sign in to comment.