File: Polly\HttpRetryResiliencePipelineBuilderExtensions.cs
Web Access
Project: src\src\Libraries\Microsoft.Extensions.Http.Resilience\Microsoft.Extensions.Http.Resilience.csproj (Microsoft.Extensions.Http.Resilience)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Net.Http;
using Microsoft.Extensions.Http.Resilience.Internal;
using Microsoft.Shared.DiagnosticIds;
using Microsoft.Shared.Diagnostics;
using Polly;
using Polly.Retry;
 
namespace Microsoft.Extensions.Http.Resilience;
 
/// <summary>
/// Extensions for adding an HTTP retry strategy to <see cref="ResiliencePipelineBuilder{TResult}"/>.
/// </summary>
[Experimental(diagnosticId: DiagnosticIds.Experiments.Resilience, UrlFormat = DiagnosticIds.UrlFormat)]
public static class HttpRetryResiliencePipelineBuilderExtensions
{
    private static readonly HttpRetryBudgetOptionsValidator _budgetValidator = new();
 
    /// <summary>
    /// Adds a retry strategy limited by a retry budget to the builder. Use this method instead of
    /// <see cref="RetryResiliencePipelineBuilderExtensions.AddRetry{TResult}(ResiliencePipelineBuilder{TResult}, RetryStrategyOptions{TResult})"/>
    /// in custom pipelines, because <c>AddRetry</c> can't apply <see cref="HttpRetryStrategyOptions.RetryBudget"/> and rejects
    /// options that set it.
    /// </summary>
    /// <param name="builder">The builder instance.</param>
    /// <param name="options">The retry strategy options.</param>
    /// <returns>The value of <paramref name="builder"/>.</returns>
    /// <remarks>
    /// The budget needs a strategy that counts every request sent through the pipeline, in addition to the retry strategy.
    /// This method adds both. The budget is configured by <see cref="HttpRetryStrategyOptions.RetryBudget"/>. When it's
    /// <see langword="null"/>, a budget with default <see cref="HttpRetryBudgetOptions"/> values is used, without modifying
    /// <paramref name="options"/>. Other retry options, such as <see cref="RetryStrategyOptions{TResult}.MaxRetryAttempts"/>,
    /// are used as is. See <see cref="HttpRetryBudgetOptions"/> for more details.
    /// </remarks>
    /// <exception cref="ValidationException">Thrown when <paramref name="options"/> are invalid.</exception>
    public static ResiliencePipelineBuilder<HttpResponseMessage> AddRetryWithBudget(this ResiliencePipelineBuilder<HttpResponseMessage> builder, HttpRetryStrategyOptions options)
    {
        _ = Throw.IfNull(builder);
        _ = Throw.IfNull(options);
 
        return AddBudgetedRetry(builder, options, options.RetryBudget ?? new HttpRetryBudgetOptions());
    }
 
    // Used by the standard resilience handler, where a null budget means that retries are not limited.
    internal static ResiliencePipelineBuilder<HttpResponseMessage> AddRetryWithOptionalBudget(this ResiliencePipelineBuilder<HttpResponseMessage> builder, HttpRetryStrategyOptions options)
    {
        return options.RetryBudget is null
            ? RetryResiliencePipelineBuilderExtensions.AddRetry(builder, options)
            : AddBudgetedRetry(builder, options, options.RetryBudget);
    }
 
    // The options instance can be shared by multiple pipelines (for example, with SelectPipelineByAuthority),
    // so the wrapped predicate goes into a copy instead of mutating the caller's instance.
    internal static HttpRetryStrategyOptions CreateBudgetedCopy(HttpRetryStrategyOptions options, HttpRetryBudgetOptions budgetOptions)
    {
        return new HttpRetryStrategyOptions
        {
            Name = options.Name,
            MaxRetryAttempts = options.MaxRetryAttempts,
            BackoffType = options.BackoffType,
            UseJitter = options.UseJitter,
            Delay = options.Delay,
            MaxDelay = options.MaxDelay,
 
            // The ShouldRetryAfterHeader setter overwrites DelayGenerator, so it must be assigned first.
            ShouldRetryAfterHeader = options.ShouldRetryAfterHeader,
            DelayGenerator = options.DelayGenerator,
            OnRetry = options.OnRetry,
            Randomizer = options.Randomizer,
            RetryBudget = budgetOptions,
 
            // A null predicate is kept as null so that Polly's options validation rejects it at registration.
            ShouldHandle = options.ShouldHandle is null ? null! : RetryBudgetStrategy.WrapShouldHandle(options.ShouldHandle, options.MaxRetryAttempts),
        };
    }
 
    [UnconditionalSuppressMessage(
        "Trimming",
        "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code",
        Justification = "All members of the budget strategy options are preserved by the DynamicDependency attribute.")]
    [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(RetryBudgetStrategyOptions))]
    private static ResiliencePipelineBuilder<HttpResponseMessage> AddBudgetedRetry(
        ResiliencePipelineBuilder<HttpResponseMessage> builder,
        HttpRetryStrategyOptions options,
        HttpRetryBudgetOptions budgetOptions)
    {
        var validation = _budgetValidator.Validate($"{options.Name}.{nameof(HttpRetryStrategyOptions.RetryBudget)}", budgetOptions);
        if (validation.Failed)
        {
            throw new ValidationException(validation.FailureMessage);
        }
 
        // Snapshot the values so that later changes to the options instance don't affect the built pipelines.
        var retryRatio = budgetOptions.RetryRatio;
        var samplingDuration = budgetOptions.SamplingDuration;
        var minimumThroughput = budgetOptions.MinimumThroughput;
 
        _ = builder.AddStrategy(
            context => new RetryBudgetStrategy(new RetryBudget(retryRatio, samplingDuration, minimumThroughput, context.TimeProvider), context.Telemetry),
            new RetryBudgetStrategyOptions { Name = options.Name });
 
        var budgetedOptions = CreateBudgetedCopy(options, budgetOptions);
 
        return RetryBudgetValidationScope.ValidateWithBudgetApplied(() => RetryResiliencePipelineBuilderExtensions.AddRetry(builder, budgetedOptions));
    }
}