Skip to content

Commit 2b44e7a

Browse files
Handle nested inner exceptions (#2166)
- 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.
1 parent 3a78be9 commit 2b44e7a

File tree

2 files changed

+104
-37
lines changed

2 files changed

+104
-37
lines changed

src/Polly.Core/PredicateBuilder.TResult.cs

+53-15
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ public PredicateBuilder<TResult> Handle<TException>(Func<TException, bool> predi
3636
/// </summary>
3737
/// <typeparam name="TException">The type of the inner exception to handle.</typeparam>
3838
/// <returns>The same instance of the <see cref="PredicateBuilder{TResult}"/> for chaining.</returns>
39+
/// <remarks>
40+
/// This method will also handle any exception found for <see cref="Exception.InnerException"/> of
41+
/// an <see cref="Exception"/>, or at any level of nesting within an <see cref="AggregateException"/>.
42+
/// </remarks>
3943
public PredicateBuilder<TResult> HandleInner<TException>()
4044
where TException : Exception => HandleInner<TException>(static _ => true);
4145

@@ -46,12 +50,46 @@ public PredicateBuilder<TResult> HandleInner<TException>()
4650
/// <param name="predicate">The predicate function to use for handling the inner exception.</param>
4751
/// <returns>The same instance of the <see cref="PredicateBuilder{TResult}"/> for chaining.</returns>
4852
/// <exception cref="ArgumentNullException">Thrown when the <paramref name="predicate"/> is <see langword="null"/>.</exception>
53+
/// <remarks>
54+
/// This method will also handle any exception found for <see cref="Exception.InnerException"/> of
55+
/// an <see cref="Exception"/>, or at any level of nesting within an <see cref="AggregateException"/>.
56+
/// </remarks>
4957
public PredicateBuilder<TResult> HandleInner<TException>(Func<TException, bool> predicate)
5058
where TException : Exception
5159
{
5260
Guard.NotNull(predicate);
5361

54-
return Add(outcome => outcome.Exception?.InnerException is TException innerException && predicate(innerException));
62+
return Add(outcome => HandleInner(outcome.Exception, predicate));
63+
64+
static bool HandleInner(Exception? exception, Func<TException, bool> predicate)
65+
{
66+
if (exception is AggregateException aggregate)
67+
{
68+
foreach (var innerException in aggregate.Flatten().InnerExceptions)
69+
{
70+
if (HandleNested(predicate, innerException))
71+
{
72+
return true;
73+
}
74+
}
75+
}
76+
77+
return HandleNested(predicate, exception);
78+
79+
static bool HandleNested(Func<TException, bool> predicate, Exception? current)
80+
{
81+
if (current is null)
82+
{
83+
return false;
84+
}
85+
else if (current is TException exceptionOfT)
86+
{
87+
return predicate(exceptionOfT);
88+
}
89+
90+
return HandleNested(predicate, current.InnerException);
91+
}
92+
}
5593
}
5694

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

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

103-
private static Predicate<Outcome<TResult>> CreatePredicate(Predicate<Outcome<TResult>>[] predicates)
104-
=> outcome =>
105-
{
106-
foreach (var predicate in predicates)
107-
{
108-
if (predicate(outcome))
109-
{
110-
return true;
111-
}
112-
}
113-
114-
return false;
115-
};
141+
private static Predicate<Outcome<TResult>> CreatePredicate(Predicate<Outcome<TResult>>[] predicates) =>
142+
outcome =>
143+
{
144+
foreach (var predicate in predicates)
145+
{
146+
if (predicate(outcome))
147+
{
148+
return true;
149+
}
150+
}
151+
152+
return false;
153+
};
116154

117155
private PredicateBuilder<TResult> Add(Predicate<Outcome<TResult>> predicate)
118156
{

test/Polly.Core.Tests/PredicateBuilderTests.cs

+51-22
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,47 @@ public class PredicateBuilderTests
99
{
1010
public static TheoryData<Action<PredicateBuilder<string>>, Outcome<string>, bool> HandleResultData = new()
1111
{
12-
{ builder => builder.HandleResult("val"), Outcome.FromResult("val"), true },
13-
{ builder => builder.HandleResult("val"), Outcome.FromResult("val2"), false },
14-
{ builder => builder.HandleResult("val"), Outcome.FromException<string>(new InvalidOperationException()), false },
15-
{ builder => builder.HandleResult("val", StringComparer.OrdinalIgnoreCase) ,Outcome.FromResult("VAL"), true },
16-
{ builder => builder.HandleResult(r => r == "val"), Outcome.FromResult("val"), true },
17-
{ builder => builder.HandleResult(r => r == "val2"), Outcome.FromResult("val"), false },
18-
{ builder => builder.Handle<InvalidOperationException>(), Outcome.FromException<string>(new InvalidOperationException()), true },
19-
{ builder => builder.Handle<InvalidOperationException>(), Outcome.FromException<string>(new FormatException()), false },
20-
{ builder => builder.Handle<InvalidOperationException>(e => false), Outcome.FromException<string>(new InvalidOperationException()), false },
21-
{ builder => builder.HandleInner<InvalidOperationException>(e => false), Outcome.FromException<string>(new InvalidOperationException()), false },
22-
{ builder => builder.HandleInner<InvalidOperationException>(), Outcome.FromResult("value"), false },
23-
{ builder => builder.Handle<InvalidOperationException>(), Outcome.FromResult("value"), false },
24-
{ builder => builder.Handle<InvalidOperationException>().HandleResult("value"), Outcome.FromResult("value"), true },
25-
{ builder => builder.Handle<InvalidOperationException>().HandleResult("value"), Outcome.FromResult("value2"), false },
26-
{ builder => builder.HandleInner<FormatException>(), Outcome.FromException<string>(new InvalidOperationException("dummy", new FormatException() )), true },
27-
{ builder => builder.HandleInner<ArgumentNullException>(e => false), Outcome.FromException<string>(new InvalidOperationException("dummy", new FormatException() )), false },
28-
{ builder => builder.HandleInner<FormatException>(e => e.Message == "m"), Outcome.FromException<string>(new InvalidOperationException("dummy", new FormatException("m") )), true },
29-
{ builder => builder.HandleInner<FormatException>(e => e.Message == "x"), Outcome.FromException<string>(new InvalidOperationException("dummy", new FormatException("m") )), false },
12+
{ builder => builder.HandleResult("val"), CreateOutcome("val"), true },
13+
{ builder => builder.HandleResult("val"), CreateOutcome("val2"), false },
14+
{ builder => builder.HandleResult("val"), CreateOutcome(new InvalidOperationException()), false },
15+
{ builder => builder.HandleResult("val", StringComparer.OrdinalIgnoreCase), CreateOutcome("VAL"), true },
16+
{ builder => builder.HandleResult(r => r == "val"), CreateOutcome("val"), true },
17+
{ builder => builder.HandleResult(r => r == "val2"), CreateOutcome("val"), false },
18+
{ builder => builder.Handle<InvalidOperationException>(), CreateOutcome(new InvalidOperationException()), true },
19+
{ builder => builder.Handle<InvalidOperationException>(), CreateOutcome(new FormatException()), false },
20+
{ builder => builder.Handle<InvalidOperationException>(e => false), CreateOutcome(new InvalidOperationException()), false },
21+
{ builder => builder.HandleInner<InvalidOperationException>(e => false), CreateOutcome(new InvalidOperationException()), false },
22+
{ builder => builder.HandleInner<InvalidOperationException>(), CreateOutcome("value"), false },
23+
{ builder => builder.Handle<InvalidOperationException>(), CreateOutcome("value"), false },
24+
{ builder => builder.Handle<InvalidOperationException>().HandleResult("value"), CreateOutcome("value"), true },
25+
{ builder => builder.Handle<InvalidOperationException>().HandleResult("value"), CreateOutcome("value2"), false },
26+
{ builder => builder.HandleInner<FormatException>(), CreateOutcome(new InvalidOperationException("dummy", new FormatException() )), true },
27+
{ builder => builder.HandleInner<ArgumentNullException>(e => false), CreateOutcome(new InvalidOperationException("dummy", new FormatException() )), false },
28+
{ builder => builder.HandleInner<FormatException>(e => e.Message == "m"), CreateOutcome(new InvalidOperationException("dummy", new FormatException("m") )), true },
29+
{ builder => builder.HandleInner<FormatException>(e => e.Message == "x"), CreateOutcome(new InvalidOperationException("dummy", new FormatException("m") )), false },
30+
#pragma warning disable CA2201
31+
//// See https://github.com/App-vNext/Polly/issues/2161
32+
{ builder => builder.HandleInner<InvalidOperationException>(), CreateOutcome(new InvalidOperationException("1")), true },
33+
{ builder => builder.HandleInner<InvalidOperationException>(), CreateOutcome(new Exception("1", new InvalidOperationException("2"))), true },
34+
{ builder => builder.HandleInner<InvalidOperationException>(), CreateOutcome(new FormatException("1", new InvalidOperationException("2"))), true },
35+
{ builder => builder.HandleInner<InvalidOperationException>(), CreateOutcome(new Exception("1", new Exception("2", new InvalidOperationException("3")))), true },
36+
{ builder => builder.HandleInner<InvalidOperationException>(), CreateOutcome(new AggregateException("1", new Exception("2a"), new InvalidOperationException("2b"))), true },
37+
{ builder => builder.HandleInner<InvalidOperationException>(), CreateOutcome(new AggregateException("1", new Exception("2", new InvalidOperationException("3")))), true },
38+
{ builder => builder.HandleInner<InvalidOperationException>(), CreateOutcome(new AggregateException("1", new FormatException("2", new NotSupportedException("3")))), false },
39+
{ builder => builder.HandleInner<InvalidOperationException>(), CreateOutcome(new AggregateException("1")), false },
40+
{ builder => builder.HandleInner<InvalidOperationException>(ex => ex.Message is "3"), CreateOutcome(new AggregateException("1", new FormatException("2", new NotSupportedException("3")))), false },
41+
{ builder => builder.HandleInner<InvalidOperationException>(ex => ex.Message is "unreachable"), CreateOutcome(new AggregateException("1", new FormatException("2", new NotSupportedException("3")))), false },
42+
{ builder => builder.HandleInner<InvalidOperationException>(ex => ex.Message is "1"), CreateOutcome(new InvalidOperationException("1")), true },
43+
{ builder => builder.HandleInner<InvalidOperationException>(ex => ex.Message is "2"), CreateOutcome(new Exception("1", new InvalidOperationException("2"))), true },
44+
{ builder => builder.HandleInner<InvalidOperationException>(ex => ex.Message is "3"), CreateOutcome(new Exception("1", new Exception("2", new InvalidOperationException("3")))), true },
45+
{ builder => builder.HandleInner<InvalidOperationException>(ex => ex.Message is "2b"), CreateOutcome(new AggregateException("1", new Exception("2a"), new InvalidOperationException("2b"))), true },
46+
{ builder => builder.HandleInner<InvalidOperationException>(ex => ex.Message is "3"), CreateOutcome(new AggregateException("1", new Exception("2", new InvalidOperationException("3")))), true },
47+
{ builder => builder.HandleInner<InvalidOperationException>(ex => ex.Message is "unreachable"), CreateOutcome(new InvalidOperationException("1")), false },
48+
{ builder => builder.HandleInner<InvalidOperationException>(ex => ex.Message is "unreachable"), CreateOutcome(new Exception("1", new InvalidOperationException("2"))), false },
49+
{ builder => builder.HandleInner<InvalidOperationException>(ex => ex.Message is "unreachable"), CreateOutcome(new Exception("1", new Exception("2", new InvalidOperationException("3")))), false },
50+
{ builder => builder.HandleInner<InvalidOperationException>(ex => ex.Message is "unreachable"), CreateOutcome(new AggregateException("1", new Exception("2a"), new InvalidOperationException("2b"))), false },
51+
{ builder => builder.HandleInner<InvalidOperationException>(ex => ex.Message is "unreachable"), CreateOutcome(new AggregateException("1", new Exception("2", new InvalidOperationException("3")))), false },
52+
#pragma warning restore CA2201
3053
};
3154

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

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

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

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

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

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

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

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

110133
handled.Should().BeTrue();
111134
}
135+
136+
private static Outcome<string> CreateOutcome(Exception exception)
137+
=> Outcome.FromException<string>(exception);
138+
139+
private static Outcome<string> CreateOutcome(string result)
140+
=> Outcome.FromResult(result);
112141
}

0 commit comments

Comments
 (0)