Skip to main content

Overview

Reactive strategies handle specific exceptions thrown or results returned by callbacks. This guide demonstrates how to create a Result Reporting Strategy that listens for specific results and reports them to other components.
Reactive strategies inherit from ResilienceStrategy<T> and use predicates to determine whether to handle an outcome.

Implementation

Strategy Class

Reactive strategies should be internal and not exposed in the public API. Configure them through extension methods and options.
// 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<T> : ResilienceStrategy<T>
{
    private readonly Func<ResultReportingPredicateArguments<T>, ValueTask<bool>> _shouldHandle;
    private readonly Func<OnReportResultArguments<T>, ValueTask> _onReportResult;
    private readonly ResilienceStrategyTelemetry _telemetry;

    public ResultReportingResilienceStrategy(
        Func<ResultReportingPredicateArguments<T>, ValueTask<bool>> shouldHandle,
        Func<OnReportResultArguments<T>, ValueTask> onReportResult,
        ResilienceStrategyTelemetry telemetry)
    {
        _shouldHandle = shouldHandle;
        _onReportResult = onReportResult;
        _telemetry = telemetry;
    }

    protected override async ValueTask<Outcome<T>> ExecuteCore<TState>(
        Func<ResilienceContext, TState, ValueTask<Outcome<T>>> callback,
        ResilienceContext context,
        TState state)
    {
        // Execute the given callback and adhere to the ContinueOnCapturedContext property value.
        Outcome<T> outcome = await callback(context, state).ConfigureAwait(context.ContinueOnCapturedContext);

        // Check if the outcome should be reported using the "ShouldHandle" predicate.
        if (await _shouldHandle(new ResultReportingPredicateArguments<T>(context, outcome)).ConfigureAwait(context.ContinueOnCapturedContext))
        {
            // Bundle information about the event into arguments.
            var args = new OnReportResultArguments<T>(context, outcome);

            // Report this as a resilience event with information 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.

Predicate Arguments

The predicate arguments follow the {StrategyName}PredicateArguments naming pattern:
public readonly struct ResultReportingPredicateArguments<TResult>
{
    public ResultReportingPredicateArguments(ResilienceContext context, Outcome<TResult> 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<TResult> Outcome { get; }
}
Reactive arguments must always contain both Context and Outcome properties.

Event Arguments

For reporting outcomes, define event-specific arguments:
public readonly struct OnReportResultArguments<TResult>
{
    public OnReportResultArguments(ResilienceContext context, Outcome<TResult> 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<TResult> Outcome { get; }
}

Options

1

Define Generic Options

Create a public options class that inherits from ResilienceStrategyOptions:
public class ResultReportingStrategyOptions<TResult> : ResilienceStrategyOptions
{
    public ResultReportingStrategyOptions()
    {
        // Assign a default name to the options for more detailed 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<ResultReportingPredicateArguments<TResult>, ValueTask<bool>> ShouldHandle { get; set; } = args =>
    {
        return new ValueTask<bool>(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] attribute enforces the consumer to specify this property, used when some properties do not have sensible defaults and are required.
    [Required]
    public Func<OnReportResultArguments<TResult>, ValueTask>? OnReportResult { get; set; }
}
2

Add Non-Generic Support (Optional)

For non-generic pipelines, derive from the 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<object>
{
}
Using options as a public contract ensures flexibility and allows you to add new members without breaking changes.

Extension Methods

Create extension methods to register the strategy with the pipeline builder:
public static class ResultReportingResilienceStrategyBuilderExtensions
{
    // Add extensions for the generic builder.
    // Extensions should return the builder to support a fluent API.
    public static ResiliencePipelineBuilder<TResult> AddResultReporting<TResult>(
        this ResiliencePipelineBuilder<TResult> builder,
        ResultReportingStrategyOptions<TResult> options)
    {
        // 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 ShouldHandle and OnReportResult values are sourced from the options.
                var strategy = new ResultReportingResilienceStrategy<TResult>(
                    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<object>(
                    options.ShouldHandle,
                    options.OnReportResult!,
                    context.Telemetry);

                return strategy;
            },
            options);
    }
}

Usage Examples

// Add reactive strategy to the builder
new ResiliencePipelineBuilder<HttpResponseMessage>()
    .AddResultReporting(new ResultReportingStrategyOptions<HttpResponseMessage>
    {
        // 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;
        }
    });

Key Takeaways

  • Inherit from ResilienceStrategy<T>
  • Keep the strategy class internal
  • Use ShouldHandle predicate to determine if an outcome should be handled
  • Report events using ResilienceStrategyTelemetry
  • Always include Context and Outcome properties
  • Use readonly structs for arguments
  • Follow naming convention: {DelegateName}Arguments
  • Inherit from ResilienceStrategyOptions
  • Provide sensible defaults where possible
  • Use validation attributes (e.g., [Required])
  • Support both generic and non-generic variants
  • Return the builder to support fluent API
  • Use AddStrategy method for registration
  • Automatic validation of options

Resources

Result Reporting Sample

Complete working example from this guide

Retry Strategy Source

Built-in retry strategy implementation

Fallback Strategy Source

Built-in fallback strategy implementation

Extensibility Overview

Learn about extensibility basics

Build docs developers (and LLMs) love