Skip to content

Commit 66c5af2

Browse files
authored
ResilienceStrategyRegistry API improvements (#1388)
1 parent 20598ad commit 66c5af2

File tree

8 files changed

+189
-39
lines changed

8 files changed

+189
-39
lines changed

src/Polly.Core/Registry/ResilienceStrategyRegistry.TResult.cs

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -41,25 +41,29 @@ public bool TryGet(TKey key, [NotNullWhen(true)] out ResilienceStrategy<TResult>
4141

4242
if (_builders.TryGetValue(key, out var configure))
4343
{
44-
var context = new ConfigureBuilderContext<TKey>(key, _builderNameFormatter(key), _strategyKeyFormatter(key));
45-
46-
#if NETCOREAPP3_0_OR_GREATER
47-
strategy = _strategies.GetOrAdd(key, static (_, factory) =>
48-
{
49-
return new ResilienceStrategy<TResult>(CreateStrategy(factory.instance._activator, factory.context, factory.configure));
50-
},
51-
(instance: this, context, configure));
52-
#else
53-
strategy = _strategies.GetOrAdd(key, _ => new ResilienceStrategy<TResult>(CreateStrategy(_activator, context, configure)));
54-
#endif
55-
44+
strategy = GetOrAdd(key, configure);
5645
return true;
5746
}
5847

5948
strategy = null;
6049
return false;
6150
}
6251

52+
public ResilienceStrategy<TResult> GetOrAdd(TKey key, Action<ResilienceStrategyBuilder<TResult>, ConfigureBuilderContext<TKey>> configure)
53+
{
54+
var context = new ConfigureBuilderContext<TKey>(key, _builderNameFormatter(key), _strategyKeyFormatter(key));
55+
56+
#if NETCOREAPP3_0_OR_GREATER
57+
return _strategies.GetOrAdd(key, static (_, factory) =>
58+
{
59+
return new ResilienceStrategy<TResult>(CreateStrategy(factory.instance._activator, factory.context, factory.configure));
60+
},
61+
(instance: this, context, configure));
62+
#else
63+
return _strategies.GetOrAdd(key, _ => new ResilienceStrategy<TResult>(CreateStrategy(_activator, context, configure)));
64+
#endif
65+
}
66+
6367
public bool TryAddBuilder(TKey key, Action<ResilienceStrategyBuilder<TResult>, ConfigureBuilderContext<TKey>> configure) => _builders.TryAdd(key, configure);
6468

6569
public bool RemoveBuilder(TKey key) => _builders.TryRemove(key, out _);

src/Polly.Core/Registry/ResilienceStrategyRegistry.cs

Lines changed: 70 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -118,24 +118,83 @@ public override bool TryGetStrategy(TKey key, [NotNullWhen(true)] out Resilience
118118

119119
if (_builders.TryGetValue(key, out var configure))
120120
{
121-
var context = new ConfigureBuilderContext<TKey>(key, _builderNameFormatter(key), _strategyKeyFormatter(key));
122-
123-
#if NETCOREAPP3_0_OR_GREATER
124-
strategy = _strategies.GetOrAdd(key, static (_, factory) =>
125-
{
126-
return CreateStrategy(factory.instance._activator, factory.context, factory.configure);
127-
},
128-
(instance: this, context, configure));
129-
#else
130-
strategy = _strategies.GetOrAdd(key, _ => CreateStrategy(_activator, context, configure));
131-
#endif
121+
strategy = GetOrAddStrategy(key, configure);
132122
return true;
133123
}
134124

135125
strategy = null;
136126
return false;
137127
}
138128

129+
/// <summary>
130+
/// Gets existing strategy or creates a new one using the <paramref name="configure"/> callback.
131+
/// </summary>
132+
/// <param name="key">The key used to identify the resilience strategy.</param>
133+
/// <param name="configure">The callback that configures the strategy builder.</param>
134+
/// <returns>An instance of strategy.</returns>
135+
public ResilienceStrategy GetOrAddStrategy(TKey key, Action<ResilienceStrategyBuilder> configure)
136+
{
137+
Guard.NotNull(configure);
138+
139+
return GetOrAddStrategy(key, (builder, _) => configure(builder));
140+
}
141+
142+
/// <summary>
143+
/// Gets existing strategy or creates a new one using the <paramref name="configure"/> callback.
144+
/// </summary>
145+
/// <param name="key">The key used to identify the resilience strategy.</param>
146+
/// <param name="configure">The callback that configures the strategy builder.</param>
147+
/// <returns>An instance of strategy.</returns>
148+
public ResilienceStrategy GetOrAddStrategy(TKey key, Action<ResilienceStrategyBuilder, ConfigureBuilderContext<TKey>> configure)
149+
{
150+
Guard.NotNull(configure);
151+
152+
if (_strategies.TryGetValue(key, out var strategy))
153+
{
154+
return strategy;
155+
}
156+
157+
var context = new ConfigureBuilderContext<TKey>(key, _builderNameFormatter(key), _strategyKeyFormatter(key));
158+
159+
#if NETCOREAPP3_0_OR_GREATER
160+
return _strategies.GetOrAdd(key, static (_, factory) =>
161+
{
162+
return CreateStrategy(factory.instance._activator, factory.context, factory.configure);
163+
},
164+
(instance: this, context, configure));
165+
#else
166+
return _strategies.GetOrAdd(key, _ => CreateStrategy(_activator, context, configure));
167+
#endif
168+
}
169+
170+
/// <summary>
171+
/// Gets existing strategy or creates a new one using the <paramref name="configure"/> callback.
172+
/// </summary>
173+
/// <typeparam name="TResult">The type of result that the resilience strategy handles.</typeparam>
174+
/// <param name="key">The key used to identify the resilience strategy.</param>
175+
/// <param name="configure">The callback that configures the strategy builder.</param>
176+
/// <returns>An instance of strategy.</returns>
177+
public ResilienceStrategy<TResult> GetOrAddStrategy<TResult>(TKey key, Action<ResilienceStrategyBuilder<TResult>> configure)
178+
{
179+
Guard.NotNull(configure);
180+
181+
return GetOrAddStrategy<TResult>(key, (builder, _) => configure(builder));
182+
}
183+
184+
/// <summary>
185+
/// Gets existing strategy or creates a new one using the <paramref name="configure"/> callback.
186+
/// </summary>
187+
/// <typeparam name="TResult">The type of result that the resilience strategy handles.</typeparam>
188+
/// <param name="key">The key used to identify the resilience strategy.</param>
189+
/// <param name="configure">The callback that configures the strategy builder.</param>
190+
/// <returns>An instance of strategy.</returns>
191+
public ResilienceStrategy<TResult> GetOrAddStrategy<TResult>(TKey key, Action<ResilienceStrategyBuilder<TResult>, ConfigureBuilderContext<TKey>> configure)
192+
{
193+
Guard.NotNull(configure);
194+
195+
return GetGenericRegistry<TResult>().GetOrAdd(key, configure);
196+
}
197+
139198
/// <summary>
140199
/// Tries to add a resilience strategy builder to the registry.
141200
/// </summary>

src/Polly.Extensions/DependencyInjection/AddResilienceStrategyContext.cs

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
using System;
22
using Microsoft.Extensions.DependencyInjection;
33
using Microsoft.Extensions.Options;
4-
using Polly.Extensions.Utils;
4+
using Polly.Extensions.Registry;
55
using Polly.Registry;
66

77
namespace Polly.Extensions.DependencyInjection;
@@ -53,12 +53,7 @@ internal AddResilienceStrategyContext(ConfigureBuilderContext<TKey> registryCont
5353
/// 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.
5454
/// </para>
5555
/// </remarks>
56-
public void EnableReloads<TOptions>(string? name = null)
57-
{
58-
var monitor = ServiceProvider.GetRequiredService<IOptionsMonitor<TOptions>>();
59-
60-
RegistryContext.EnableReloads(() => new OptionsReloadHelper<TOptions>(monitor, name ?? Options.DefaultName).GetCancellationToken);
61-
}
56+
public void EnableReloads<TOptions>(string? name = null) => RegistryContext.EnableReloads(ServiceProvider.GetRequiredService<IOptionsMonitor<TOptions>>(), name);
6257

6358
/// <summary>
6459
/// Gets the options identified by <paramref name="name"/>.

src/Polly.Extensions/DependencyInjection/PollyServiceCollectionExtensions.cs

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,13 @@ public static class PollyServiceCollectionExtensions
2525
/// <param name="configure">An action that configures the resilience strategy.</param>
2626
/// <returns>The updated <see cref="IServiceCollection"/> with the registered resilience strategy.</returns>
2727
/// <exception cref="InvalidOperationException">Thrown if the resilience strategy builder with the provided key has already been added to the registry.</exception>
28+
/// <exception cref="ArgumentNullException">Thrown when <paramref name="services"/> or <paramref name="configure"/> is <see langword="null"/>.</exception>
2829
/// <remarks>
2930
/// You can retrieve the registered strategy by resolving the <see cref="ResilienceStrategyProvider{TKey}"/> class from the dependency injection container.
3031
/// <para>
3132
/// This call enables the telemetry for the registered resilience strategy.
3233
/// </para>
3334
/// </remarks>
34-
/// <exception cref="ArgumentNullException">Thrown when <paramref name="services"/> or <paramref name="configure"/> is <see langword="null"/>.</exception>
3535
public static IServiceCollection AddResilienceStrategy<TKey, TResult>(
3636
this IServiceCollection services,
3737
TKey key,
@@ -54,13 +54,13 @@ public static IServiceCollection AddResilienceStrategy<TKey, TResult>(
5454
/// <param name="configure">An action that configures the resilience strategy.</param>
5555
/// <returns>The updated <see cref="IServiceCollection"/> with the registered resilience strategy.</returns>
5656
/// <exception cref="InvalidOperationException">Thrown if the resilience strategy builder with the provided key has already been added to the registry.</exception>
57+
/// <exception cref="ArgumentNullException">Thrown when <paramref name="services"/> or <paramref name="configure"/> is <see langword="null"/>.</exception>
5758
/// <remarks>
5859
/// You can retrieve the registered strategy by resolving the <see cref="ResilienceStrategyProvider{TKey}"/> class from the dependency injection container.
5960
/// <para>
6061
/// This call enables the telemetry for the registered resilience strategy.
6162
/// </para>
6263
/// </remarks>
63-
/// <exception cref="ArgumentNullException">Thrown when <paramref name="services"/> or <paramref name="configure"/> is <see langword="null"/>.</exception>
6464
public static IServiceCollection AddResilienceStrategy<TKey, TResult>(
6565
this IServiceCollection services,
6666
TKey key,
@@ -85,7 +85,7 @@ public static IServiceCollection AddResilienceStrategy<TKey, TResult>(
8585
});
8686
});
8787

88-
return AddResilienceStrategyRegistry<TKey>(services);
88+
return AddResilienceStrategy<TKey>(services);
8989
}
9090

9191
/// <summary>
@@ -97,13 +97,13 @@ public static IServiceCollection AddResilienceStrategy<TKey, TResult>(
9797
/// <param name="configure">An action that configures the resilience strategy.</param>
9898
/// <returns>The updated <see cref="IServiceCollection"/> with the registered resilience strategy.</returns>
9999
/// <exception cref="InvalidOperationException">Thrown if the resilience strategy builder with the provided key has already been added to the registry.</exception>
100+
/// <exception cref="ArgumentNullException">Thrown when <paramref name="services"/> or <paramref name="configure"/> is <see langword="null"/>.</exception>
100101
/// <remarks>
101102
/// You can retrieve the registered strategy by resolving the <see cref="ResilienceStrategyProvider{TKey}"/> class from the dependency injection container.
102103
/// <para>
103104
/// This call enables the telemetry for the registered resilience strategy.
104105
/// </para>
105106
/// </remarks>
106-
/// <exception cref="ArgumentNullException">Thrown when <paramref name="services"/> or <paramref name="configure"/> is <see langword="null"/>.</exception>
107107
public static IServiceCollection AddResilienceStrategy<TKey>(
108108
this IServiceCollection services,
109109
TKey key,
@@ -125,13 +125,13 @@ public static IServiceCollection AddResilienceStrategy<TKey>(
125125
/// <param name="configure">An action that configures the resilience strategy.</param>
126126
/// <returns>The updated <see cref="IServiceCollection"/> with the registered resilience strategy.</returns>
127127
/// <exception cref="InvalidOperationException">Thrown if the resilience strategy builder with the provided key has already been added to the registry.</exception>
128+
/// <exception cref="ArgumentNullException">Thrown when <paramref name="services"/> or <paramref name="configure"/> is <see langword="null"/>.</exception>
128129
/// <remarks>
129130
/// You can retrieve the registered strategy by resolving the <see cref="ResilienceStrategyProvider{TKey}"/> class from the dependency injection container.
130131
/// <para>
131132
/// This call enables the telemetry for the registered resilience strategy.
132133
/// </para>
133134
/// </remarks>
134-
/// <exception cref="ArgumentNullException">Thrown when <paramref name="services"/> or <paramref name="configure"/> is <see langword="null"/>.</exception>
135135
public static IServiceCollection AddResilienceStrategy<TKey>(
136136
this IServiceCollection services,
137137
TKey key,
@@ -156,12 +156,28 @@ public static IServiceCollection AddResilienceStrategy<TKey>(
156156
});
157157
});
158158

159-
return AddResilienceStrategyRegistry<TKey>(services);
159+
return AddResilienceStrategy<TKey>(services);
160160
}
161161

162-
private static IServiceCollection AddResilienceStrategyRegistry<TKey>(this IServiceCollection services)
162+
/// <summary>
163+
/// Adds the infrastructure that allows configuring and retrieving resilience strategies using the <typeparamref name="TKey"/> key.
164+
/// </summary>
165+
/// <typeparam name="TKey">The type of the key used to identify the resilience strategy.</typeparam>
166+
/// <param name="services">The <see cref="IServiceCollection"/> to add the resilience strategy to.</param>
167+
/// <returns>The updated <see cref="IServiceCollection"/> with additional services added.</returns>
168+
/// <exception cref="ArgumentNullException">Thrown when <paramref name="services"/> is <see langword="null"/>.</exception>
169+
/// <remarks>
170+
/// You can retrieve the strategy registry by resolving the <see cref="ResilienceStrategyProvider{TKey}"/>
171+
/// or <see cref="ResilienceStrategyRegistry{TKey}"/> class from the dependency injection container.
172+
/// <para>
173+
/// This call enables telemetry for all resilience strategies created using <see cref="ResilienceStrategyRegistry{TKey}"/>.
174+
/// </para>
175+
/// </remarks>
176+
public static IServiceCollection AddResilienceStrategy<TKey>(this IServiceCollection services)
163177
where TKey : notnull
164178
{
179+
Guard.NotNull(services);
180+
165181
// check marker to ensure the APIs bellow are called only once for each TKey type
166182
// this prevents polluting the service collection with unnecessary Configure calls
167183
if (services.Contains(RegistryMarker<TKey>.ServiceDescriptor))
@@ -172,7 +188,7 @@ private static IServiceCollection AddResilienceStrategyRegistry<TKey>(this IServ
172188
services.AddOptions();
173189
services.Add(RegistryMarker<TKey>.ServiceDescriptor);
174190
services.AddResilienceStrategyBuilder();
175-
services.AddResilienceStrategyRegistry<TKey>();
191+
services.AddResilienceStrategy<TKey>();
176192

177193
services.TryAddSingleton(serviceProvider =>
178194
{

src/Polly.Extensions/Polly.Extensions.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,6 @@
2929
<PackageReference Include="Microsoft.Extensions.Options" />
3030
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
3131
<PackageReference Include="System.Diagnostics.DiagnosticSource" />
32-
<Reference Include="System.Net.Http" Condition="'$(TargetFrameworkIdentifier)' == '.NETFramework'"/>
32+
<Reference Include="System.Net.Http" Condition="'$(TargetFrameworkIdentifier)' == '.NETFramework'" />
3333
</ItemGroup>
3434
</Project>
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
using Microsoft.Extensions.Options;
2+
using Polly.Extensions.Utils;
3+
using Polly.Registry;
4+
using Polly.Utils;
5+
6+
namespace Polly.Extensions.Registry;
7+
8+
/// <summary>
9+
/// Extensions for <see cref="ConfigureBuilderContext{TKey}"/>.
10+
/// </summary>
11+
public static class ConfigureBuilderContextExtensions
12+
{
13+
/// <summary>
14+
/// Enables dynamic reloading of the resilience strategy whenever the <typeparamref name="TOptions"/> options are changed.
15+
/// </summary>
16+
/// <typeparam name="TKey">The type of the key used to identify the resilience strategy.</typeparam>
17+
/// <typeparam name="TOptions">The options type to listen to.</typeparam>
18+
/// <param name="context">The builder context.</param>
19+
/// <param name="optionsMonitor">The options monitor.</param>
20+
/// <param name="name">The named options, if any.</param>
21+
/// <remarks>
22+
/// You can decide based on the <paramref name="name"/> to listen for changes in global options or named options.
23+
/// If <paramref name="name"/> is <see langword="null"/> then the global options are listened to.
24+
/// <para>
25+
/// 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.
26+
/// </para>
27+
/// </remarks>
28+
public static void EnableReloads<TKey, TOptions>(this ConfigureBuilderContext<TKey> context, IOptionsMonitor<TOptions> optionsMonitor, string? name = null)
29+
where TKey : notnull
30+
{
31+
Guard.NotNull(context);
32+
Guard.NotNull(optionsMonitor);
33+
34+
context.EnableReloads(() => new OptionsReloadHelper<TOptions>(optionsMonitor, name ?? Options.DefaultName).GetCancellationToken);
35+
}
36+
}

test/Polly.Core.Tests/Registry/ResilienceStrategyRegistryTests.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using Polly.Registry;
44
using Polly.Retry;
55
using Polly.Telemetry;
6+
using Polly.Timeout;
67

78
namespace Polly.Core.Tests.Registry;
89

@@ -429,5 +430,34 @@ public void EnableReloads_Generic_Ok()
429430
tries.Should().Be(retryCount + 1);
430431
}
431432

433+
[Fact]
434+
public void GetOrAddStrategy_Ok()
435+
{
436+
var id = new StrategyId(typeof(string), "A");
437+
var called = 0;
438+
439+
var registry = CreateRegistry();
440+
var strategy = registry.GetOrAddStrategy(id, builder => { builder.AddTimeout(TimeSpan.FromSeconds(1)); called++; });
441+
var otherStrategy = registry.GetOrAddStrategy(id, builder => { builder.AddTimeout(TimeSpan.FromSeconds(1)); called++; });
442+
443+
strategy.Should().BeOfType<TimeoutResilienceStrategy>();
444+
strategy.Should().BeSameAs(otherStrategy);
445+
called.Should().Be(1);
446+
}
447+
448+
[Fact]
449+
public void GetOrAddStrategy_Generic_Ok()
450+
{
451+
var id = new StrategyId(typeof(string), "A");
452+
var called = 0;
453+
454+
var registry = CreateRegistry();
455+
var strategy = registry.GetOrAddStrategy<string>(id, builder => { builder.AddTimeout(TimeSpan.FromSeconds(1)); called++; });
456+
var otherStrategy = registry.GetOrAddStrategy<string>(id, builder => { builder.AddTimeout(TimeSpan.FromSeconds(1)); called++; });
457+
458+
strategy.Strategy.Should().BeOfType<TimeoutResilienceStrategy>();
459+
strategy.Should().BeSameAs(otherStrategy);
460+
}
461+
432462
private ResilienceStrategyRegistry<StrategyId> CreateRegistry() => new(_options);
433463
}

test/Polly.Extensions.Tests/DependencyInjection/PollyServiceCollectionExtensionTests.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,16 @@ public void AddResilienceStrategy_Multiple_Ok()
254254
.HaveCount(30);
255255
}
256256

257+
[Fact]
258+
public void AddResilienceStrategyInfra_Ok()
259+
{
260+
var provider = new ServiceCollection().AddResilienceStrategy<string>().BuildServiceProvider();
261+
262+
provider.GetRequiredService<ResilienceStrategyRegistry<string>>().Should().NotBeNull();
263+
provider.GetRequiredService<ResilienceStrategyProvider<string>>().Should().NotBeNull();
264+
provider.GetRequiredService<ResilienceStrategyBuilder>().DiagnosticSource.Should().NotBeNull();
265+
}
266+
257267
private void AddResilienceStrategy(string key, Action<ResilienceStrategyBuilderContext>? onBuilding = null)
258268
{
259269
_services.AddResilienceStrategy(key, builder =>

0 commit comments

Comments
 (0)