From cc01f38088d6bf14836b8ed9b23e251add6eb0b9 Mon Sep 17 00:00:00 2001 From: martintmk <103487740+martintmk@users.noreply.github.com> Date: Thu, 21 Sep 2023 08:58:14 +0200 Subject: [PATCH] [Docs] General extensibility and implementation of proactive strategies (#1602) --- docs/advanced/extensibility.md | 3 - docs/extensibility/index.md | 120 +++++++++++ docs/extensibility/proactive-strategy.md | 168 +++++++++++++++ docs/extensibility/reactive-strategy.md | 1 + docs/index.md | 4 +- docs/toc.yml | 11 +- .../Proactive/ThresholdExceededArguments.cs | 26 +++ .../Proactive/TimingResilienceStrategy.cs | 59 ++++++ ...mingResilienceStrategyBuilderExtensions.cs | 37 ++++ .../Proactive/TimingStrategyOptions.cs | 27 +++ samples/Extensibility/Program.cs | 95 +-------- src/Polly.Core/README.md | 194 +----------------- src/Snippets/Core/Snippets.cs | 142 ------------- src/Snippets/Docs/Extensibility.cs | 40 ++++ 14 files changed, 501 insertions(+), 426 deletions(-) delete mode 100644 docs/advanced/extensibility.md create mode 100644 docs/extensibility/index.md create mode 100644 docs/extensibility/proactive-strategy.md create mode 100644 docs/extensibility/reactive-strategy.md create mode 100644 samples/Extensibility/Proactive/ThresholdExceededArguments.cs create mode 100644 samples/Extensibility/Proactive/TimingResilienceStrategy.cs create mode 100644 samples/Extensibility/Proactive/TimingResilienceStrategyBuilderExtensions.cs create mode 100644 samples/Extensibility/Proactive/TimingStrategyOptions.cs delete mode 100644 src/Snippets/Core/Snippets.cs create mode 100644 src/Snippets/Docs/Extensibility.cs diff --git a/docs/advanced/extensibility.md b/docs/advanced/extensibility.md deleted file mode 100644 index 62ac8e12090..00000000000 --- a/docs/advanced/extensibility.md +++ /dev/null @@ -1,3 +0,0 @@ -# Extensibility - -🚧 This documentation is being written as part of the Polly v8 release. diff --git a/docs/extensibility/index.md b/docs/extensibility/index.md new file mode 100644 index 00000000000..6b7e52b7621 --- /dev/null +++ b/docs/extensibility/index.md @@ -0,0 +1,120 @@ +# Extensibility + +This article explains how to extend Polly with new [resilience strategies](../strategies/index.md). Polly identifies two types of resilience strategies: + +- **Reactive**: These strategies handle specific exceptions that are thrown, or results that are returned, by the callbacks executed through the strategy. +- **Proactive**: Unlike reactive strategies, proactive strategies do not focus on handling errors by the callbacks might throw or return. They can make proactive decisions to cancel or reject the execution of callbacks (e.g., using a rate limiter or a timeout resilience strategy). + +This guide will help you create a new illustrative resilience strategy for each type. + +## Basics of extensibility + +Regardless of whether the strategy is reactive or proactive, every new resilience strategy should include the following components: + +- Options detailing the strategy's configuration. These should inherit from [`ResilienceStrategyOptions`](xref:Polly.ResilienceStrategyOptions). +- Extensions for `ResiliencePipelineBuilder` or `ResiliencePipelineBuilder`. +- Custom argument types for delegates that contain information about a specific event. + +The strategy options contain properties of following types: + +- **Common types**: Such as `int`, `bool`, `TimeSpan`, etc. +- **Delegates**: For example when strategy need to raise an event, or generate a value. In general, the delegates should by asynchronous. +- **Arguments**: Used by the delegates to pass the information to consumers. + +## Delegates + +Individual resilience strategies make use of several delegate types: + +- **Predicates**: Vital for determining whether a resilience strategy should handle the given execution result. +- **Events**: Triggered when significant actions or states occur within the resilience strategy. +- **Generators**: Invoked when the resilience strategy needs specific information or values from the caller. + +Recommended signatures for these delegates are: + +### Predicates + +- `Func, ValueTask>` (Reactive) + +### Events + +- `Func, ValueTask>` (Reactive) +- `Func` (Proactive) + +### Generators + +- `Func, ValueTask>` (Reactive) +- `Func>` (Proactive) + +These delegates accept either `Args` or `Args` arguments, which encapsulate event information. Note that all these delegates are asynchronous and return a `ValueTask`. Learn more about [arguments](#arguments) in the sections bellow. + +> [!NOTE] +> When setting up delegates, consider using the `ResilienceContext.ContinueOnCapturedContext` property if your user code interacts with a synchronization context (as in asynchronous UI applications like Windows Forms or WPF). + +### How to use delegates + +Below are some examples illustrating the usage of these delegates: + + +```cs +new ResiliencePipelineBuilder() + .AddRetry(new RetryStrategyOptions + { + // Non-Generic predicate for multiple result types + ShouldHandle = args => args.Outcome switch + { + { Exception: InvalidOperationException } => PredicateResult.True(), + { Result: string result } when result == "Failure" => PredicateResult.True(), + { Result: int result } when result == -1 => PredicateResult.True(), + _ => PredicateResult.False() + }, + }) + .Build(); + +new ResiliencePipelineBuilder() + .AddRetry(new RetryStrategyOptions + { + // Generic predicate for a single result type + ShouldHandle = args => args.Outcome switch + { + { Exception: InvalidOperationException } => PredicateResult.True(), + { Result: { } result } when result == "Failure" => PredicateResult.True(), + _ => PredicateResult.False() + }, + }) + .Build(); +``` + + +## Arguments + +Arguments are used by individual delegate types to flow information to the consumer. Arguments should always have an `Arguments` suffix and include a `Context` property. Using arguments boosts the extensibility and maintainability of the API, as adding new members becomes a non-breaking change. For proactive strategies, the arguments structure might resemble: + + +```cs +// Structs for arguments encapsulate details about specific events within the resilience strategy. +// Relevant properties to the event can be exposed. In this event, the actual execution time and the exceeded threshold are included. +public readonly struct ThresholdExceededArguments +{ + public ThresholdExceededArguments(ResilienceContext context, TimeSpan threshold, TimeSpan duration) + { + Context = context; + Threshold = threshold; + Duration = duration; + } + + public TimeSpan Threshold { get; } + + public TimeSpan Duration { get; } + + // As per convention, all arguments should provide a "Context" property. + public ResilienceContext Context { get; } +} +``` + + +## Implementing a resilience strategy + +To understand the details of implementing a strategy, use the links below: + +- [Proactive strategy](proactive-strategy.md): Explains how to implement a proactive resilience strategy. +- [Reactive strategy](reactive-strategy.md): Explains how to implement a reactive resilience strategy. diff --git a/docs/extensibility/proactive-strategy.md b/docs/extensibility/proactive-strategy.md new file mode 100644 index 00000000000..1434f2a6daa --- /dev/null +++ b/docs/extensibility/proactive-strategy.md @@ -0,0 +1,168 @@ +# 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. + +## Implementation + +Proactive resilience strategies are derived from the [`ResilienceStrategy`](xref:Polly.ResilienceStrategy) base class. For this strategy, the implementation is: + + +```cs +// Strategies should be internal and not exposed in the library's public API. +// Configure the strategy through extension methods and options. +internal sealed class TimingResilienceStrategy : ResilienceStrategy +{ + private readonly TimeSpan _threshold; + private readonly Func? _thresholdExceeded; + private readonly ResilienceStrategyTelemetry _telemetry; + + public TimingResilienceStrategy( + TimeSpan threshold, + Func? thresholdExceeded, + ResilienceStrategyTelemetry telemetry) + { + _threshold = threshold; + _telemetry = telemetry; + _thresholdExceeded = thresholdExceeded; + } + + protected override async ValueTask> ExecuteCore( + Func>> callback, + ResilienceContext context, + TState state) + { + var stopwatch = Stopwatch.StartNew(); + + // Execute the given callback and adhere to the ContinueOnCapturedContext property value. + Outcome outcome = await callback(context, state).ConfigureAwait(context.ContinueOnCapturedContext); + + if (stopwatch.Elapsed > _threshold) + { + // Bundle information about the event into arguments. + var args = new ThresholdExceededArguments(context, _threshold, stopwatch.Elapsed); + + // Report this as a resilience event if the execution took longer than the threshold. + _telemetry.Report( + new ResilienceEvent(ResilienceEventSeverity.Warning, "ExecutionThresholdExceeded"), + context, + args); + + if (_thresholdExceeded is not null) + { + await _thresholdExceeded(args).ConfigureAwait(context.ContinueOnCapturedContext); + } + } + + // Return the outcome directly. + return outcome; + } +} +``` + + +Review the code and comments to understand the implementation. Take note of the `ThresholdExceededArguments` struct: + + +```cs +// Structs for arguments encapsulate details about specific events within the resilience strategy. +// Relevant properties to the event can be exposed. In this event, the actual execution time and the exceeded threshold are included. +public readonly struct ThresholdExceededArguments +{ + public ThresholdExceededArguments(ResilienceContext context, TimeSpan threshold, TimeSpan duration) + { + Context = context; + Threshold = threshold; + Duration = duration; + } + + public TimeSpan Threshold { get; } + + public TimeSpan Duration { get; } + + // As per convention, all arguments should provide a "Context" property. + public ResilienceContext Context { get; } +} +``` + + +Arguments should always have an `Arguments` suffix and include a `Context` property. Using arguments boosts the extensibility and maintainability of the API, as adding new members becomes a non-breaking change. The `ThresholdExceededArguments` provides details about the actual execution time and threshold, allowing listeners to respond to this event or supply a custom callback for such situations. + +## Options + +In the previous section, we implemented the `TimingResilienceStrategy`. Now, it's time to integrate it with Polly and its public API. + +Let's define the public `TimingStrategyOptions` to configure our strategy: + + +```cs +public class TimingStrategyOptions : ResilienceStrategyOptions +{ + public TimingStrategyOptions() + { + // Assign a default name to the options for more detailed telemetry insights. + Name = "Timing"; + } + + // Apply validation attributes to guarantee the options' validity. + // The pipeline will handle validation automatically during its construction. + [Range(typeof(TimeSpan), "00:00:00", "1.00:00:00")] + [Required] + public TimeSpan? Threshold { get; set; } + + // Provide the delegate to be called when the threshold is surpassed. + // Ideally, arguments should share the delegate's name, but with an "Arguments" suffix. + public Func? ThresholdExceeded { get; set; } +} +``` + + +Options represent our public contract with the consumer. By using them, we can easily add new members without breaking changes and perform validation consistently. + +## Extensions + +So far, we've covered: + +- The public `TimingStrategyOptions` and its associated arguments. +- The proactive strategy implementation named `TimingResilienceStrategy`. + +The final step is to integrate these components by adding new extensions for both `ResiliencePipelineBuilder` and `ResiliencePipelineBuilder`. Since both builders inherit from the same base class, we can introduce a single extension for `ResiliencePipelineBuilderBase` to serve both. + + +```cs +public static class TimingResilienceStrategyBuilderExtensions +{ + // The extensions should return the builder to support a fluent API. + // For proactive strategies, we can target both "ResiliencePipelineBuilderBase" and "ResiliencePipelineBuilder" + // using generic constraints. + public static TBuilder AddTiming(this TBuilder builder, TimingStrategyOptions options) + where TBuilder : ResiliencePipelineBuilderBase + { + // Add the strategy through the AddStrategy method. This method accepts a factory delegate + // and automatically validates the options. + + return builder.AddStrategy( + context => + { + // The "context" provides various properties for the strategy's use. + // In this case, we simply use the "Telemetry" and pass it to the strategy. + // The Threshold and ThresholdExceeded values are sourced from the options. + var strategy = new TimingResilienceStrategy( + options.Threshold!.Value, + options.ThresholdExceeded, + context.Telemetry); + + return strategy; + }, + options); + } +} +``` + + +## Resources + +For further understanding of proactive resilience strategies, consider exploring these resources: + +- [Timing strategy sample](https://github.com/App-vNext/Polly/tree/main/samples/Extensibility/Proactive): A practical example from this guide. +- [Timeout resilience strategy](https://github.com/App-vNext/Polly/tree/main/src/Polly.Core/Timeout): Discover the built-in timeout resilience strategy implementation. +- [Rate limiter resilience strategy](https://github.com/App-vNext/Polly/tree/main/src/Polly.RateLimiting): Discover how rate limiter strategy is implemented. diff --git a/docs/extensibility/reactive-strategy.md b/docs/extensibility/reactive-strategy.md new file mode 100644 index 00000000000..4df5d8cefea --- /dev/null +++ b/docs/extensibility/reactive-strategy.md @@ -0,0 +1 @@ +# Reactive resilience strategy diff --git a/docs/index.md b/docs/index.md index da6e8c2e3cd..5ea87a81e3a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -34,16 +34,16 @@ Polly has a rich documentation that covers various topics, such as: - [Resilience pipelines](pipelines/index.md): How to combine and reuse strategies in a flexible and modular way. - [Telemetry and monitoring](advanced/telemetry.md): How to access and analyze the data generated by Polly strategies and pipelines. - [Dependency injection](advanced/dependency-injection.md): How to integrate Polly with dependency injection frameworks and containers. -- [Extensibility](advanced/extensibility.md): How to create and use custom strategies and extensions for Polly. - [Performance](advanced/performance.md): Tips on optimizing and getting the best performance from Polly. - [Chaos engineering](advanced/simmy.md): How to use Polly to inject faults and test the resilience of your system. +- [Extensibility](extensibility/index.md): How to create and use custom strategies and extensions for Polly. You can also find many resources and community contributions, such as: - [Samples](https://github.com/App-vNext/Polly/tree/main/samples): Samples in this repository that serve as an introduction to Polly. - [Practical Samples](https://github.com/App-vNext/Polly-Samples): Practical examples for using various implementations of Polly. Please feel free to contribute to the Polly-Samples repository in order to assist others who are either learning Polly for the first time, or are seeking advanced examples and novel approaches provided by our generous community. - [Polly-Contrib](community/polly-contrib.md): Community projects and libraries that extend and enhance Polly's functionality and ecosystem. -- [Libraries and contributions](community/libraries-and-contributions): Dependencies and contributors that make Polly possible and awesome. +- [Libraries and contributions](community/libraries-and-contributions.md): Dependencies and contributors that make Polly possible and awesome. - Microsoft's [eShopOnContainers project](https://github.com/dotnet-architecture/eShopOnContainers): Sample project demonstrating a .NET Microservices architecture and using Polly for resilience. You can browse the documentation using the sidebar or visit the [API](api/index.md) section for the reference documentation. diff --git a/docs/toc.yml b/docs/toc.yml index 36f494a5751..f4b182103e0 100644 --- a/docs/toc.yml +++ b/docs/toc.yml @@ -37,8 +37,6 @@ items: - name: Telemetry and monitoring href: advanced/telemetry.md - - name: Extensibility - href: advanced/extensibility.md - name: Dependency injection href: advanced/dependency-injection.md - name: Resilience context @@ -48,6 +46,15 @@ - name: Chaos engineering href: advanced/simmy.md +- name: Extensibility + href: extensibility/index.md + expanded: true + items: + - name: Proactive strategy + href: extensibility/proactive-strategy.md + - name: Reactive strategy + href: extensibility/reactive-strategy.md + - name: Community and resources expanded: true items: diff --git a/samples/Extensibility/Proactive/ThresholdExceededArguments.cs b/samples/Extensibility/Proactive/ThresholdExceededArguments.cs new file mode 100644 index 00000000000..dd6d6749db2 --- /dev/null +++ b/samples/Extensibility/Proactive/ThresholdExceededArguments.cs @@ -0,0 +1,26 @@ +using Polly; + +namespace Extensibility.Proactive; + +#region ext-proactive-args + +// Structs for arguments encapsulate details about specific events within the resilience strategy. +// Relevant properties to the event can be exposed. In this event, the actual execution time and the exceeded threshold are included. +public readonly struct ThresholdExceededArguments +{ + public ThresholdExceededArguments(ResilienceContext context, TimeSpan threshold, TimeSpan duration) + { + Context = context; + Threshold = threshold; + Duration = duration; + } + + public TimeSpan Threshold { get; } + + public TimeSpan Duration { get; } + + // As per convention, all arguments should provide a "Context" property. + public ResilienceContext Context { get; } +} + +#endregion diff --git a/samples/Extensibility/Proactive/TimingResilienceStrategy.cs b/samples/Extensibility/Proactive/TimingResilienceStrategy.cs new file mode 100644 index 00000000000..2f3459733a6 --- /dev/null +++ b/samples/Extensibility/Proactive/TimingResilienceStrategy.cs @@ -0,0 +1,59 @@ +using Polly; +using Polly.Telemetry; +using System.Diagnostics; + +namespace Extensibility.Proactive; + +#region ext-proactive-strategy + +// Strategies should be internal and not exposed in the library's public API. +// Configure the strategy through extension methods and options. +internal sealed class TimingResilienceStrategy : ResilienceStrategy +{ + private readonly TimeSpan _threshold; + private readonly Func? _thresholdExceeded; + private readonly ResilienceStrategyTelemetry _telemetry; + + public TimingResilienceStrategy( + TimeSpan threshold, + Func? thresholdExceeded, + ResilienceStrategyTelemetry telemetry) + { + _threshold = threshold; + _telemetry = telemetry; + _thresholdExceeded = thresholdExceeded; + } + + protected override async ValueTask> ExecuteCore( + Func>> callback, + ResilienceContext context, + TState state) + { + var stopwatch = Stopwatch.StartNew(); + + // Execute the given callback and adhere to the ContinueOnCapturedContext property value. + Outcome outcome = await callback(context, state).ConfigureAwait(context.ContinueOnCapturedContext); + + if (stopwatch.Elapsed > _threshold) + { + // Bundle information about the event into arguments. + var args = new ThresholdExceededArguments(context, _threshold, stopwatch.Elapsed); + + // Report this as a resilience event if the execution took longer than the threshold. + _telemetry.Report( + new ResilienceEvent(ResilienceEventSeverity.Warning, "ExecutionThresholdExceeded"), + context, + args); + + if (_thresholdExceeded is not null) + { + await _thresholdExceeded(args).ConfigureAwait(context.ContinueOnCapturedContext); + } + } + + // Return the outcome directly. + return outcome; + } +} + +#endregion diff --git a/samples/Extensibility/Proactive/TimingResilienceStrategyBuilderExtensions.cs b/samples/Extensibility/Proactive/TimingResilienceStrategyBuilderExtensions.cs new file mode 100644 index 00000000000..851b66ecc0e --- /dev/null +++ b/samples/Extensibility/Proactive/TimingResilienceStrategyBuilderExtensions.cs @@ -0,0 +1,37 @@ +using Polly; + +namespace Extensibility.Proactive; + +#pragma warning disable IDE0022 // Use expression body for method + +#region ext-proactive-extensions + +public static class TimingResilienceStrategyBuilderExtensions +{ + // The extensions should return the builder to support a fluent API. + // For proactive strategies, we can target both "ResiliencePipelineBuilderBase" and "ResiliencePipelineBuilder" + // using generic constraints. + public static TBuilder AddTiming(this TBuilder builder, TimingStrategyOptions options) + where TBuilder : ResiliencePipelineBuilderBase + { + // Add the strategy through the AddStrategy method. This method accepts a factory delegate + // and automatically validates the options. + + return builder.AddStrategy( + context => + { + // The "context" provides various properties for the strategy's use. + // In this case, we simply use the "Telemetry" and pass it to the strategy. + // The Threshold and ThresholdExceeded values are sourced from the options. + var strategy = new TimingResilienceStrategy( + options.Threshold!.Value, + options.ThresholdExceeded, + context.Telemetry); + + return strategy; + }, + options); + } +} + +#endregion diff --git a/samples/Extensibility/Proactive/TimingStrategyOptions.cs b/samples/Extensibility/Proactive/TimingStrategyOptions.cs new file mode 100644 index 00000000000..3fa5586a911 --- /dev/null +++ b/samples/Extensibility/Proactive/TimingStrategyOptions.cs @@ -0,0 +1,27 @@ +using Polly; +using System.ComponentModel.DataAnnotations; + +namespace Extensibility.Proactive; + +#region ext-proactive-options + +public class TimingStrategyOptions : ResilienceStrategyOptions +{ + public TimingStrategyOptions() + { + // Assign a default name to the options for more detailed telemetry insights. + Name = "Timing"; + } + + // Apply validation attributes to guarantee the options' validity. + // The pipeline will handle validation automatically during its construction. + [Range(typeof(TimeSpan), "00:00:00", "1.00:00:00")] + [Required] + public TimeSpan? Threshold { get; set; } + + // Provide the delegate to be called when the threshold is surpassed. + // Ideally, arguments should share the delegate's name, but with an "Arguments" suffix. + public Func? ThresholdExceeded { get; set; } +} + +#endregion diff --git a/samples/Extensibility/Program.cs b/samples/Extensibility/Program.cs index 79d638781c8..d684bd29aae 100644 --- a/samples/Extensibility/Program.cs +++ b/samples/Extensibility/Program.cs @@ -1,23 +1,25 @@ -using Polly; -using Polly.Telemetry; +using Extensibility.Proactive; +using Polly; +using System.Net.Http.Headers; // ------------------------------------------------------------------------ -// Usage of custom strategy +// Usage of custom proactive strategy // ------------------------------------------------------------------------ var pipeline = new ResiliencePipelineBuilder() // This is custom extension defined in this sample - .AddMyResilienceStrategy(new MySimpleStrategyOptions + .AddTiming(new TimingStrategyOptions { - OnCustomEvent = args => + Threshold = TimeSpan.FromSeconds(1), + ThresholdExceeded = args => { - Console.WriteLine("OnCustomEvent"); + Console.WriteLine("Execution threshold exceeded!"); return default; }, }) .Build(); // Execute the pipeline -pipeline.Execute(() => { }); +await pipeline.ExecuteAsync(async token => await Task.Delay(1500, token), CancellationToken.None); // ------------------------------------------------------------------------ // SIMPLE EXTENSIBILITY MODEL (INLINE STRATEGY) @@ -47,85 +49,6 @@ protected override ValueTask> ExecuteCore( } } -// ------------------------------------------------------------------------ -// STANDARD EXTENSIBILITY MODEL -// ------------------------------------------------------------------------ - -// ------------------------------------------------------------------------ -// 1. Create options for your custom strategy -// ------------------------------------------------------------------------ - -// 1.A Define arguments for events that your strategy uses (optional) -public readonly record struct OnCustomEventArguments(ResilienceContext Context); - -// 1.B Define the options. public class MySimpleStrategyOptions : ResilienceStrategyOptions { - // Use the arguments in the delegates. - // The recommendation is to use asynchronous delegates. - public Func? OnCustomEvent { get; set; } -} - -// ------------------------------------------------------------------------ -// 2. Create a custom resilience strategy that derives from ResilienceStrategy -// ------------------------------------------------------------------------ - -// The strategy should be internal and not exposed as part of any public API. -// Instead, expose options and extensions for resilience strategy builder. -// -// For reactive strategies, you can use ReactiveResilienceStrategy as base class. -internal class MyResilienceStrategy : ResilienceStrategy -{ - private readonly ResilienceStrategyTelemetry _telemetry; - private readonly Func? _onCustomEvent; - - public MyResilienceStrategy(ResilienceStrategyTelemetry telemetry, MySimpleStrategyOptions options) - { - _telemetry = telemetry; - _onCustomEvent = options.OnCustomEvent; - } - - protected override async ValueTask> ExecuteCore( - Func>> callback, - ResilienceContext context, - TState state) - { - // Here, do something before callback execution - // ... - - // Execute the provided callback - var outcome = await callback(context, state); - - // Here, do something after callback execution - // ... - - // You can then report important telemetry events - _telemetry.Report( - new ResilienceEvent(ResilienceEventSeverity.Information, "MyCustomEvent"), - context, - new OnCustomEventArguments(context)); - - // Call the delegate if provided by the user - if (_onCustomEvent is not null) - { - await _onCustomEvent(new OnCustomEventArguments(context)); - } - - return outcome; - } -} - -// ------------------------------------------------------------------------ -// 3. Expose new extensions for ResiliencePipelineBuilder -// ------------------------------------------------------------------------ -public static class MyResilienceStrategyExtensions -{ - // Add new extension that works for both "ResiliencePipelineBuilder" and "ResiliencePipelineBuilder" - public static TBuilder AddMyResilienceStrategy(this TBuilder builder, MySimpleStrategyOptions options) - where TBuilder : ResiliencePipelineBuilderBase - { - return builder.AddStrategy( - context => new MyResilienceStrategy(context.Telemetry, options), // Provide a factory that creates the strategy - options); // Pass the options, note that the options instance is automatically validated by the builder - } } diff --git a/src/Polly.Core/README.md b/src/Polly.Core/README.md index b9ea2561885..94c865af273 100644 --- a/src/Polly.Core/README.md +++ b/src/Polly.Core/README.md @@ -1,10 +1,6 @@ # Polly V8 API Documentation -The Polly V8 API offers a unified, non-allocating resilience API, detailed in the sections below. - -## Introduction - -At the core of Polly V8 is the [`ResiliencePipeline`](ResiliencePipeline.cs) class, responsible for executing user-provided callbacks. This class handles all scenarios covered in Polly V7, such as: +The Polly V8 API offers a unified, non-allocating resilience API. At the core of Polly V8 is the [`ResiliencePipeline`](ResiliencePipeline.cs) class, responsible for executing user-provided callbacks. This class handles all scenarios covered in Polly V7, such as: - `ISyncPolicy` - `IAsyncPolicy` @@ -60,190 +56,6 @@ The `ResiliencePipeline` class unifies the four different policies that were ava > [!NOTE] > Polly also provides a `ResiliencePipeline` class. This specialized pipeline is useful for scenarios where the consumer is concerned with only a single type of result. -### Building resilience pipeline - -- Use `ResiliencePipelineBuilder` to construct an instance of `ResiliencePipeline`. -- Use `ResiliencePipelineBuilder` to construct an instance of `ResiliencePipeline`. - -- `ResilienceStrategy`: Base class for all proactive resilience strategies. -- `ResilienceStrategy`: Base class for all reactive resilience strategies. - -Polly provides a variety of extension methods to add resilience strategies to each type of builder. - -### Example: Custom Proactive Strategy - -Here's an example of a proactive strategy that executes a user-provided callback: - - -```cs -internal class MyCustomStrategy : ResilienceStrategy -{ - protected override async ValueTask> ExecuteCore( - Func>> callback, - ResilienceContext context, - TState state) - { - // Perform actions before execution - - var outcome = await callback(context, state).ConfigureAwait(context.ContinueOnCapturedContext); - - // Perform actions after execution - - return outcome; - } -} -``` - - -### About Synchronous and Asynchronous Executions - -Polly's core, from version 8, fundamentally focuses on asynchronous executions. However, it also supports synchronous executions, which require minimal effort for authors developing custom resilience strategies. This support is enabled by passing and wrapping the synchronous callback provided by the user into an asynchronous one, which returns a completed `ValueTask` upon completion. This feature allows custom resilience strategies to treat all executions as asynchronous. In cases of synchronous execution, the method simply returns a completed task upon awaiting. - -## Creating a `ResiliencePipeline` - -The API exposes the following builder classes for creating resilience pipelines: - -- [`ResiliencePipelineBuilder`](ResiliencePipelineBuilder.cs): Useful for creating resilience strategies capable of executing any type of callback. Typically, these strategies are focused on exception handling. -- [`ResiliencePipelineBuilder`](ResiliencePipelineBuilder.TResult.cs): Aimed at creating generic resilience strategies that execute callbacks returning a specific result type. -- [`ResiliencePipelineBuilderBase`](ResiliencePipelineBuilderBase.cs): This serves as the base class for both of the builders mentioned above. It can be used as a target for strategy extensions compatible with either of the two. - -To construct a resilience pipeline, chain various extensions on the `ResiliencePipelineBuilder` and conclude with a `Build` method call. - -Explore [resilience pipelines](../../docs/resilience-pipelines.md) page to explore the consumption of resilience pipelines from the user perspective. - -### Creating a non-generic pipeline - - -```cs -ResiliencePipeline pipeline = new ResiliencePipelineBuilder() - .AddRetry(new()) - .AddCircuitBreaker(new()) - .AddTimeout(TimeSpan.FromSeconds(1)) - .Build(); -``` - - -### Creating a generic pipeline - - -```cs -ResiliencePipeline pipeline = new ResiliencePipelineBuilder() - .AddRetry(new()) - .AddCircuitBreaker(new()) - .AddTimeout(TimeSpan.FromSeconds(1)) - .Build(); -``` - - -## Extensibility - -Extending the resilience functionality is straightforward. You can create extensions for `ResiliencePipelineBuilder` by leveraging the `AddStrategy` extension methods. If you aim to design a resilience strategy that is compatible with both generic and non-generic builders, consider using `ResiliencePipelineBuilderBase` as your target class. - -Here's an example: +## Resources - -```cs -public static TBuilder AddMyCustomStrategy(this TBuilder builder, MyCustomStrategyOptions options) - where TBuilder : ResiliencePipelineBuilderBase -{ - return builder.AddStrategy(context => new MyCustomStrategy(), options); -} - -public class MyCustomStrategyOptions : ResilienceStrategyOptions -{ - public MyCustomStrategyOptions() - { - Name = "MyCustomStrategy"; - } -} -``` - - -To gain insights into implementing custom resilience strategies, you can explore the following Polly strategy examples: - -- [**Retry**](Retry/): Demonstrates how to implement a reactive resilience strategy. -- [**Timeout**](Timeout/): Provides guidance on implementing a proactive resilience strategy. -- [**Extensibility Sample**](../../samples/Extensibility/): Offers a practical example of creating a custom resilience strategy. - -## Resilience Strategy Delegates - -Individual resilience strategies make use of several delegate types: - -- **Predicates**: Vital for determining whether a resilience strategy should handle the given execution result. -- **Events**: Triggered when significant actions or states occur within the resilience strategy. -- **Generators**: Invoked when the resilience strategy needs specific information or values from the caller. - -Recommended signatures for these delegates are: - -### Predicates - -- `Func, ValueTask>` (Reactive) - -### Events - -- `Func, ValueTask>` (Reactive) -- `Func` (Proactive) - -### Generators - -- `Func, ValueTask>` (Reactive) -- `Func>` (Proactive) - -These delegates accept either `Args` or `Args` arguments, which encapsulate event information. Note that all these delegates are asynchronous and return a `ValueTask`. - -> [!NOTE] -> When setting up delegates, consider using the `ResilienceContext.ContinueOnCapturedContext` property if your user code interacts with a synchronization context (as in asynchronous UI applications like Windows Forms or WPF). - -For proactive strategies, the `Args` structure might resemble: - - -```cs -public readonly struct OnTimeoutArguments -{ - public OnTimeoutArguments(ResilienceContext context, TimeSpan timeout) - { - Context = context; - Timeout = timeout; - } - - public ResilienceContext Context { get; } // Include the Context property - - public TimeSpan Timeout { get; } // Additional event-related properties -} -``` - - -### Example: Usage of Delegates - -Below are some examples illustrating the usage of these delegates: - - -```cs -new ResiliencePipelineBuilder() - .AddRetry(new RetryStrategyOptions - { - // Non-Generic predicate for multiple result types - ShouldHandle = args => args.Outcome switch - { - { Exception: InvalidOperationException } => PredicateResult.True(), - { Result: string result } when result == "Failure" => PredicateResult.True(), - { Result: int result } when result == -1 => PredicateResult.True(), - _ => PredicateResult.False() - }, - }) - .Build(); - -new ResiliencePipelineBuilder() - .AddRetry(new RetryStrategyOptions - { - // Generic predicate for a single result type - ShouldHandle = args => args.Outcome switch - { - { Exception: InvalidOperationException } => PredicateResult.True(), - { Result: { } result } when result == "Failure" => PredicateResult.True(), - _ => PredicateResult.False() - }, - }) - .Build(); -``` - +Visit to learn more about Polly. diff --git a/src/Snippets/Core/Snippets.cs b/src/Snippets/Core/Snippets.cs deleted file mode 100644 index 92d6dc0d542..00000000000 --- a/src/Snippets/Core/Snippets.cs +++ /dev/null @@ -1,142 +0,0 @@ -using Polly.Retry; - -namespace Snippets.Core; - -internal static class Snippets -{ - public static void NonGenericPipeline() - { - #region create-non-generic-pipeline - - ResiliencePipeline pipeline = new ResiliencePipelineBuilder() - .AddRetry(new()) - .AddCircuitBreaker(new()) - .AddTimeout(TimeSpan.FromSeconds(1)) - .Build(); - - #endregion - } - - public static void GenericPipeline() - { - #region create-generic-pipeline - - ResiliencePipeline pipeline = new ResiliencePipelineBuilder() - .AddRetry(new()) - .AddCircuitBreaker(new()) - .AddTimeout(TimeSpan.FromSeconds(1)) - .Build(); - - #endregion - } - - #region on-retry-args - - public readonly struct OnRetryArguments - { - public OnRetryArguments(ResilienceContext context, Outcome outcome, int attemptNumber) - { - Context = context; - Outcome = outcome; - AttemptNumber = attemptNumber; - } - - public ResilienceContext Context { get; } // Include the Context property - - public Outcome Outcome { get; } // Includes the outcome associated with the event - - public int AttemptNumber { get; } - } - - #endregion - - #region on-timeout-args - - public readonly struct OnTimeoutArguments - { - public OnTimeoutArguments(ResilienceContext context, TimeSpan timeout) - { - Context = context; - Timeout = timeout; - } - - public ResilienceContext Context { get; } // Include the Context property - - public TimeSpan Timeout { get; } // Additional event-related properties - } - - #endregion - - #region add-my-custom-strategy - - public static TBuilder AddMyCustomStrategy(this TBuilder builder, MyCustomStrategyOptions options) - where TBuilder : ResiliencePipelineBuilderBase - { - return builder.AddStrategy(context => new MyCustomStrategy(), options); - } - - public class MyCustomStrategyOptions : ResilienceStrategyOptions - { - public MyCustomStrategyOptions() - { - Name = "MyCustomStrategy"; - } - } - - #endregion - - #region my-custom-strategy - - internal class MyCustomStrategy : ResilienceStrategy - { - protected override async ValueTask> ExecuteCore( - Func>> callback, - ResilienceContext context, - TState state) - { - // Perform actions before execution - - var outcome = await callback(context, state).ConfigureAwait(context.ContinueOnCapturedContext); - - // Perform actions after execution - - return outcome; - } - } - - #endregion - - public static void DelegateUsage() - { - #region delegate-usage - - new ResiliencePipelineBuilder() - .AddRetry(new RetryStrategyOptions - { - // Non-Generic predicate for multiple result types - ShouldHandle = args => args.Outcome switch - { - { Exception: InvalidOperationException } => PredicateResult.True(), - { Result: string result } when result == "Failure" => PredicateResult.True(), - { Result: int result } when result == -1 => PredicateResult.True(), - _ => PredicateResult.False() - }, - }) - .Build(); - - new ResiliencePipelineBuilder() - .AddRetry(new RetryStrategyOptions - { - // Generic predicate for a single result type - ShouldHandle = args => args.Outcome switch - { - { Exception: InvalidOperationException } => PredicateResult.True(), - { Result: { } result } when result == "Failure" => PredicateResult.True(), - _ => PredicateResult.False() - }, - }) - .Build(); - - #endregion; - } -} diff --git a/src/Snippets/Docs/Extensibility.cs b/src/Snippets/Docs/Extensibility.cs new file mode 100644 index 00000000000..95348bd1f8a --- /dev/null +++ b/src/Snippets/Docs/Extensibility.cs @@ -0,0 +1,40 @@ +using Polly.Retry; + +namespace Snippets.Docs; + +internal static class Extensibility +{ + public static void DelegateUsage() + { + #region delegate-usage + + new ResiliencePipelineBuilder() + .AddRetry(new RetryStrategyOptions + { + // Non-Generic predicate for multiple result types + ShouldHandle = args => args.Outcome switch + { + { Exception: InvalidOperationException } => PredicateResult.True(), + { Result: string result } when result == "Failure" => PredicateResult.True(), + { Result: int result } when result == -1 => PredicateResult.True(), + _ => PredicateResult.False() + }, + }) + .Build(); + + new ResiliencePipelineBuilder() + .AddRetry(new RetryStrategyOptions + { + // Generic predicate for a single result type + ShouldHandle = args => args.Outcome switch + { + { Exception: InvalidOperationException } => PredicateResult.True(), + { Result: { } result } when result == "Failure" => PredicateResult.True(), + _ => PredicateResult.False() + }, + }) + .Build(); + + #endregion; + } +}