Skip to main content

Overview

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.

Implementation

Strategy Class

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.

Event Arguments

Define arguments to encapsulate event details:
// 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.

Options

1

Define Options Class

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.

Extension Methods

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>.

Usage Example

// 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),
        OnThresholdExceeded = args =>
        {
            Console.WriteLine("Execution threshold exceeded!");
            return default;
        },
    })
    .Build();

Complete Example

Here’s how the timing strategy works in a complete scenario:
1

Create Pipeline

Build a resilience pipeline with the timing strategy:
var pipeline = new ResiliencePipelineBuilder()
    .AddTiming(new TimingStrategyOptions
    {
        Threshold = TimeSpan.FromSeconds(1),
        OnThresholdExceeded = args =>
        {
            Console.WriteLine($"Execution took {args.Duration.TotalSeconds}s, "
                + $"exceeding threshold of {args.Threshold.TotalSeconds}s");
            return default;
        },
    })
    .Build();
2

Execute Operations

Use the pipeline to execute operations:
// Fast operation - no event triggered
await pipeline.ExecuteAsync(async ct =>
{
    await Task.Delay(500, ct);
    return "Fast operation";
});

// Slow operation - event triggered
await pipeline.ExecuteAsync(async ct =>
{
    await Task.Delay(1500, ct);
    return "Slow operation";
});
3

Monitor Results

The strategy automatically monitors execution time and triggers events when thresholds are exceeded.

Key Differences from Reactive Strategies

  • Proactive: Inherit from ResilienceStrategy (non-generic)
  • Reactive: Inherit from ResilienceStrategy<T> (generic)
  • Proactive: Arguments only need Context property
  • Reactive: Arguments need both Context and Outcome properties
  • Proactive: Monitor or control execution behavior
  • Reactive: Handle specific results or exceptions
  • Proactive: Single extension using ResiliencePipelineBuilderBase
  • Reactive: Separate extensions for generic and non-generic builders

Key Takeaways

Strategy Implementation

  • Inherit from non-generic ResilienceStrategy
  • Keep the strategy class internal
  • Measure or control execution behavior
  • Report events using ResilienceStrategyTelemetry

Arguments

  • Use readonly structs
  • Always include Context property
  • Add event-specific properties
  • Follow naming: {DelegateName}Arguments

Options

  • Inherit from ResilienceStrategyOptions
  • Use validation attributes
  • Provide sensible defaults
  • Set default strategy name

Extension Methods

  • Target ResiliencePipelineBuilderBase
  • Return builder for fluent API
  • Use AddStrategy for registration
  • Automatic options validation

Resources

Timing Strategy Sample

Complete working example from this guide

Timeout Strategy Source

Built-in timeout strategy implementation

Rate Limiter Source

Built-in rate limiter strategy implementation

Extensibility Overview

Learn about extensibility basics

Build docs developers (and LLMs) love