From c5f0f0e9d7e27098e53c1bef7842769dbb3330b4 Mon Sep 17 00:00:00 2001 From: martintmk <103487740+martintmk@users.noreply.github.com> Date: Mon, 24 Apr 2023 15:05:12 +0200 Subject: [PATCH] Enhance callback API with handling of void-based results (#1155) --- .../Controller/CircuitStateControllerTests.cs | 4 +- .../Retry/RetryStrategyOptionsTResultTests.cs | 12 +- .../Strategy/OutcomeEventTests.cs | 99 +++++++++++- .../Strategy/OutcomeGeneratorTests.cs | 21 ++- .../Strategy/OutcomePredicateTests.cs | 82 +++++++++- .../Strategy/VoidOutcomeEventTests.cs | 18 +++ .../Strategy/VoidOutcomeGeneratorTests.cs | 18 +++ .../Strategy/VoidOutcomePredicateTests.cs | 18 +++ .../Strategy/OutcomeEvent.Handler.cs | 34 ++-- .../Strategy/OutcomeEvent.TResult.cs | 8 +- src/Polly.Core/Strategy/OutcomeEvent.Void.cs | 91 +++++++++++ src/Polly.Core/Strategy/OutcomeEvent.cs | 67 +++++++- .../Strategy/OutcomeGenerator.Handler.cs | 11 +- .../Strategy/OutcomeGenerator.TResult.cs | 6 +- .../Strategy/OutcomeGenerator.Void.cs | 62 ++++++++ src/Polly.Core/Strategy/OutcomeGenerator.cs | 14 +- .../Strategy/OutcomePredicate.Handler.cs | 12 +- .../Strategy/OutcomePredicate.Void.cs | 89 +++++++++++ src/Polly.Core/Strategy/OutcomePredicate.cs | 2 +- src/Polly.Core/Strategy/VoidOutcomeEvent.cs | 106 +++++++++++++ .../Strategy/VoidOutcomeGenerator.cs | 52 ++++++ .../Strategy/VoidOutcomePredicate.cs | 150 ++++++++++++++++++ 22 files changed, 919 insertions(+), 57 deletions(-) create mode 100644 src/Polly.Core.Tests/Strategy/VoidOutcomeEventTests.cs create mode 100644 src/Polly.Core.Tests/Strategy/VoidOutcomeGeneratorTests.cs create mode 100644 src/Polly.Core.Tests/Strategy/VoidOutcomePredicateTests.cs create mode 100644 src/Polly.Core/Strategy/OutcomeEvent.Void.cs create mode 100644 src/Polly.Core/Strategy/OutcomeGenerator.Void.cs create mode 100644 src/Polly.Core/Strategy/OutcomePredicate.Void.cs create mode 100644 src/Polly.Core/Strategy/VoidOutcomeEvent.cs create mode 100644 src/Polly.Core/Strategy/VoidOutcomeGenerator.cs create mode 100644 src/Polly.Core/Strategy/VoidOutcomePredicate.cs diff --git a/src/Polly.Core.Tests/CircuitBreaker/Controller/CircuitStateControllerTests.cs b/src/Polly.Core.Tests/CircuitBreaker/Controller/CircuitStateControllerTests.cs index 68a01caa0cf..d7abbb48b6d 100644 --- a/src/Polly.Core.Tests/CircuitBreaker/Controller/CircuitStateControllerTests.cs +++ b/src/Polly.Core.Tests/CircuitBreaker/Controller/CircuitStateControllerTests.cs @@ -30,7 +30,7 @@ public async Task IsolateAsync_Ok() { // arrange bool called = false; - _options.OnOpened.Register((outcome, args) => + _options.OnOpened.RegisterVoid((outcome, args) => { args.BreakDuration.Should().Be(TimeSpan.MaxValue); args.Context.IsSynchronous.Should().BeFalse(); @@ -65,7 +65,7 @@ public async Task BreakAsync_Ok() { // arrange bool called = false; - _options.OnClosed.Register((outcome, args) => + _options.OnClosed.RegisterVoid((outcome, args) => { args.Context.IsSynchronous.Should().BeFalse(); args.Context.IsVoid.Should().BeTrue(); diff --git a/src/Polly.Core.Tests/Retry/RetryStrategyOptionsTResultTests.cs b/src/Polly.Core.Tests/Retry/RetryStrategyOptionsTResultTests.cs index 839bde1d81c..ba5d1b948c1 100644 --- a/src/Polly.Core.Tests/Retry/RetryStrategyOptionsTResultTests.cs +++ b/src/Polly.Core.Tests/Retry/RetryStrategyOptionsTResultTests.cs @@ -77,11 +77,13 @@ public async Task AsNonGenericOptions_Ok() nonGenericOptions.StrategyName.Should().Be("my-name"); nonGenericOptions.StrategyType.Should().Be("my-type"); - (await nonGenericOptions.ShouldRetry.CreateHandler()!.ShouldHandleAsync(new Outcome(999), default)).Should().BeTrue(); - await nonGenericOptions.OnRetry.CreateHandler()!.HandleAsync(new Outcome(999), default); - called.Should().BeTrue(); - var args = new RetryDelayArguments(ResilienceContext.Get(), 2, TimeSpan.FromMinutes(1)); - (await nonGenericOptions.RetryDelayGenerator.CreateHandler(default, _ => true)!.GenerateAsync(new Outcome(999), args)).Should().Be(TimeSpan.FromSeconds(123)); + var args = new ShouldRetryArguments(ResilienceContext.Get(), 0); + var delayArgs = new RetryDelayArguments(ResilienceContext.Get(), 2, TimeSpan.FromMinutes(1)); + var retryArgs = new OnRetryArguments(ResilienceContext.Get(), 0, TimeSpan.Zero); + (await nonGenericOptions.ShouldRetry.CreateHandler()!.ShouldHandleAsync(new Outcome(999), args)).Should().BeTrue(); + await nonGenericOptions.OnRetry.CreateHandler()!.HandleAsync(new Outcome(999), retryArgs); + called.Should().BeTrue(); + (await nonGenericOptions.RetryDelayGenerator.CreateHandler(default, _ => true)!.GenerateAsync(new Outcome(999), delayArgs)).Should().Be(TimeSpan.FromSeconds(123)); } } diff --git a/src/Polly.Core.Tests/Strategy/OutcomeEventTests.cs b/src/Polly.Core.Tests/Strategy/OutcomeEventTests.cs index 6daf3b35f34..b00a75f68a5 100644 --- a/src/Polly.Core.Tests/Strategy/OutcomeEventTests.cs +++ b/src/Polly.Core.Tests/Strategy/OutcomeEventTests.cs @@ -78,11 +78,6 @@ public void CreateHandler_Empty_ReturnsNull() called.Should().BeFalse(); }, sut => - { - sut.ConfigureCallbacks(callbacks => callbacks.IsEmpty.Should().BeTrue()); - InvokeHandler(sut, new Outcome(1.0)); - }, - sut => { bool called = false; sut.Register((_, _) => { called = true; return default; }); @@ -90,6 +85,100 @@ public void CreateHandler_Empty_ReturnsNull() InvokeHandler(sut, new Outcome(1.0)); called.Should().BeFalse(); }, + sut => + { + int called = 0; + sut.Register((_, _) => { called ++; return default; }); + sut.RegisterVoid((_, _) => { called ++; return default; }); + InvokeHandler(sut, new Outcome(VoidResult.Instance)); + called.Should().Be(1); + }, + sut => + { + int called = 0; + sut.RegisterVoid((_) => called ++); + InvokeHandler(sut, new Outcome(VoidResult.Instance)); + called.Should().Be(1); + }, + sut => + { + int called = 0; + sut.RegisterVoid(() => called ++); + InvokeHandler(sut, new Outcome(VoidResult.Instance)); + called.Should().Be(1); + }, + sut => + { + int called = 0; + sut.RegisterVoid((_, _) => called ++); + InvokeHandler(sut, new Outcome(VoidResult.Instance)); + called.Should().Be(1); + }, + sut => + { + int called = 0; + sut.RegisterVoid((_, _) => called ++); + sut.RegisterVoid((_, _) => called ++); + InvokeHandler(sut, new Outcome(VoidResult.Instance)); + called.Should().Be(2); + }, + sut => + { + sut.SetVoidCallbacks(new VoidOutcomeEvent()); + InvokeHandler(sut, new Outcome(VoidResult.Instance)); + }, + sut => + { + int called = 0; + sut.Register(() => called ++); + InvokeHandler(sut, new Outcome(10)); + called.Should().Be(1); + }, + sut => + { + int called = 0; + sut.Register(() => called ++); + sut.Register(() => called ++); + InvokeHandler(sut, new Outcome(10)); + called.Should().Be(2); + }, + sut => + { + int called = 0; + sut.Register(() => called ++); + sut.Register(() => called ++); + sut.Register(() => called ++); + InvokeHandler(sut, new Outcome(10)); + called.Should().Be(3); + }, + sut => + { + int called = 0; + sut.Register(o => { o.Result.Should().Be(10); called ++; }); + InvokeHandler(sut, new Outcome(10)); + called.Should().Be(1); + }, + sut => + { + int called = 0; + sut.Register(o => { o.Exception.Should().BeOfType(); called ++; }); + InvokeHandler(sut, new Outcome(new InvalidOperationException())); + called.Should().Be(1); + }, + sut => + { + int called = 0; + sut.Register((_, _) => called ++); + InvokeHandler(sut, new Outcome(10)); + called.Should().Be(1); + }, + sut => + { + int called = 0; + sut.Register((_, _) => { called ++; return default; }); + InvokeHandler(sut, new Outcome(10)); + called.Should().Be(1); + }, }; [MemberData(nameof(Data))] diff --git a/src/Polly.Core.Tests/Strategy/OutcomeGeneratorTests.cs b/src/Polly.Core.Tests/Strategy/OutcomeGeneratorTests.cs index 7c09e37ac7d..f90abc0f4ed 100644 --- a/src/Polly.Core.Tests/Strategy/OutcomeGeneratorTests.cs +++ b/src/Polly.Core.Tests/Strategy/OutcomeGeneratorTests.cs @@ -118,19 +118,28 @@ public void CreateHandler_Empty_ReturnsNull() }, sut => { - sut.ConfigureGenerator(generator => generator.IsEmpty.Should().BeTrue()); - InvokeHandler(sut, new Outcome(10), GeneratedValue.Default); + sut.SetGenerator((_, _) => new ValueTask(GeneratedValue.Valid1)); + InvokeHandler(sut, new Outcome(10), GeneratedValue.Valid1); + }, + sut => + { + sut.SetVoidGenerator((_, _) => new ValueTask(GeneratedValue.Valid1)); + InvokeHandler(sut, new Outcome(VoidResult.Instance), GeneratedValue.Valid1); }, sut => { - sut.ConfigureGenerator(generator => generator.SetGenerator((_, _) => GeneratedValue.Valid1)); - sut.SetGenerator(new OutcomeGenerator()); + sut.SetVoidGenerator((_, _) => GeneratedValue.Valid1); + InvokeHandler(sut, new Outcome(VoidResult.Instance), GeneratedValue.Valid1); + }, + sut => + { + sut.SetVoidGenerator((_, _) => new ValueTask(GeneratedValue.Valid1)); InvokeHandler(sut, new Outcome(10), GeneratedValue.Default); }, sut => { - sut.SetGenerator((_, _) => new ValueTask(GeneratedValue.Valid1)); - InvokeHandler(sut, new Outcome(10), GeneratedValue.Valid1); + sut.SetVoidGenerator((_, _) => GeneratedValue.Valid1); + InvokeHandler(sut, new Outcome(10), GeneratedValue.Default); }, }; diff --git a/src/Polly.Core.Tests/Strategy/OutcomePredicateTests.cs b/src/Polly.Core.Tests/Strategy/OutcomePredicateTests.cs index 977c6871ab1..3f08bc79446 100644 --- a/src/Polly.Core.Tests/Strategy/OutcomePredicateTests.cs +++ b/src/Polly.Core.Tests/Strategy/OutcomePredicateTests.cs @@ -20,9 +20,6 @@ public void Empty_Ok() public void Empty_ConfigurePredicates_Ok() { _sut.IsEmpty.Should().BeTrue(); - _sut.ConfigurePredicates(p => p.IsEmpty.Should().BeTrue()); - _sut.ConfigurePredicates(p => p.IsEmpty.Should().BeTrue()); - _sut.IsEmpty.Should().BeFalse(); _sut.CreateHandler().Should().BeNull(); } @@ -89,6 +86,66 @@ public void CreateHandler_Empty_ReturnsNull() sut.HandleException((e, _) => new ValueTask(e.Message.Contains("dummy"))); InvokeHandler(sut, new InvalidOperationException("dummy"), true); }, + sut => + { + sut.HandleVoidException(); + InvokeVoidHandler(sut, new InvalidOperationException("dummy"), true); + }, + sut => + { + sut.HandleVoidException(_=> true); + InvokeVoidHandler(sut, new ArgumentNullException("dummy"), false); + }, + sut => + { + sut.HandleVoidException((_, _)=> true); + InvokeVoidHandler(sut, new ArgumentNullException("dummy"), false); + }, + sut => + { + sut.HandleVoidException((_, _)=> new ValueTask(true)); + InvokeVoidHandler(sut, new ArgumentNullException("dummy"), false); + }, + sut => + { + sut.HandleVoidException(); + InvokeVoidHandler(sut, new ArgumentNullException("dummy"), false); + }, + sut => + { + sut.HandleVoidException(); + InvokeVoidHandler(sut, new InvalidOperationException("dummy"), true); + }, + sut => + { + sut.HandleVoidException(e => e.Message.Contains("dummy")); + InvokeVoidHandler(sut, new InvalidOperationException("other message"), false); + }, + sut => + { + sut.HandleVoidException((e, _) => e.Message.Contains("dummy")); + InvokeVoidHandler(sut, new InvalidOperationException("other message"), false); + }, + sut => + { + sut.HandleVoidException((e, _) => e.Message.Contains("dummy")); + InvokeVoidHandler(sut, new InvalidOperationException("dummy"), true); + }, + sut => + { + sut.HandleVoidException((e, _) => new ValueTask(e.Message.Contains("dummy"))); + InvokeVoidHandler(sut, new InvalidOperationException("other message"), false); + }, + sut => + { + sut.HandleVoidException((e, _) => new ValueTask(e.Message.Contains("dummy"))); + InvokeVoidHandler(sut, new InvalidOperationException("dummy"), true); + }, + sut => + { + sut.SetVoidPredicates(new VoidOutcomePredicate()); + InvokeVoidHandler(sut, new InvalidOperationException("dummy"), false); + }, }; [MemberData(nameof(ExceptionPredicates))] @@ -333,6 +390,25 @@ private static void InvokeHandler(OutcomePredicate sut, Exception sut.CreateHandler()!.ShouldHandleAsync(new Outcome(10), args).AsTask().Result.Should().Be(false); } + private static void InvokeVoidHandler(OutcomePredicate sut, Exception exception, bool expectedResult) + { + var args = new TestArguments(); + var handler = sut.CreateHandler(); + + if (handler == null) + { + expectedResult.Should().BeFalse(); + return; + } + +#pragma warning disable S5034 // "ValueTask" should be consumed correctly + handler.ShouldHandleAsync(new Outcome(exception), args).AsTask().Result.Should().Be(false); + + // again with void result + handler.ShouldHandleAsync(new Outcome(exception), args).AsTask().Result.Should().Be(expectedResult); +#pragma warning restore S5034 // "ValueTask" should be consumed correctly + } + private static void InvokeResultHandler(OutcomePredicate sut, T result, bool expectedResult) { sut.CreateHandler()!.ShouldHandleAsync(new Outcome(result), new TestArguments()).AsTask().Result.Should().Be(expectedResult); diff --git a/src/Polly.Core.Tests/Strategy/VoidOutcomeEventTests.cs b/src/Polly.Core.Tests/Strategy/VoidOutcomeEventTests.cs new file mode 100644 index 00000000000..16db692b61a --- /dev/null +++ b/src/Polly.Core.Tests/Strategy/VoidOutcomeEventTests.cs @@ -0,0 +1,18 @@ +using Polly.Strategy; + +namespace Polly.Core.Tests.Strategy; + +public class VoidOutcomeEventTests +{ + [Fact] + public void IsEmpty_Ok() + { + var ev = new VoidOutcomeEvent(); + + ev.IsEmpty.Should().BeTrue(); + + ev.Register(() => { }); + + ev.IsEmpty.Should().BeFalse(); + } +} diff --git a/src/Polly.Core.Tests/Strategy/VoidOutcomeGeneratorTests.cs b/src/Polly.Core.Tests/Strategy/VoidOutcomeGeneratorTests.cs new file mode 100644 index 00000000000..6e882765e79 --- /dev/null +++ b/src/Polly.Core.Tests/Strategy/VoidOutcomeGeneratorTests.cs @@ -0,0 +1,18 @@ +using Polly.Strategy; + +namespace Polly.Core.Tests.Strategy; + +public class VoidOutcomeGeneratorTests +{ + [Fact] + public void IsEmpty_Ok() + { + var ev = new VoidOutcomeGenerator(); + + ev.IsEmpty.Should().BeTrue(); + + ev.SetGenerator((_, _) => 10); + + ev.IsEmpty.Should().BeFalse(); + } +} diff --git a/src/Polly.Core.Tests/Strategy/VoidOutcomePredicateTests.cs b/src/Polly.Core.Tests/Strategy/VoidOutcomePredicateTests.cs new file mode 100644 index 00000000000..1def81db0f5 --- /dev/null +++ b/src/Polly.Core.Tests/Strategy/VoidOutcomePredicateTests.cs @@ -0,0 +1,18 @@ +using Polly.Strategy; + +namespace Polly.Core.Tests.Strategy; + +public class VoidOutcomePredicateTests +{ + [Fact] + public void IsEmpty_Ok() + { + var ev = new VoidOutcomePredicate(); + + ev.IsEmpty.Should().BeTrue(); + + ev.HandleException(); + + ev.IsEmpty.Should().BeFalse(); + } +} diff --git a/src/Polly.Core/Strategy/OutcomeEvent.Handler.cs b/src/Polly.Core/Strategy/OutcomeEvent.Handler.cs index b4dcd6099fe..95abc798fcb 100644 --- a/src/Polly.Core/Strategy/OutcomeEvent.Handler.cs +++ b/src/Polly.Core/Strategy/OutcomeEvent.Handler.cs @@ -36,15 +36,28 @@ public TypeHandler(Type type, object callback) _callback = callback; } - public override ValueTask HandleAsync(Outcome outcome, TArgs args) + public override async ValueTask HandleAsync(Outcome outcome, TArgs args) { - if (typeof(TResult) != _type) + if (typeof(TResult) == _type) { - return default; - } + if (_type == typeof(VoidResult)) + { + var callback = (Func)_callback; + await callback(outcome.AsOutcome(), args).ConfigureAwait(args.Context.ContinueOnCapturedContext); + } + else + { + var callback = (Func, TArgs, ValueTask>)_callback; + await callback(outcome, args).ConfigureAwait(args.Context.ContinueOnCapturedContext); + } - var callback = (Func, TArgs, ValueTask>)_callback; - return callback(outcome, args); + } + else if (_type == typeof(object)) + { + var callback = (Func, TArgs, ValueTask>)_callback; + var objectOutcome = outcome.HasResult ? new Outcome(outcome.Result!) : new Outcome(outcome.Exception!); + await callback(objectOutcome, args).ConfigureAwait(args.Context.ContinueOnCapturedContext); + } } } @@ -55,14 +68,17 @@ private sealed class TypesHandler : Handler public TypesHandler(IEnumerable> callbacks) => _handlers = callbacks.ToDictionary(v => v.Key, v => new TypeHandler(v.Key, v.Value)); - public override ValueTask HandleAsync(Outcome outcome, TArgs args) + public override async ValueTask HandleAsync(Outcome outcome, TArgs args) { if (_handlers.TryGetValue(typeof(TResult), out var handler)) { - return handler.HandleAsync(outcome, args); + await handler.HandleAsync(outcome, args).ConfigureAwait(args.Context.ContinueOnCapturedContext); } - return default; + if (_handlers.TryGetValue(typeof(object), out handler)) + { + await handler.HandleAsync(outcome, args).ConfigureAwait(args.Context.ContinueOnCapturedContext); + } } } } diff --git a/src/Polly.Core/Strategy/OutcomeEvent.TResult.cs b/src/Polly.Core/Strategy/OutcomeEvent.TResult.cs index 819dc764515..7c0482ef73f 100644 --- a/src/Polly.Core/Strategy/OutcomeEvent.TResult.cs +++ b/src/Polly.Core/Strategy/OutcomeEvent.TResult.cs @@ -19,7 +19,7 @@ public sealed class OutcomeEvent public bool IsEmpty => _callbacks.Count == 0; /// - /// Adds a callback for the specified result type. + /// Registers a callback for the specified result type. /// /// The event callback associated with the result type. /// The current updated instance. @@ -35,7 +35,7 @@ public OutcomeEvent Register(Action callback) } /// - /// Adds a callback for the specified result type. + /// Registers a callback for the specified result type. /// /// The event callback associated with the result type. /// The current updated instance. @@ -51,7 +51,7 @@ public OutcomeEvent Register(Action> callback) } /// - /// Adds a callback for the specified result type. + /// Registers a callback for the specified result type. /// /// The event callback associated with the result type. /// The current updated instance. @@ -67,7 +67,7 @@ public OutcomeEvent Register(Action, TArgs> cal } /// - /// Adds a callback for the specified result type. + /// Registers a callback for the specified result type. /// /// The event callback associated with the result type. /// The current updated instance. diff --git a/src/Polly.Core/Strategy/OutcomeEvent.Void.cs b/src/Polly.Core/Strategy/OutcomeEvent.Void.cs new file mode 100644 index 00000000000..ea8cb88a64c --- /dev/null +++ b/src/Polly.Core/Strategy/OutcomeEvent.Void.cs @@ -0,0 +1,91 @@ +using System; +using Polly.Strategy; + +namespace Polly.Strategy; + +public sealed partial class OutcomeEvent + where TArgs : IResilienceArguments +{ + /// + /// Registers a callback for void-based results. + /// + /// The event callback associated with the result type. + /// The current updated instance. + public OutcomeEvent RegisterVoid(Action callback) + { + Guard.NotNull(callback); + + return ConfigureVoidCallbacks(c => c.Register(callback)); + } + + /// + /// Registers a callback for void-based results. + /// + /// The event callback associated with the void result type. + /// The current updated instance. + public OutcomeEvent RegisterVoid(Action callback) + { + Guard.NotNull(callback); + + return ConfigureVoidCallbacks(c => c.Register(callback)); + } + + /// + /// Registers a callback for void-based results. + /// + /// The event callback associated with the void result type. + /// The current updated instance. + public OutcomeEvent RegisterVoid(Action callback) + { + Guard.NotNull(callback); + + return ConfigureVoidCallbacks(c => c.Register(callback)); + } + + /// + /// Registers a callback for void-based results. + /// + /// The event callback associated with the void result type. + /// The current updated instance. + public OutcomeEvent RegisterVoid(Func callback) + { + Guard.NotNull(callback); + + return ConfigureVoidCallbacks(c => c.Register(callback)); + } + + /// + /// Registers a callback for void-based results. + /// + /// Action that configures the callbacks. + /// The current updated instance. + private OutcomeEvent ConfigureVoidCallbacks(Action> configure) + { + Guard.NotNull(configure); + + if (!_callbacks.TryGetValue(typeof(VoidResult), out var callbacks)) + { + SetVoidCallbacks(new VoidOutcomeEvent()); + callbacks = _callbacks[typeof(VoidResult)]; + } + + configure((VoidOutcomeEvent)callbacks.callback); + return this; + } + + /// + /// Sets callbacks for void-based results. + /// + /// The callbacks instance. + /// The current updated instance. + /// + /// This method replaces all previously registered callbacks for void-based results. + /// + public OutcomeEvent SetVoidCallbacks(VoidOutcomeEvent callbacks) + { + Guard.NotNull(callbacks); + + _callbacks[typeof(VoidResult)] = (callbacks, callbacks.CreateHandler); + return this; + } +} diff --git a/src/Polly.Core/Strategy/OutcomeEvent.cs b/src/Polly.Core/Strategy/OutcomeEvent.cs index ebc07f9511a..7d21d062d10 100644 --- a/src/Polly.Core/Strategy/OutcomeEvent.cs +++ b/src/Polly.Core/Strategy/OutcomeEvent.cs @@ -4,7 +4,7 @@ namespace Polly.Strategy; /// -/// The base class for events that use and in the registered event callbacks. +/// Class for events that use and in the registered event callbacks. /// /// The type of arguments the event uses. public sealed partial class OutcomeEvent @@ -18,7 +18,7 @@ public sealed partial class OutcomeEvent public bool IsEmpty => _callbacks.Count == 0; /// - /// Adds a callback for the specified result type. + /// Registers a callback for the specified result type. /// /// The result type to add a callback for. /// The event callback associated with the result type. @@ -31,7 +31,7 @@ public OutcomeEvent Register(Action callback) } /// - /// Adds a callback for the specified result type. + /// Registers a callback for the specified result type. /// /// The result type to add a callback for. /// The event callback associated with the result type. @@ -44,7 +44,7 @@ public OutcomeEvent Register(Action> callback) } /// - /// Adds a callback for the specified result type. + /// Registers a callback for the specified result type. /// /// The result type to add a callback for. /// The event callback associated with the result type. @@ -57,7 +57,7 @@ public OutcomeEvent Register(Action, TArgs> cal } /// - /// Adds a callback for the specified result type. + /// Registers a callback for the specified result type. /// /// The result type to add a callback for. /// The event callback associated with the result type. @@ -70,12 +70,60 @@ public OutcomeEvent Register(Func, TArgs, Value } /// - /// Adds a callback for the specified result type. + /// Registers a callback for all result types including void-based results. + /// + /// The event callback associated with the result type. + /// The current updated instance. + public OutcomeEvent Register(Action callback) + { + Guard.NotNull(callback); + + return ConfigureCallbacks(c => c.Register(callback)); + } + + /// + /// Registers a callback for all result types including void-based results. + /// + /// The event callback associated with the result type. + /// The current updated instance. + public OutcomeEvent Register(Action callback) + { + Guard.NotNull(callback); + + return ConfigureCallbacks(c => c.Register(outcome => callback(outcome.AsOutcome()))); + } + + /// + /// Registers a callback for all result types including void-based results. + /// + /// The event callback associated with the result type. + /// The current updated instance. + public OutcomeEvent Register(Action callback) + { + Guard.NotNull(callback); + + return ConfigureCallbacks(c => c.Register((outcome, args) => callback(outcome.AsOutcome(), args))); + } + + /// + /// Registers a callback for all result types including void-based results. + /// + /// The event callback associated with the result type. + /// The current updated instance. + public OutcomeEvent Register(Func callback) + { + Guard.NotNull(callback); + + return ConfigureCallbacks(c => c.Register((outcome, args) => callback(outcome.AsOutcome(), args))); + } + + /// + /// Registers a callback for the specified result type. /// /// The result type to add a callbacks for. /// Action that configures the callbacks. /// The current updated instance. - public OutcomeEvent ConfigureCallbacks(Action> configure) + private OutcomeEvent ConfigureCallbacks(Action> configure) { Guard.NotNull(configure); @@ -95,6 +143,9 @@ public OutcomeEvent ConfigureCallbacks(ActionThe result type to add a callbacks for. /// The callbacks instance. /// The current updated instance. + /// + /// This method replaces all previously registered callbacks for the specified result type. + /// public OutcomeEvent SetCallbacks(OutcomeEvent callbacks) { Guard.NotNull(callbacks); @@ -104,7 +155,7 @@ public OutcomeEvent SetCallbacks(OutcomeEvent ca } /// - /// Creates a handler that invokes the registered event callbacks. + /// Creates an event handler. /// /// Handler instance or null if no callbacks are registered. public Handler? CreateHandler() diff --git a/src/Polly.Core/Strategy/OutcomeGenerator.Handler.cs b/src/Polly.Core/Strategy/OutcomeGenerator.Handler.cs index 52e32e0f513..e3b657c6ddc 100644 --- a/src/Polly.Core/Strategy/OutcomeGenerator.Handler.cs +++ b/src/Polly.Core/Strategy/OutcomeGenerator.Handler.cs @@ -7,7 +7,7 @@ namespace Polly.Strategy; public sealed partial class OutcomeGenerator { /// - /// The resulting handler for the outcome. + /// The resulting generator handler. /// public abstract class Handler { @@ -59,7 +59,14 @@ public override async ValueTask GenerateAsync(Outcome } else if (typeof(TResult) == _type) { - value = await ((Func, TArgs, ValueTask>)_generator)(outcome, args).ConfigureAwait(args.Context.ContinueOnCapturedContext); + if (typeof(TResult) == typeof(VoidResult)) + { + value = await ((Func>)_generator)(outcome.AsOutcome(), args).ConfigureAwait(args.Context.ContinueOnCapturedContext); + } + else + { + value = await ((Func, TArgs, ValueTask>)_generator)(outcome, args).ConfigureAwait(args.Context.ContinueOnCapturedContext); + } } if (IsValid(value)) diff --git a/src/Polly.Core/Strategy/OutcomeGenerator.TResult.cs b/src/Polly.Core/Strategy/OutcomeGenerator.TResult.cs index 53c0f3d4f84..07733a7f3e5 100644 --- a/src/Polly.Core/Strategy/OutcomeGenerator.TResult.cs +++ b/src/Polly.Core/Strategy/OutcomeGenerator.TResult.cs @@ -23,7 +23,7 @@ public sealed class OutcomeGenerator public bool IsEmpty => _generator is null; /// - /// Adds a result generator for a specific result type. + /// Sets a result generator for a specific result type. /// /// The value generator. /// The current updated instance. @@ -35,7 +35,7 @@ public OutcomeGenerator SetGenerator(Func - /// Adds a result generator for a specific result type. + /// Sets a result generator for a specific result type. /// /// The value generator. /// The current updated instance. @@ -49,7 +49,7 @@ public OutcomeGenerator SetGenerator(Func - /// Creates a handler for the specified generators. + /// Creates a generator handler. /// /// Handler instance or null if no generators are registered. public Func, TArgs, ValueTask>? CreateHandler() => _generator; diff --git a/src/Polly.Core/Strategy/OutcomeGenerator.Void.cs b/src/Polly.Core/Strategy/OutcomeGenerator.Void.cs new file mode 100644 index 00000000000..16ac338f715 --- /dev/null +++ b/src/Polly.Core/Strategy/OutcomeGenerator.Void.cs @@ -0,0 +1,62 @@ +namespace Polly.Strategy; + +public sealed partial class OutcomeGenerator + where TArgs : IResilienceArguments +{ + /// + /// Sets a result generator for void-based results. + /// + /// The value generator. + /// The current updated instance. + public OutcomeGenerator SetVoidGenerator(Func generator) + { + Guard.NotNull(generator); + + return ConfigureVoidGenerator(g => g.SetGenerator((outcome, args) => generator(outcome, args))); + } + + /// + /// Sets a result generator for void-based results. + /// + /// The value generator. + /// The current updated instance. + public OutcomeGenerator SetVoidGenerator(Func> generator) + { + Guard.NotNull(generator); + + return ConfigureVoidGenerator(g => g.SetGenerator((outcome, args) => generator(outcome, args))); + } + + /// + /// Sets a result generator for void-based results. + /// + /// The generator builder. + /// The current updated instance. + public OutcomeGenerator SetVoidGenerator(VoidOutcomeGenerator generator) + { + Guard.NotNull(generator); + + _generators[typeof(VoidResult)] = (generator, generator.CreateHandler); + + return this; + } + + /// + /// Sets a result generator for void-based results. + /// + /// The callbacks that configures the generator. + /// The current updated instance. + public OutcomeGenerator ConfigureVoidGenerator(Action> configure) + { + Guard.NotNull(configure); + + if (!_generators.TryGetValue(typeof(VoidResult), out var generator)) + { + SetVoidGenerator(new VoidOutcomeGenerator()); + generator = _generators[typeof(VoidResult)]; + } + + configure((VoidOutcomeGenerator)generator.generator); + return this; + } +} diff --git a/src/Polly.Core/Strategy/OutcomeGenerator.cs b/src/Polly.Core/Strategy/OutcomeGenerator.cs index 8b5393e9809..5cfcfa03eb6 100644 --- a/src/Polly.Core/Strategy/OutcomeGenerator.cs +++ b/src/Polly.Core/Strategy/OutcomeGenerator.cs @@ -20,7 +20,7 @@ public sealed partial class OutcomeGenerator public bool IsEmpty => _generators.Count == 0; /// - /// Adds a result generator for a specific result type. + /// Sets a result generator for a specific result type. /// /// The result type to add a generator for. /// The value generator. @@ -33,7 +33,7 @@ public OutcomeGenerator SetGenerator(Func - /// Adds a result generator for a specific result type. + /// Sets a result generator for a specific result type. /// /// The result type to add a generator for. /// The value generator. @@ -46,7 +46,7 @@ public OutcomeGenerator SetGenerator(Func - /// Adds a result generator for all result types including the void-based results. + /// Sets a result generator for all result types including the void-based results. /// /// The value generator. /// The current updated instance. @@ -58,7 +58,7 @@ public OutcomeGenerator SetGenerator(Func } /// - /// Adds a result generator for all result types including the void-based results. + /// Sets a result generator for all result types including the void-based results. /// /// The value generator. /// The current updated instance. @@ -70,7 +70,7 @@ public OutcomeGenerator SetGenerator(Func - /// Adds a result generator for specific result type. + /// Sets a result generator for specific result type. /// /// The result type to add a generator for. /// The generator builder. @@ -85,12 +85,12 @@ public OutcomeGenerator SetGenerator(OutcomeGenerator - /// Adds a result generator for all result types including the void-based results. + /// Sets a result generator for all result types including the void-based results. /// /// The result type to add a generator for. /// The callbacks that configures the generator. /// The current updated instance. - public OutcomeGenerator ConfigureGenerator(Action> configure) + private OutcomeGenerator ConfigureGenerator(Action> configure) { Guard.NotNull(configure); diff --git a/src/Polly.Core/Strategy/OutcomePredicate.Handler.cs b/src/Polly.Core/Strategy/OutcomePredicate.Handler.cs index b4061c34930..ca213620341 100644 --- a/src/Polly.Core/Strategy/OutcomePredicate.Handler.cs +++ b/src/Polly.Core/Strategy/OutcomePredicate.Handler.cs @@ -54,9 +54,17 @@ public override ValueTask ShouldHandleAsync(Outcome outc private ValueTask ShouldHandlerCoreAsync(Outcome outcome, TArgs args) { - var predicate = (Func, TArgs, ValueTask>)_predicate; + if (typeof(TResult) == typeof(VoidResult)) + { + var predicate = (Func>)_predicate; + return predicate(outcome.AsOutcome(), args); + } + else + { + var predicate = (Func, TArgs, ValueTask>)_predicate; - return predicate(outcome, args); + return predicate(outcome, args); + } } } diff --git a/src/Polly.Core/Strategy/OutcomePredicate.Void.cs b/src/Polly.Core/Strategy/OutcomePredicate.Void.cs new file mode 100644 index 00000000000..aed28a1039d --- /dev/null +++ b/src/Polly.Core/Strategy/OutcomePredicate.Void.cs @@ -0,0 +1,89 @@ +namespace Polly.Strategy; + +public sealed partial class OutcomePredicate + where TArgs : IResilienceArguments +{ + /// + /// Adds an exception predicate for void-based results. + /// + /// The exception type to add a predicate for. + /// The current updated instance. + public OutcomePredicate HandleVoidException() + where TException : Exception + { + return ConfigureVoidPredicates(p => p.HandleException()); + } + + /// + /// Adds an exception predicate for void-based results. + /// + /// The exception type to add a predicate for. + /// The predicate to determine if the exception should be retried. + /// The current updated instance. + public OutcomePredicate HandleVoidException(Func predicate) + where TException : Exception + { + Guard.NotNull(predicate); + + return ConfigureVoidPredicates(p => p.HandleException(predicate)); + } + + /// + /// Adds an exception predicate for void-based results. + /// + /// The exception type to add a predicate for. + /// The predicate to determine if the exception should be retried. + /// The current updated instance. + public OutcomePredicate HandleVoidException(Func predicate) + where TException : Exception + { + Guard.NotNull(predicate); + + return ConfigureVoidPredicates(p => p.HandleException(predicate)); + } + + /// + /// Adds an exception predicate for void-based results. + /// + /// The exception type to add a predicate for. + /// The predicate to determine if the exception should be retried. + /// The current updated instance. + public OutcomePredicate HandleVoidException(Func> predicate) + where TException : Exception + { + Guard.NotNull(predicate); + + return ConfigureVoidPredicates(p => p.HandleException(predicate)); + } + + /// + /// Sets the predicates for the void-based results. + /// + /// The configured predicates. + /// The current updated instance. + public OutcomePredicate SetVoidPredicates(VoidOutcomePredicate predicates) + { + Guard.NotNull(predicates); + _predicates[typeof(VoidResult)] = (predicates, predicates.CreateHandler); + return this; + } + + /// + /// Adds a result predicate for void outcomes. + /// + /// Callback that configures a result predicate. + /// The current updated instance. + private OutcomePredicate ConfigureVoidPredicates(Action> configure) + { + Guard.NotNull(configure); + + if (!_predicates.TryGetValue(typeof(VoidResult), out var predicate)) + { + SetVoidPredicates(new VoidOutcomePredicate()); + predicate = _predicates[typeof(VoidResult)]; + } + + configure((VoidOutcomePredicate)predicate.predicate); + return this; + } +} diff --git a/src/Polly.Core/Strategy/OutcomePredicate.cs b/src/Polly.Core/Strategy/OutcomePredicate.cs index 798353dc896..3c64686b6e1 100644 --- a/src/Polly.Core/Strategy/OutcomePredicate.cs +++ b/src/Polly.Core/Strategy/OutcomePredicate.cs @@ -157,7 +157,7 @@ public OutcomePredicate HandleOutcome(Func, TAr /// The result type to add a predicate for. /// Callback that configures a result predicate. /// The current updated instance. - public OutcomePredicate ConfigurePredicates(Action> configure) + private OutcomePredicate ConfigurePredicates(Action> configure) { Guard.NotNull(configure); diff --git a/src/Polly.Core/Strategy/VoidOutcomeEvent.cs b/src/Polly.Core/Strategy/VoidOutcomeEvent.cs new file mode 100644 index 00000000000..171d3b3c843 --- /dev/null +++ b/src/Polly.Core/Strategy/VoidOutcomeEvent.cs @@ -0,0 +1,106 @@ +using System; +using Polly.Strategy; + +namespace Polly.Strategy; + +/// +/// Class for void-based events that use and in the registered event callbacks. +/// +/// The type of arguments the event uses. +public sealed class VoidOutcomeEvent + where TArgs : IResilienceArguments +{ + private readonly List> _callbacks = new(); + + /// + /// Gets a value indicating whether the event is empty. + /// + public bool IsEmpty => _callbacks.Count == 0; + + /// + /// Registers a callback for void-based results. + /// + /// The event callback associated with the void result type. + /// The current updated instance. + public VoidOutcomeEvent Register(Action callback) + { + Guard.NotNull(callback); + + return Register((_, _) => + { + callback(); + return default; + }); + } + + /// + /// Registers a callback for void-based results. + /// + /// The event callback associated with the void result type. + /// The current updated instance. + public VoidOutcomeEvent Register(Action callback) + { + Guard.NotNull(callback); + + return Register((outcome, _) => + { + callback(outcome); + return default; + }); + } + + /// + /// Registers a callback for void-based results. + /// + /// The event callback associated with the void result type. + /// The current updated instance. + public VoidOutcomeEvent Register(Action callback) + { + Guard.NotNull(callback); + + return Register((outcome, args) => + { + callback(outcome, args); + return default; + }); + } + + /// + /// Registers a callback for void-based results. + /// + /// The event callback associated with the void result type. + /// The current updated instance. + public VoidOutcomeEvent Register(Func callback) + { + Guard.NotNull(callback); + + _callbacks.Add(callback); + + return this; + } + + /// + /// Creates a handler that invokes the registered event callbacks. + /// + /// Handler instance or null if no callbacks are registered. + public Func? CreateHandler() + { + return _callbacks.Count switch + { + 0 => null, + 1 => _callbacks[0], + _ => CreateHandler(_callbacks.ToArray()) + }; + } + + private static Func CreateHandler(Func[] callbacks) + { + return async (outcome, args) => + { + foreach (var predicate in callbacks) + { + await predicate(outcome, args).ConfigureAwait(args.Context.ContinueOnCapturedContext); + } + }; + } +} diff --git a/src/Polly.Core/Strategy/VoidOutcomeGenerator.cs b/src/Polly.Core/Strategy/VoidOutcomeGenerator.cs new file mode 100644 index 00000000000..3fcce5287e8 --- /dev/null +++ b/src/Polly.Core/Strategy/VoidOutcomeGenerator.cs @@ -0,0 +1,52 @@ +using System; +using Polly.Strategy; + +namespace Polly.Strategy; + +/// +/// A class that generates values based on the void-based and . +/// +/// The arguments the generator uses. +/// The type of the generated value. +public sealed class VoidOutcomeGenerator + where TArgs : IResilienceArguments +{ + private Func>? _generator; + + /// + /// Gets a value indicating whether the generator is empty. + /// + public bool IsEmpty => _generator is null; + + /// + /// Sets a result generator. + /// + /// The value generator. + /// The current updated instance. + public VoidOutcomeGenerator SetGenerator(Func generator) + { + Guard.NotNull(generator); + + return SetGenerator((outcome, args) => new ValueTask(generator(outcome, args))); + } + + /// + /// Sets a result generator. + /// + /// The value generator. + /// The current updated instance. + public VoidOutcomeGenerator SetGenerator(Func> generator) + { + Guard.NotNull(generator); + + _generator = generator; + + return this; + } + + /// + /// Creates a generator handler. + /// + /// Handler instance or null if no generators are registered. + public Func>? CreateHandler() => _generator; +} diff --git a/src/Polly.Core/Strategy/VoidOutcomePredicate.cs b/src/Polly.Core/Strategy/VoidOutcomePredicate.cs new file mode 100644 index 00000000000..aa2f68f60e2 --- /dev/null +++ b/src/Polly.Core/Strategy/VoidOutcomePredicate.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using Polly.Strategy; + +namespace Polly.Strategy; + +/// +/// Predicate that uses void-based as an input. +/// +/// The type of arguments the predicate uses. +public sealed class VoidOutcomePredicate + where TArgs : IResilienceArguments +{ + private readonly List>> _predicates = new(); + + /// + /// Gets a value indicating whether the predicate is empty. + /// + public bool IsEmpty => _predicates.Count == 0; + + /// + /// Adds an exception predicate for void-based results. + /// + /// The exception type to add a predicate for. + /// The current updated instance. + public VoidOutcomePredicate HandleException() + where TException : Exception + { + return HandleOutcome((outcome, _) => + { + if (outcome.Exception is TException) + { + return new ValueTask(true); + } + + return new ValueTask(false); + }); + } + + /// + /// Adds an exception predicate for void-based results. + /// + /// The exception type to add a predicate for. + /// The predicate to determine if the exception should be retried. + /// The current updated instance. + public VoidOutcomePredicate HandleException(Func predicate) + where TException : Exception + { + Guard.NotNull(predicate); + + return HandleOutcome((outcome, _) => + { + if (outcome.Exception is TException typedException) + { + return new ValueTask(predicate(typedException)); + } + + return new ValueTask(false); + }); + } + + /// + /// Adds an exception predicate for void-based results. + /// + /// The exception type to add a predicate for. + /// The predicate to determine if the exception should be retried. + /// The current updated instance. + public VoidOutcomePredicate HandleException(Func predicate) + where TException : Exception + { + Guard.NotNull(predicate); + + return HandleOutcome((outcome, args) => + { + if (outcome.Exception is TException typedException) + { + return new ValueTask(predicate(typedException, args)); + } + + return new ValueTask(false); + }); + } + + /// + /// Adds an exception predicate for void-based results. + /// + /// The exception type to add a predicate for. + /// The predicate to determine if the exception should be retried. + /// The current updated instance. + public VoidOutcomePredicate HandleException(Func> predicate) + where TException : Exception + { + Guard.NotNull(predicate); + + return HandleOutcome((outcome, args) => + { + if (outcome.Exception is TException typedException) + { + return predicate(typedException, args); + } + + return new ValueTask(false); + }); + } + + /// + /// Adds a result predicate for the specified result type. + /// + /// The predicate to determine if the result should be retried. + /// The current updated instance. + private VoidOutcomePredicate HandleOutcome(Func> predicate) + { + Guard.NotNull(predicate); + + _predicates.Add(predicate); + return this; + } + + /// + /// Creates a handler for the specified predicates. + /// + /// Handler instance or null if no predicates are registered. + public Func>? CreateHandler() + { + var pairs = _predicates.ToArray(); + + return pairs.Length switch + { + 0 => null, + 1 => _predicates[0], + _ => CreateHandler(_predicates.ToArray()) + }; + } + + private static Func> CreateHandler(Func>[] predicates) + { + return async (outcome, args) => + { + foreach (var predicate in predicates) + { + if (await predicate(outcome, args).ConfigureAwait(args.Context.ContinueOnCapturedContext)) + { + return true; + } + } + + return false; + }; + } +}