Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 16 additions & 12 deletions src/Polly.Core/Registry/ResilienceStrategyRegistry.TResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,25 +41,29 @@ public bool TryGet(TKey key, [NotNullWhen(true)] out ResilienceStrategy<TResult>

if (_builders.TryGetValue(key, out var configure))
{
var context = new ConfigureBuilderContext<TKey>(key, _builderNameFormatter(key), _strategyKeyFormatter(key));

#if NETCOREAPP3_0_OR_GREATER
strategy = _strategies.GetOrAdd(key, static (_, factory) =>
{
return new ResilienceStrategy<TResult>(CreateStrategy(factory.instance._activator, factory.context, factory.configure));
},
(instance: this, context, configure));
#else
strategy = _strategies.GetOrAdd(key, _ => new ResilienceStrategy<TResult>(CreateStrategy(_activator, context, configure)));
#endif

strategy = GetOrAdd(key, configure);
return true;
}

strategy = null;
return false;
}

public ResilienceStrategy<TResult> GetOrAdd(TKey key, Action<ResilienceStrategyBuilder<TResult>, ConfigureBuilderContext<TKey>> configure)
{
var context = new ConfigureBuilderContext<TKey>(key, _builderNameFormatter(key), _strategyKeyFormatter(key));

#if NETCOREAPP3_0_OR_GREATER
return _strategies.GetOrAdd(key, static (_, factory) =>
{
return new ResilienceStrategy<TResult>(CreateStrategy(factory.instance._activator, factory.context, factory.configure));
},
(instance: this, context, configure));
#else
return _strategies.GetOrAdd(key, _ => new ResilienceStrategy<TResult>(CreateStrategy(_activator, context, configure)));
#endif
}

public bool TryAddBuilder(TKey key, Action<ResilienceStrategyBuilder<TResult>, ConfigureBuilderContext<TKey>> configure) => _builders.TryAdd(key, configure);

public bool RemoveBuilder(TKey key) => _builders.TryRemove(key, out _);
Expand Down
81 changes: 70 additions & 11 deletions src/Polly.Core/Registry/ResilienceStrategyRegistry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,24 +118,83 @@ public override bool TryGetStrategy(TKey key, [NotNullWhen(true)] out Resilience

if (_builders.TryGetValue(key, out var configure))
{
var context = new ConfigureBuilderContext<TKey>(key, _builderNameFormatter(key), _strategyKeyFormatter(key));

#if NETCOREAPP3_0_OR_GREATER
strategy = _strategies.GetOrAdd(key, static (_, factory) =>
{
return CreateStrategy(factory.instance._activator, factory.context, factory.configure);
},
(instance: this, context, configure));
#else
strategy = _strategies.GetOrAdd(key, _ => CreateStrategy(_activator, context, configure));
#endif
strategy = GetOrAddStrategy(key, configure);
return true;
}

strategy = null;
return false;
}

/// <summary>
/// Gets existing strategy or creates a new one using the <paramref name="configure"/> callback.
/// </summary>
/// <param name="key">The key used to identify the resilience strategy.</param>
/// <param name="configure">The callback that configures the strategy builder.</param>
/// <returns>An instance of strategy.</returns>
public ResilienceStrategy GetOrAddStrategy(TKey key, Action<ResilienceStrategyBuilder> configure)
{
Guard.NotNull(configure);

return GetOrAddStrategy(key, (builder, _) => configure(builder));
}

/// <summary>
/// Gets existing strategy or creates a new one using the <paramref name="configure"/> callback.
/// </summary>
/// <param name="key">The key used to identify the resilience strategy.</param>
/// <param name="configure">The callback that configures the strategy builder.</param>
/// <returns>An instance of strategy.</returns>
public ResilienceStrategy GetOrAddStrategy(TKey key, Action<ResilienceStrategyBuilder, ConfigureBuilderContext<TKey>> configure)
{
Guard.NotNull(configure);

if (_strategies.TryGetValue(key, out var strategy))
{
return strategy;
}

var context = new ConfigureBuilderContext<TKey>(key, _builderNameFormatter(key), _strategyKeyFormatter(key));

#if NETCOREAPP3_0_OR_GREATER
return _strategies.GetOrAdd(key, static (_, factory) =>
{
return CreateStrategy(factory.instance._activator, factory.context, factory.configure);
},
(instance: this, context, configure));
#else
return _strategies.GetOrAdd(key, _ => CreateStrategy(_activator, context, configure));
#endif
}

/// <summary>
/// Gets existing strategy or creates a new one using the <paramref name="configure"/> callback.
/// </summary>
/// <typeparam name="TResult">The type of result that the resilience strategy handles.</typeparam>
/// <param name="key">The key used to identify the resilience strategy.</param>
/// <param name="configure">The callback that configures the strategy builder.</param>
/// <returns>An instance of strategy.</returns>
public ResilienceStrategy<TResult> GetOrAddStrategy<TResult>(TKey key, Action<ResilienceStrategyBuilder<TResult>> configure)
{
Guard.NotNull(configure);

return GetOrAddStrategy<TResult>(key, (builder, _) => configure(builder));
}

/// <summary>
/// Gets existing strategy or creates a new one using the <paramref name="configure"/> callback.
/// </summary>
/// <typeparam name="TResult">The type of result that the resilience strategy handles.</typeparam>
/// <param name="key">The key used to identify the resilience strategy.</param>
/// <param name="configure">The callback that configures the strategy builder.</param>
/// <returns>An instance of strategy.</returns>
public ResilienceStrategy<TResult> GetOrAddStrategy<TResult>(TKey key, Action<ResilienceStrategyBuilder<TResult>, ConfigureBuilderContext<TKey>> configure)
{
Guard.NotNull(configure);

return GetGenericRegistry<TResult>().GetOrAdd(key, configure);
}

/// <summary>
/// Tries to add a resilience strategy builder to the registry.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Polly.Extensions.Utils;
using Polly.Extensions.Registry;
using Polly.Registry;

namespace Polly.Extensions.DependencyInjection;
Expand Down Expand Up @@ -53,12 +53,7 @@ internal AddResilienceStrategyContext(ConfigureBuilderContext<TKey> registryCont
/// You can listen for changes only for single options. If you call this method multiple times, the preceding calls are ignored and only the last one wins.
/// </para>
/// </remarks>
public void EnableReloads<TOptions>(string? name = null)
{
var monitor = ServiceProvider.GetRequiredService<IOptionsMonitor<TOptions>>();

RegistryContext.EnableReloads(() => new OptionsReloadHelper<TOptions>(monitor, name ?? Options.DefaultName).GetCancellationToken);
}
public void EnableReloads<TOptions>(string? name = null) => RegistryContext.EnableReloads(ServiceProvider.GetRequiredService<IOptionsMonitor<TOptions>>(), name);

/// <summary>
/// Gets the options identified by <paramref name="name"/>.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ public static IServiceCollection AddResilienceStrategy<TKey, TResult>(
});
});

return AddResilienceStrategyRegistry<TKey>(services);
return AddResilienceStrategy<TKey>(services);
}

/// <summary>
Expand Down Expand Up @@ -156,12 +156,29 @@ public static IServiceCollection AddResilienceStrategy<TKey>(
});
});

return AddResilienceStrategyRegistry<TKey>(services);
return AddResilienceStrategy<TKey>(services);
}

private static IServiceCollection AddResilienceStrategyRegistry<TKey>(this IServiceCollection services)
/// <summary>
/// Adds the infrastructure that allows configuring and retrieving resilience strategies using the <typeparamref name="TKey"/> key.
/// </summary>
/// <typeparam name="TKey">The type of the key used to identify the resilience strategy.</typeparam>
/// <param name="services">The <see cref="IServiceCollection"/> to add the resilience strategy to.</param>
/// <returns>The updated <see cref="IServiceCollection"/> with additional services added.</returns>
/// <exception cref="InvalidOperationException">Thrown if the resilience strategy builder with the provided key has already been added to the registry.</exception>
/// <remarks>
/// You can retrieve the strategy registry by resolving the <see cref="ResilienceStrategyProvider{TKey}"/>
/// or <see cref="ResilienceStrategyRegistry{TKey}"/> class from the dependency injection container.
/// <para>
/// This call enables the telemetry for al resilience strategies created using <see cref="ResilienceStrategyRegistry{TKey}"/>.
/// </para>
/// </remarks>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="services"/> is <see langword="null"/>.</exception>
public static IServiceCollection AddResilienceStrategy<TKey>(this IServiceCollection services)
where TKey : notnull
{
Guard.NotNull(services);

// 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<TKey>.ServiceDescriptor))
Expand All @@ -172,7 +189,7 @@ private static IServiceCollection AddResilienceStrategyRegistry<TKey>(this IServ
services.AddOptions();
services.Add(RegistryMarker<TKey>.ServiceDescriptor);
services.AddResilienceStrategyBuilder();
services.AddResilienceStrategyRegistry<TKey>();
services.AddResilienceStrategy<TKey>();

services.TryAddSingleton(serviceProvider =>
{
Expand Down
2 changes: 1 addition & 1 deletion src/Polly.Extensions/Polly.Extensions.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,6 @@
<PackageReference Include="Microsoft.Extensions.Options" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
<PackageReference Include="System.Diagnostics.DiagnosticSource" />
<Reference Include="System.Net.Http" Condition="'$(TargetFrameworkIdentifier)' == '.NETFramework'"/>
<Reference Include="System.Net.Http" Condition="'$(TargetFrameworkIdentifier)' == '.NETFramework'" />
</ItemGroup>
</Project>
36 changes: 36 additions & 0 deletions src/Polly.Extensions/Registry/ConfigureBuilderContextExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using Microsoft.Extensions.Options;
using Polly.Extensions.Utils;
using Polly.Registry;
using Polly.Utils;

namespace Polly.Extensions.Registry;

/// <summary>
/// Extensions for <see cref="ConfigureBuilderContext{TKey}"/>.
/// </summary>
public static class ConfigureBuilderContextExtensions
{
/// <summary>
/// Enables dynamic reloading of the resilience strategy whenever the <typeparamref name="TOptions"/> options are changed.
/// </summary>
/// <typeparam name="TKey">The type of the key used to identify the resilience strategy.</typeparam>
/// <typeparam name="TOptions">The options type to listen to.</typeparam>
/// <param name="context">The builder context.</param>
/// <param name="optionsMonitor">The options monitor.</param>
/// <param name="name">The named options, if any.</param>
/// <remarks>
/// You can decide based on the <paramref name="name"/> to listen for changes in global options or named options.
/// If <paramref name="name"/> is <see langword="null"/> then the global options are listened to.
/// <para>
/// You can listen for changes only for single options. If you call this method multiple times, the preceding calls are ignored and only the last one wins.
/// </para>
/// </remarks>
public static void EnableReloads<TKey, TOptions>(this ConfigureBuilderContext<TKey> context, IOptionsMonitor<TOptions> optionsMonitor, string? name = null)
where TKey : notnull
{
Guard.NotNull(context);
Guard.NotNull(optionsMonitor);

context.EnableReloads(() => new OptionsReloadHelper<TOptions>(optionsMonitor, name ?? Options.DefaultName).GetCancellationToken);
}
}
30 changes: 30 additions & 0 deletions test/Polly.Core.Tests/Registry/ResilienceStrategyRegistryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Polly.Registry;
using Polly.Retry;
using Polly.Telemetry;
using Polly.Timeout;

namespace Polly.Core.Tests.Registry;

Expand Down Expand Up @@ -429,5 +430,34 @@ public void EnableReloads_Generic_Ok()
tries.Should().Be(retryCount + 1);
}

[Fact]
public void GetOrAddStrategy_Ok()
{
var id = new StrategyId(typeof(string), "A");
var called = 0;

var registry = CreateRegistry();
var strategy = registry.GetOrAddStrategy(id, builder => { builder.AddTimeout(TimeSpan.FromSeconds(1)); called++; });
var otherStrategy = registry.GetOrAddStrategy(id, builder => { builder.AddTimeout(TimeSpan.FromSeconds(1)); called++; });

strategy.Should().BeOfType<TimeoutResilienceStrategy>();
strategy.Should().BeSameAs(otherStrategy);
called.Should().Be(1);
}

[Fact]
public void GetOrAddStrategy_Generic_Ok()
{
var id = new StrategyId(typeof(string), "A");
var called = 0;

var registry = CreateRegistry();
var strategy = registry.GetOrAddStrategy<string>(id, builder => { builder.AddTimeout(TimeSpan.FromSeconds(1)); called++; });
var otherStrategy = registry.GetOrAddStrategy<string>(id, builder => { builder.AddTimeout(TimeSpan.FromSeconds(1)); called++; });

strategy.Strategy.Should().BeOfType<TimeoutResilienceStrategy>();
strategy.Should().BeSameAs(otherStrategy);
}

private ResilienceStrategyRegistry<StrategyId> CreateRegistry() => new(_options);
}
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,16 @@ public void AddResilienceStrategy_Multiple_Ok()
.HaveCount(30);
}

[Fact]
public void AddResilienceStrategyInfra_Ok()
{
var provider = new ServiceCollection().AddResilienceStrategy<string>().BuildServiceProvider();

provider.GetRequiredService<ResilienceStrategyRegistry<string>>().Should().NotBeNull();
provider.GetRequiredService<ResilienceStrategyProvider<string>>().Should().NotBeNull();
provider.GetRequiredService<ResilienceStrategyBuilder>().DiagnosticSource.Should().NotBeNull();
}

private void AddResilienceStrategy(string key, Action<ResilienceStrategyBuilderContext>? onBuilding = null)
{
_services.AddResilienceStrategy(key, builder =>
Expand Down