Repository: App-vNext/Polly

Issue:
[V8] ShouldHandle, ShouldRetry etc. predicates should have reasonable defaults
Currently when using the new API, it is required to specify `ShouldHandle` predicate for CircuitBreaker, `ShouldRetry` predicate for Retry and probably more.

I think it would be beneficial to have some reasonable default predicates so that users don't need to specify it every time. For example I would consider reasonable default to handle all requests that throw exceptions (other than `OperationCancelledException`).

Also it would be nice to have the defaults for Http scenarios (handling 5xx requests), because these will be common as well.


Hints:
Yeah, I agree here, let's address this in alpha2. Would provide a nice out-of-box experience.

Patch:
diff --git a/src/Polly.Core/CircuitBreaker/CircuitBreakerStrategyOptions.cs b/src/Polly.Core/CircuitBreaker/CircuitBreakerStrategyOptions.cs
index 10fd7e77888..4c0c539fe96 100644
--- a/src/Polly.Core/CircuitBreaker/CircuitBreakerStrategyOptions.cs
+++ b/src/Polly.Core/CircuitBreaker/CircuitBreakerStrategyOptions.cs
@@ -37,10 +37,16 @@ public abstract class CircuitBreakerStrategyOptions<TResult> : ResilienceStrateg
     /// Gets or sets the predicates for the circuit breaker.
     /// </summary>
     /// <remarks>
-    /// Defaults to <see langword="null"/>. This property is required.
+    /// Defaults to a delegate that handles circuit breaker on any exception except <see cref="OperationCanceledException"/>.
+    /// This property is required.
     /// </remarks>
     [Required]
-    public Func<OutcomeArguments<TResult, CircuitBreakerPredicateArguments>, ValueTask<bool>>? ShouldHandle { get; set; }
+    public Func<OutcomeArguments<TResult, CircuitBreakerPredicateArguments>, ValueTask<bool>> ShouldHandle { get; set; } = args => args.Exception switch
+    {
+        OperationCanceledException => PredicateResult.False,
+        Exception => PredicateResult.True,
+        _ => PredicateResult.False
+    };
 
     /// <summary>
     /// Gets or sets the event that is raised when the circuit resets to a <see cref="CircuitState.Closed"/> state.
diff --git a/src/Polly.Core/Hedging/HedgingStrategyOptions.TResult.cs b/src/Polly.Core/Hedging/HedgingStrategyOptions.TResult.cs
index 44ec334f2b6..a448243f5e7 100644
--- a/src/Polly.Core/Hedging/HedgingStrategyOptions.TResult.cs
+++ b/src/Polly.Core/Hedging/HedgingStrategyOptions.TResult.cs
@@ -45,10 +45,16 @@ public class HedgingStrategyOptions<TResult> : ResilienceStrategyOptions
     /// Gets or sets the predicate that determines whether a hedging should be performed for a given result.
     /// </summary>
     /// <remarks>
-    /// This property is required. Defaults to <see langword="null"/>.
+    /// Defaults to a delegate that hedges on any exception except <see cref="OperationCanceledException"/>.
+    /// This property is required.
     /// </remarks>
     [Required]
-    public Func<OutcomeArguments<TResult, HandleHedgingArguments>, ValueTask<bool>>? ShouldHandle { get; set; }
+    public Func<OutcomeArguments<TResult, HandleHedgingArguments>, ValueTask<bool>> ShouldHandle { get; set; } = args => args.Exception switch
+    {
+        OperationCanceledException => PredicateResult.False,
+        Exception => PredicateResult.True,
+        _ => PredicateResult.False
+    };
 
     /// <summary>
     /// Gets or sets the hedging action generator that creates hedged actions.
diff --git a/src/Polly.Core/Retry/RetryStrategyOptions.TResult.cs b/src/Polly.Core/Retry/RetryStrategyOptions.TResult.cs
index a112556ef94..48ecd7c3347 100644
--- a/src/Polly.Core/Retry/RetryStrategyOptions.TResult.cs
+++ b/src/Polly.Core/Retry/RetryStrategyOptions.TResult.cs
@@ -61,10 +61,16 @@ public class RetryStrategyOptions<TResult> : ResilienceStrategyOptions
     /// Gets or sets an outcome predicate that is used to register the predicates to determine if a retry should be performed.
     /// </summary>
     /// <remarks>
-    /// Defaults to <see langword="null"/>. This property is required.
+    /// Defaults to a delegate that retries on any exception except <see cref="OperationCanceledException"/>.
+    /// This property is required.
     /// </remarks>
     [Required]
-    public Func<OutcomeArguments<TResult, ShouldRetryArguments>, ValueTask<bool>>? ShouldRetry { get; set; }
+    public Func<OutcomeArguments<TResult, ShouldRetryArguments>, ValueTask<bool>> ShouldRetry { get; set; } = args => args.Exception switch
+    {
+        OperationCanceledException => PredicateResult.False,
+        Exception => PredicateResult.True,
+        _ => PredicateResult.False
+    };
 
     /// <summary>
     /// Gets or sets the generator instance that is used to calculate the time between retries.


Test patch:
diff --git a/test/Polly.Core.Tests/CircuitBreaker/AdvancedCircuitBreakerOptionsTests.cs b/test/Polly.Core.Tests/CircuitBreaker/AdvancedCircuitBreakerOptionsTests.cs
index d6b5d99d791..2859a9c0887 100644
--- a/test/Polly.Core.Tests/CircuitBreaker/AdvancedCircuitBreakerOptionsTests.cs
+++ b/test/Polly.Core.Tests/CircuitBreaker/AdvancedCircuitBreakerOptionsTests.cs
@@ -17,7 +17,7 @@ public void Ctor_Defaults()
         options.OnOpened.Should().BeNull();
         options.OnClosed.Should().BeNull();
         options.OnHalfOpened.Should().BeNull();
-        options.ShouldHandle.Should().BeNull();
+        options.ShouldHandle.Should().NotBeNull();
         options.StrategyType.Should().Be("CircuitBreaker");
         options.StrategyName.Should().BeNull();
 
@@ -27,10 +27,21 @@ public void Ctor_Defaults()
         options.MinimumThroughput = 2;
         options.SamplingDuration = TimeSpan.FromMilliseconds(500);
 
-        options.ShouldHandle = _ => PredicateResult.True;
         ValidationHelper.ValidateObject(options, "Dummy.");
     }
 
+    [Fact]
+    public async Task ShouldHandle_EnsureDefaults()
+    {
+        var options = new AdvancedCircuitBreakerStrategyOptions();
+        var args = new CircuitBreakerPredicateArguments();
+        var context = ResilienceContext.Get();
+
+        (await options.ShouldHandle(new(context, new Outcome<object>("dummy"), args))).Should().Be(false);
+        (await options.ShouldHandle(new(context, new Outcome<object>(new OperationCanceledException()), args))).Should().Be(false);
+        (await options.ShouldHandle(new(context, new Outcome<object>(new InvalidOperationException()), args))).Should().Be(true);
+    }
+
     [Fact]
     public void Ctor_Generic_Defaults()
     {
@@ -43,7 +54,7 @@ public void Ctor_Generic_Defaults()
         options.OnOpened.Should().BeNull();
         options.OnClosed.Should().BeNull();
         options.OnHalfOpened.Should().BeNull();
-        options.ShouldHandle.Should().BeNull();
+        options.ShouldHandle.Should().NotBeNull();
         options.StrategyType.Should().Be("CircuitBreaker");
         options.StrategyName.Should().BeNull();
 
@@ -53,7 +64,6 @@ public void Ctor_Generic_Defaults()
         options.MinimumThroughput = 2;
         options.SamplingDuration = TimeSpan.FromMilliseconds(500);
 
-        options.ShouldHandle = _ => PredicateResult.True;
         ValidationHelper.ValidateObject(options, "Dummy.");
     }
 
diff --git a/test/Polly.Core.Tests/CircuitBreaker/SimpleCircuitBreakerOptionsTests.cs b/test/Polly.Core.Tests/CircuitBreaker/SimpleCircuitBreakerOptionsTests.cs
index b35968bade0..d60f7050984 100644
--- a/test/Polly.Core.Tests/CircuitBreaker/SimpleCircuitBreakerOptionsTests.cs
+++ b/test/Polly.Core.Tests/CircuitBreaker/SimpleCircuitBreakerOptionsTests.cs
@@ -16,7 +16,7 @@ public void Ctor_Defaults()
         options.OnOpened.Should().BeNull();
         options.OnClosed.Should().BeNull();
         options.OnHalfOpened.Should().BeNull();
-        options.ShouldHandle.Should().BeNull();
+        options.ShouldHandle.Should().NotBeNull();
         options.StrategyType.Should().Be("CircuitBreaker");
         options.StrategyName.Should().BeNull();
 
@@ -24,10 +24,21 @@ public void Ctor_Defaults()
         options.FailureThreshold = 1;
         options.BreakDuration = TimeSpan.FromMilliseconds(500);
 
-        options.ShouldHandle = _ => PredicateResult.True;
         ValidationHelper.ValidateObject(options, "Dummy.");
     }
 
+    [Fact]
+    public async Task ShouldHandle_EnsureDefaults()
+    {
+        var options = new SimpleCircuitBreakerStrategyOptions();
+        var args = new CircuitBreakerPredicateArguments();
+        var context = ResilienceContext.Get();
+
+        (await options.ShouldHandle(new(context, new Outcome<object>("dummy"), args))).Should().Be(false);
+        (await options.ShouldHandle(new(context, new Outcome<object>(new OperationCanceledException()), args))).Should().Be(false);
+        (await options.ShouldHandle(new(context, new Outcome<object>(new InvalidOperationException()), args))).Should().Be(true);
+    }
+
     [Fact]
     public void Ctor_Generic_Defaults()
     {
@@ -38,7 +49,7 @@ public void Ctor_Generic_Defaults()
         options.OnOpened.Should().BeNull();
         options.OnClosed.Should().BeNull();
         options.OnHalfOpened.Should().BeNull();
-        options.ShouldHandle.Should().BeNull();
+        options.ShouldHandle.Should().NotBeNull();
         options.StrategyType.Should().Be("CircuitBreaker");
         options.StrategyName.Should().BeNull();
 
diff --git a/test/Polly.Core.Tests/Hedging/HedgingStrategyOptionsTests.cs b/test/Polly.Core.Tests/Hedging/HedgingStrategyOptionsTests.cs
index c7a4a3ba50e..59b4e5d6019 100644
--- a/test/Polly.Core.Tests/Hedging/HedgingStrategyOptionsTests.cs
+++ b/test/Polly.Core.Tests/Hedging/HedgingStrategyOptionsTests.cs
@@ -12,7 +12,7 @@ public async Task Ctor_EnsureDefaults()
         var options = new HedgingStrategyOptions<int>();
 
         options.StrategyType.Should().Be("Hedging");
-        options.ShouldHandle.Should().BeNull();
+        options.ShouldHandle.Should().NotBeNull();
         options.HedgingActionGenerator.Should().NotBeNull();
         options.HedgingDelay.Should().Be(TimeSpan.FromSeconds(2));
         options.MaxHedgedAttempts.Should().Be(2);
@@ -23,6 +23,18 @@ public async Task Ctor_EnsureDefaults()
         (await action()).Result.Should().Be(99);
     }
 
+    [Fact]
+    public async Task ShouldHandle_EnsureDefaults()
+    {
+        var options = new HedgingStrategyOptions<int>();
+        var args = new HandleHedgingArguments();
+        var context = ResilienceContext.Get();
+
+        (await options.ShouldHandle(new(context, new Outcome<int>(0), args))).Should().Be(false);
+        (await options.ShouldHandle(new(context, new Outcome<int>(new OperationCanceledException()), args))).Should().Be(false);
+        (await options.ShouldHandle(new(context, new Outcome<int>(new InvalidOperationException()), args))).Should().Be(true);
+    }
+
     [Fact]
     public void Validation()
     {
diff --git a/test/Polly.Core.Tests/Retry/RetryStrategyOptionsTests.cs b/test/Polly.Core.Tests/Retry/RetryStrategyOptionsTests.cs
index 544ec879af2..7242cb10b62 100644
--- a/test/Polly.Core.Tests/Retry/RetryStrategyOptionsTests.cs
+++ b/test/Polly.Core.Tests/Retry/RetryStrategyOptionsTests.cs
@@ -12,7 +12,7 @@ public void Ctor_Ok()
         var options = new RetryStrategyOptions<int>();
 
         options.StrategyType.Should().Be("Retry");
-        options.ShouldRetry.Should().BeNull();
+        options.ShouldRetry.Should().NotBeNull();
 
         options.RetryDelayGenerator.Should().BeNull();
 
@@ -23,6 +23,18 @@ public void Ctor_Ok()
         options.BaseDelay.Should().Be(TimeSpan.FromSeconds(2));
     }
 
+    [Fact]
+    public async Task ShouldHandle_EnsureDefaults()
+    {
+        var options = new RetryStrategyOptions<int>();
+        var args = new ShouldRetryArguments(0);
+        var context = ResilienceContext.Get();
+
+        (await options.ShouldRetry(new(context, new Outcome<int>(0), args))).Should().Be(false);
+        (await options.ShouldRetry(new(context, new Outcome<int>(new OperationCanceledException()), args))).Should().Be(false);
+        (await options.ShouldRetry(new(context, new Outcome<int>(new InvalidOperationException()), args))).Should().Be(true);
+    }
+
     [Fact]
     public void InvalidOptions()
     {