Skip to content

Commit 4153d2a

Browse files
authored
Make the ReactiveResilienceStrategy type-safe (#1462)
1 parent b1ec863 commit 4153d2a

26 files changed

Lines changed: 354 additions & 198 deletions

src/Polly.Core/CircuitBreaker/CircuitBreakerCompositeStrategyBuilderExtensions.cs

Lines changed: 19 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,17 @@ public static class CircuitBreakerCompositeStrategyBuilderExtensions
2424
/// </remarks>
2525
/// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/> or <paramref name="options"/> is <see langword="null"/>.</exception>
2626
/// <exception cref="ValidationException">Thrown when <paramref name="options"/> are invalid.</exception>
27+
[UnconditionalSuppressMessage(
28+
"Trimming",
29+
"IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code",
30+
Justification = "All options members preserved.")]
31+
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(CircuitBreakerStrategyOptions))]
2732
public static CompositeStrategyBuilder AddCircuitBreaker(this CompositeStrategyBuilder builder, CircuitBreakerStrategyOptions options)
2833
{
2934
Guard.NotNull(builder);
3035
Guard.NotNull(options);
3136

32-
return builder.AddCircuitBreakerCore(options);
37+
return builder.AddStrategy(context => CreateStrategy(context, options), options);
3338
}
3439

3540
/// <summary>
@@ -47,39 +52,29 @@ public static CompositeStrategyBuilder AddCircuitBreaker(this CompositeStrategyB
4752
/// </remarks>
4853
/// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/> or <paramref name="options"/> is <see langword="null"/>.</exception>
4954
/// <exception cref="ValidationException">Thrown when <paramref name="options"/> are invalid.</exception>
50-
public static CompositeStrategyBuilder<TResult> AddCircuitBreaker<TResult>(this CompositeStrategyBuilder<TResult> builder, CircuitBreakerStrategyOptions<TResult> options)
51-
{
52-
Guard.NotNull(builder);
53-
Guard.NotNull(options);
54-
55-
return builder.AddCircuitBreakerCore(options);
56-
}
57-
5855
[UnconditionalSuppressMessage(
5956
"Trimming",
6057
"IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code",
6158
Justification = "All options members preserved.")]
62-
private static TBuilder AddCircuitBreakerCore<TBuilder, TResult>(this TBuilder builder, CircuitBreakerStrategyOptions<TResult> options)
63-
where TBuilder : CompositeStrategyBuilderBase
59+
public static CompositeStrategyBuilder<TResult> AddCircuitBreaker<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TResult>(
60+
this CompositeStrategyBuilder<TResult> builder,
61+
CircuitBreakerStrategyOptions<TResult> options)
6462
{
65-
return builder.AddStrategy(
66-
context =>
67-
{
68-
var behavior = new AdvancedCircuitBehavior(
69-
options.FailureRatio,
70-
options.MinimumThroughput,
71-
HealthMetrics.Create(options.SamplingDuration, context.TimeProvider));
63+
Guard.NotNull(builder);
64+
Guard.NotNull(options);
7265

73-
return CreateStrategy<TResult, CircuitBreakerStrategyOptions<TResult>>(context, options, behavior);
74-
},
75-
options);
66+
return builder.AddStrategy(context => CreateStrategy(context, options), options);
7667
}
7768

78-
internal static CircuitBreakerResilienceStrategy<TResult> CreateStrategy<TResult, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TOptions>(
69+
internal static CircuitBreakerResilienceStrategy<TResult> CreateStrategy<TResult>(
7970
StrategyBuilderContext context,
80-
CircuitBreakerStrategyOptions<TResult> options,
81-
CircuitBehavior behavior)
71+
CircuitBreakerStrategyOptions<TResult> options)
8272
{
73+
var behavior = new AdvancedCircuitBehavior(
74+
options.FailureRatio,
75+
options.MinimumThroughput,
76+
HealthMetrics.Create(options.SamplingDuration, context.TimeProvider));
77+
8378
var controller = new CircuitStateController<TResult>(
8479
options.BreakDuration,
8580
options.OnOpened,

src/Polly.Core/CircuitBreaker/CircuitBreakerResilienceStrategy.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public CircuitBreakerResilienceStrategy(
2121
_controller.Dispose);
2222
}
2323

24-
protected override async ValueTask<Outcome<T>> ExecuteCore<TState>(Func<ResilienceContext, TState, ValueTask<Outcome<T>>> callback, ResilienceContext context, TState state)
24+
protected internal override async ValueTask<Outcome<T>> ExecuteCore<TState>(Func<ResilienceContext, TState, ValueTask<Outcome<T>>> callback, ResilienceContext context, TState state)
2525
{
2626
if (await _controller.OnActionPreExecuteAsync(context).ConfigureAwait(context.ContinueOnCapturedContext) is Outcome<T> outcome)
2727
{

src/Polly.Core/CompositeStrategyBuilderExtensions.cs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,53 @@ public static TBuilder AddStrategy<TBuilder>(this TBuilder builder, Func<Strateg
7070
return builder;
7171
}
7272

73+
/// <summary>
74+
/// Adds a reactive strategy to the builder.
75+
/// </summary>
76+
/// <param name="builder">The builder instance.</param>
77+
/// <param name="factory">The factory that creates a resilience strategy.</param>
78+
/// <param name="options">The options associated with the strategy. If none are provided the default instance of <see cref="ResilienceStrategyOptions"/> is created.</param>
79+
/// <returns>The same builder instance.</returns>
80+
/// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/>, <paramref name="factory"/> or <paramref name="options"/> is <see langword="null"/>.</exception>
81+
/// <exception cref="InvalidOperationException">Thrown when this builder was already used to create a strategy. The builder cannot be modified after it has been used.</exception>
82+
/// <exception cref="ValidationException">Thrown when <paramref name="options"/> is invalid.</exception>
83+
[RequiresUnreferencedCode(Constants.OptionsValidation)]
84+
public static CompositeStrategyBuilder AddStrategy(
85+
this CompositeStrategyBuilder builder, Func<StrategyBuilderContext, ReactiveResilienceStrategy<object>> factory,
86+
ResilienceStrategyOptions options)
87+
{
88+
Guard.NotNull(builder);
89+
Guard.NotNull(factory);
90+
Guard.NotNull(options);
91+
92+
builder.AddStrategyCore(context => new ReactiveResilienceStrategyBridge<object>(factory(context)), options);
93+
return builder;
94+
}
95+
96+
/// <summary>
97+
/// Adds a reactive strategy to the builder.
98+
/// </summary>
99+
/// <typeparam name="TResult">The type of the result.</typeparam>
100+
/// <param name="builder">The builder instance.</param>
101+
/// <param name="factory">The factory that creates a resilience strategy.</param>
102+
/// <param name="options">The options associated with the strategy. If none are provided the default instance of <see cref="ResilienceStrategyOptions"/> is created.</param>
103+
/// <returns>The same builder instance.</returns>
104+
/// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/>, <paramref name="factory"/> or <paramref name="options"/> is <see langword="null"/>.</exception>
105+
/// <exception cref="InvalidOperationException">Thrown when this builder was already used to create a strategy. The builder cannot be modified after it has been used.</exception>
106+
/// <exception cref="ValidationException">Thrown when <paramref name="options"/> is invalid.</exception>
107+
[RequiresUnreferencedCode(Constants.OptionsValidation)]
108+
public static CompositeStrategyBuilder<TResult> AddStrategy<TResult>(
109+
this CompositeStrategyBuilder<TResult> builder, Func<StrategyBuilderContext, ReactiveResilienceStrategy<TResult>> factory,
110+
ResilienceStrategyOptions options)
111+
{
112+
Guard.NotNull(builder);
113+
Guard.NotNull(factory);
114+
Guard.NotNull(options);
115+
116+
builder.AddStrategyCore(context => new ReactiveResilienceStrategyBridge<TResult>(factory(context)), options);
117+
return builder;
118+
}
119+
73120
internal sealed class EmptyOptions : ResilienceStrategyOptions
74121
{
75122
public static readonly EmptyOptions Instance = new();

src/Polly.Core/Fallback/FallbackCompositeStrategyBuilderExtensions.cs

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,18 @@ public static class FallbackCompositeStrategyBuilderExtensions
1818
/// <returns>The builder instance with the fallback strategy added.</returns>
1919
/// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/> or <paramref name="options"/> is <see langword="null"/>.</exception>
2020
/// <exception cref="ValidationException">Thrown when <paramref name="options"/> are invalid.</exception>
21-
public static CompositeStrategyBuilder<TResult> AddFallback<TResult>(this CompositeStrategyBuilder<TResult> builder, FallbackStrategyOptions<TResult> options)
21+
[UnconditionalSuppressMessage(
22+
"Trimming",
23+
"IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code",
24+
Justification = "All options members preserved.")]
25+
public static CompositeStrategyBuilder<TResult> AddFallback<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TResult>(
26+
this CompositeStrategyBuilder<TResult> builder,
27+
FallbackStrategyOptions<TResult> options)
2228
{
2329
Guard.NotNull(builder);
2430
Guard.NotNull(options);
2531

26-
builder.AddFallbackCore<TResult, FallbackStrategyOptions<TResult>>(options);
27-
return builder;
32+
return builder.AddStrategy(context => CreateFallback(context, options), options);
2833
}
2934

3035
/// <summary>
@@ -35,34 +40,30 @@ public static CompositeStrategyBuilder<TResult> AddFallback<TResult>(this Compos
3540
/// <returns>The builder instance with the fallback strategy added.</returns>
3641
/// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/> or <paramref name="options"/> is <see langword="null"/>.</exception>
3742
/// <exception cref="ValidationException">Thrown when <paramref name="options"/> are invalid.</exception>
43+
[UnconditionalSuppressMessage(
44+
"Trimming",
45+
"IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code",
46+
Justification = "All options members preserved.")]
47+
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(FallbackStrategyOptions))]
3848
internal static CompositeStrategyBuilder AddFallback(this CompositeStrategyBuilder builder, FallbackStrategyOptions options)
3949
{
4050
Guard.NotNull(builder);
4151
Guard.NotNull(options);
4252

43-
builder.AddFallbackCore<object, FallbackStrategyOptions>(options);
44-
return builder;
53+
return builder.AddStrategy(context => CreateFallback(context, options), options);
4554
}
4655

47-
[UnconditionalSuppressMessage(
48-
"Trimming",
49-
"IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code",
50-
Justification = "All options members preserved.")]
51-
internal static void AddFallbackCore<TResult, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] TOptions>(
52-
this CompositeStrategyBuilderBase builder,
56+
private static ReactiveResilienceStrategy<TResult> CreateFallback<TResult>(
57+
StrategyBuilderContext context,
5358
FallbackStrategyOptions<TResult> options)
5459
{
55-
builder.AddStrategy(context =>
56-
{
57-
var handler = new FallbackHandler<TResult>(
58-
options.ShouldHandle!,
59-
options.FallbackAction!);
60+
var handler = new FallbackHandler<TResult>(
61+
options.ShouldHandle!,
62+
options.FallbackAction!);
6063

61-
return new FallbackResilienceStrategy<TResult>(
62-
handler,
63-
options.OnFallback,
64-
context.Telemetry);
65-
},
66-
options);
64+
return new FallbackResilienceStrategy<TResult>(
65+
handler,
66+
options.OnFallback,
67+
context.Telemetry);
6768
}
6869
}

src/Polly.Core/Fallback/FallbackResilienceStrategy.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public FallbackResilienceStrategy(FallbackHandler<T> handler, Func<OutcomeArgume
1717
_telemetry = telemetry;
1818
}
1919

20-
protected override async ValueTask<Outcome<T>> ExecuteCore<TState>(Func<ResilienceContext, TState, ValueTask<Outcome<T>>> callback, ResilienceContext context, TState state)
20+
protected internal override async ValueTask<Outcome<T>> ExecuteCore<TState>(Func<ResilienceContext, TState, ValueTask<Outcome<T>>> callback, ResilienceContext context, TState state)
2121
{
2222
var outcome = await ExecuteCallbackSafeAsync(callback, context, state).ConfigureAwait(context.ContinueOnCapturedContext);
2323
var handleFallbackArgs = new OutcomeArguments<T, FallbackPredicateArguments>(context, outcome, default);

src/Polly.Core/Hedging/HedgingCompositeStrategyBuilderExtensions.cs

Lines changed: 30 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,18 @@ public static class HedgingCompositeStrategyBuilderExtensions
1919
/// <returns>The builder instance with the hedging strategy added.</returns>
2020
/// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/> or <paramref name="options"/> is <see langword="null"/>.</exception>
2121
/// <exception cref="ValidationException">Thrown when <paramref name="options"/> are invalid.</exception>
22-
public static CompositeStrategyBuilder<TResult> AddHedging<TResult>(this CompositeStrategyBuilder<TResult> builder, HedgingStrategyOptions<TResult> options)
22+
[UnconditionalSuppressMessage(
23+
"Trimming",
24+
"IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code",
25+
Justification = "All options members preserved.")]
26+
public static CompositeStrategyBuilder<TResult> AddHedging<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TResult>(
27+
this CompositeStrategyBuilder<TResult> builder,
28+
HedgingStrategyOptions<TResult> options)
2329
{
2430
Guard.NotNull(builder);
2531
Guard.NotNull(options);
2632

27-
builder.AddHedgingCore<TResult, HedgingStrategyOptions<TResult>>(options);
28-
return builder;
33+
return builder.AddStrategy(context => CreateHedgingStrategy(context, options, isGeneric: true), options);
2934
}
3035

3136
/// <summary>
@@ -36,39 +41,36 @@ public static CompositeStrategyBuilder<TResult> AddHedging<TResult>(this Composi
3641
/// <returns>The builder instance with the hedging strategy added.</returns>
3742
/// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/> or <paramref name="options"/> is <see langword="null"/>.</exception>
3843
/// <exception cref="ValidationException">Thrown when <paramref name="options"/> are invalid.</exception>
44+
[UnconditionalSuppressMessage(
45+
"Trimming",
46+
"IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code",
47+
Justification = "All options members preserved.")]
48+
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(HedgingStrategyOptions))]
3949
internal static CompositeStrategyBuilder AddHedging(this CompositeStrategyBuilder builder, HedgingStrategyOptions options)
4050
{
4151
Guard.NotNull(builder);
4252
Guard.NotNull(options);
4353

44-
builder.AddHedgingCore<object, HedgingStrategyOptions>(options);
45-
return builder;
54+
return builder.AddStrategy(context => CreateHedgingStrategy(context, options, isGeneric: false), options);
4655
}
4756

48-
[UnconditionalSuppressMessage(
49-
"Trimming",
50-
"IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code",
51-
Justification = "All options members preserved.")]
52-
internal static void AddHedgingCore<TResult, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TOptions>(
53-
this CompositeStrategyBuilderBase builder,
54-
HedgingStrategyOptions<TResult> options)
57+
private static HedgingResilienceStrategy<TResult> CreateHedgingStrategy<TResult>(
58+
StrategyBuilderContext context,
59+
HedgingStrategyOptions<TResult> options,
60+
bool isGeneric)
5561
{
56-
builder.AddStrategy(context =>
57-
{
58-
var handler = new HedgingHandler<TResult>(
59-
options.ShouldHandle!,
60-
options.HedgingActionGenerator,
61-
IsGeneric: builder is not CompositeStrategyBuilder);
62+
var handler = new HedgingHandler<TResult>(
63+
options.ShouldHandle!,
64+
options.HedgingActionGenerator,
65+
IsGeneric: isGeneric);
6266

63-
return new HedgingResilienceStrategy<TResult>(
64-
options.HedgingDelay,
65-
options.MaxHedgedAttempts,
66-
handler,
67-
options.OnHedging,
68-
options.HedgingDelayGenerator,
69-
context.TimeProvider,
70-
context.Telemetry);
71-
},
72-
options);
67+
return new HedgingResilienceStrategy<TResult>(
68+
options.HedgingDelay,
69+
options.MaxHedgedAttempts,
70+
handler,
71+
options.OnHedging,
72+
options.HedgingDelayGenerator,
73+
context.TimeProvider,
74+
context.Telemetry);
7375
}
7476
}

src/Polly.Core/Hedging/HedgingResilienceStrategy.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public HedgingResilienceStrategy(
4141
public Func<OutcomeArguments<T, OnHedgingArguments>, ValueTask>? OnHedging { get; }
4242

4343
[ExcludeFromCodeCoverage] // coverlet issue
44-
protected override async ValueTask<Outcome<T>> ExecuteCore<TState>(
44+
protected internal override async ValueTask<Outcome<T>> ExecuteCore<TState>(
4545
Func<ResilienceContext, TState, ValueTask<Outcome<T>>> callback,
4646
ResilienceContext context,
4747
TState state)

0 commit comments

Comments
 (0)