diff --git a/src/Polly.Core.Tests/Polly.Core.Tests.csproj b/src/Polly.Core.Tests/Polly.Core.Tests.csproj
index ba461576479..477effa9186 100644
--- a/src/Polly.Core.Tests/Polly.Core.Tests.csproj
+++ b/src/Polly.Core.Tests/Polly.Core.Tests.csproj
@@ -7,7 +7,7 @@
enable
true
100
- $(NoWarn);SA1600;SA1204
+ $(NoWarn);SA1600;SA1204;SA1602
[Polly.Core]*
diff --git a/src/Polly.Core.Tests/Retry/RetryDelayArgumentsTests.cs b/src/Polly.Core.Tests/Retry/RetryDelayArgumentsTests.cs
new file mode 100644
index 00000000000..927d9a4be35
--- /dev/null
+++ b/src/Polly.Core.Tests/Retry/RetryDelayArgumentsTests.cs
@@ -0,0 +1,15 @@
+using Polly.Retry;
+
+namespace Polly.Core.Tests.Retry;
+
+public class RetryDelayArgumentsTests
+{
+ [Fact]
+ public void Ctor_Ok()
+ {
+ var args = new RetryDelayArguments(ResilienceContext.Get(), 2);
+
+ args.Context.Should().NotBeNull();
+ args.Attempt.Should().Be(2);
+ }
+}
diff --git a/src/Polly.Core.Tests/Retry/RetryDelayGeneratorTests.cs b/src/Polly.Core.Tests/Retry/RetryDelayGeneratorTests.cs
new file mode 100644
index 00000000000..7e33fb1ab46
--- /dev/null
+++ b/src/Polly.Core.Tests/Retry/RetryDelayGeneratorTests.cs
@@ -0,0 +1,35 @@
+using System;
+using System.Threading.Tasks;
+using Polly.Retry;
+using Polly.Strategy;
+using Xunit;
+
+namespace Polly.Core.Tests.Retry;
+
+public class RetryDelayGeneratorTests
+{
+ [Fact]
+ public async Task NoGeneratorRegisteredForType_EnsureDefaultValue()
+ {
+ var result = await new RetryDelayGenerator()
+ .SetGenerator((_, _) => TimeSpan.Zero)
+ .CreateHandler()!
+ .Generate(new Outcome(true), new RetryDelayArguments(ResilienceContext.Get(), 0));
+
+ result.Should().Be(TimeSpan.MinValue);
+ }
+
+ public static readonly TheoryData ValidDelays = new() { TimeSpan.Zero, TimeSpan.FromMilliseconds(123) };
+
+ [MemberData(nameof(ValidDelays))]
+ [Theory]
+ public async Task GeneratorRegistered_EnsureValueNotIgnored(TimeSpan delay)
+ {
+ var result = await new RetryDelayGenerator()
+ .SetGenerator((_, _) => delay)
+ .CreateHandler()!
+ .Generate(new Outcome(0), new RetryDelayArguments(ResilienceContext.Get(), 0));
+
+ result.Should().Be(delay);
+ }
+}
diff --git a/src/Polly.Core.Tests/Retry/RetryStrategyOptionsTests.cs b/src/Polly.Core.Tests/Retry/RetryStrategyOptionsTests.cs
index 82120482660..aca9027fecb 100644
--- a/src/Polly.Core.Tests/Retry/RetryStrategyOptionsTests.cs
+++ b/src/Polly.Core.Tests/Retry/RetryStrategyOptionsTests.cs
@@ -11,5 +11,8 @@ public void Ctor_Ok()
options.ShouldRetry.Should().NotBeNull();
options.ShouldRetry.IsEmpty.Should().BeTrue();
+
+ options.RetryDelayGenerator.Should().NotBeNull();
+ options.RetryDelayGenerator.IsEmpty.Should().BeTrue();
}
}
diff --git a/src/Polly.Core.Tests/Strategy/OutcomeGeneratorTests.cs b/src/Polly.Core.Tests/Strategy/OutcomeGeneratorTests.cs
new file mode 100644
index 00000000000..29909c68eed
--- /dev/null
+++ b/src/Polly.Core.Tests/Strategy/OutcomeGeneratorTests.cs
@@ -0,0 +1,148 @@
+using System;
+using System.Threading.Tasks;
+using Polly.Strategy;
+
+namespace Polly.Core.Tests.Strategy;
+
+public class OutcomeGeneratorTests
+{
+ private readonly DummyGenerator _sut = new();
+
+ [Fact]
+ public void Empty_Ok()
+ {
+ _sut.IsEmpty.Should().BeTrue();
+
+ _sut.SetGenerator((_, _) => GeneratedValue.Invalid);
+
+ _sut.IsEmpty.Should().BeFalse();
+ }
+
+ [Fact]
+ public void CreateHandler_Empty_ReturnsNull()
+ {
+ _sut.CreateHandler().Should().BeNull();
+ }
+
+ public static readonly TheoryData> Data = new()
+ {
+ sut =>
+ {
+ sut.SetGenerator((_, _) => GeneratedValue.Valid1);
+ InvokeHandler(sut, new Outcome(0), GeneratedValue.Valid1);
+ },
+ sut =>
+ {
+ sut.SetGenerator((_, _) => new ValueTask(GeneratedValue.Valid1));
+ InvokeHandler(sut, new Outcome(0), GeneratedValue.Valid1);
+ },
+ sut =>
+ {
+ sut.SetGenerator((_, _) => GeneratedValue.Valid1);
+ InvokeHandler(sut, new Outcome(true), GeneratedValue.Default);
+ },
+ sut =>
+ {
+ sut.SetGenerator((_, _) => new ValueTask(GeneratedValue.Valid1));
+ InvokeHandler(sut, new Outcome(true), GeneratedValue.Default);
+ },
+ sut =>
+ {
+ sut.SetGenerator((_, _) => GeneratedValue.Invalid);
+ InvokeHandler(sut, new Outcome(0), GeneratedValue.Default);
+ },
+ sut =>
+ {
+ sut.SetGenerator((_, _) => new ValueTask(GeneratedValue.Invalid));
+ InvokeHandler(sut, new Outcome(0), GeneratedValue.Default);
+ },
+ sut =>
+ {
+ sut.SetGenerator((_, _) => GeneratedValue.Valid1);
+ sut.SetGenerator((_, _) => GeneratedValue.Valid2);
+ InvokeHandler(sut, new Outcome(0), GeneratedValue.Valid2);
+ },
+ sut =>
+ {
+ sut.SetGenerator((_, _) => new ValueTask(GeneratedValue.Valid1));
+ sut.SetGenerator((_, _) => new ValueTask(GeneratedValue.Valid2));
+ InvokeHandler(sut, new Outcome(0), GeneratedValue.Valid2);
+ },
+ sut =>
+ {
+ sut.SetGenerator((_, _) => GeneratedValue.Valid1);
+ sut.SetGenerator((_, _) => GeneratedValue.Valid2);
+ InvokeHandler(sut, new Outcome(0), GeneratedValue.Valid1);
+ InvokeHandler(sut, new Outcome(true), GeneratedValue.Valid2);
+ },
+ sut =>
+ {
+ sut.SetGenerator((_, _) => GeneratedValue.Valid1);
+ sut.SetGenerator((_, _) => GeneratedValue.Valid1);
+ InvokeHandler(sut, new Outcome(true), GeneratedValue.Default);
+ },
+ };
+
+ [MemberData(nameof(Data))]
+ [Theory]
+ public void ResultHandler_SinglePredicate_Ok(Action callback)
+ {
+ _sut.Invoking(s => callback(s)).Should().NotThrow();
+ callback(_sut);
+ }
+
+ [Fact]
+ public void AddResultHandlers_DifferentResultType_NotInvoked()
+ {
+ var callbacks = new List();
+
+ for (var i = 0; i < 10; i++)
+ {
+ var index = i;
+
+ _sut.SetGenerator((_, _) =>
+ {
+ callbacks.Add(index);
+ return GeneratedValue.Valid1;
+ });
+
+ _sut.SetGenerator((_, _) =>
+ {
+ callbacks.Add(index);
+ return GeneratedValue.Valid1;
+ });
+ }
+
+ InvokeHandler(_sut, new Outcome(1), GeneratedValue.Valid1);
+
+ callbacks.Distinct().Should().HaveCount(1);
+ }
+
+ private static void InvokeHandler(DummyGenerator sut, Outcome outcome, GeneratedValue expectedResult)
+ {
+ var args = new Args();
+ sut.CreateHandler()!.Generate(outcome, args).AsTask().Result.Should().Be(expectedResult);
+ }
+
+ public sealed class DummyGenerator : OutcomeGenerator
+ {
+ protected override GeneratedValue DefaultValue => GeneratedValue.Default;
+
+ protected override bool IsValid(GeneratedValue value) => value == GeneratedValue.Valid1 || value == GeneratedValue.Valid2;
+ }
+
+ public enum GeneratedValue
+ {
+ Default,
+ Valid1,
+ Valid2,
+ Invalid
+ }
+
+ public class Args : IResilienceArguments
+ {
+ public Args() => Context = ResilienceContext.Get();
+
+ public ResilienceContext Context { get; private set; }
+ }
+}
diff --git a/src/Polly.Core/Retry/RetryDelayArguments.cs b/src/Polly.Core/Retry/RetryDelayArguments.cs
new file mode 100644
index 00000000000..3fa9c0d050e
--- /dev/null
+++ b/src/Polly.Core/Retry/RetryDelayArguments.cs
@@ -0,0 +1,28 @@
+using Polly.Strategy;
+
+namespace Polly.Retry;
+
+#pragma warning disable CA1815 // Override equals and operator equals on value types
+
+///
+/// Represents the arguments used in for generating the next retry delay.
+///
+public readonly struct RetryDelayArguments : IResilienceArguments
+{
+ internal RetryDelayArguments(ResilienceContext context, int attempt)
+ {
+ Attempt = attempt;
+ Context = context;
+ }
+
+ ///
+ /// Gets the zero-based attempt number.
+ ///
+ ///
+ /// The first attempt is 0, the second attempt is 1, and so on.
+ ///
+ public int Attempt { get; }
+
+ ///
+ public ResilienceContext Context { get; }
+}
diff --git a/src/Polly.Core/Retry/RetryDelayGenerator.cs b/src/Polly.Core/Retry/RetryDelayGenerator.cs
new file mode 100644
index 00000000000..deb12f6061d
--- /dev/null
+++ b/src/Polly.Core/Retry/RetryDelayGenerator.cs
@@ -0,0 +1,18 @@
+using Polly.Strategy;
+
+namespace Polly.Retry;
+
+///
+/// This class generates the customized retries used in retry strategy.
+///
+///
+/// If the generator returns a negative value, it's value is ignored.
+///
+public sealed class RetryDelayGenerator : OutcomeGenerator
+{
+ ///
+ protected override TimeSpan DefaultValue => TimeSpan.MinValue;
+
+ ///
+ protected override bool IsValid(TimeSpan value) => value >= TimeSpan.Zero;
+}
diff --git a/src/Polly.Core/Retry/RetryStrategyOptions.cs b/src/Polly.Core/Retry/RetryStrategyOptions.cs
index a0270620dad..da1e346ec04 100644
--- a/src/Polly.Core/Retry/RetryStrategyOptions.cs
+++ b/src/Polly.Core/Retry/RetryStrategyOptions.cs
@@ -10,6 +10,18 @@ public class RetryStrategyOptions
///
/// Gets or sets the instance used to determine if a retry should be performed.
///
+ ///
+ /// By default, the predicate is empty and no results or exceptions are retried.
+ ///
[Required]
public ShouldRetryPredicate ShouldRetry { get; set; } = new();
+
+ ///
+ /// Gets or sets the instance that is used to generated the delay between retries.
+ ///
+ ///
+ /// By default, the generator is empty and it does not affect the delay between retries.
+ ///
+ [Required]
+ public RetryDelayGenerator RetryDelayGenerator { get; set; } = new();
}
diff --git a/src/Polly.Core/Strategy/OutcomeGenerator.Handler.cs b/src/Polly.Core/Strategy/OutcomeGenerator.Handler.cs
new file mode 100644
index 00000000000..e2ee79b899b
--- /dev/null
+++ b/src/Polly.Core/Strategy/OutcomeGenerator.Handler.cs
@@ -0,0 +1,91 @@
+using System.Collections.Generic;
+
+namespace Polly.Strategy;
+
+#pragma warning disable CA1034 // Nested types should not be visible
+#pragma warning disable CA1005 // Avoid excessive parameters on generic types
+#pragma warning disable S2436 // Types and methods should not have too many generic parameters
+
+public abstract partial class OutcomeGenerator
+{
+ ///
+ /// The resulting handler for the outcome.
+ ///
+ public abstract class Handler
+ {
+ private protected Handler(TGeneratedValue defaultValue, Predicate isValid)
+ {
+ DefaultValue = defaultValue;
+ IsValid = isValid;
+ }
+
+ internal TGeneratedValue DefaultValue { get; }
+
+ internal Predicate IsValid { get; }
+
+ ///
+ /// Determines if the handler should handle the outcome.
+ ///
+ /// The result type to add a predicate for.
+ /// The operation outcome.
+ /// The arguments.
+ /// The result of the handle operation.
+ public abstract ValueTask Generate(Outcome outcome, TArgs args);
+ }
+
+ private sealed class TypeHandler : Handler
+ {
+ private readonly Type _type;
+ private readonly object _generator;
+
+ public TypeHandler(
+ Type type,
+ object generator,
+ TGeneratedValue defaultValue,
+ Predicate isValid)
+ : base(defaultValue, isValid)
+ {
+ _type = type;
+ _generator = generator;
+ }
+
+ public override async ValueTask Generate(Outcome outcome, TArgs args)
+ {
+ if (typeof(TResult) == _type)
+ {
+ var value = await ((Func, TArgs, ValueTask>)_generator)(outcome, args).ConfigureAwait(args.Context.ContinueOnCapturedContext);
+
+ if (IsValid(value))
+ {
+ return value;
+ }
+
+ return DefaultValue;
+ }
+
+ return DefaultValue;
+ }
+ }
+
+ private sealed class TypesHandler : Handler
+ {
+ private readonly Dictionary _generators;
+
+ public TypesHandler(
+ IEnumerable> generators,
+ TGeneratedValue defaultValue,
+ Predicate isValid)
+ : base(defaultValue, isValid)
+ => _generators = generators.ToDictionary(v => v.Key, v => new TypeHandler(v.Key, v.Value, defaultValue, isValid));
+
+ public override ValueTask Generate(Outcome outcome, TArgs args)
+ {
+ if (_generators.TryGetValue(typeof(TResult), out var handler))
+ {
+ return handler.Generate(outcome, args);
+ }
+
+ return new ValueTask(DefaultValue);
+ }
+ }
+}
diff --git a/src/Polly.Core/Strategy/OutcomeGenerator.cs b/src/Polly.Core/Strategy/OutcomeGenerator.cs
new file mode 100644
index 00000000000..8abde15e8fd
--- /dev/null
+++ b/src/Polly.Core/Strategy/OutcomeGenerator.cs
@@ -0,0 +1,81 @@
+using System;
+using Polly.Strategy;
+
+namespace Polly.Strategy;
+
+#pragma warning disable S2436 // Types and methods should not have too many generic parameters
+#pragma warning disable CA1005 // Too many generic arguments, but this is the only way to make the API work without unnecessary extensions
+
+///
+/// The base class for all generators that generate a value based on the .
+///
+/// The type of generated value.
+/// The arguments the generator uses.
+/// The class that implements .
+public abstract partial class OutcomeGenerator
+ where TArgs : IResilienceArguments
+ where TSelf : OutcomeGenerator
+{
+ private readonly Dictionary _generators = new();
+
+ ///
+ /// Gets the default value returned by the generator.
+ ///
+ protected abstract TGeneratedValue DefaultValue { get; }
+
+ ///
+ /// Gets a value indicating whether the generator is empty.
+ ///
+ public bool IsEmpty => _generators.Count == 0;
+
+ ///
+ /// Adds a result generator for the specified result type.
+ ///
+ /// The result type to add a generator for.
+ /// The generator to determine if the result should be retried.
+ /// The current updated instance.
+ public TSelf SetGenerator(Func, TArgs, TGeneratedValue> generator)
+ {
+ Guard.NotNull(generator);
+
+ return SetGenerator((outcome, args) => new ValueTask(generator(outcome, args)));
+ }
+
+ ///
+ /// Adds a result generator for the specified result type.
+ ///
+ /// The result type to add a generator for.
+ /// The generator to determine if the result should be retried.
+ /// The current updated instance.
+ public TSelf SetGenerator(Func, TArgs, ValueTask> generator)
+ {
+ Guard.NotNull(generator);
+
+ _generators[typeof(TResult)] = generator;
+
+ return (TSelf)this;
+ }
+
+ ///
+ /// Determines if the generated value is valid.
+ ///
+ /// The value returned by the user-provided generator.
+ /// True if generated value is valid, false otherwise.
+ protected abstract bool IsValid(TGeneratedValue value);
+
+ ///
+ /// Creates a handler for the specified generators.
+ ///
+ /// Handler instance or null if no generators are registered.
+ protected internal Handler? CreateHandler()
+ {
+ var pairs = _generators.ToArray();
+
+ return pairs.Length switch
+ {
+ 0 => null,
+ 1 => new TypeHandler(pairs[0].Key, pairs[0].Value, DefaultValue, IsValid),
+ _ => new TypesHandler(pairs, DefaultValue, IsValid)
+ };
+ }
+}