diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 3bc9ad43e50..750a03c4571 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -18,4 +18,7 @@ updates: # Ignore the libraries which are pinned - dependency-name: "System.ComponentModel.Annotations" - dependency-name: "System.Threading.Tasks.Extensions" - - dependency-name: "System.ValueTuplens" + - dependency-name: "System.ValueTuple" + - dependency-name: "Microsoft.Extensions.Options" + - dependency-name: "Microsoft.Extensions.Logging.Abstractions" + - dependency-name: "System.Diagnostics.DiagnosticSource" diff --git a/build.cake b/build.cake index de503ef98d5..e3345dd6288 100644 --- a/build.cake +++ b/build.cake @@ -216,6 +216,7 @@ Task("__RunMutationTests") .Does(() => { TestProject(File("./src/Polly.Core/Polly.Core.csproj"), File("./src/Polly.Core.Tests/Polly.Core.Tests.csproj"), "Polly.Core.csproj"); + TestProject(File("./src/Polly.Extensions/Polly.Extensions.csproj"), File("./src/Polly.Extensions.Tests/Polly.Extensions.Tests.csproj"), "Polly.Extensions.csproj"); TestProject(File("./src/Polly/Polly.csproj"), File("./src/Polly.Specs/Polly.Specs.csproj"), "Polly.csproj"); void TestProject(FilePath proj, FilePath testProj, string project) @@ -264,6 +265,9 @@ Task("__CreateSignedNuGetPackages") Information("Building Polly.{0}.nupkg", nugetVersion); DotNetPack(System.IO.Path.Combine(srcDir, "Polly", "Polly.csproj"), dotNetPackSettings); + + Information("Building Polly.Extensions.{0}.nupkg", nugetVersion); + DotNetPack(System.IO.Path.Combine(srcDir, "Polly.Extensions", "Polly.Extensions.csproj"), dotNetPackSettings); }); ////////////////////////////////////////////////////////////////////// diff --git a/eng/Common.targets b/eng/Common.targets index e4b94f0dba5..3883e0b8f0d 100644 --- a/eng/Common.targets +++ b/eng/Common.targets @@ -14,7 +14,6 @@ - + - diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 26be7691315..6c7f5f4d342 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -6,6 +6,7 @@ + @@ -13,9 +14,22 @@ + - + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Polly.Core.Tests/Builder/ResilienceStrategyBuilderOptionsTests.cs b/src/Polly.Core.Tests/Builder/ResilienceStrategyBuilderOptionsTests.cs index 5564e29921d..1b7b1952816 100644 --- a/src/Polly.Core.Tests/Builder/ResilienceStrategyBuilderOptionsTests.cs +++ b/src/Polly.Core.Tests/Builder/ResilienceStrategyBuilderOptionsTests.cs @@ -1,3 +1,4 @@ +using Moq; using Polly.Builder; using Polly.Telemetry; using Polly.Utils; @@ -16,4 +17,25 @@ public void Ctor_EnsureDefaults() options.TimeProvider.Should().Be(TimeProvider.System); options.TelemetryFactory.Should().Be(NullResilienceTelemetryFactory.Instance); } + + [Fact] + public void Ctor_Copy_EnsureCopied() + { + var options = new ResilienceStrategyBuilderOptions + { + BuilderName = "test", + TelemetryFactory = Mock.Of(), + TimeProvider = new FakeTimeProvider().Object + }; + + options.Properties.Set(new ResiliencePropertyKey("A"), 1); + options.Properties.Set(new ResiliencePropertyKey("B"), 2); + + var other = new ResilienceStrategyBuilderOptions(options); + + other.BuilderName.Should().Be("test"); + other.TelemetryFactory.Should().BeSameAs(options.TelemetryFactory); + other.TimeProvider.Should().BeSameAs(options.TimeProvider); + other.Properties.Should().BeEquivalentTo(options.Properties); + } } diff --git a/src/Polly.Core.Tests/Utils/TimeProviderExtensionsTests.cs b/src/Polly.Core.Tests/Utils/TimeProviderExtensionsTests.cs index b85d0b5dceb..0335a4de968 100644 --- a/src/Polly.Core.Tests/Utils/TimeProviderExtensionsTests.cs +++ b/src/Polly.Core.Tests/Utils/TimeProviderExtensionsTests.cs @@ -38,12 +38,28 @@ await TestUtils.AssertWithTimeoutAsync(async () => } } + [Fact] + public async Task DelayAsync_SystemSynchronous_Ok() + { + var delay = TimeSpan.FromMilliseconds(5); + var context = ResilienceContext.Get(); + context.Initialize(isSynchronous: true); + + await TestUtils.AssertWithTimeoutAsync(async () => + { + var watch = Stopwatch.StartNew(); + await TimeProvider.System.DelayAsync(delay, context); + var elapsed = watch.Elapsed; + elapsed.Should().BeGreaterThanOrEqualTo(delay); + }); + } + [InlineData(false, false)] [InlineData(false, true)] [InlineData(true, false)] [InlineData(true, true)] [Theory] - public async Task DelayAsync_CancellationRequestedbefore_Throws(bool synchronous, bool mocked) + public async Task DelayAsync_CancellationRequestedBefore_Throws(bool synchronous, bool mocked) { using var tcs = new CancellationTokenSource(); tcs.Cancel(); diff --git a/src/Polly.Core/Builder/ResilienceStrategyBuilderOptions.cs b/src/Polly.Core/Builder/ResilienceStrategyBuilderOptions.cs index eb514f0e30e..c0eabecc027 100644 --- a/src/Polly.Core/Builder/ResilienceStrategyBuilderOptions.cs +++ b/src/Polly.Core/Builder/ResilienceStrategyBuilderOptions.cs @@ -1,3 +1,4 @@ +using System.ComponentModel; using System.ComponentModel.DataAnnotations; using Polly.Telemetry; @@ -8,6 +9,34 @@ namespace Polly.Builder; /// public class ResilienceStrategyBuilderOptions { + /// + /// Initializes a new instance of the class. + /// + public ResilienceStrategyBuilderOptions() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The options to copy the values from. + [EditorBrowsable(EditorBrowsableState.Never)] + public ResilienceStrategyBuilderOptions(ResilienceStrategyBuilderOptions other) + { + Guard.NotNull(other); + + BuilderName = other.BuilderName; + TelemetryFactory = other.TelemetryFactory; + TimeProvider = other.TimeProvider; + + IDictionary props = Properties; + + foreach (KeyValuePair pair in other.Properties) + { + props.Add(pair.Key, pair.Value); + } + } + /// /// Gets or sets the name of the builder. /// diff --git a/src/Polly.Core/Polly.Core.csproj b/src/Polly.Core/Polly.Core.csproj index 6c649319ef2..74de2dfa1f0 100644 --- a/src/Polly.Core/Polly.Core.csproj +++ b/src/Polly.Core/Polly.Core.csproj @@ -1,7 +1,7 @@  - net7.0;net6.0;netstandard2.0;net472;net461 + net7.0;net6.0;netstandard2.0;net472;net462 Polly.Core Polly enable @@ -11,6 +11,7 @@ true 100 true + true diff --git a/src/Polly.Extensions.Tests/DependencyInjection/PollyDependencyInjectionKeysTests.cs b/src/Polly.Extensions.Tests/DependencyInjection/PollyDependencyInjectionKeysTests.cs new file mode 100644 index 00000000000..a85028dfcfc --- /dev/null +++ b/src/Polly.Extensions.Tests/DependencyInjection/PollyDependencyInjectionKeysTests.cs @@ -0,0 +1,12 @@ +using Polly.Extensions.DependencyInjection; + +namespace Polly.Extensions.Tests.DependencyInjection; + +public class PollyDependencyInjectionKeysTests +{ + [Fact] + public void ServiceProvider_Ok() + { + PollyDependencyInjectionKeys.ServiceProvider.Key.Should().Be("Polly.DependencyInjection.ServiceProvider"); + } +} diff --git a/src/Polly.Extensions.Tests/DependencyInjection/PollyServiceCollectionExtensionTests.cs b/src/Polly.Extensions.Tests/DependencyInjection/PollyServiceCollectionExtensionTests.cs new file mode 100644 index 00000000000..ddb785911d5 --- /dev/null +++ b/src/Polly.Extensions.Tests/DependencyInjection/PollyServiceCollectionExtensionTests.cs @@ -0,0 +1,208 @@ +using System.Globalization; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Polly.Builder; +using Polly.Extensions.DependencyInjection; +using Polly.Registry; +using Polly.Telemetry; + +namespace Polly.Extensions.Tests.DependencyInjection; + +public class PollyServiceCollectionExtensionTests +{ + private const string Key = "my-strategy"; + private ServiceCollection _services; + + public PollyServiceCollectionExtensionTests() => _services = new ServiceCollection(); + + [Fact] + public void AddResilienceStrategy_ArgValidation() + { + _services = null!; + Assert.Throws(() => AddResilienceStrategy(Key)); + + _services = new ServiceCollection(); + Assert.Throws(() => _services.AddResilienceStrategy(Key, null!)); + } + + [Fact] + public void AddResilienceStrategy_EnsureRegisteredServices() + { + AddResilienceStrategy(Key); + + var serviceProvider = _services.BuildServiceProvider(); + + serviceProvider.GetServices().Should().NotBeNull(); + serviceProvider.GetServices>().Should().NotBeNull(); + serviceProvider.GetServices>().Should().NotBeNull(); + serviceProvider.GetServices().Should().NotBeSameAs(serviceProvider.GetServices()); + } + + [Fact] + public void AddResilienceStrategy_MultipleRegistries_Ok() + { + AddResilienceStrategy(Key); + _services.AddResilienceStrategy(10, context => context.Builder.AddStrategy(new TestStrategy())); + + var serviceProvider = _services.BuildServiceProvider(); + + serviceProvider.GetRequiredService>().Get(Key).Should().NotBeNull(); + serviceProvider.GetRequiredService>().Get(10).Should().NotBeNull(); + } + + [Fact] + public void AddResilienceStrategy_EnsureContextFilled() + { + var asserted = false; + + _services.AddResilienceStrategy(Key, context => + { + context.Key.Should().Be(Key); + context.Builder.Should().NotBeNull(); + context.ServiceProvider.Should().NotBeNull(); + context.Builder.AddStrategy(new TestStrategy()); + asserted = true; + }); + + CreateProvider().Get(Key); + + asserted.Should().BeTrue(); + } + + [Fact] + public void AddResilienceStrategy_EnsureResilienceStrategyBuilderOptionsApplied() + { + var telemetry = Mock.Of(); + var telemetryFactory = Mock.Of(v => v.Create(It.IsAny()) == telemetry); + var asserted = false; + var key = new ResiliencePropertyKey("A"); + ResilienceStrategyBuilderOptions? globalOptions = null; + + _services.Configure(options => + { + options.BuilderName = "dummy"; + options.TelemetryFactory = telemetryFactory; + options.Properties.Set(key, 123); + globalOptions = options; + }); + + AddResilienceStrategy(Key, context => + { + context.BuilderProperties.Should().NotBeSameAs(globalOptions!.Properties); + context.BuilderName.Should().Be("dummy"); + context.Telemetry.Should().Be(telemetry); + context.BuilderProperties.TryGetValue(key, out var val).Should().BeTrue(); + val.Should().Be(123); + asserted = true; + }); + + CreateProvider().Get(Key); + + asserted.Should().BeTrue(); + } + + [Fact] + public void AddResilienceStrategy_EnsureServicesNotAddedTwice() + { + AddResilienceStrategy(Key); + var count = _services.Count; + + AddResilienceStrategy(Key); + + _services.Count.Should().Be(count + 1); + } + + [Fact] + public void AddResilienceStrategy_Single_Ok() + { + AddResilienceStrategy(Key); + + var provider = CreateProvider(); + + var strategy = provider.Get(Key); + strategy.Should().BeOfType(); + provider.Get("my-strategy").Should().BeSameAs(provider.Get("my-strategy")); + } + + [Fact] + public void AddResilienceStrategy_Twice_LastOneWins() + { + var firstCalled = false; + var secondCalled = false; + + AddResilienceStrategy(Key, _ => firstCalled = true); + AddResilienceStrategy(Key, _ => secondCalled = true); + + CreateProvider().Get(Key); + + firstCalled.Should().BeFalse(); + secondCalled.Should().BeTrue(); + } + + [Fact] + public void AddResilienceStrategy_Multiple_Ok() + { + for (var i = 0; i < 10; i++) + { + AddResilienceStrategy(i.ToString(CultureInfo.InvariantCulture)); + } + + var provider = CreateProvider(); + + Enumerable.Range(0, 10).Select(i => i.ToString(CultureInfo.InvariantCulture)).Distinct().Should().HaveCount(10); + } + + [Fact] + public void AddResilienceStrategy_CustomTelemetryFactory_EnsureUsed() + { + var telemetry = new Mock(MockBehavior.Strict); + var factory = new Mock(MockBehavior.Strict); + factory.Setup(v => v.Create(It.IsAny())).Returns(telemetry.Object); + + var asserted = false; + + _services.AddSingleton(factory.Object); + _services.AddResilienceStrategy( + Key, + context => + { + context.Builder.Options.TelemetryFactory.Should().Be(factory.Object); + context.Builder.AddStrategy(context => + { + context.Telemetry.Should().Be(telemetry.Object); + + asserted = true; + return new TestStrategy(); + }); + }); + + CreateProvider().Get(Key); + + asserted.Should().BeTrue(); + } + + private void AddResilienceStrategy(string key, Action? onBuilding = null) + { + _services.AddResilienceStrategy(key, context => + { + context.Builder.AddStrategy(context => + { + onBuilding?.Invoke(context); + return new TestStrategy(); + }); + }); + } + + private ResilienceStrategyProvider CreateProvider() + { + return _services.BuildServiceProvider().GetRequiredService>(); + } + + private class TestStrategy : ResilienceStrategy + { + protected override ValueTask ExecuteCoreAsync( + Func> callback, + ResilienceContext context, + TState state) => throw new NotSupportedException(); + } +} diff --git a/src/Polly.Extensions.Tests/Polly.Extensions.Tests.csproj b/src/Polly.Extensions.Tests/Polly.Extensions.Tests.csproj new file mode 100644 index 00000000000..2611628c5ef --- /dev/null +++ b/src/Polly.Extensions.Tests/Polly.Extensions.Tests.csproj @@ -0,0 +1,20 @@ + + + net7.0;net6.0 + $(TargetFrameworks);net481 + Test + true + enable + true + 100 + $(NoWarn);SA1600;SA1204 + [Polly.Extensions]* + + + + + + + + + diff --git a/src/Polly.Extensions/DependencyInjection/AddResilienceStrategyContext.cs b/src/Polly.Extensions/DependencyInjection/AddResilienceStrategyContext.cs new file mode 100644 index 00000000000..4f75fef7fa9 --- /dev/null +++ b/src/Polly.Extensions/DependencyInjection/AddResilienceStrategyContext.cs @@ -0,0 +1,33 @@ +using Polly.Builder; + +namespace Polly.Extensions.DependencyInjection; + +/// +/// Represents the context for adding a resilience strategy with the specified key. +/// +/// The type of the key used to identify the resilience strategy. +public sealed class AddResilienceStrategyContext + where TKey : notnull +{ + internal AddResilienceStrategyContext(TKey key, ResilienceStrategyBuilder builder, IServiceProvider serviceProvider) + { + Key = key; + Builder = builder; + ServiceProvider = serviceProvider; + } + + /// + /// Gets the key used to identify the resilience strategy. + /// + public TKey Key { get; } + + /// + /// Gets the used to build the resilience strategy. + /// + public ResilienceStrategyBuilder Builder { get; } + + /// + /// Gets the that provides access to the dependency injection container. + /// + public IServiceProvider ServiceProvider { get; } +} diff --git a/src/Polly.Extensions/DependencyInjection/ConfigureResilienceStrategyRegistryOptions.cs b/src/Polly.Extensions/DependencyInjection/ConfigureResilienceStrategyRegistryOptions.cs new file mode 100644 index 00000000000..d5d143f1493 --- /dev/null +++ b/src/Polly.Extensions/DependencyInjection/ConfigureResilienceStrategyRegistryOptions.cs @@ -0,0 +1,9 @@ +namespace Polly.Extensions.DependencyInjection; + +internal sealed class ConfigureResilienceStrategyRegistryOptions + where TKey : notnull +{ + public List Actions { get; } = new(); + + public record Entry(TKey Key, Action> Configure); +} diff --git a/src/Polly.Extensions/DependencyInjection/PollyDependencyInjectionKeys.cs b/src/Polly.Extensions/DependencyInjection/PollyDependencyInjectionKeys.cs new file mode 100644 index 00000000000..ac81a61f614 --- /dev/null +++ b/src/Polly.Extensions/DependencyInjection/PollyDependencyInjectionKeys.cs @@ -0,0 +1,12 @@ +namespace Polly.Extensions.DependencyInjection; + +/// +/// The resilience keys used in the dependency injection scenarios. +/// +public static class PollyDependencyInjectionKeys +{ + /// + /// The key used to store and access the from . + /// + public static readonly ResiliencePropertyKey ServiceProvider = new("Polly.DependencyInjection.ServiceProvider"); +} diff --git a/src/Polly.Extensions/DependencyInjection/PollyServiceCollectionExtensions.cs b/src/Polly.Extensions/DependencyInjection/PollyServiceCollectionExtensions.cs new file mode 100644 index 00000000000..295b68cf843 --- /dev/null +++ b/src/Polly.Extensions/DependencyInjection/PollyServiceCollectionExtensions.cs @@ -0,0 +1,126 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; +using Polly.Builder; +using Polly.Registry; +using Polly.Telemetry; +using Polly.Utils; + +namespace Polly.Extensions.DependencyInjection; + +/// +/// Provides extension methods for registering resilience strategies using the . +/// +public static class PollyServiceCollectionExtensions +{ + /// + /// Adds a resilience strategy to service collection. + /// + /// The type of the key used to identify the resilience strategy. + /// The to add the resilience strategy to. + /// The key used to identify the resilience strategy. + /// An action that configures the resilience strategy. + /// The updated with the registered resilience strategy. + /// Thrown if the resilience strategy builder with the provided key has already been added to the registry. + /// + /// You can retrieve the registered strategy by resolving the class from the dependency injection container. + /// + public static IServiceCollection AddResilienceStrategy( + this IServiceCollection services, + TKey key, + Action> configure) + where TKey : notnull + { + Guard.NotNull(services); + Guard.NotNull(configure); + + services.AddOptions(); + services.Configure>(options => + { + options.Actions.Add(new ConfigureResilienceStrategyRegistryOptions.Entry(key, configure)); + }); + + // check marker to ensure the APIs bellow are called only once for each TKey type + // this prevents polluting the service collection with unnecessary Configure calls + if (services.Contains(RegistryMarker.ServiceDescriptor)) + { + return services; + } + + services.Add(RegistryMarker.ServiceDescriptor); + services.AddResilienceStrategyBuilder(); + services.AddResilienceStrategyRegistry(); + + return services; + } + + private static IServiceCollection AddResilienceStrategyRegistry(this IServiceCollection services) + where TKey : notnull + { + services.TryAddSingleton(serviceProvider => + { + var options = serviceProvider.GetRequiredService>>().Value; + var configureActions = serviceProvider.GetRequiredService>>().Value.Actions; + var registry = new ResilienceStrategyRegistry(options); + + foreach (var entry in configureActions) + { + // the last added builder with the same key wins, this allows overriding the builders + registry.RemoveBuilder(entry.Key); + registry.TryAddBuilder(entry.Key, (key, builder) => + { + var context = new AddResilienceStrategyContext(key, builder, serviceProvider); + entry.Configure(context); + }); + } + + return registry; + }); + + services.TryAddSingleton>(serviceProvider => + { + return serviceProvider.GetRequiredService>(); + }); + + // configure options + services + .AddOptions>() + .Configure((options, serviceProvider) => + { + options.BuilderFactory = () => serviceProvider.GetRequiredService(); + }); + + return services; + } + + private static void AddResilienceStrategyBuilder(this IServiceCollection services) + { + services + .AddOptions() + .PostConfigure((options, serviceProvider) => + { + if (options.TelemetryFactory == NullResilienceTelemetryFactory.Instance && + serviceProvider.GetService() is ResilienceTelemetryFactory factory) + { + options.TelemetryFactory = factory; + } + + options.Properties.Set(PollyDependencyInjectionKeys.ServiceProvider, serviceProvider); + }); + + services.TryAddTransient(serviceProvider => + { + var globalOptions = serviceProvider.GetRequiredService>().Value; + + return new ResilienceStrategyBuilder + { + Options = new ResilienceStrategyBuilderOptions(globalOptions) + }; + }); + } + + private class RegistryMarker + { + public static readonly ServiceDescriptor ServiceDescriptor = ServiceDescriptor.Singleton(new RegistryMarker()); + } +} diff --git a/src/Polly.Extensions/Polly.Extensions.csproj b/src/Polly.Extensions/Polly.Extensions.csproj new file mode 100644 index 00000000000..b501fcc02ae --- /dev/null +++ b/src/Polly.Extensions/Polly.Extensions.csproj @@ -0,0 +1,31 @@ + + + net7.0;net6.0;netstandard2.0;net472;net462 + Polly.Extensions + Polly.Extensions + enable + true + Library + true + true + 100 + true + + + + + + + + + + + + + + + + + + + diff --git a/src/Polly.Extensions/README.md b/src/Polly.Extensions/README.md new file mode 100644 index 00000000000..a4eb701cf61 --- /dev/null +++ b/src/Polly.Extensions/README.md @@ -0,0 +1,33 @@ +# About Polly.Hosting + +The `Polly.Hosting` enables the following features: + + +- Integrates Polly with the standard `IServiceCollection` Dependency Injection (DI) container. +- Implements `ResilienceTelemetryFactory` that adds [logging](https://learn.microsoft.com/dotnet/core/extensions/logging?tabs=command-line) and [metering](https://learn.microsoft.com/dotnet/core/diagnostics/metrics) for all strategies created using the DI extension points. + +Example: + +``` csharp +var services = new ServiceCollection(); + +// Define your strategy +services.AddResilienceStrategy( + "my-key", + context => context.Builder.AddTimeout(TimeSpan.FromSeconds(10))); + +// Define your strategy using custom options +services.AddResilienceStrategy( + "my-timeout", + context => + { + var myOptions = context.ServiceProvider.GetRequiredService>().Value; + context.Builder.AddTimeout(myOptions.Timeout); + }); + +// Use your strategy +var serviceProvider = services.BuildServiceProvider(); +var strategyProvider = serviceProvider.GetRequiredService>(); +var resilienceStrategy = strategyProvider.Get("my-key"); +``` + diff --git a/src/Polly.sln b/src/Polly.sln index 3551a409021..9508b82d578 100644 --- a/src/Polly.sln +++ b/src/Polly.sln @@ -9,8 +9,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution ..\eng\Analyzers.targets = ..\eng\Analyzers.targets ..\build.cake = ..\build.cake ..\.github\workflows\build.yml = ..\.github\workflows\build.yml + ..\.github\dependabot.yml = ..\.github\dependabot.yml Directory.Build.props = Directory.Build.props Directory.Build.targets = Directory.Build.targets + Directory.Packages.props = Directory.Packages.props ..\GitVersionConfig.yaml = ..\GitVersionConfig.yaml ..\global.json = ..\global.json ..\README.md = ..\README.md @@ -37,6 +39,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "eng", "eng", "{04E3C7C5-31F EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Polly.Core.Benchmarks", "Polly.Core.Benchmarks\Polly.Core.Benchmarks.csproj", "{CC306C35-E3BC-4F0B-AB8C-B9D4C82DC3DE}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Polly.Extensions", "Polly.Extensions\Polly.Extensions.csproj", "{F2FDE6BF-DA86-4DDE-A55C-E2A064CD30D8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Polly.Extensions.Tests", "Polly.Extensions.Tests\Polly.Extensions.Tests.csproj", "{BB2843CA-B518-48A1-BAD9-B63238F21608}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -67,6 +73,14 @@ Global {CC306C35-E3BC-4F0B-AB8C-B9D4C82DC3DE}.Debug|Any CPU.Build.0 = Debug|Any CPU {CC306C35-E3BC-4F0B-AB8C-B9D4C82DC3DE}.Release|Any CPU.ActiveCfg = Release|Any CPU {CC306C35-E3BC-4F0B-AB8C-B9D4C82DC3DE}.Release|Any CPU.Build.0 = Release|Any CPU + {F2FDE6BF-DA86-4DDE-A55C-E2A064CD30D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F2FDE6BF-DA86-4DDE-A55C-E2A064CD30D8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F2FDE6BF-DA86-4DDE-A55C-E2A064CD30D8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F2FDE6BF-DA86-4DDE-A55C-E2A064CD30D8}.Release|Any CPU.Build.0 = Release|Any CPU + {BB2843CA-B518-48A1-BAD9-B63238F21608}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BB2843CA-B518-48A1-BAD9-B63238F21608}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BB2843CA-B518-48A1-BAD9-B63238F21608}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BB2843CA-B518-48A1-BAD9-B63238F21608}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Polly/Polly.csproj b/src/Polly/Polly.csproj index dd4d4255bb6..9cdd550c45b 100644 --- a/src/Polly/Polly.csproj +++ b/src/Polly/Polly.csproj @@ -1,7 +1,7 @@  - netstandard2.0;net472;net461; + netstandard2.0;net472;net462; Polly Library 70