diff --git a/src/Polly.Specs/Polly.Specs.csproj b/src/Polly.Specs/Polly.Specs.csproj index 721b153e152..da631aba5dd 100644 --- a/src/Polly.Specs/Polly.Specs.csproj +++ b/src/Polly.Specs/Polly.Specs.csproj @@ -10,6 +10,7 @@ + diff --git a/src/Polly.Specs/ResilienceStrategyConversionExtensionsTests.cs b/src/Polly.Specs/ResilienceStrategyConversionExtensionsTests.cs new file mode 100644 index 00000000000..d4228ba087b --- /dev/null +++ b/src/Polly.Specs/ResilienceStrategyConversionExtensionsTests.cs @@ -0,0 +1,188 @@ +using Polly.Strategy; +using Polly.TestUtils; + +namespace Polly.Specs; + +public class ResilienceStrategyConversionExtensionsTests +{ + private static readonly ResiliencePropertyKey Incoming = new ResiliencePropertyKey("incoming-key"); + + private static readonly ResiliencePropertyKey Executing = new ResiliencePropertyKey("executing-key"); + + private static readonly ResiliencePropertyKey Outgoing = new ResiliencePropertyKey("outgoing-key"); + + private readonly TestResilienceStrategy _strategy; + private readonly ResilienceStrategy _genericStrategy; + private bool _isSynchronous; + private bool _isVoid; + private Type? _resultType; + + public ResilienceStrategyConversionExtensionsTests() + { + _strategy = new TestResilienceStrategy(); + _strategy.Before = (context, ct) => + { + context.IsVoid.Should().Be(_isVoid); + context.IsSynchronous.Should().Be(_isSynchronous); + context.Properties.Set(Outgoing, "outgoing-value"); + context.Properties.GetValue(Incoming, string.Empty).Should().Be("incoming-value"); + + if (_resultType != null) + { + context.ResultType.Should().Be(_resultType); + } + }; + + _genericStrategy = new ResilienceStrategyBuilder() + .AddStrategy(_strategy) + .Build(); + } + + [Fact] + public void AsSyncPolicy_Ok() + { + _isVoid = true; + _isSynchronous = true; + var context = new Context + { + [Incoming.Key] = "incoming-value" + }; + + _strategy.AsSyncPolicy().Execute(c => + { + context[Executing.Key] = "executing-value"; + }, + context); + + AssertContext(context); + } + + [Fact] + public void AsSyncPolicy_Generic_Ok() + { + _isVoid = false; + _isSynchronous = true; + _resultType = typeof(string); + var context = new Context + { + [Incoming.Key] = "incoming-value" + }; + + var result = _genericStrategy.AsSyncPolicy().Execute(c => { context[Executing.Key] = "executing-value"; return "dummy"; }, context); + AssertContext(context); + result.Should().Be("dummy"); + } + + [Fact] + public void AsSyncPolicy_Result_Ok() + { + _isVoid = false; + _isSynchronous = true; + _resultType = typeof(string); + var context = new Context + { + [Incoming.Key] = "incoming-value" + }; + + var result = _strategy.AsSyncPolicy().Execute(c => { context[Executing.Key] = "executing-value"; return "dummy"; }, context); + + AssertContext(context); + result.Should().Be("dummy"); + } + + [Fact] + public async Task AsAsyncPolicy_Ok() + { + _isVoid = true; + _isSynchronous = false; + var context = new Context + { + [Incoming.Key] = "incoming-value" + }; + + await _strategy.AsAsyncPolicy().ExecuteAsync(c => + { + context[Executing.Key] = "executing-value"; + return Task.CompletedTask; + }, + context); + + AssertContext(context); + } + + [Fact] + public async Task AsAsyncPolicy_Generic_Ok() + { + _isVoid = false; + _isSynchronous = false; + _resultType = typeof(string); + var context = new Context + { + [Incoming.Key] = "incoming-value" + }; + + var result = await _genericStrategy.AsAsyncPolicy().ExecuteAsync(c => + { + context[Executing.Key] = "executing-value"; + return Task.FromResult("dummy"); + }, + context); + AssertContext(context); + result.Should().Be("dummy"); + } + + [Fact] + public async Task AsAsyncPolicy_Result_Ok() + { + _isVoid = false; + _isSynchronous = false; + _resultType = typeof(string); + var context = new Context + { + [Incoming.Key] = "incoming-value" + }; + + var result = await _strategy.AsAsyncPolicy().ExecuteAsync(c => + { + context[Executing.Key] = "executing-value"; + return Task.FromResult("dummy"); + }, + context); + + AssertContext(context); + result.Should().Be("dummy"); + } + + [Fact] + public void RetryStrategy_AsSyncPolicy_Ok() + { + var policy = new ResilienceStrategyBuilder() + .AddRetry(new RetryStrategyOptions + { + ShouldRetry = _ => PredicateResult.True, + BackoffType = RetryBackoffType.Constant, + RetryCount = 5, + BaseDelay = TimeSpan.FromMilliseconds(1) + }) + .Build() + .AsSyncPolicy(); + + var tries = 0; + policy.Execute( + () => + { + tries++; + return "dummy"; + }) + .Should() + .Be("dummy"); + + tries.Should().Be(6); + } + + private static void AssertContext(Context context) + { + context[Outgoing.Key].Should().Be("outgoing-value"); + context[Executing.Key].Should().Be("executing-value"); + } +} diff --git a/src/Polly/Context.cs b/src/Polly/Context.cs index 61980296155..bb6e6147342 100644 --- a/src/Polly/Context.cs +++ b/src/Polly/Context.cs @@ -1,4 +1,4 @@ -namespace Polly; +namespace Polly; /// /// Context that carries with a single execution through a Policy. Commonly-used properties are directly on the class. Backed by a dictionary of string key / object value pairs, to which user-defined values may be added. @@ -37,7 +37,7 @@ public Context() /// /// A key unique to the call site of the current execution. /// Policy instances are commonly reused across multiple call sites. Set an OperationKey so that logging and metrics can distinguish usages of policy instances at different call sites. - /// The value is set by using the constructor taking an operationKey parameter. + /// The value is set by using the constructor taking an operationKey parameter. /// public string OperationKey { get; } diff --git a/src/Polly/ResilienceStrategyConversionExtensions.cs b/src/Polly/ResilienceStrategyConversionExtensions.cs new file mode 100644 index 00000000000..bd6c8a28538 --- /dev/null +++ b/src/Polly/ResilienceStrategyConversionExtensions.cs @@ -0,0 +1,46 @@ +using Polly.Utilities.Wrappers; +using Polly.Utils; + +namespace Polly; + +/// +/// Extensions for conversion of resilience strategies to policies. +/// +public static class ResilienceStrategyConversionExtensions +{ + /// + /// Converts a to an . + /// + /// The strategy instance. + /// An instance of . + /// Thrown when is . + public static IAsyncPolicy AsAsyncPolicy(this ResilienceStrategy strategy) + => new ResilienceStrategyAsyncPolicy(strategy ?? throw new ArgumentNullException(nameof(strategy))); + + /// + /// Converts a to an . + /// + /// The strategy instance. + /// An instance of . + /// Thrown when is . + public static IAsyncPolicy AsAsyncPolicy(this ResilienceStrategy strategy) + => new ResilienceStrategyAsyncPolicy(strategy ?? throw new ArgumentNullException(nameof(strategy))); + + /// + /// Converts a to an . + /// + /// The strategy instance. + /// An instance of . + /// Thrown when is . + public static ISyncPolicy AsSyncPolicy(this ResilienceStrategy strategy) + => new ResilienceStrategySyncPolicy(strategy ?? throw new ArgumentNullException(nameof(strategy))); + + /// + /// Converts a to an . + /// + /// The strategy instance. + /// An instance of . + /// Thrown when is . + public static ISyncPolicy AsSyncPolicy(this ResilienceStrategy strategy) + => new ResilienceStrategySyncPolicy(strategy ?? throw new ArgumentNullException(nameof(strategy))); +} diff --git a/src/Polly/Utilities/Wrappers/ResilienceContextFactory.cs b/src/Polly/Utilities/Wrappers/ResilienceContextFactory.cs new file mode 100644 index 00000000000..a22f72f1bc9 --- /dev/null +++ b/src/Polly/Utilities/Wrappers/ResilienceContextFactory.cs @@ -0,0 +1,42 @@ +namespace Polly.Utilities.Wrappers; + +internal static class ResilienceContextFactory +{ + public static readonly ResiliencePropertyKey ContextKey = new("Polly.Legacy.Context"); + + public static ResilienceContext Create(Context context, CancellationToken cancellationToken, bool continueOnCapturedContext) + { + var resilienceContext = ResilienceContext.Get(); + resilienceContext.CancellationToken = cancellationToken; + resilienceContext.ContinueOnCapturedContext = continueOnCapturedContext; + + foreach (var pair in context) + { + var props = (IDictionary)resilienceContext.Properties; + props.Add(pair.Key, pair.Value); + } + + resilienceContext.Properties.Set(ContextKey, context); + + return resilienceContext; + } + + public static void Restore(ResilienceContext context) + { + var originalContext = context.GetContext(); + + foreach (var pair in context.Properties) + { + if (pair.Key == ContextKey.Key) + { + continue; + } + + originalContext[pair.Key] = pair.Value; + } + + ResilienceContext.Return(context); + } + + public static Context GetContext(this ResilienceContext resilienceContext) => resilienceContext.Properties.GetValue(ContextKey, null!); +} diff --git a/src/Polly/Utilities/Wrappers/ResilienceStrategyAsyncPolicy.TResult.cs b/src/Polly/Utilities/Wrappers/ResilienceStrategyAsyncPolicy.TResult.cs new file mode 100644 index 00000000000..afe761ded94 --- /dev/null +++ b/src/Polly/Utilities/Wrappers/ResilienceStrategyAsyncPolicy.TResult.cs @@ -0,0 +1,28 @@ +namespace Polly.Utilities.Wrappers; + +internal sealed class ResilienceStrategyAsyncPolicy : AsyncPolicy +{ + private readonly ResilienceStrategy _strategy; + + public ResilienceStrategyAsyncPolicy(ResilienceStrategy strategy) => _strategy = strategy; + + protected sealed override async Task ImplementationAsync(Func> action, Context context, CancellationToken cancellationToken, bool continueOnCapturedContext) + { + var resilienceContext = ResilienceContextFactory.Create(context, cancellationToken, continueOnCapturedContext); + + try + { + return await _strategy.ExecuteAsync( + static async (context, state) => + { + return await state(context.GetContext(), context.CancellationToken).ConfigureAwait(context.ContinueOnCapturedContext); + }, + resilienceContext, + action).ConfigureAwait(continueOnCapturedContext); + } + finally + { + ResilienceContextFactory.Restore(resilienceContext); + } + } +} diff --git a/src/Polly/Utilities/Wrappers/ResilienceStrategyAsyncPolicy.cs b/src/Polly/Utilities/Wrappers/ResilienceStrategyAsyncPolicy.cs new file mode 100644 index 00000000000..de385a6067a --- /dev/null +++ b/src/Polly/Utilities/Wrappers/ResilienceStrategyAsyncPolicy.cs @@ -0,0 +1,56 @@ +namespace Polly.Utilities.Wrappers; + +internal sealed class ResilienceStrategyAsyncPolicy : AsyncPolicy +{ + private readonly ResilienceStrategy _strategy; + + public ResilienceStrategyAsyncPolicy(ResilienceStrategy strategy) => _strategy = strategy; + + protected sealed override async Task ImplementationAsync( + Func action, + Context context, + CancellationToken cancellationToken, + bool continueOnCapturedContext) + { + var resilienceContext = ResilienceContextFactory.Create(context, cancellationToken, continueOnCapturedContext); + + try + { + await _strategy.ExecuteAsync( + static async (context, state) => + { + await state(context.GetContext(), context.CancellationToken).ConfigureAwait(context.ContinueOnCapturedContext); + }, + resilienceContext, + action).ConfigureAwait(continueOnCapturedContext); + } + finally + { + ResilienceContextFactory.Restore(resilienceContext); + } + } + + protected override async Task ImplementationAsync( + Func> action, + Context context, + CancellationToken cancellationToken, + bool continueOnCapturedContext) + { + var resilienceContext = ResilienceContextFactory.Create(context, cancellationToken, continueOnCapturedContext); + + try + { + return await _strategy.ExecuteAsync( + static async (context, state) => + { + return await state(context.GetContext(), context.CancellationToken).ConfigureAwait(context.ContinueOnCapturedContext); + }, + resilienceContext, + action).ConfigureAwait(continueOnCapturedContext); + } + finally + { + ResilienceContextFactory.Restore(resilienceContext); + } + } +} diff --git a/src/Polly/Utilities/Wrappers/ResilienceStrategySyncPolicy.TResult.cs b/src/Polly/Utilities/Wrappers/ResilienceStrategySyncPolicy.TResult.cs new file mode 100644 index 00000000000..b77ba8a841e --- /dev/null +++ b/src/Polly/Utilities/Wrappers/ResilienceStrategySyncPolicy.TResult.cs @@ -0,0 +1,28 @@ +namespace Polly.Utilities.Wrappers; + +internal sealed class ResilienceStrategySyncPolicy : Policy +{ + private readonly ResilienceStrategy _strategy; + + public ResilienceStrategySyncPolicy(ResilienceStrategy strategy) => _strategy = strategy; + + protected sealed override TResult Implementation(Func action, Context context, CancellationToken cancellationToken) + { + var resilienceContext = ResilienceContextFactory.Create(context, cancellationToken, true); + + try + { + return _strategy.Execute( + static (context, state) => + { + return state(context.GetContext(), context.CancellationToken); + }, + resilienceContext, + action); + } + finally + { + ResilienceContextFactory.Restore(resilienceContext); + } + } +} diff --git a/src/Polly/Utilities/Wrappers/ResilienceStrategySyncPolicy.cs b/src/Polly/Utilities/Wrappers/ResilienceStrategySyncPolicy.cs new file mode 100644 index 00000000000..997d2280bd7 --- /dev/null +++ b/src/Polly/Utilities/Wrappers/ResilienceStrategySyncPolicy.cs @@ -0,0 +1,48 @@ +namespace Polly.Utilities.Wrappers; + +internal sealed class ResilienceStrategySyncPolicy : Policy +{ + private readonly ResilienceStrategy _strategy; + + public ResilienceStrategySyncPolicy(ResilienceStrategy strategy) => _strategy = strategy; + + protected override void Implementation(Action action, Context context, CancellationToken cancellationToken) + { + var resilienceContext = ResilienceContextFactory.Create(context, cancellationToken, true); + + try + { + _strategy.Execute( + static (context, state) => + { + state(context.GetContext(), context.CancellationToken); + }, + resilienceContext, + action); + } + finally + { + ResilienceContextFactory.Restore(resilienceContext); + } + } + + protected sealed override TResult Implementation(Func action, Context context, CancellationToken cancellationToken) + { + var resilienceContext = ResilienceContextFactory.Create(context, cancellationToken, true); + + try + { + return _strategy.Execute( + static (context, state) => + { + return state(context.GetContext(), context.CancellationToken); + }, + resilienceContext, + action); + } + finally + { + ResilienceContextFactory.Restore(resilienceContext); + } + } +}