diff --git a/docs/extensibility/proactive-strategy.md b/docs/extensibility/proactive-strategy.md index 1434f2a6daa..75136c8e7fb 100644 --- a/docs/extensibility/proactive-strategy.md +++ b/docs/extensibility/proactive-strategy.md @@ -1,6 +1,6 @@ # Proactive resilience strategy -This section guides you in creating a **Timing resilience strategy** that tracks the execution times of callbacks and reports when the execution time exceeds the expected duration. This is a prime example of a proactive strategy because we aren't concerned with the individual results produced by the callbacks. Hence, this strategy can be used across various result types. +This document guides you in creating a **Timing resilience strategy** that tracks the execution times of callbacks and reports when the execution time exceeds the expected duration. This is a prime example of a proactive strategy because we aren't concerned with the individual results produced by the callbacks. Hence, this strategy can be used across various result types. ## Implementation @@ -139,7 +139,6 @@ public static class TimingResilienceStrategyBuilderExtensions { // Add the strategy through the AddStrategy method. This method accepts a factory delegate // and automatically validates the options. - return builder.AddStrategy( context => { @@ -159,6 +158,26 @@ public static class TimingResilienceStrategyBuilderExtensions ``` +## Usage + + +```cs +// Add the proactive strategy to the builder +var pipeline = new ResiliencePipelineBuilder() + // This is custom extension defined in this sample + .AddTiming(new TimingStrategyOptions + { + Threshold = TimeSpan.FromSeconds(1), + ThresholdExceeded = args => + { + Console.WriteLine("Execution threshold exceeded!"); + return default; + }, + }) + .Build(); +``` + + ## Resources For further understanding of proactive resilience strategies, consider exploring these resources: diff --git a/docs/extensibility/reactive-strategy.md b/docs/extensibility/reactive-strategy.md index 4df5d8cefea..63f4640a704 100644 --- a/docs/extensibility/reactive-strategy.md +++ b/docs/extensibility/reactive-strategy.md @@ -1 +1,250 @@ # Reactive resilience strategy + +This document describes how to set up a **Result reporting resilience strategy**. This strategy lets you listen for specific results and report them to other components. It serves as a good example of a reactive strategy because it deals with specific results. + +## Implementation + +Reactive resilience strategies inherit from the [`ResilienceStrategy`](xref:Polly.ResilienceStrategy`1) base class. The implementation for this specific strategy is as follows: + + +```cs +// Strategies should be internal and not exposed in the library's public API. +// Use extension methods and options to configure the strategy. +internal sealed class ResultReportingResilienceStrategy : ResilienceStrategy +{ + private readonly Func, ValueTask> _shouldHandle; + private readonly Func, ValueTask> _onReportResult; + private readonly ResilienceStrategyTelemetry _telemetry; + + public ResultReportingResilienceStrategy( + Func, ValueTask> shouldHandle, + Func, ValueTask> onReportResult, + ResilienceStrategyTelemetry telemetry) + { + _shouldHandle = shouldHandle; + _onReportResult = onReportResult; + _telemetry = telemetry; + } + + protected override async ValueTask> ExecuteCore( + Func>> callback, + ResilienceContext context, + TState state) + { + Outcome outcome = await callback(context, state).ConfigureAwait(context.ContinueOnCapturedContext); + + // Check if the outcome should be reported using the "ShouldHandle" predicate. + if (await _shouldHandle(new ResultReportingPredicateArguments(context, outcome)).ConfigureAwait(context.ContinueOnCapturedContext)) + { + var args = new OnReportResultArguments(context, outcome); + + // Report the event with an informational severity level to the telemetry infrastructure. + _telemetry.Report(new ResilienceEvent(ResilienceEventSeverity.Information, "ResultReported"), context, outcome, args); + + // Call the "OnReportResult" callback. + await _onReportResult(args).ConfigureAwait(context.ContinueOnCapturedContext); + } + + return outcome; + } +} +``` + + +Reactive strategies use the `ShouldHandle` predicate to decide whether to handle the outcome of a user callback. The convention is to name the predicate's arguments using the `PredicateArguments` pattern and return a `ValueTask`. Here, we use `ResultReportingPredicateArguments`: + + +```cs +public struct ResultReportingPredicateArguments +{ + public ResultReportingPredicateArguments(ResilienceContext context, Outcome outcome) + { + Context = context; + Outcome = outcome; + } + + // Always include the "Context" property in the arguments. + public ResilienceContext Context { get; } + + // Always have the "Outcome" property in reactive arguments. + public Outcome Outcome { get; } +} +``` + + +Reactive arguments always contain the `Context` and `Outcome` properties. + +Additionally, to report the outcome, the strategy uses `OnReportResultArguments`: + + +```cs +public struct OnReportResultArguments +{ + public OnReportResultArguments(ResilienceContext context, Outcome outcome) + { + Context = context; + Outcome = outcome; + } + + // Always include the "Context" property in the arguments. + public ResilienceContext Context { get; } + + // Always have the "Outcome" property in reactive arguments. + public Outcome Outcome { get; } +} +``` + + +Using arguments in callbacks supports a more maintainable and extensible API. + +## Options + +In the previous section, we implemented the `ResultReportingResilienceStrategy`. Now, we need to integrate it with Polly and its public API. + +Define the public `ResultReportingStrategyOptions` to configure our strategy: + + +```cs +public class ResultReportingStrategyOptions : ResilienceStrategyOptions +{ + public ResultReportingStrategyOptions() + { + // Set a default name for the options to enhance telemetry insights. + Name = "ResultReporting"; + } + + // Options for reactive strategies should always include a "ShouldHandle" delegate. + // Set a sensible default when possible. Here, we handle all exceptions. + public Func, ValueTask> ShouldHandle { get; set; } = args => + { + return new ValueTask(args.Outcome.Exception is not null); + }; + + // This illustrates an event delegate. Note that the arguments struct carries the same name as the delegate but with an "Arguments" suffix. + // The event follows the async convention and must be set by the user. + // + // The [Required] enforces the consumer to specify this property, used when some properties do not have sensible defaults and are required. + [Required] + public Func, ValueTask>? OnReportResult { get; set; } +} +``` + + +If you want to support non-generic options for the `ResiliencePipelineBuilder`, you can expose them as well: + + +```cs +// Simply derive from the generic options, using 'object' as the result type. +// This allows the strategy to manage all results. +public class ResultReportingStrategyOptions : ResultReportingStrategyOptions +{ +} +``` + + +Using options as a public contract helps us ensure flexibility with consumers. By adopting this method, you can effortlessly introduce new members without inducing breaking changes and maintain consistent validation. + +## Extensions + +Up until now, we've discussed: + +- The public `ResultReportingStrategyOptions` and the related arguments. +- The proactive strategy implementation called `ResultReportingResilienceStrategy`. + +The next step is to combine these elements by introducing new extensions for `ResiliencePipelineBuilder` and, optionally, `ResiliencePipelineBuilder`. + + +```cs +public static class ResultReportingResilienceStrategyBuilderExtensions +{ + // Add extensions for the generic builder. + // Extensions should return the builder to support a fluent API. + public static ResiliencePipelineBuilder AddResultReporting(this ResiliencePipelineBuilder builder, ResultReportingStrategyOptions options) + { + // Incorporate the strategy using the AddStrategy method. This method receives a factory delegate + // and automatically checks the options. + return builder.AddStrategy( + context => + { + // The "context" offers various properties for the strategy to use. + // Here, we simply use the "Telemetry" and hand it over to the strategy. + // The ShouldHandle and OnReportResult values come from the options. + var strategy = new ResultReportingResilienceStrategy( + options.ShouldHandle, + options.OnReportResult!, + context.Telemetry); + + return strategy; + }, + options); + } + + // Optionally, if suitable for the strategy, add support for non-generic builders. + // Observe the use of the non-generic ResultReportingStrategyOptions. + public static ResiliencePipelineBuilder AddResultReporting(this ResiliencePipelineBuilder builder, ResultReportingStrategyOptions options) + { + return builder.AddStrategy( + context => + { + var strategy = new ResultReportingResilienceStrategy( + options.ShouldHandle, + options.OnReportResult!, + context.Telemetry); + + return strategy; + }, + options); + } +} +``` + + +## Usage + + +```cs +// Add reactive strategy to the builder +new ResiliencePipelineBuilder() + .AddResultReporting(new ResultReportingStrategyOptions + { + // Define what outcomes to handle + ShouldHandle = args => args.Outcome switch + { + { Exception: { } } => PredicateResult.True(), + { Result: { StatusCode: HttpStatusCode.InternalServerError } } => PredicateResult.True(), + _ => PredicateResult.False() + }, + OnReportResult = args => + { + Console.WriteLine($"Result: {args.Outcome}"); + return default; + } + }); + +// You can also use the non-generic ResiliencePipelineBuilder to handle any kind of result. +new ResiliencePipelineBuilder() + .AddResultReporting(new ResultReportingStrategyOptions + { + // Define what outcomes to handle + ShouldHandle = args => args.Outcome switch + { + { Exception: { } } => PredicateResult.True(), + { Result: HttpResponseMessage message } when message.StatusCode == HttpStatusCode.InternalServerError => PredicateResult.True(), + _ => PredicateResult.False() + }, + OnReportResult = args => + { + Console.WriteLine($"Result: {args.Outcome}"); + return default; + } + }); +``` + + +## Resources + +For further understanding of reactive resilience strategies, consider exploring these resources: + +- [Result reporting strategy sample](https://github.com/App-vNext/Polly/tree/main/samples/Extensibility/Reactive): A practical example from this guide. +- [Retry resilience strategy](https://github.com/App-vNext/Polly/tree/main/src/Polly.Core/Retry): Discover the built-in retry resilience strategy implementation. +- [Fallback resilience strategy](https://github.com/App-vNext/Polly/tree/main/src/Polly.Core/Fallback): Discover the built-in fallback resilience strategy implementation. diff --git a/docs/general.md b/docs/general.md index 6a243973a80..1dcfca05908 100644 --- a/docs/general.md +++ b/docs/general.md @@ -24,7 +24,7 @@ By default, asynchronous continuations and retries do not execute on a captured ResilienceContext context = ResilienceContextPool.Shared.Get(continueOnCapturedContext: true); await pipeline.ExecuteAsync( - async context => + static async context => { // Execute your code, honoring the ContinueOnCapturedContext setting await MyMethodAsync(context.CancellationToken).ConfigureAwait(context.ContinueOnCapturedContext); @@ -51,7 +51,7 @@ The `CancellationToken` you pass to the `ExecuteAsync(...)` method serves multip ```cs // Execute your code with cancellation support await pipeline.ExecuteAsync( - async token => await MyMethodAsync(token), + static async token => await MyMethodAsync(token), cancellationToken); // Use ResilienceContext for more advanced scenarios diff --git a/samples/Extensibility/Proactive/TimingResilienceStrategyBuilderExtensions.cs b/samples/Extensibility/Proactive/TimingResilienceStrategyBuilderExtensions.cs index 851b66ecc0e..a50adb8ded2 100644 --- a/samples/Extensibility/Proactive/TimingResilienceStrategyBuilderExtensions.cs +++ b/samples/Extensibility/Proactive/TimingResilienceStrategyBuilderExtensions.cs @@ -16,7 +16,6 @@ public static TBuilder AddTiming(this TBuilder builder, TimingStrategy { // Add the strategy through the AddStrategy method. This method accepts a factory delegate // and automatically validates the options. - return builder.AddStrategy( context => { diff --git a/samples/Extensibility/Program.cs b/samples/Extensibility/Program.cs index d684bd29aae..a909da4ece7 100644 --- a/samples/Extensibility/Program.cs +++ b/samples/Extensibility/Program.cs @@ -1,10 +1,11 @@ using Extensibility.Proactive; +using Extensibility.Reactive; using Polly; -using System.Net.Http.Headers; +using System.Net; -// ------------------------------------------------------------------------ -// Usage of custom proactive strategy -// ------------------------------------------------------------------------ +#region ext-proactive-strategy-usage + +// Add the proactive strategy to the builder var pipeline = new ResiliencePipelineBuilder() // This is custom extension defined in this sample .AddTiming(new TimingStrategyOptions @@ -18,6 +19,48 @@ }) .Build(); +#endregion + +#region ext-reactive-strategy-usage + +// Add reactive strategy to the builder +new ResiliencePipelineBuilder() + .AddResultReporting(new ResultReportingStrategyOptions + { + // Define what outcomes to handle + ShouldHandle = args => args.Outcome switch + { + { Exception: { } } => PredicateResult.True(), + { Result: { StatusCode: HttpStatusCode.InternalServerError } } => PredicateResult.True(), + _ => PredicateResult.False() + }, + OnReportResult = args => + { + Console.WriteLine($"Result: {args.Outcome}"); + return default; + } + }); + +// You can also use the non-generic ResiliencePipelineBuilder to handle any kind of result. +new ResiliencePipelineBuilder() + .AddResultReporting(new ResultReportingStrategyOptions + { + // Define what outcomes to handle + ShouldHandle = args => args.Outcome switch + { + { Exception: { } } => PredicateResult.True(), + { Result: HttpResponseMessage message } when message.StatusCode == HttpStatusCode.InternalServerError => PredicateResult.True(), + _ => PredicateResult.False() + }, + OnReportResult = args => + { + Console.WriteLine($"Result: {args.Outcome}"); + return default; + } + }); + +#endregion + // Execute the pipeline await pipeline.ExecuteAsync(async token => await Task.Delay(1500, token), CancellationToken.None); diff --git a/samples/Extensibility/Reactive/OnReportResultArguments.cs b/samples/Extensibility/Reactive/OnReportResultArguments.cs new file mode 100644 index 00000000000..fce0aeaabfa --- /dev/null +++ b/samples/Extensibility/Reactive/OnReportResultArguments.cs @@ -0,0 +1,22 @@ +using Polly; + +namespace Extensibility.Reactive; + +#region ext-reactive-event-args + +public struct OnReportResultArguments +{ + public OnReportResultArguments(ResilienceContext context, Outcome outcome) + { + Context = context; + Outcome = outcome; + } + + // Always include the "Context" property in the arguments. + public ResilienceContext Context { get; } + + // Always have the "Outcome" property in reactive arguments. + public Outcome Outcome { get; } +} + +#endregion diff --git a/samples/Extensibility/Reactive/ResultReportingPredicateArguments.cs b/samples/Extensibility/Reactive/ResultReportingPredicateArguments.cs new file mode 100644 index 00000000000..e4419d04fca --- /dev/null +++ b/samples/Extensibility/Reactive/ResultReportingPredicateArguments.cs @@ -0,0 +1,22 @@ +using Polly; + +namespace Extensibility.Reactive; + +#region ext-reactive-predicate-args + +public struct ResultReportingPredicateArguments +{ + public ResultReportingPredicateArguments(ResilienceContext context, Outcome outcome) + { + Context = context; + Outcome = outcome; + } + + // Always include the "Context" property in the arguments. + public ResilienceContext Context { get; } + + // Always have the "Outcome" property in reactive arguments. + public Outcome Outcome { get; } +} + +#endregion diff --git a/samples/Extensibility/Reactive/ResultReportingResilienceStrategy.cs b/samples/Extensibility/Reactive/ResultReportingResilienceStrategy.cs new file mode 100644 index 00000000000..510d0fe0973 --- /dev/null +++ b/samples/Extensibility/Reactive/ResultReportingResilienceStrategy.cs @@ -0,0 +1,49 @@ +using Polly; +using Polly.Telemetry; + +namespace Extensibility.Reactive; + +#region ext-reactive-strategy + +// Strategies should be internal and not exposed in the library's public API. +// Use extension methods and options to configure the strategy. +internal sealed class ResultReportingResilienceStrategy : ResilienceStrategy +{ + private readonly Func, ValueTask> _shouldHandle; + private readonly Func, ValueTask> _onReportResult; + private readonly ResilienceStrategyTelemetry _telemetry; + + public ResultReportingResilienceStrategy( + Func, ValueTask> shouldHandle, + Func, ValueTask> onReportResult, + ResilienceStrategyTelemetry telemetry) + { + _shouldHandle = shouldHandle; + _onReportResult = onReportResult; + _telemetry = telemetry; + } + + protected override async ValueTask> ExecuteCore( + Func>> callback, + ResilienceContext context, + TState state) + { + Outcome outcome = await callback(context, state).ConfigureAwait(context.ContinueOnCapturedContext); + + // Check if the outcome should be reported using the "ShouldHandle" predicate. + if (await _shouldHandle(new ResultReportingPredicateArguments(context, outcome)).ConfigureAwait(context.ContinueOnCapturedContext)) + { + var args = new OnReportResultArguments(context, outcome); + + // Report the event with an informational severity level to the telemetry infrastructure. + _telemetry.Report(new ResilienceEvent(ResilienceEventSeverity.Information, "ResultReported"), context, outcome, args); + + // Call the "OnReportResult" callback. + await _onReportResult(args).ConfigureAwait(context.ContinueOnCapturedContext); + } + + return outcome; + } +} + +#endregion diff --git a/samples/Extensibility/Reactive/ResultReportingResilienceStrategyBuilderExtensions.cs b/samples/Extensibility/Reactive/ResultReportingResilienceStrategyBuilderExtensions.cs new file mode 100644 index 00000000000..9bc73557490 --- /dev/null +++ b/samples/Extensibility/Reactive/ResultReportingResilienceStrategyBuilderExtensions.cs @@ -0,0 +1,51 @@ +using Polly; + +namespace Extensibility.Reactive; + +#pragma warning disable IDE0022 // Use expression body for method + +#region ext-reactive-extensions + +public static class ResultReportingResilienceStrategyBuilderExtensions +{ + // Add extensions for the generic builder. + // Extensions should return the builder to support a fluent API. + public static ResiliencePipelineBuilder AddResultReporting(this ResiliencePipelineBuilder builder, ResultReportingStrategyOptions options) + { + // Incorporate the strategy using the AddStrategy method. This method receives a factory delegate + // and automatically checks the options. + return builder.AddStrategy( + context => + { + // The "context" offers various properties for the strategy to use. + // Here, we simply use the "Telemetry" and hand it over to the strategy. + // The ShouldHandle and OnReportResult values come from the options. + var strategy = new ResultReportingResilienceStrategy( + options.ShouldHandle, + options.OnReportResult!, + context.Telemetry); + + return strategy; + }, + options); + } + + // Optionally, if suitable for the strategy, add support for non-generic builders. + // Observe the use of the non-generic ResultReportingStrategyOptions. + public static ResiliencePipelineBuilder AddResultReporting(this ResiliencePipelineBuilder builder, ResultReportingStrategyOptions options) + { + return builder.AddStrategy( + context => + { + var strategy = new ResultReportingResilienceStrategy( + options.ShouldHandle, + options.OnReportResult!, + context.Telemetry); + + return strategy; + }, + options); + } +} + +#endregion diff --git a/samples/Extensibility/Reactive/ResultReportingStrategyOptions.cs b/samples/Extensibility/Reactive/ResultReportingStrategyOptions.cs new file mode 100644 index 00000000000..a35169685b2 --- /dev/null +++ b/samples/Extensibility/Reactive/ResultReportingStrategyOptions.cs @@ -0,0 +1,42 @@ +using Polly; +using System.ComponentModel.DataAnnotations; + +namespace Extensibility.Reactive; + +#region ext-reactive-options + +public class ResultReportingStrategyOptions : ResilienceStrategyOptions +{ + public ResultReportingStrategyOptions() + { + // Set a default name for the options to enhance telemetry insights. + Name = "ResultReporting"; + } + + // Options for reactive strategies should always include a "ShouldHandle" delegate. + // Set a sensible default when possible. Here, we handle all exceptions. + public Func, ValueTask> ShouldHandle { get; set; } = args => + { + return new ValueTask(args.Outcome.Exception is not null); + }; + + // This illustrates an event delegate. Note that the arguments struct carries the same name as the delegate but with an "Arguments" suffix. + // The event follows the async convention and must be set by the user. + // + // The [Required] enforces the consumer to specify this property, used when some properties do not have sensible defaults and are required. + [Required] + public Func, ValueTask>? OnReportResult { get; set; } +} + +#endregion + +#region ext-reactive-non-generic-options + +// Simply derive from the generic options, using 'object' as the result type. +// This allows the strategy to manage all results. +public class ResultReportingStrategyOptions : ResultReportingStrategyOptions +{ +} + + +#endregion diff --git a/src/Snippets/Docs/General.cs b/src/Snippets/Docs/General.cs index 4b12a9daa62..a91ec674488 100644 --- a/src/Snippets/Docs/General.cs +++ b/src/Snippets/Docs/General.cs @@ -13,7 +13,7 @@ public static async Task SynchronizationContext() ResilienceContext context = ResilienceContextPool.Shared.Get(continueOnCapturedContext: true); await pipeline.ExecuteAsync( - async context => + static async context => { // Execute your code, honoring the ContinueOnCapturedContext setting await MyMethodAsync(context.CancellationToken).ConfigureAwait(context.ContinueOnCapturedContext); @@ -38,7 +38,7 @@ public static async Task CancellationTokenSample() // Execute your code with cancellation support await pipeline.ExecuteAsync( - async token => await MyMethodAsync(token), + static async token => await MyMethodAsync(token), cancellationToken); // Use ResilienceContext for more advanced scenarios