Skip to content

Commit

Permalink
Added ConfigureResiliencePipelineRegistry<TKey> method to IServiceCol…
Browse files Browse the repository at this point in the history
…lection to allow deferred configuration of ResiliencePipelineRegistry<TKey> (App-vNext#1860)

* Added ConfigureResiliencePipelineRegistry<TKey> method to IServiceCollection to allow deferred configuration of ResiliencePipelineRegistry<TKey>

* Reworked per feedback from initial implementation

* Removed previous implementation that was restored when I synced the changes last.

* Added unit tests to ensure pipelines added correctly via new AddResiliencePipelines method

* Fixed several small formatting issues and added guards to stop user-provided delegates being null
  • Loading branch information
damienhoneyford authored Jan 2, 2024
1 parent 2e4fde1 commit 37107b0
Show file tree
Hide file tree
Showing 6 changed files with 249 additions and 22 deletions.
33 changes: 33 additions & 0 deletions docs/advanced/dependency-injection.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,39 @@ await pipeline.ExecuteAsync(
```
<!-- endSnippet -->

## Deferred addition of pipelines

If you want to use a key for a resilience pipeline that may not be available
immediately you can use the `AddResiliencePipelines()` method to defer adding
them until just prior to the `ResiliencePipelineProvider<TKey>` is instantiated
by the DI container, allowing the `IServiceProvider` to be used if required.

<!-- snippet: di-deferred-addition -->
```cs
services
.AddResiliencePipelines<string>((ctx) =>
{
var config = ctx.ServiceProvider.GetRequiredService<IConfiguration>();

var configSection = config.GetSection("ResiliencePipelines");
if (configSection is not null)
{
foreach (var pipelineConfig in configSection.GetChildren())
{
var pipelineName = pipelineConfig.GetValue<string>("Name");
if (!string.IsNullOrEmpty(pipelineName))
{
ctx.AddResiliencePipeline(pipelineName, (builder, context) =>
{
// Load configuration and configure pipeline...
});
}
}
}
});
```
<!-- endSnippet -->

## Dynamic reloads

Dynamic reloading is a feature of the pipeline registry that is also surfaced when
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
using Polly.Utils;

namespace Polly.DependencyInjection;

/// <summary>
/// Represents the context for configuring resilience pipelines with the specified key.
/// </summary>
/// <typeparam name="TKey">The type of the key used to identify the resilience pipeline.</typeparam>
public sealed class AddResiliencePipelinesContext<TKey>
where TKey : notnull
{
private readonly ConfigureResiliencePipelineRegistryOptions<TKey> _options;

internal AddResiliencePipelinesContext(
ConfigureResiliencePipelineRegistryOptions<TKey> options,
IServiceProvider serviceProvider)
{
_options = options;
ServiceProvider = serviceProvider;
}

/// <summary>
/// Gets the <see cref="IServiceProvider"/> that provides access to the dependency injection container.
/// </summary>
public IServiceProvider ServiceProvider { get; }

/// <summary>
/// Adds a resilience pipeline to the registry.
/// </summary>
/// <param name="key">The key used to identify the resilience pipeline.</param>
/// <param name="configure">An action that configures the resilience pipeline.</param>
/// <remarks>
/// You can retrieve the registered pipeline by resolving the <see cref="Registry.ResiliencePipelineProvider{TKey}"/> class from the dependency injection container.
/// <para>
/// This call enables the telemetry for the registered resilience pipeline.
/// </para>
/// </remarks>
public void AddResiliencePipeline(
TKey key,
Action<ResiliencePipelineBuilder, AddResiliencePipelineContext<TKey>> configure)
{
Guard.NotNull(configure);

_options.Actions.Add((registry) =>
{
// the last added builder with the same key wins, this allows overriding the builders
registry.TryAddBuilder(key, (builder, context) =>
{
configure(builder, new AddResiliencePipelineContext<TKey>(context, ServiceProvider));
});
});
}

/// <summary>
/// Adds a resilience pipeline to the registry.
/// </summary>
/// <typeparam name="TResult">The type of result that the resilience pipeline handles.</typeparam>
/// <param name="key">The key used to identify the resilience pipeline.</param>
/// <param name="configure">An action that configures the resilience pipeline.</param>
/// <remarks>
/// You can retrieve the registered pipeline by resolving the <see cref="Registry.ResiliencePipelineProvider{TKey}"/> class from the dependency injection container.
/// <para>
/// This call enables the telemetry for the registered resilience pipeline.
/// </para>
/// </remarks>
public void AddResiliencePipeline<TResult>(
TKey key,
Action<ResiliencePipelineBuilder<TResult>, AddResiliencePipelineContext<TKey>> configure)
{
Guard.NotNull(configure);

_options.Actions.Add((registry) =>
{
// the last added builder with the same key wins, this allows overriding the builders
registry.TryAddBuilder<TResult>(key, (builder, context) =>
{
configure(builder, new AddResiliencePipelineContext<TKey>(context, ServiceProvider));
});
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,20 +68,10 @@ public static IServiceCollection AddResiliencePipeline<TKey, TResult>(
Guard.NotNull(services);
Guard.NotNull(configure);

services
.AddOptions<ConfigureResiliencePipelineRegistryOptions<TKey>>()
.Configure<IServiceProvider>((options, serviceProvider) =>
{
options.Actions.Add((registry) =>
{
registry.TryAddBuilder<TResult>(key, (builder, context) =>
{
configure(builder, new AddResiliencePipelineContext<TKey>(context, serviceProvider));
});
});
});

return services.AddResiliencePipelineRegistry<TKey>();
return services.AddResiliencePipelines<TKey>((context) =>
{
context.AddResiliencePipeline(key, configure);
});
}

/// <summary>
Expand Down Expand Up @@ -135,18 +125,43 @@ public static IServiceCollection AddResiliencePipeline<TKey>(
Guard.NotNull(services);
Guard.NotNull(configure);

return services.AddResiliencePipelines<TKey>((context) =>
{
context.AddResiliencePipeline(key, configure);
});
}

/// <summary>
/// Allows deferred addition of one or more resilience pipelines to the service collection.
/// </summary>
/// <typeparam name="TKey">The type of the key used to identify the resilience pipelines.</typeparam>
/// <param name="services">The <see cref="IServiceCollection"/> to add resilience pipelines to.</param>
/// <param name="configure">An action that allows configuration of resilience pipelines.</param>
/// <returns>The updated <see cref="IServiceCollection"/> with the addition configuration added.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="services"/> or <paramref name="configure"/> is <see langword="null"/>.</exception>
/// <remarks>
/// This method can be useful if you want to add resilience pipelines as late as possible, e.g.
/// to allow other services / configuration to be available before providing the key.
/// <para>
/// You can retrieve the registered pipeline by resolving the <see cref="ResiliencePipelineProvider{TKey}"/> class from the dependency injection container.
/// </para>
/// <para>
/// This call enables the telemetry for the registered resilience pipeline.
/// </para>
/// </remarks>
public static IServiceCollection AddResiliencePipelines<TKey>(
this IServiceCollection services,
Action<AddResiliencePipelinesContext<TKey>> configure)
where TKey : notnull
{
Guard.NotNull(services);
Guard.NotNull(configure);

services
.AddOptions<ConfigureResiliencePipelineRegistryOptions<TKey>>()
.Configure<IServiceProvider>((options, serviceProvider) =>
{
options.Actions.Add((registry) =>
{
// the last added builder with the same key wins, this allows overriding the builders
registry.TryAddBuilder(key, (builder, context) =>
{
configure(builder, new AddResiliencePipelineContext<TKey>(context, serviceProvider));
});
});
configure(new AddResiliencePipelinesContext<TKey>(options, serviceProvider));
});

return services.AddResiliencePipelineRegistry<TKey>();
Expand Down
5 changes: 5 additions & 0 deletions src/Polly.Extensions/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
#nullable enable
static Polly.PollyServiceCollectionExtensions.AddResiliencePipelines<TKey>(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action<Polly.DependencyInjection.AddResiliencePipelinesContext<TKey>!>! configure) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
Polly.DependencyInjection.AddResiliencePipelinesContext<TKey>
Polly.DependencyInjection.AddResiliencePipelinesContext<TKey>.AddResiliencePipeline(TKey key, System.Action<Polly.ResiliencePipelineBuilder!, Polly.DependencyInjection.AddResiliencePipelineContext<TKey>!>! configure) -> void
Polly.DependencyInjection.AddResiliencePipelinesContext<TKey>.AddResiliencePipeline<TResult>(TKey key, System.Action<Polly.ResiliencePipelineBuilder<TResult>!, Polly.DependencyInjection.AddResiliencePipelineContext<TKey>!>! configure) -> void
Polly.DependencyInjection.AddResiliencePipelinesContext<TKey>.ServiceProvider.get -> System.IServiceProvider!
29 changes: 29 additions & 0 deletions src/Snippets/Docs/DependencyInjection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,35 @@ await pipeline.ExecuteAsync(
#endregion
}

public static async Task DeferredAddition(IServiceCollection services)
{
#region di-deferred-addition

services
.AddResiliencePipelines<string>((ctx) =>
{
var config = ctx.ServiceProvider.GetRequiredService<IConfiguration>();

var configSection = config.GetSection("ResiliencePipelines");
if (configSection is not null)
{
foreach (var pipelineConfig in configSection.GetChildren())
{
var pipelineName = pipelineConfig.GetValue<string>("Name");
if (!string.IsNullOrEmpty(pipelineName))
{
ctx.AddResiliencePipeline(pipelineName, (builder, context) =>
{
// Load configuration and configure pipeline...
});
}
}
}
});

#endregion
}

public static async Task DynamicReloads(IServiceCollection services, IConfigurationSection configurationSection)
{
#region di-dynamic-reloads
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,70 @@ static void ConfigureBuilder(ResiliencePipelineBuilder builder)
}
}

[Fact]
public void AddResiliencePipelines_Multiple_Ok()
{
_services.AddResiliencePipelines<string>(ctx =>
{
for (var i = 0; i < 10; i++)
{
ctx.AddResiliencePipeline(i.ToString(CultureInfo.InvariantCulture),
(builder, _) => builder.AddStrategy(new TestStrategy()));
ctx.AddResiliencePipeline<string>(i.ToString(CultureInfo.InvariantCulture),
(builder, _) => builder.AddStrategy(new TestStrategy()));
ctx.AddResiliencePipeline<int>(i.ToString(CultureInfo.InvariantCulture),
(builder, _) => builder.AddStrategy(new TestStrategy()));
}
});

var provider = CreateProvider();

Enumerable
.Range(0, 10)
.SelectMany(i =>
{
var name = i.ToString(CultureInfo.InvariantCulture);

return new object[]
{
provider.GetPipeline(name),
provider.GetPipeline<string>(name),
provider.GetPipeline<int>(name)
};
})
.Distinct()
.Should()
.HaveCount(30);
}

[Fact]
public void AddResiliencePipelines_MultipleRegistries_Ok()
{
_services.AddResiliencePipelines<string>(ctx =>
{
ctx.AddResiliencePipeline(Key, (builder, _) => builder.AddStrategy(new TestStrategy()));
ctx.AddResiliencePipeline<string>(Key, (builder, _) => builder.AddStrategy(new TestStrategy()));
ctx.AddResiliencePipeline<int>(Key, (builder, _) => builder.AddStrategy(new TestStrategy()));
});

_services.AddResiliencePipelines<int>(ctx =>
{
ctx.AddResiliencePipeline(10, (builder, _) => builder.AddStrategy(new TestStrategy()));
ctx.AddResiliencePipeline<string>(10, (builder, _) => builder.AddStrategy(new TestStrategy()));
ctx.AddResiliencePipeline<int>(10, (builder, _) => builder.AddStrategy(new TestStrategy()));
});

var serviceProvider = _services.BuildServiceProvider();

serviceProvider.GetRequiredService<ResiliencePipelineRegistry<string>>().GetPipeline(Key).Should().NotBeNull();
serviceProvider.GetRequiredService<ResiliencePipelineRegistry<string>>().GetPipeline<string>(Key).Should().NotBeNull();
serviceProvider.GetRequiredService<ResiliencePipelineRegistry<string>>().GetPipeline<int>(Key).Should().NotBeNull();

serviceProvider.GetRequiredService<ResiliencePipelineRegistry<int>>().GetPipeline(10).Should().NotBeNull();
serviceProvider.GetRequiredService<ResiliencePipelineRegistry<int>>().GetPipeline<string>(10).Should().NotBeNull();
serviceProvider.GetRequiredService<ResiliencePipelineRegistry<int>>().GetPipeline<int>(10).Should().NotBeNull();
}

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

0 comments on commit 37107b0

Please sign in to comment.