From b0a6a38e352db1dfa477053a6135be57ecc7a8c2 Mon Sep 17 00:00:00 2001
From: martintmk <103487740+martintmk@users.noreply.github.com>
Date: Tue, 6 Jun 2023 12:26:04 +0200
Subject: [PATCH] Bridge between Polly V7 and Polly V8 (#1255)
---
src/Polly.Specs/Polly.Specs.csproj | 1 +
...lienceStrategyConversionExtensionsTests.cs | 188 ++++++++++++++++++
src/Polly/Context.cs | 4 +-
.../ResilienceStrategyConversionExtensions.cs | 46 +++++
.../Wrappers/ResilienceContextFactory.cs | 42 ++++
.../ResilienceStrategyAsyncPolicy.TResult.cs | 28 +++
.../Wrappers/ResilienceStrategyAsyncPolicy.cs | 56 ++++++
.../ResilienceStrategySyncPolicy.TResult.cs | 28 +++
.../Wrappers/ResilienceStrategySyncPolicy.cs | 48 +++++
9 files changed, 439 insertions(+), 2 deletions(-)
create mode 100644 src/Polly.Specs/ResilienceStrategyConversionExtensionsTests.cs
create mode 100644 src/Polly/ResilienceStrategyConversionExtensions.cs
create mode 100644 src/Polly/Utilities/Wrappers/ResilienceContextFactory.cs
create mode 100644 src/Polly/Utilities/Wrappers/ResilienceStrategyAsyncPolicy.TResult.cs
create mode 100644 src/Polly/Utilities/Wrappers/ResilienceStrategyAsyncPolicy.cs
create mode 100644 src/Polly/Utilities/Wrappers/ResilienceStrategySyncPolicy.TResult.cs
create mode 100644 src/Polly/Utilities/Wrappers/ResilienceStrategySyncPolicy.cs
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);
+ }
+ }
+}