File: Resilience\ResilienceHttpClientBuilderExtensions.Resilience.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;
using System.Net.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.ExceptionSummarization;
using Microsoft.Extensions.Http.Resilience;
using Microsoft.Extensions.Http.Resilience.Internal;
using Microsoft.Shared.Diagnostics;
using Polly;
using Polly.Registry;
using Polly.Telemetry;
 
namespace Microsoft.Extensions.DependencyInjection;
 
/// <summary>
/// Extensions for <see cref="IHttpClientBuilder"/>.
/// </summary>
public static partial class ResilienceHttpClientBuilderExtensions
{
    /// <summary>
    /// Adds a resilience pipeline handler that uses a named inline resilience pipeline.
    /// </summary>
    /// <param name="builder">The builder instance.</param>
    /// <param name="pipelineName">The custom identifier for the resilience pipeline, used in the name of the pipeline.</param>
    /// <param name="configure">The callback that configures the pipeline.</param>
    /// <returns>The value of <paramref name="builder"/>.</returns>
    /// <remarks>
    /// The final pipeline name is combination of <see cref="IHttpClientBuilder.Name"/> and <paramref name="pipelineName"/>.
    /// Use pipeline name identifier if your HTTP client contains multiple resilience handlers.
    /// </remarks>
    public static IHttpResiliencePipelineBuilder AddResilienceHandler(
        this IHttpClientBuilder builder,
        string pipelineName,
        Action<ResiliencePipelineBuilder<HttpResponseMessage>> configure)
    {
        _ = Throw.IfNull(builder);
        _ = Throw.IfNullOrEmpty(pipelineName);
        _ = Throw.IfNull(configure);
 
        return builder.AddResilienceHandler(pipelineName, (builder, _) => configure(builder));
    }
 
    /// <summary>
    /// Adds a resilience pipeline handler that uses a named inline resilience pipeline.
    /// </summary>
    /// <param name="builder">The builder instance.</param>
    /// <param name="pipelineName">The custom identifier for the resilience pipeline, used in the name of the pipeline.</param>
    /// <param name="configure">The callback that configures the pipeline.</param>
    /// <returns>The value of <paramref name="builder"/>.</returns>
    /// <remarks>
    /// The final pipeline name is combination of <see cref="IHttpClientBuilder.Name"/> and <paramref name="pipelineName"/>.
    /// Use pipeline name identifier if your HTTP client contains multiple resilience handlers.
    /// </remarks>
    public static IHttpResiliencePipelineBuilder AddResilienceHandler(
        this IHttpClientBuilder builder,
        string pipelineName,
        Action<ResiliencePipelineBuilder<HttpResponseMessage>, ResilienceHandlerContext> configure)
    {
        _ = Throw.IfNull(builder);
        _ = Throw.IfNullOrEmpty(pipelineName);
        _ = Throw.IfNull(configure);
 
        var pipelineBuilder = builder.AddHttpResiliencePipeline(pipelineName, configure);
 
        _ = builder.AddHttpMessageHandler(serviceProvider =>
        {
            var selector = CreatePipelineSelector(serviceProvider, pipelineBuilder.PipelineName);
            var provider = serviceProvider.GetRequiredService<ResiliencePipelineProvider<HttpKey>>();
 
            return new ResilienceHandler(selector);
        });
 
        return pipelineBuilder;
    }
 
    private static Func<HttpRequestMessage, ResiliencePipeline<HttpResponseMessage>> CreatePipelineSelector(IServiceProvider serviceProvider, string pipelineName)
    {
        var resilienceProvider = serviceProvider.GetRequiredService<ResiliencePipelineProvider<HttpKey>>();
        var pipelineKeyProvider = serviceProvider.GetPipelineKeyProvider(pipelineName);
 
        if (pipelineKeyProvider == null)
        {
            var pipeline = resilienceProvider.GetPipeline<HttpResponseMessage>(new HttpKey(pipelineName, string.Empty));
            return _ => pipeline;
        }
        else
        {
            TouchPipelineKey(pipelineKeyProvider);
 
            return request =>
            {
                var key = pipelineKeyProvider(request);
                return resilienceProvider.GetPipeline<HttpResponseMessage>(new HttpKey(pipelineName, key));
            };
        }
    }
 
    private static void TouchPipelineKey(Func<HttpRequestMessage, string> provider)
    {
        // this piece of code eagerly checks that the pipeline key provider is correctly configured
        // combined with HttpClient auto-activation we can detect any issues on startup
#pragma warning disable S1075 // URIs should not be hardcoded - this URL is not used for any real request, nor in any telemetry
        using var request = new HttpRequestMessage(HttpMethod.Get, "https://localhost:123");
#pragma warning restore S1075 // URIs should not be hardcoded
        _ = provider(request);
    }
 
    private static HttpResiliencePipelineBuilder AddHttpResiliencePipeline(
        this IHttpClientBuilder builder,
        string name,
        Action<ResiliencePipelineBuilder<HttpResponseMessage>, ResilienceHandlerContext> configure)
    {
        var pipelineName = PipelineNameHelper.GetName(builder.Name, name);
        var key = new HttpKey(pipelineName, string.Empty);
 
        _ = builder.Services.AddResiliencePipeline<HttpKey, HttpResponseMessage>(key, (builder, context) => configure(builder, new ResilienceHandlerContext(context)));
 
        ConfigureHttpServices(builder.Services);
 
        return new(pipelineName, builder.Services);
    }
 
    private static void ConfigureHttpServices(IServiceCollection services)
    {
        // don't add any new service if this method is called multiple times
        if (services.Contains(Marker.ServiceDescriptor))
        {
            return;
        }
 
        services.Add(Marker.ServiceDescriptor);
 
        // This code configure the multi-instance support of the registry
        _ = services.Configure<ResiliencePipelineRegistryOptions<HttpKey>>(options =>
        {
            options.BuilderNameFormatter = key => key.Name;
            options.InstanceNameFormatter = key => key.InstanceName;
            options.BuilderComparer = HttpKey.BuilderComparer;
        });
 
        _ = services
            .AddExceptionSummarizer(b => b.AddHttpProvider())
            .Configure<TelemetryOptions>(options => options.MeteringEnrichers.Add(new HttpResilienceMetricsEnricher()));
    }
 
    private sealed class Marker
    {
        public static readonly ServiceDescriptor ServiceDescriptor = ServiceDescriptor.Singleton<Marker, Marker>();
    }
 
    private sealed record HttpResiliencePipelineBuilder(string PipelineName, IServiceCollection Services) : IHttpResiliencePipelineBuilder;
}