Proactive strategies make decisions to cancel or reject callback execution without focusing on individual results. This guide demonstrates how to create a Timing Strategy that tracks execution times and reports when thresholds are exceeded.
Proactive strategies inherit from ResilienceStrategy (non-generic) and work across various result types.
Proactive strategies derive from the non-generic ResilienceStrategy base class:
// 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<OnThresholdExceededArguments, ValueTask>? _onThresholdExceeded; private readonly ResilienceStrategyTelemetry _telemetry; public TimingResilienceStrategy( TimeSpan threshold, Func<OnThresholdExceededArguments, ValueTask>? onThresholdExceeded, ResilienceStrategyTelemetry telemetry) { _threshold = threshold; _telemetry = telemetry; _onThresholdExceeded = onThresholdExceeded; } protected override async ValueTask<Outcome<TResult>> ExecuteCore<TResult, TState>( Func<ResilienceContext, TState, ValueTask<Outcome<TResult>>> callback, ResilienceContext context, TState state) { var stopwatch = Stopwatch.StartNew(); // Execute the given callback and adhere to the ContinueOnCapturedContext property value. Outcome<TResult> outcome = await callback(context, state).ConfigureAwait(context.ContinueOnCapturedContext); if (stopwatch.Elapsed > _threshold) { // Bundle information about the event into arguments. var args = new OnThresholdExceededArguments(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 (_onThresholdExceeded is not null) { await _onThresholdExceeded(args).ConfigureAwait(context.ContinueOnCapturedContext); } } // Return the outcome directly. return outcome; }}
Proactive strategies measure or control execution behavior rather than handling specific result types.
// 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 OnThresholdExceededArguments{ public OnThresholdExceededArguments(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. This design makes the API more extensible and maintainable.
Create a public options class that inherits from ResilienceStrategyOptions:
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<OnThresholdExceededArguments, ValueTask>? OnThresholdExceeded { get; set; }}
2
Use Validation Attributes
Apply data annotation attributes to validate options:
[Required] for mandatory properties
[Range] for value constraints
Pipeline automatically validates during construction
Options represent the public contract with consumers. Use validation attributes to ensure correctness.
Proactive strategies can use a single extension method that works for both generic and non-generic builders:
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<T>" // using generic constraints. public static TBuilder AddTiming<TBuilder>(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" property and pass it to the strategy. // The Threshold and OnThresholdExceeded values are sourced from the options. var strategy = new TimingResilienceStrategy( options.Threshold!.Value, options.OnThresholdExceeded, context.Telemetry); return strategy; }, options); }}
By using generic constraints on ResiliencePipelineBuilderBase, a single extension method supports both ResiliencePipelineBuilder and ResiliencePipelineBuilder<T>.
// Add the proactive strategy to the buildervar pipeline = new ResiliencePipelineBuilder() // This is custom extension defined in this sample .AddTiming(new TimingStrategyOptions { Threshold = TimeSpan.FromSeconds(1), OnThresholdExceeded = args => { Console.WriteLine("Execution threshold exceeded!"); return default; }, }) .Build();