Выполнения HTTP-запросов с помощью IHttpClientFactory в ASP.NET Core

Примечание.

Это не последняя версия этой статьи. В текущем выпуске см . версию .NET 8 этой статьи.

Предупреждение

Эта версия ASP.NET Core больше не поддерживается. Дополнительные сведения см. в статье о политике поддержки .NET и .NET Core. В текущем выпуске см . версию .NET 8 этой статьи.

Внимание

Эта информация относится к предварительному выпуску продукта, который может быть существенно изменен до его коммерческого выпуска. Майкрософт не предоставляет никаких гарантий, явных или подразумеваемых, относительно приведенных здесь сведений.

В текущем выпуске см . версию .NET 8 этой статьи.

Авторы Кирк Ларкин (Kirk Larkin), Стив Гордон (Steve Gordon), Гленн Кондрон (Glenn Condron) и Райан Новак (Ryan Nowak).

IHttpClientFactory можно зарегистрировать и использовать для настройки и создания экземпляров HttpClient в приложении. IHttpClientFactory предоставляет следующие преимущества:

  • Центральное расположение для именования и настройки логических экземпляров HttpClient. Например, можно зарегистрировать и настроить клиент github для доступа к GitHub. Можно зарегистрировать клиент по умолчанию для общего доступа.
  • Кодификация концепции исходящего ПО промежуточного слоя путем делегирования обработчиков в HttpClient. Предоставление расширений для ПО промежуточного слоя на основе Polly для делегирования обработчиков в HttpClient.
  • Управление созданием пулов и временем существования базовых экземпляров HttpClientMessageHandler. Автоматическое управление позволяет избежать обычных проблем со службой доменных имен (DNS), которые возникают при управлении временем существования HttpClient вручную.
  • Настройка параметров ведения журнала (через ILogger) для всех запросов, отправленных через клиентов, созданных фабрикой.

Пример кода в этой версии раздела использует System.Text.Json для десериализации содержимого JSON, возвращаемого в ответах HTTP. Для примеров, использующих Json.NET и ReadAsAsync<T>, воспользуйтесь средством выбора версии, чтобы выбрать версию 2.x этого раздела.

Шаблоны потребления

Существует несколько способов использования IHttpClientFactory в приложении:

Оптимальный подход зависит от требований приложения.

Базовое использование

Зарегистрируйте IHttpClientFactory, вызвав AddHttpClient в Program.cs:

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddHttpClient();

IHttpClientFactory можно запросить с помощью внедрения зависимостей (DI). Следующий код использует IHttpClientFactory для создания экземпляра HttpClient:

public class BasicModel : PageModel
{
    private readonly IHttpClientFactory _httpClientFactory;

    public BasicModel(IHttpClientFactory httpClientFactory) =>
        _httpClientFactory = httpClientFactory;

    public IEnumerable<GitHubBranch>? GitHubBranches { get; set; }

    public async Task OnGet()
    {
        var httpRequestMessage = new HttpRequestMessage(
            HttpMethod.Get,
            "https://api.github.com/repos/dotnet/AspNetCore.Docs/branches")
        {
            Headers =
            {
                { HeaderNames.Accept, "application/vnd.github.v3+json" },
                { HeaderNames.UserAgent, "HttpRequestsSample" }
            }
        };

        var httpClient = _httpClientFactory.CreateClient();
        var httpResponseMessage = await httpClient.SendAsync(httpRequestMessage);

        if (httpResponseMessage.IsSuccessStatusCode)
        {
            using var contentStream =
                await httpResponseMessage.Content.ReadAsStreamAsync();
            
            GitHubBranches = await JsonSerializer.DeserializeAsync
                <IEnumerable<GitHubBranch>>(contentStream);
        }
    }
}

Подобное использование IHttpClientFactory — это хороший способ рефакторинга имеющегося приложения. Он не оказывает влияния на использование HttpClient. Там, где в существующем приложении создаются экземпляры HttpClient, используйте вызовы к CreateClient.

Именованные клиенты

Именованные клиенты являются хорошим выбором в следующих случаях:

  • Приложение требует много отдельных использований HttpClient.
  • Многие HttpClient имеют другую конфигурацию.

Укажите конфигурацию для именованного клиента HttpClient во время регистрации в Program.cs:

builder.Services.AddHttpClient("GitHub", httpClient =>
{
    httpClient.BaseAddress = new Uri("https://api.github.com/");

    // using Microsoft.Net.Http.Headers;
    // The GitHub API requires two headers.
    httpClient.DefaultRequestHeaders.Add(
        HeaderNames.Accept, "application/vnd.github.v3+json");
    httpClient.DefaultRequestHeaders.Add(
        HeaderNames.UserAgent, "HttpRequestsSample");
});

В приведенном выше коде клиент регистрируется с:

  • базовым адресом https://api.github.com/;
  • двумя заголовками, необходимыми для работы с API GitHub.

CreateClient

При каждом вызове CreateClient:

  • создается новый экземпляр HttpClient;
  • вызывается действие настройки.

Чтобы создать именованный клиент, передайте его имя в CreateClient:

public class NamedClientModel : PageModel
{
    private readonly IHttpClientFactory _httpClientFactory;

    public NamedClientModel(IHttpClientFactory httpClientFactory) =>
        _httpClientFactory = httpClientFactory;

    public IEnumerable<GitHubBranch>? GitHubBranches { get; set; }

    public async Task OnGet()
    {
        var httpClient = _httpClientFactory.CreateClient("GitHub");
        var httpResponseMessage = await httpClient.GetAsync(
            "repos/dotnet/AspNetCore.Docs/branches");

        if (httpResponseMessage.IsSuccessStatusCode)
        {
            using var contentStream =
                await httpResponseMessage.Content.ReadAsStreamAsync();
            
            GitHubBranches = await JsonSerializer.DeserializeAsync
                <IEnumerable<GitHubBranch>>(contentStream);
        }
    }
}

В приведенном выше коде в запросе не требуется указывать имя узла. Достаточно передать только путь, так как используется базовый адрес, заданный для клиента.

Типизированные клиенты

Типизированные клиенты:

  • предоставляют те же возможности, что и именованные клиенты, без необходимости использовать строки в качестве ключей.
  • Это помогает IntelliSense и компилятору при использовании клиентов.
  • Они предоставляют единое расположение для настройки и взаимодействия с конкретным клиентом HttpClient. Например, можно использовать один типизированный клиент:
    • для одной серверной конечной точки;
    • для инкапсуляции всей логики, связанной с конечной точкой.
  • Поддерживаются работа с внедрением зависимостей и возможность вставки в нужное место в приложении.

Типизированный клиент принимает параметр HttpClient в конструкторе:

public class GitHubService
{
    private readonly HttpClient _httpClient;

    public GitHubService(HttpClient httpClient)
    {
        _httpClient = httpClient;

        _httpClient.BaseAddress = new Uri("https://api.github.com/");

        // using Microsoft.Net.Http.Headers;
        // The GitHub API requires two headers.
        _httpClient.DefaultRequestHeaders.Add(
            HeaderNames.Accept, "application/vnd.github.v3+json");
        _httpClient.DefaultRequestHeaders.Add(
            HeaderNames.UserAgent, "HttpRequestsSample");
    }

    public async Task<IEnumerable<GitHubBranch>?> GetAspNetCoreDocsBranchesAsync() =>
        await _httpClient.GetFromJsonAsync<IEnumerable<GitHubBranch>>(
            "repos/dotnet/AspNetCore.Docs/branches");
}

В предыдущем коде:

  • Конфигурация перемещается в типизированный клиент.
  • Предоставленный экземпляр HttpClient сохраняется в виде закрытого поля.

Можно создать связанные с API методы, которые предоставляют функциональные возможности HttpClient. Например, метод GetAspNetCoreDocsBranches инкапсулирует код для получения ветвей GitHub по документации.

Следующий код вызывает AddHttpClient в Program.cs для регистрации класса типизированного клиента GitHubService:

builder.Services.AddHttpClient<GitHubService>();

Типизированный клиент регистрируется во внедрении зависимостей как временный. В приведенном выше коде AddHttpClient регистрирует GitHubService как временную службу. Эта регистрация использует фабричный метод для следующих задач:

  1. Создайте экземпляр HttpClient.
  2. Создайте экземпляр GitHubService, передав его конструктору экземпляр HttpClient.

Типизированный клиент можно внедрить и использовать напрямую:

public class TypedClientModel : PageModel
{
    private readonly GitHubService _gitHubService;

    public TypedClientModel(GitHubService gitHubService) =>
        _gitHubService = gitHubService;

    public IEnumerable<GitHubBranch>? GitHubBranches { get; set; }

    public async Task OnGet()
    {
        try
        {
            GitHubBranches = await _gitHubService.GetAspNetCoreDocsBranchesAsync();
        }
        catch (HttpRequestException)
        {
            // ...
        }
    }
}

Конфигурацию для типизированного клиента также можно указать во время регистрации, Program.csа не в конструкторе типизированного клиента:

builder.Services.AddHttpClient<GitHubService>(httpClient =>
{
    httpClient.BaseAddress = new Uri("https://api.github.com/");

    // ...
});

Созданные клиенты

IHttpClientFactory можно использовать в сочетании с библиотеками сторонних разработчиков, например Refit. Refit — это библиотека REST для .NET. Она преобразует REST API в динамические интерфейсы. Вызовите AddRefitClient, чтобы создать динамическую реализацию интерфейса, которая использует HttpClient для внешних HTTP-вызовов.

Настраиваемый интерфейс представляет внешний API:

public interface IGitHubClient
{
    [Get("/repos/dotnet/AspNetCore.Docs/branches")]
    Task<IEnumerable<GitHubBranch>> GetAspNetCoreDocsBranchesAsync();
}

Вызовите AddRefitClient, чтобы создать динамическую реализацию и вызвать ConfigureHttpClient для настройки базового HttpClient:

builder.Services.AddRefitClient<IGitHubClient>()
    .ConfigureHttpClient(httpClient =>
    {
        httpClient.BaseAddress = new Uri("https://api.github.com/");

        // using Microsoft.Net.Http.Headers;
        // The GitHub API requires two headers.
        httpClient.DefaultRequestHeaders.Add(
            HeaderNames.Accept, "application/vnd.github.v3+json");
        httpClient.DefaultRequestHeaders.Add(
            HeaderNames.UserAgent, "HttpRequestsSample");
    });

Используйте внедрение зависимостей, чтобы получить доступ к динамической реализации IGitHubClient:

public class RefitModel : PageModel
{
    private readonly IGitHubClient _gitHubClient;

    public RefitModel(IGitHubClient gitHubClient) =>
        _gitHubClient = gitHubClient;

    public IEnumerable<GitHubBranch>? GitHubBranches { get; set; }

    public async Task OnGet()
    {
        try
        {
            GitHubBranches = await _gitHubClient.GetAspNetCoreDocsBranchesAsync();
        }
        catch (ApiException)
        {
            // ...
        }
    }
}

Выполнение запросов POST, PUT и DELETE

В предыдущих примерах все HTTP-запросы используют HTTP-команду GET. HttpClient также поддерживает другие HTTP-команды, в том числе:

  • POST
  • PUT
  • DELETE
  • PATCH

Полный список поддерживаемых HTTP-команд см. в статье HttpMethod.

В следующем примере показано, как выполнить HTTP-запрос POST:

public async Task CreateItemAsync(TodoItem todoItem)
{
    var todoItemJson = new StringContent(
        JsonSerializer.Serialize(todoItem),
        Encoding.UTF8,
        Application.Json); // using static System.Net.Mime.MediaTypeNames;

    using var httpResponseMessage =
        await _httpClient.PostAsync("/api/TodoItems", todoItemJson);

    httpResponseMessage.EnsureSuccessStatusCode();
}

В приведенном выше коде метод CreateItemAsync выполняет следующие задачи:

  • сериализует параметр TodoItem в JSON с помощью System.Text.Json.
  • создает экземпляр StringContent для упаковки сериализованного JSON для отправки в тексте HTTP-запроса.
  • вызывает метод PostAsync для отправки содержимого JSON по указанному URL-адресу. Это относительный URL-адрес, который добавляется в свойство HttpClient.BaseAddress.
  • Вызывает EnsureSuccessStatusCode для выдачи исключения, если код состояния ответа не указывает на успешное выполнение.

HttpClient также поддерживает другие типы содержимого. Например, MultipartContent и StreamContent. Полный список поддерживаемого содержимого см. в статье HttpContent.

Ниже приводится пример HTTP-запроса PUT.

public async Task SaveItemAsync(TodoItem todoItem)
{
    var todoItemJson = new StringContent(
        JsonSerializer.Serialize(todoItem),
        Encoding.UTF8,
        Application.Json);

    using var httpResponseMessage =
        await _httpClient.PutAsync($"/api/TodoItems/{todoItem.Id}", todoItemJson);

    httpResponseMessage.EnsureSuccessStatusCode();
}

Приведенный выше код похож на код в примере с POST. Метод SaveItemAsync вызывает PutAsync вместо PostAsync.

Ниже приводится пример HTTP-запроса DELETE.

public async Task DeleteItemAsync(long itemId)
{
    using var httpResponseMessage =
        await _httpClient.DeleteAsync($"/api/TodoItems/{itemId}");

    httpResponseMessage.EnsureSuccessStatusCode();
}

В приведенном выше коде метод DeleteItemAsync вызывает DeleteAsync. Поскольку HTTP-запросы DELETE обычно не содержат текст, метод DeleteAsync не предоставляет перегрузку, которая принимает экземпляр HttpContent.

Дополнительные сведения об использовании различных HTTP-команд с HttpClient см. в статье HttpClient.

ПО промежуточного слоя для исходящих запросов

В HttpClient существует концепция делегирования обработчиков, которые можно связать друг с другом для исходящих HTTP-запросов. IHttpClientFactory:

  • Упрощает определение обработчиков для применения к каждому именованному клиенту.
  • Поддерживает регистрацию и объединение в цепочки нескольких обработчиков для создания конвейера ПО промежуточного слоя для исходящих запросов. Каждый из этих обработчиков может выполнять работу до и после исходящего запроса. Этот шаблон:
    • похож на входящий конвейер ПО промежуточного слоя в ASP.NET Core;
    • Предоставляет механизм для управления перекрестными проблемами в отношении HTTP-запросов, таких как:
      • кэширование
      • обработка ошибок
      • последовательное упорядочение
      • Ведение журналов

Чтобы создать делегированный обработчик, сделайте следующее:

  • Создайте объект, производный от DelegatingHandler.
  • Переопределите SendAsync. Выполните код до передачи запроса следующему обработчику в конвейере:
public class ValidateHeaderHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        if (!request.Headers.Contains("X-API-KEY"))
        {
            return new HttpResponseMessage(HttpStatusCode.BadRequest)
            {
                Content = new StringContent(
                    "The API key header X-API-KEY is required.")
            };
        }

        return await base.SendAsync(request, cancellationToken);
    }
}

Предыдущий код проверяет, находится ли заголовок X-API-KEY в запросе. Если X-API-KEY отсутствует, возвращается BadRequest.

Можно добавить сразу несколько обработчиков в конфигурацию для HttpClient с использованием Microsoft.Extensions.DependencyInjection.HttpClientBuilderExtensions.AddHttpMessageHandler:

builder.Services.AddTransient<ValidateHeaderHandler>();

builder.Services.AddHttpClient("HttpMessageHandler")
    .AddHttpMessageHandler<ValidateHeaderHandler>();

В приведенном выше коде ValidateHeaderHandler регистрируется с помощью внедрения зависимостей. После регистрации можно вызвать AddHttpMessageHandler, передав тип обработчика.

Можно зарегистрировать несколько обработчиков в порядке, в котором они должны выполняться. Каждый обработчик содержит следующий обработчик, пока последний HttpClientHandler не выполнит запрос:

builder.Services.AddTransient<SampleHandler1>();
builder.Services.AddTransient<SampleHandler2>();

builder.Services.AddHttpClient("MultipleHttpMessageHandlers")
    .AddHttpMessageHandler<SampleHandler1>()
    .AddHttpMessageHandler<SampleHandler2>();

В приведенном выше коде SampleHandler1 выполняется первым, перед SampleHandler2.

Использование внедрения зависимостей в ПО промежуточного слоя для исходящих запросов

Когда IHttpClientFactory создает новый делегированный обработчик, он использует внедрение зависимостей для выполнения параметров конструктора обработчика. IHttpClientFactory создает отдельную область внедрения зависимостей для каждого обработчика, что может стать причиной аномального поведения, когда обработчик использует службу с назначенной областью.

Например, рассмотрим следующий интерфейс и его реализацию, которая представляет задачу в виде операции с идентификатором OperationId:

public interface IOperationScoped
{
    string OperationId { get; }
}

public class OperationScoped : IOperationScoped
{
    public string OperationId { get; } = Guid.NewGuid().ToString()[^4..];
}

Как можно понять из названия, IOperationScoped регистрируется с помощью внедрения зависимостей с использованием времени существования с назначенной областью:

builder.Services.AddScoped<IOperationScoped, OperationScoped>();

Следующий делегирующий обработчик принимает и использует IOperationScoped для задания заголовка X-OPERATION-ID для исходящего запроса:

public class OperationHandler : DelegatingHandler
{
    private readonly IOperationScoped _operationScoped;

    public OperationHandler(IOperationScoped operationScoped) =>
        _operationScoped = operationScoped;

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        request.Headers.Add("X-OPERATION-ID", _operationScoped.OperationId);

        return await base.SendAsync(request, cancellationToken);
    }
}

Откройте ссылку для скачивания HttpRequestsSample, перейдите к /Operation и обновите страницу. Значение области запроса изменяется с каждым запросом, но значение области обработчика изменяется только каждые 5 секунд.

Обработчики могут зависеть от служб из любой области. Службы, которые зависят от обработчиков, удаляются при удалении обработчика.

Используйте один из следующих методов для предоставления общего доступа к состоянию отдельных запросов с помощью обработчиков сообщений:

  • Передайте данные в обработчик с помощью HttpRequestMessage.Options.
  • Используйте IHttpContextAccessor для доступа к текущему запросу.
  • Создайте пользовательский объект хранилища AsyncLocal<T> для передачи данных.

Использование обработчиков на основе Polly

IHttpClientFactory интегрируется с библиотекой сторонних разработчиков Polly. Polly — это комплексная библиотека, обеспечивающая отказоустойчивость и обработку временных сбоев в .NET. Она позволяет разработчикам выражать политики, например политику повтора, размыкателя цепи, времени ожидания, изоляции отсеков и отката, более эффективным и потокобезопасным образом.

Для использования политик Polly с настроенными экземплярами HttpClient предоставляются методы расширения. Расширения Polly поддерживают добавление обработчиков на основе Polly клиентам. Polly нужен пакет NuGet Microsoft.Extensions.Http.Polly.

Обработка временных сбоев

Чаще всего ошибки происходят, когда внешние вызовы HTTP являются временными. AddTransientHttpErrorPolicy позволяет определить политику для обработки временных ошибок. Политики, настроенные с помощью AddTransientHttpErrorPolicy, обрабатывают следующие ответы:

AddTransientHttpErrorPolicy предоставляет доступ к объекту PolicyBuilder, настроенному для обработки ошибок, представляющих возможный временный сбой:

builder.Services.AddHttpClient("PollyWaitAndRetry")
    .AddTransientHttpErrorPolicy(policyBuilder =>
        policyBuilder.WaitAndRetryAsync(
            3, retryNumber => TimeSpan.FromMilliseconds(600)));

В приведенном выше коде определена политика WaitAndRetryAsync. Неудачные запросы повторяются до трех раз с задержкой 600 мс между попытками.

Динамический выбор политик

Предоставляются методы расширения для добавления обработчиков на основе Polly, например AddPolicyHandler. Следующая перегрузка AddPolicyHandler проверяет запрос для определения применимой политики:

var timeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(10));
var longTimeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(30));

builder.Services.AddHttpClient("PollyDynamic")
    .AddPolicyHandler(httpRequestMessage =>
        httpRequestMessage.Method == HttpMethod.Get ? timeoutPolicy : longTimeoutPolicy);

Если в приведенном выше коде исходящий запрос является запросом HTTP GET, применяется время ожидания 10 секунд. Для остальных методов HTTP время ожидания — 30 секунд.

Добавление нескольких обработчиков Polly

Общепринятой практикой является вложение политик Polly:

builder.Services.AddHttpClient("PollyMultiple")
    .AddTransientHttpErrorPolicy(policyBuilder =>
        policyBuilder.RetryAsync(3))
    .AddTransientHttpErrorPolicy(policyBuilder =>
        policyBuilder.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));

В предыдущем примере:

  • Добавляются два обработчика.
  • Первый обработчик использует AddTransientHttpErrorPolicy, чтобы добавить политику повтора. Неудачные запросы выполняются повторно до трех раз.
  • Второй вызов AddTransientHttpErrorPolicy добавляет политику размыкателя цепи. Дополнительные внешние запросы блокируются в течение 30 секунд в случае пяти неудачных попыток подряд. Политики размыкателя цепи отслеживают состояние. Все вызовы через этот клиент имеют одинаковое состояние цепи.

Добавление политик из реестра Polly

Подход к управлению регулярно используемыми политиками заключается в их однократном определении и регистрации с помощью PolicyRegistry. Например:

var timeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(10));
var longTimeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(30));

var policyRegistry = builder.Services.AddPolicyRegistry();

policyRegistry.Add("Regular", timeoutPolicy);
policyRegistry.Add("Long", longTimeoutPolicy);

builder.Services.AddHttpClient("PollyRegistryRegular")
    .AddPolicyHandlerFromRegistry("Regular");

builder.Services.AddHttpClient("PollyRegistryLong")
    .AddPolicyHandlerFromRegistry("Long");

В предыдущем коде:

  • Две политики Regular и Long добавляются в реестр Polly.
  • AddPolicyHandlerFromRegistry настраивает отдельные именованные клиенты для использования этих политик из реестра Polly.

Дополнительные сведения о IHttpClientFactory и интеграции Polly см. на вики-сайте Polly.

Управление HttpClient и временем существования

При каждом вызове CreateClient в IHttpClientFactory возвращается новый экземпляр HttpClient. HttpMessageHandler создается для каждого именованного клиента. Фабрика обеспечивает управление временем существования экземпляров HttpMessageHandler.

IHttpClientFactory объединяет в пул все экземпляры HttpMessageHandler, созданные фабрикой, чтобы уменьшить потребление ресурсов. Экземпляр HttpMessageHandler можно использовать повторно из пула при создании экземпляра HttpClient, если его время существования еще не истекло.

Создавать пулы обработчиков желательно, так как каждый обработчик обычно управляет собственными базовыми HTTP-подключениями. Создание лишних обработчиков может привести к задержке подключения. Некоторые обработчики поддерживают подключения открытыми в течение неопределенного периода, что может помешать обработчику отреагировать на изменения службы доменных имен (DNS).

Время существования обработчика по умолчанию — две минуты. Значение по умолчанию можно переопределить для каждого именованного клиента:

builder.Services.AddHttpClient("HandlerLifetime")
    .SetHandlerLifetime(TimeSpan.FromMinutes(5));

Экземпляры HttpClient обычно можно рассматривать как объекты .NET, не требующие освобождения. Высвобождение отменяет исходящие запросы и гарантирует, что указанный экземпляр HttpClient не может использоваться после вызова Dispose. IHttpClientFactory отслеживает и высвобождает ресурсы, используемые экземплярами HttpClient.

До появления IHttpClientFactory один экземпляр HttpClient часто сохраняли в активном состоянии в течение длительного времени. После перехода на IHttpClientFactory это уже не нужно.

Альтернативы интерфейсу IHttpClientFactory

Использование IHttpClientFactory в приложении с внедрением зависимостей позволяет:

  • предотвращать проблемы нехватки ресурсов путем объединения экземпляров HttpMessageHandler в пулы;
  • предотвращать проблемы устаревания записей DNS путем регулярной утилизации экземпляров HttpMessageHandler.

Существуют альтернативные способы решения указанных выше проблем с помощью долгосрочного экземпляра SocketsHttpHandler.

  • Создайте экземпляр SocketsHttpHandler при запуске приложения и используйте его в течение всего жизненного цикла приложения.
  • Присвойте PooledConnectionLifetime соответствующее значение в соответствии со временем обновления записей DNS.
  • По мере необходимости создавайте экземпляры HttpClient с помощью new HttpClient(handler, disposeHandler: false).

Описанные выше подходы решают проблемы, связанные с управлением ресурсами, которые в IHttpClientFactory решаются сходным образом.

  • SocketsHttpHandler обеспечивает совместное использование подключений экземплярами HttpClient. Этот позволяет предотвратить нехватку сокетов.
  • SocketsHttpHandler уничтожает подключения в соответствии со значением PooledConnectionLifetime, чтобы предотвратить проблемы устаревания записей DNS.

Ведение журнала

Клиенты, созданные через IHttpClientFactory, записывают сообщения журнала для всех запросов. Установите соответствующий уровень информации в конфигурации ведения журнала, чтобы просматривать сообщения журнала по умолчанию. Дополнительное ведение журнала, например запись заголовков запросов, включено только на уровне трассировки.

Категория журнала для каждого клиента включает в себя имя клиента. Клиент с именем MyNamedClient, например, записывает в журнал сообщения с категорией "System.Net.Http.HttpClient.MyNamedClient.LogicalHandler". Сообщения с суффиксом LogicalHandler создаются за пределами конвейера обработчиков запросов. Во время запроса сообщения записываются в журнал до обработки запроса другими обработчиками в конвейере. Во время ответа сообщения записываются в журнал после получения ответа другими обработчиками в конвейере.

Кроме того, журнал ведется в конвейере обработчиков запросов. В примере MyNamedClient эти сообщения регистрируются с категорией журнала "System.Net.Http.HttpClient.MyNamedClient.ClientHandler". Во время запроса это происходит после выполнения всех обработчиков и непосредственно перед отправкой запроса. Во время ответа в журнале записывается состояние ответа перед его передачей обратно по конвейеру обработчиков.

Включив ведение журнала в конвейере и за его пределами, можно выполнять проверку изменений, внесенных другими обработчиками конвейера. Сюда входят изменения заголовков запросов или кода состояния ответов.

Включение имени клиента в категорию журнала позволяет фильтровать журналы по именованным клиентам.

Настройка HttpMessageHandler

Иногда необходимо контролировать конфигурацию внутреннего обработчика HttpMessageHandler, используемого клиентом.

При добавлении именованного или типизированного клиента возвращается IHttpClientBuilder. Для определения делегата можно использовать метод расширения ConfigurePrimaryHttpMessageHandler. Делегат используется для создания и настройки основного обработчика HttpMessageHandler, используемого этим клиентом:

builder.Services.AddHttpClient("ConfiguredHttpMessageHandler")
    .ConfigurePrimaryHttpMessageHandler(() =>
        new HttpClientHandler
        {
            AllowAutoRedirect = true,
            UseDefaultCredentials = true
        });

Файлы cookie

Объединение экземпляров HttpMessageHandler в пул приводит к совместному использованию объектов CookieContainer. Непредвиденное совместное использование объектов CookieContainer часто приводит к ошибкам в коде. Для приложений, которым требуются файлы cookie, рекомендуется один из следующих подходов:

  • отключите автоматическую обработку файлов cookie;
  • не используйте IHttpClientFactory.

Чтобы отключить автоматическую обработку файлов ConfigurePrimaryHttpMessageHandler, вызовите cookie:

builder.Services.AddHttpClient("NoAutomaticCookies")
    .ConfigurePrimaryHttpMessageHandler(() =>
        new HttpClientHandler
        {
            UseCookies = false
        });

Использование IHttpClientFactory в консольном приложении

В консольном приложении добавьте в проект следующие ссылки на пакеты:

В следующем примере :

  • IHttpClientFactory и GitHubService регистрируются в контейнере службы универсального узла.
  • GitHubService запрашивается из внедрения зависимостей, которое запрашивает экземпляр IHttpClientFactory.
  • GitHubService использует IHttpClientFactory для создания экземпляра HttpClient, который используется для получения ветвей GitHub для документации.
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

var host = new HostBuilder()
    .ConfigureServices(services =>
    {
        services.AddHttpClient();
        services.AddTransient<GitHubService>();
    })
    .Build();

try
{
    var gitHubService = host.Services.GetRequiredService<GitHubService>();
    var gitHubBranches = await gitHubService.GetAspNetCoreDocsBranchesAsync();

    Console.WriteLine($"{gitHubBranches?.Count() ?? 0} GitHub Branches");

    if (gitHubBranches is not null)
    {
        foreach (var gitHubBranch in gitHubBranches)
        {
            Console.WriteLine($"- {gitHubBranch.Name}");
        }
    }
}
catch (Exception ex)
{
    host.Services.GetRequiredService<ILogger<Program>>()
        .LogError(ex, "Unable to load branches from GitHub.");
}

public class GitHubService
{
    private readonly IHttpClientFactory _httpClientFactory;

    public GitHubService(IHttpClientFactory httpClientFactory) =>
        _httpClientFactory = httpClientFactory;

    public async Task<IEnumerable<GitHubBranch>?> GetAspNetCoreDocsBranchesAsync()
    {
        var httpRequestMessage = new HttpRequestMessage(
            HttpMethod.Get,
            "https://api.github.com/repos/dotnet/AspNetCore.Docs/branches")
        {
            Headers =
            {
                { "Accept", "application/vnd.github.v3+json" },
                { "User-Agent", "HttpRequestsConsoleSample" }
            }
        };

        var httpClient = _httpClientFactory.CreateClient();
        var httpResponseMessage = await httpClient.SendAsync(httpRequestMessage);

        httpResponseMessage.EnsureSuccessStatusCode();

        using var contentStream =
            await httpResponseMessage.Content.ReadAsStreamAsync();
        
        return await JsonSerializer.DeserializeAsync
            <IEnumerable<GitHubBranch>>(contentStream);
    }
}

public record GitHubBranch(
    [property: JsonPropertyName("name")] string Name);

ПО промежуточного слоя для распространения заголовков

Распространение заголовков — это ПО промежуточного слоя ASP.NET Core для распространения HTTP-заголовков из входящего запроса на исходящие запросы HttpClient. Чтобы использовать распространение заголовков, сделайте следующее:

  • Установите пакет Microsoft.AspNetCore.HeaderPropagation.

  • Настройте HttpClient и конвейер ПО промежуточного слоя в Program.cs:

    // Add services to the container.
    builder.Services.AddControllers();
    
    builder.Services.AddHttpClient("PropagateHeaders")
        .AddHeaderPropagation();
    
    builder.Services.AddHeaderPropagation(options =>
    {
        options.Headers.Add("X-TraceId");
    });
    
    var app = builder.Build();
    
    // Configure the HTTP request pipeline.
    app.UseHttpsRedirection();
    
    app.UseHeaderPropagation();
    
    app.MapControllers();
    
  • Создайте исходящие запросы с помощью настроенного экземпляра HttpClient, который включает добавленные заголовки.

Дополнительные ресурсы

Авторы Кирк Ларкин (Kirk Larkin), Стив Гордон (Steve Gordon), Гленн Кондрон (Glenn Condron) и Райан Новак (Ryan Nowak).

IHttpClientFactory можно зарегистрировать и использовать для настройки и создания экземпляров HttpClient в приложении. IHttpClientFactory предоставляет следующие преимущества:

  • Центральное расположение для именования и настройки логических экземпляров HttpClient. Например, можно зарегистрировать и настроить клиент github для доступа к GitHub. Можно зарегистрировать клиент по умолчанию для общего доступа.
  • Кодификация концепции исходящего ПО промежуточного слоя путем делегирования обработчиков в HttpClient. Предоставление расширений для ПО промежуточного слоя на основе Polly для делегирования обработчиков в HttpClient.
  • Управление созданием пулов и временем существования базовых экземпляров HttpClientMessageHandler. Автоматическое управление позволяет избежать обычных проблем со службой доменных имен (DNS), которые возникают при управлении временем существования HttpClient вручную.
  • Настройка параметров ведения журнала (через ILogger) для всех запросов, отправленных через клиентов, созданных фабрикой.

Просмотреть или скачать пример кода (описание скачивания).

Пример кода в этой версии раздела использует System.Text.Json для десериализации содержимого JSON, возвращаемого в ответах HTTP. Для примеров, использующих Json.NET и ReadAsAsync<T>, воспользуйтесь средством выбора версии, чтобы выбрать версию 2.x этого раздела.

Шаблоны потребления

Существует несколько способов использования IHttpClientFactory в приложении:

Оптимальный подход зависит от требований приложения.

Базовое использование

IHttpClientFactory можно зарегистрировать, вызвав AddHttpClient:

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddHttpClient();
        // Remaining code deleted for brevity.

IHttpClientFactory можно запросить с помощью внедрения зависимостей (DI). Следующий код использует IHttpClientFactory для создания экземпляра HttpClient:

public class BasicUsageModel : PageModel
{
    private readonly IHttpClientFactory _clientFactory;

    public IEnumerable<GitHubBranch> Branches { get; private set; }

    public bool GetBranchesError { get; private set; }

    public BasicUsageModel(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }

    public async Task OnGet()
    {
        var request = new HttpRequestMessage(HttpMethod.Get,
            "https://api.github.com/repos/dotnet/AspNetCore.Docs/branches");
        request.Headers.Add("Accept", "application/vnd.github.v3+json");
        request.Headers.Add("User-Agent", "HttpClientFactory-Sample");

        var client = _clientFactory.CreateClient();

        var response = await client.SendAsync(request);

        if (response.IsSuccessStatusCode)
        {
            using var responseStream = await response.Content.ReadAsStreamAsync();
            Branches = await JsonSerializer.DeserializeAsync
                <IEnumerable<GitHubBranch>>(responseStream);
        }
        else
        {
            GetBranchesError = true;
            Branches = Array.Empty<GitHubBranch>();
        }
    }
}

Подобное использование IHttpClientFactory — это хороший способ рефакторинга имеющегося приложения. Он не оказывает влияния на использование HttpClient. Там, где в существующем приложении создаются экземпляры HttpClient, используйте вызовы к CreateClient.

Именованные клиенты

Именованные клиенты являются хорошим выбором в следующих случаях:

  • Приложение требует много отдельных использований HttpClient.
  • Многие HttpClient имеют другую конфигурацию.

Конфигурацию для именованного клиента HttpClient можно указать во время регистрации в Startup.ConfigureServices:

services.AddHttpClient("github", c =>
{
    c.BaseAddress = new Uri("https://api.github.com/");
    // Github API versioning
    c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
    // Github requires a user-agent
    c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});

В приведенном выше коде клиент регистрируется с:

  • базовым адресом https://api.github.com/;
  • двумя заголовками, необходимыми для работы с API GitHub.

CreateClient

При каждом вызове CreateClient:

  • создается новый экземпляр HttpClient;
  • вызывается действие настройки.

Чтобы создать именованный клиент, передайте его имя в CreateClient:

public class NamedClientModel : PageModel
{
    private readonly IHttpClientFactory _clientFactory;

    public IEnumerable<GitHubPullRequest> PullRequests { get; private set; }

    public bool GetPullRequestsError { get; private set; }

    public bool HasPullRequests => PullRequests.Any();

    public NamedClientModel(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }

    public async Task OnGet()
    {
        var request = new HttpRequestMessage(HttpMethod.Get,
            "repos/dotnet/AspNetCore.Docs/pulls");

        var client = _clientFactory.CreateClient("github");

        var response = await client.SendAsync(request);

        if (response.IsSuccessStatusCode)
        {
            using var responseStream = await response.Content.ReadAsStreamAsync();
            PullRequests = await JsonSerializer.DeserializeAsync
                    <IEnumerable<GitHubPullRequest>>(responseStream);
        }
        else
        {
            GetPullRequestsError = true;
            PullRequests = Array.Empty<GitHubPullRequest>();
        }
    }
}

В приведенном выше коде в запросе не требуется указывать имя узла. Достаточно передать только путь, так как используется базовый адрес, заданный для клиента.

Типизированные клиенты

Типизированные клиенты:

  • предоставляют те же возможности, что и именованные клиенты, без необходимости использовать строки в качестве ключей.
  • Это помогает IntelliSense и компилятору при использовании клиентов.
  • Они предоставляют единое расположение для настройки и взаимодействия с конкретным клиентом HttpClient. Например, можно использовать один типизированный клиент:
    • для одной серверной конечной точки;
    • для инкапсуляции всей логики, связанной с конечной точкой.
  • Поддерживаются работа с внедрением зависимостей и возможность вставки в нужное место в приложении.

Типизированный клиент принимает параметр HttpClient в конструкторе:

public class GitHubService
{
    public HttpClient Client { get; }

    public GitHubService(HttpClient client)
    {
        client.BaseAddress = new Uri("https://api.github.com/");
        // GitHub API versioning
        client.DefaultRequestHeaders.Add("Accept",
            "application/vnd.github.v3+json");
        // GitHub requires a user-agent
        client.DefaultRequestHeaders.Add("User-Agent",
            "HttpClientFactory-Sample");

        Client = client;
    }

    public async Task<IEnumerable<GitHubIssue>> GetAspNetDocsIssues()
    {
        return await Client.GetFromJsonAsync<IEnumerable<GitHubIssue>>(
          "/repos/aspnet/AspNetCore.Docs/issues?state=open&sort=created&direction=desc");
    }
}

В предыдущем коде:

  • Конфигурация перемещается в типизированный клиент.
  • Объект HttpClient предоставляется в виде открытого свойства.

Можно создать связанные с API методы, которые предоставляют функциональные возможности HttpClient. Например, метод GetAspNetDocsIssues инкапсулирует код для получения открытых вопросов.

Следующий код вызывает AddHttpClient в Startup.ConfigureServices для регистрации класса типизированного клиента:

services.AddHttpClient<GitHubService>();

Типизированный клиент регистрируется во внедрении зависимостей как временный. В приведенном выше коде AddHttpClient регистрирует GitHubService как временную службу. Эта регистрация использует фабричный метод для следующих задач:

  1. Создайте экземпляр HttpClient.
  2. Создайте экземпляр GitHubService, передав его конструктору экземпляр HttpClient.

Типизированный клиент можно внедрить и использовать напрямую:

public class TypedClientModel : PageModel
{
    private readonly GitHubService _gitHubService;

    public IEnumerable<GitHubIssue> LatestIssues { get; private set; }

    public bool HasIssue => LatestIssues.Any();

    public bool GetIssuesError { get; private set; }

    public TypedClientModel(GitHubService gitHubService)
    {
        _gitHubService = gitHubService;
    }

    public async Task OnGet()
    {
        try
        {
            LatestIssues = await _gitHubService.GetAspNetDocsIssues();
        }
        catch(HttpRequestException)
        {
            GetIssuesError = true;
            LatestIssues = Array.Empty<GitHubIssue>();
        }
    }
}

Конфигурацию для типизированного клиента можно указать во время регистрации в Startup.ConfigureServices, а не в конструкторе типизированного клиента:

services.AddHttpClient<RepoService>(c =>
{
    c.BaseAddress = new Uri("https://api.github.com/");
    c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
    c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});

HttpClient может быть инкапсулирован в типизированном клиенте. Вместо предоставления его как свойства определите метод для внутреннего вызова экземпляра HttpClient:

public class RepoService
{
    // _httpClient isn't exposed publicly
    private readonly HttpClient _httpClient;

    public RepoService(HttpClient client)
    {
        _httpClient = client;
    }

    public async Task<IEnumerable<string>> GetRepos()
    {
        var response = await _httpClient.GetAsync("aspnet/repos");

        response.EnsureSuccessStatusCode();

        using var responseStream = await response.Content.ReadAsStreamAsync();
        return await JsonSerializer.DeserializeAsync
            <IEnumerable<string>>(responseStream);
    }
}

В приведенном выше коде HttpClient хранится в закрытом поле. Доступ к HttpClient осуществляется с помощью общедоступного метода GetRepos.

Созданные клиенты

IHttpClientFactory можно использовать в сочетании с библиотеками сторонних разработчиков, например Refit. Refit — это библиотека REST для .NET. Она преобразует REST API в динамические интерфейсы. Реализация интерфейса формируется динамически с помощью RestService с использованием HttpClient для совершения внешних вызовов HTTP.

Для представления внешнего API и его ответа определяются интерфейс и ответ:

public interface IHelloClient
{
    [Get("/helloworld")]
    Task<Reply> GetMessageAsync();
}

public class Reply
{
    public string Message { get; set; }
}

Можно добавить типизированный клиент, используя Refit для создания реализации:

public void ConfigureServices(IServiceCollection services)
{
    services.AddHttpClient("hello", c =>
    {
        c.BaseAddress = new Uri("http://localhost:5000");
    })
    .AddTypedClient(c => Refit.RestService.For<IHelloClient>(c));

    services.AddControllers();
}

При необходимости можно использовать определенный интерфейс с реализацией, предоставленной внедрением зависимостей и Refit:

[ApiController]
public class ValuesController : ControllerBase
{
    private readonly IHelloClient _client;

    public ValuesController(IHelloClient client)
    {
        _client = client;
    }

    [HttpGet("/")]
    public async Task<ActionResult<Reply>> Index()
    {
        return await _client.GetMessageAsync();
    }
}

Выполнение запросов POST, PUT и DELETE

В предыдущих примерах все HTTP-запросы используют HTTP-команду GET. HttpClient также поддерживает другие HTTP-команды, в том числе:

  • POST
  • PUT
  • DELETE
  • PATCH

Полный список поддерживаемых HTTP-команд см. в статье HttpMethod.

В следующем примере показано, как выполнить HTTP-запрос POST:

public async Task CreateItemAsync(TodoItem todoItem)
{
    var todoItemJson = new StringContent(
        JsonSerializer.Serialize(todoItem, _jsonSerializerOptions),
        Encoding.UTF8,
        "application/json");

    using var httpResponse =
        await _httpClient.PostAsync("/api/TodoItems", todoItemJson);

    httpResponse.EnsureSuccessStatusCode();
}

В приведенном выше коде метод CreateItemAsync выполняет следующие задачи:

  • сериализует параметр TodoItem в JSON с помощью System.Text.Json. Для настройки процесса сериализации используется экземпляр JsonSerializerOptions.
  • создает экземпляр StringContent для упаковки сериализованного JSON для отправки в тексте HTTP-запроса.
  • вызывает метод PostAsync для отправки содержимого JSON по указанному URL-адресу. Это относительный URL-адрес, который добавляется в свойство HttpClient.BaseAddress.
  • вызывает метод EnsureSuccessStatusCode, чтобы создавать исключение, если код состояния ответа означает неудачное выполнение.

HttpClient также поддерживает другие типы содержимого. Например, MultipartContent и StreamContent. Полный список поддерживаемого содержимого см. в статье HttpContent.

Ниже приводится пример HTTP-запроса PUT.

public async Task SaveItemAsync(TodoItem todoItem)
{
    var todoItemJson = new StringContent(
        JsonSerializer.Serialize(todoItem),
        Encoding.UTF8,
        "application/json");

    using var httpResponse =
        await _httpClient.PutAsync($"/api/TodoItems/{todoItem.Id}", todoItemJson);

    httpResponse.EnsureSuccessStatusCode();
}

Приведенный выше код практически аналогичен коду в примере с POST. Метод SaveItemAsync вызывает PutAsync вместо PostAsync.

Ниже приводится пример HTTP-запроса DELETE.

public async Task DeleteItemAsync(long itemId)
{
    using var httpResponse =
        await _httpClient.DeleteAsync($"/api/TodoItems/{itemId}");

    httpResponse.EnsureSuccessStatusCode();
}

В приведенном выше коде метод DeleteItemAsync вызывает DeleteAsync. Поскольку HTTP-запросы DELETE обычно не содержат текст, метод DeleteAsync не предоставляет перегрузку, которая принимает экземпляр HttpContent.

Дополнительные сведения об использовании различных HTTP-команд с HttpClient см. в статье HttpClient.

ПО промежуточного слоя для исходящих запросов

В HttpClient существует концепция делегирования обработчиков, которые можно связать друг с другом для исходящих HTTP-запросов. IHttpClientFactory:

  • Упрощает определение обработчиков для применения к каждому именованному клиенту.
  • Поддерживает регистрацию и объединение в цепочки нескольких обработчиков для создания конвейера ПО промежуточного слоя для исходящих запросов. Каждый из этих обработчиков может выполнять работу до и после исходящего запроса. Этот шаблон:
    • похож на входящий конвейер ПО промежуточного слоя в ASP.NET Core;
    • Предоставляет механизм для управления перекрестными проблемами в отношении HTTP-запросов, таких как:
      • кэширование
      • обработка ошибок
      • последовательное упорядочение
      • Ведение журналов

Чтобы создать делегированный обработчик, сделайте следующее:

  • Создайте объект, производный от DelegatingHandler.
  • Переопределите SendAsync. Выполните код до передачи запроса следующему обработчику в конвейере:
public class ValidateHeaderHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        if (!request.Headers.Contains("X-API-KEY"))
        {
            return new HttpResponseMessage(HttpStatusCode.BadRequest)
            {
                Content = new StringContent(
                    "You must supply an API key header called X-API-KEY")
            };
        }

        return await base.SendAsync(request, cancellationToken);
    }
}

Предыдущий код проверяет, находится ли заголовок X-API-KEY в запросе. Если X-API-KEY отсутствует, возвращается BadRequest.

Можно добавить сразу несколько обработчиков в конфигурацию для HttpClient с использованием Microsoft.Extensions.DependencyInjection.HttpClientBuilderExtensions.AddHttpMessageHandler:

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<ValidateHeaderHandler>();

    services.AddHttpClient("externalservice", c =>
    {
        // Assume this is an "external" service which requires an API KEY
        c.BaseAddress = new Uri("https://localhost:5001/");
    })
    .AddHttpMessageHandler<ValidateHeaderHandler>();

    // Remaining code deleted for brevity.

В приведенном выше коде ValidateHeaderHandler регистрируется с помощью внедрения зависимостей. После регистрации можно вызвать AddHttpMessageHandler, передав тип обработчика.

Можно зарегистрировать несколько обработчиков в порядке, в котором они должны выполняться. Каждый обработчик содержит следующий обработчик, пока последний HttpClientHandler не выполнит запрос:

services.AddTransient<SecureRequestHandler>();
services.AddTransient<RequestDataHandler>();

services.AddHttpClient("clientwithhandlers")
    // This handler is on the outside and called first during the 
    // request, last during the response.
    .AddHttpMessageHandler<SecureRequestHandler>()
    // This handler is on the inside, closest to the request being 
    // sent.
    .AddHttpMessageHandler<RequestDataHandler>();

Использование внедрения зависимостей в ПО промежуточного слоя для исходящих запросов

Когда IHttpClientFactory создает новый делегированный обработчик, он использует внедрение зависимостей для выполнения параметров конструктора обработчика. IHttpClientFactory создает отдельную область внедрения зависимостей для каждого обработчика, что может стать причиной аномального поведения, когда обработчик использует службу с назначенной областью.

Например, рассмотрим следующий интерфейс и его реализацию, которая представляет задачу в виде операции с идентификатором OperationId:

public interface IOperationScoped 
{
    string OperationId { get; }
}

public class OperationScoped : IOperationScoped
{
    public string OperationId { get; } = Guid.NewGuid().ToString()[^4..];
}

Как можно понять из названия, IOperationScoped регистрируется с помощью внедрения зависимостей с использованием времени существования с назначенной областью:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<TodoContext>(options =>
        options.UseInMemoryDatabase("TodoItems"));

    services.AddHttpContextAccessor();

    services.AddHttpClient<TodoClient>((sp, httpClient) =>
    {
        var httpRequest = sp.GetRequiredService<IHttpContextAccessor>().HttpContext.Request;

        // For sample purposes, assume TodoClient is used in the context of an incoming request.
        httpClient.BaseAddress = new Uri(UriHelper.BuildAbsolute(httpRequest.Scheme,
                                         httpRequest.Host, httpRequest.PathBase));
        httpClient.Timeout = TimeSpan.FromSeconds(5);
    });

    services.AddScoped<IOperationScoped, OperationScoped>();
    
    services.AddTransient<OperationHandler>();
    services.AddTransient<OperationResponseHandler>();

    services.AddHttpClient("Operation")
        .AddHttpMessageHandler<OperationHandler>()
        .AddHttpMessageHandler<OperationResponseHandler>()
        .SetHandlerLifetime(TimeSpan.FromSeconds(5));

    services.AddControllers();
    services.AddRazorPages();
}

Следующий делегирующий обработчик принимает и использует IOperationScoped для задания заголовка X-OPERATION-ID для исходящего запроса:

public class OperationHandler : DelegatingHandler
{
    private readonly IOperationScoped _operationService;

    public OperationHandler(IOperationScoped operationScoped)
    {
        _operationService = operationScoped;
    }

    protected async override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        request.Headers.Add("X-OPERATION-ID", _operationService.OperationId);

        return await base.SendAsync(request, cancellationToken);
    }
}

В скачиваемом ресурсе HttpRequestsSample] перейдите к /Operation и обновите страницу. Значение области запроса изменяется с каждым запросом, но значение области обработчика изменяется только каждые 5 секунд.

Обработчики могут зависеть от служб из любой области. Службы, которые зависят от обработчиков, удаляются при удалении обработчика.

Используйте один из следующих методов для предоставления общего доступа к состоянию отдельных запросов с помощью обработчиков сообщений:

  • Передайте данные в обработчик с помощью HttpRequestMessage.Options.
  • Используйте IHttpContextAccessor для доступа к текущему запросу.
  • Создайте пользовательский объект хранилища AsyncLocal<T> для передачи данных.

Использование обработчиков на основе Polly

IHttpClientFactory интегрируется с библиотекой сторонних разработчиков Polly. Polly — это комплексная библиотека, обеспечивающая отказоустойчивость и обработку временных сбоев в .NET. Она позволяет разработчикам выражать политики, например политику повтора, размыкателя цепи, времени ожидания, изоляции отсеков и отката, более эффективным и потокобезопасным образом.

Для использования политик Polly с настроенными экземплярами HttpClient предоставляются методы расширения. Расширения Polly поддерживают добавление обработчиков на основе Polly клиентам. Polly нужен пакет NuGet Microsoft.Extensions.Http.Polly.

Обработка временных сбоев

Чаще всего ошибки происходят, когда внешние вызовы HTTP являются временными. AddTransientHttpErrorPolicy позволяет определить политику для обработки временных ошибок. Политики, настроенные с помощью AddTransientHttpErrorPolicy, обрабатывают следующие ответы:

AddTransientHttpErrorPolicy предоставляет доступ к объекту PolicyBuilder, настроенному для обработки ошибок, представляющих возможный временный сбой:

public void ConfigureServices(IServiceCollection services)
{           
    services.AddHttpClient<UnreliableEndpointCallerService>()
        .AddTransientHttpErrorPolicy(p => 
            p.WaitAndRetryAsync(3, _ => TimeSpan.FromMilliseconds(600)));

    // Remaining code deleted for brevity.

В приведенном выше коде определена политика WaitAndRetryAsync. Неудачные запросы повторяются до трех раз с задержкой 600 мс между попытками.

Динамический выбор политик

Предоставляются методы расширения для добавления обработчиков на основе Polly, например AddPolicyHandler. Следующая перегрузка AddPolicyHandler проверяет запрос для определения применимой политики:

var timeout = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(10));
var longTimeout = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(30));

services.AddHttpClient("conditionalpolicy")
// Run some code to select a policy based on the request
    .AddPolicyHandler(request => 
        request.Method == HttpMethod.Get ? timeout : longTimeout);

Если в приведенном выше коде исходящий запрос является запросом HTTP GET, применяется время ожидания 10 секунд. Для остальных методов HTTP время ожидания — 30 секунд.

Добавление нескольких обработчиков Polly

Общепринятой практикой является вложение политик Polly:

services.AddHttpClient("multiplepolicies")
    .AddTransientHttpErrorPolicy(p => p.RetryAsync(3))
    .AddTransientHttpErrorPolicy(
        p => p.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));

В предыдущем примере:

  • Добавляются два обработчика.
  • Первый обработчик использует AddTransientHttpErrorPolicy, чтобы добавить политику повтора. Неудачные запросы выполняются повторно до трех раз.
  • Второй вызов AddTransientHttpErrorPolicy добавляет политику размыкателя цепи. Дополнительные внешние запросы блокируются в течение 30 секунд в случае пяти неудачных попыток подряд. Политики размыкателя цепи отслеживают состояние. Все вызовы через этот клиент имеют одинаковое состояние цепи.

Добавление политик из реестра Polly

Подход к управлению регулярно используемыми политиками заключается в их однократном определении и регистрации с помощью PolicyRegistry.

В приведенном ниже коде выполняется следующее:

  • Добавляются политики regular и long.
  • AddPolicyHandlerFromRegistry добавляет политики regular и long из реестра.
public void ConfigureServices(IServiceCollection services)
{           
    var timeout = Policy.TimeoutAsync<HttpResponseMessage>(
        TimeSpan.FromSeconds(10));
    var longTimeout = Policy.TimeoutAsync<HttpResponseMessage>(
        TimeSpan.FromSeconds(30));
    
    var registry = services.AddPolicyRegistry();

    registry.Add("regular", timeout);
    registry.Add("long", longTimeout);
    
    services.AddHttpClient("regularTimeoutHandler")
        .AddPolicyHandlerFromRegistry("regular");

    services.AddHttpClient("longTimeoutHandler")
       .AddPolicyHandlerFromRegistry("long");

    // Remaining code deleted for brevity.

Дополнительные сведения о IHttpClientFactory и интеграции Polly см. на вики-сайте Polly.

Управление HttpClient и временем существования

При каждом вызове CreateClient в IHttpClientFactory возвращается новый экземпляр HttpClient. HttpMessageHandler создается для каждого именованного клиента. Фабрика обеспечивает управление временем существования экземпляров HttpMessageHandler.

IHttpClientFactory объединяет в пул все экземпляры HttpMessageHandler, созданные фабрикой, чтобы уменьшить потребление ресурсов. Экземпляр HttpMessageHandler можно использовать повторно из пула при создании экземпляра HttpClient, если его время существования еще не истекло.

Создавать пулы обработчиков желательно, так как каждый обработчик обычно управляет собственными базовыми HTTP-подключениями. Создание лишних обработчиков может привести к задержке подключения. Некоторые обработчики поддерживают подключения открытыми в течение неопределенного периода, что может помешать обработчику отреагировать на изменения службы доменных имен (DNS).

Время существования обработчика по умолчанию — две минуты. Значение по умолчанию можно переопределить для каждого именованного клиента:

public void ConfigureServices(IServiceCollection services)
{           
    services.AddHttpClient("extendedhandlerlifetime")
        .SetHandlerLifetime(TimeSpan.FromMinutes(5));

    // Remaining code deleted for brevity.

Экземпляры HttpClient обычно можно рассматривать как объекты .NET, не требующие освобождения. Высвобождение отменяет исходящие запросы и гарантирует, что указанный экземпляр HttpClient не может использоваться после вызова Dispose. IHttpClientFactory отслеживает и высвобождает ресурсы, используемые экземплярами HttpClient.

До появления IHttpClientFactory один экземпляр HttpClient часто сохраняли в активном состоянии в течение длительного времени. После перехода на IHttpClientFactory это уже не нужно.

Альтернативы интерфейсу IHttpClientFactory

Использование IHttpClientFactory в приложении с внедрением зависимостей позволяет:

  • предотвращать проблемы нехватки ресурсов путем объединения экземпляров HttpMessageHandler в пулы;
  • предотвращать проблемы устаревания записей DNS путем регулярной утилизации экземпляров HttpMessageHandler.

Существуют альтернативные способы решения указанных выше проблем с помощью долгосрочного экземпляра SocketsHttpHandler.

  • Создайте экземпляр SocketsHttpHandler при запуске приложения и используйте его в течение всего жизненного цикла приложения.
  • Присвойте PooledConnectionLifetime соответствующее значение в соответствии со временем обновления записей DNS.
  • По мере необходимости создавайте экземпляры HttpClient с помощью new HttpClient(handler, disposeHandler: false).

Описанные выше подходы решают проблемы, связанные с управлением ресурсами, которые в IHttpClientFactory решаются сходным образом.

  • SocketsHttpHandler обеспечивает совместное использование подключений экземплярами HttpClient. Этот позволяет предотвратить нехватку сокетов.
  • SocketsHttpHandler уничтожает подключения в соответствии со значением PooledConnectionLifetime, чтобы предотвратить проблемы устаревания записей DNS.

Файлы cookie

Объединение экземпляров HttpMessageHandler в пул приводит к совместному использованию объектов CookieContainer. Непредвиденное совместное использование объектов CookieContainer часто приводит к ошибкам в коде. Для приложений, которым требуются файлы cookie, рекомендуется один из следующих подходов:

  • отключите автоматическую обработку файлов cookie;
  • не используйте IHttpClientFactory.

Чтобы отключить автоматическую обработку файлов ConfigurePrimaryHttpMessageHandler, вызовите cookie:

services.AddHttpClient("configured-disable-automatic-cookies")
    .ConfigurePrimaryHttpMessageHandler(() =>
    {
        return new HttpClientHandler()
        {
            UseCookies = false,
        };
    });

Ведение журнала

Клиенты, созданные через IHttpClientFactory, записывают сообщения журнала для всех запросов. Установите соответствующий уровень информации в конфигурации ведения журнала, чтобы просматривать сообщения журнала по умолчанию. Дополнительное ведение журнала, например запись заголовков запросов, включено только на уровне трассировки.

Категория журнала для каждого клиента включает в себя имя клиента. Клиент с именем MyNamedClient, например, записывает в журнал сообщения с категорией "System.Net.Http.HttpClient.MyNamedClient.LogicalHandler". Сообщения с суффиксом LogicalHandler создаются за пределами конвейера обработчиков запросов. Во время запроса сообщения записываются в журнал до обработки запроса другими обработчиками в конвейере. Во время ответа сообщения записываются в журнал после получения ответа другими обработчиками в конвейере.

Кроме того, журнал ведется в конвейере обработчиков запросов. В примере MyNamedClient эти сообщения регистрируются с категорией журнала "System.Net.Http.HttpClient.MyNamedClient.ClientHandler". Во время запроса это происходит после выполнения всех обработчиков и непосредственно перед отправкой запроса. Во время ответа в журнале записывается состояние ответа перед его передачей обратно по конвейеру обработчиков.

Включив ведение журнала в конвейере и за его пределами, можно выполнять проверку изменений, внесенных другими обработчиками конвейера. Сюда входят изменения заголовков запросов или кода состояния ответов.

Включение имени клиента в категорию журнала позволяет фильтровать журналы по именованным клиентам.

Настройка HttpMessageHandler

Иногда необходимо контролировать конфигурацию внутреннего обработчика HttpMessageHandler, используемого клиентом.

При добавлении именованного или типизированного клиента возвращается IHttpClientBuilder. Для определения делегата можно использовать метод расширения ConfigurePrimaryHttpMessageHandler. Делегат используется для создания и настройки основного обработчика HttpMessageHandler, используемого этим клиентом:

public void ConfigureServices(IServiceCollection services)
{            
    services.AddHttpClient("configured-inner-handler")
        .ConfigurePrimaryHttpMessageHandler(() =>
        {
            return new HttpClientHandler()
            {
                AllowAutoRedirect = false,
                UseDefaultCredentials = true
            };
        });

    // Remaining code deleted for brevity.

Использование IHttpClientFactory в консольном приложении

В консольном приложении добавьте в проект следующие ссылки на пакеты:

В следующем примере :

  • IHttpClientFactory регистрируется в контейнере службы универсального узла:
  • MyService создает экземпляр фабрики клиента из службы, который используется для создания HttpClient. HttpClient используется для получения веб-страницы.
  • Main создает область для выполнения метода GetPage службы и вывода первых 500 символов содержимого веб-страницы на консоль.
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
class Program
{
    static async Task<int> Main(string[] args)
    {
        var builder = new HostBuilder()
            .ConfigureServices((hostContext, services) =>
            {
                services.AddHttpClient();
                services.AddTransient<IMyService, MyService>();
            }).UseConsoleLifetime();

        var host = builder.Build();

        try
        {
            var myService = host.Services.GetRequiredService<IMyService>();
            var pageContent = await myService.GetPage();

            Console.WriteLine(pageContent.Substring(0, 500));
        }
        catch (Exception ex)
        {
            var logger = host.Services.GetRequiredService<ILogger<Program>>();

            logger.LogError(ex, "An error occurred.");
        }

        return 0;
    }

    public interface IMyService
    {
        Task<string> GetPage();
    }

    public class MyService : IMyService
    {
        private readonly IHttpClientFactory _clientFactory;

        public MyService(IHttpClientFactory clientFactory)
        {
            _clientFactory = clientFactory;
        }

        public async Task<string> GetPage()
        {
            // Content from BBC One: Dr. Who website (©BBC)
            var request = new HttpRequestMessage(HttpMethod.Get,
                "https://www.bbc.co.uk/programmes/b006q2x0");
            var client = _clientFactory.CreateClient();
            var response = await client.SendAsync(request);

            if (response.IsSuccessStatusCode)
            {
                return await response.Content.ReadAsStringAsync();
            }
            else
            {
                return $"StatusCode: {response.StatusCode}";
            }
        }
    }
}

ПО промежуточного слоя для распространения заголовков

Header propagation — это ПО промежуточного слоя ASP.NET Core для распространения HTTP-заголовков из входящего запроса на исходящие запросы HTTP-клиентов. Чтобы использовать распространение заголовков, сделайте следующее:

  • Укажите ссылку на пакет Microsoft.AspNetCore.HeaderPropagation.

  • Настройте ПО промежуточного слоя и HttpClient в Startup:

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();
    
        services.AddHttpClient("MyForwardingClient").AddHeaderPropagation();
        services.AddHeaderPropagation(options =>
        {
            options.Headers.Add("X-TraceId");
        });
    }
    
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
    
        app.UseHttpsRedirection();
    
        app.UseHeaderPropagation();
    
        app.UseRouting();
    
        app.UseAuthorization();
    
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }
    
  • Клиент включает настроенные заголовки в исходящие запросы:

    var client = clientFactory.CreateClient("MyForwardingClient");
    var response = client.GetAsync(...);
    

Дополнительные ресурсы

Авторы Кирк Ларкин (Kirk Larkin), Стив Гордон (Steve Gordon), Гленн Кондрон (Glenn Condron) и Райан Новак (Ryan Nowak).

IHttpClientFactory можно зарегистрировать и использовать для настройки и создания экземпляров HttpClient в приложении. IHttpClientFactory предоставляет следующие преимущества:

  • Центральное расположение для именования и настройки логических экземпляров HttpClient. Например, можно зарегистрировать и настроить клиент github для доступа к GitHub. Можно зарегистрировать клиент по умолчанию для общего доступа.
  • Кодификация концепции исходящего ПО промежуточного слоя путем делегирования обработчиков в HttpClient. Предоставление расширений для ПО промежуточного слоя на основе Polly для делегирования обработчиков в HttpClient.
  • Управление созданием пулов и временем существования базовых экземпляров HttpClientMessageHandler. Автоматическое управление позволяет избежать обычных проблем со службой доменных имен (DNS), которые возникают при управлении временем существования HttpClient вручную.
  • Настройка параметров ведения журнала (через ILogger) для всех запросов, отправленных через клиентов, созданных фабрикой.

Просмотреть или скачать пример кода (описание скачивания).

Пример кода в этой версии раздела использует System.Text.Json для десериализации содержимого JSON, возвращаемого в ответах HTTP. Для примеров, использующих Json.NET и ReadAsAsync<T>, воспользуйтесь средством выбора версии, чтобы выбрать версию 2.x этого раздела.

Шаблоны потребления

Существует несколько способов использования IHttpClientFactory в приложении:

Оптимальный подход зависит от требований приложения.

Базовое использование

IHttpClientFactory можно зарегистрировать, вызвав AddHttpClient:

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddHttpClient();
        // Remaining code deleted for brevity.

IHttpClientFactory можно запросить с помощью внедрения зависимостей (DI). Следующий код использует IHttpClientFactory для создания экземпляра HttpClient:

public class BasicUsageModel : PageModel
{
    private readonly IHttpClientFactory _clientFactory;

    public IEnumerable<GitHubBranch> Branches { get; private set; }

    public bool GetBranchesError { get; private set; }

    public BasicUsageModel(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }

    public async Task OnGet()
    {
        var request = new HttpRequestMessage(HttpMethod.Get,
            "https://api.github.com/repos/dotnet/AspNetCore.Docs/branches");
        request.Headers.Add("Accept", "application/vnd.github.v3+json");
        request.Headers.Add("User-Agent", "HttpClientFactory-Sample");

        var client = _clientFactory.CreateClient();

        var response = await client.SendAsync(request);

        if (response.IsSuccessStatusCode)
        {
            using var responseStream = await response.Content.ReadAsStreamAsync();
            Branches = await JsonSerializer.DeserializeAsync
                <IEnumerable<GitHubBranch>>(responseStream);
        }
        else
        {
            GetBranchesError = true;
            Branches = Array.Empty<GitHubBranch>();
        }
    }
}

Подобное использование IHttpClientFactory — это хороший способ рефакторинга имеющегося приложения. Он не оказывает влияния на использование HttpClient. Там, где в существующем приложении создаются экземпляры HttpClient, используйте вызовы к CreateClient.

Именованные клиенты

Именованные клиенты являются хорошим выбором в следующих случаях:

  • Приложение требует много отдельных использований HttpClient.
  • Многие HttpClient имеют другую конфигурацию.

Конфигурацию для именованного клиента HttpClient можно указать во время регистрации в Startup.ConfigureServices:

services.AddHttpClient("github", c =>
{
    c.BaseAddress = new Uri("https://api.github.com/");
    // Github API versioning
    c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
    // Github requires a user-agent
    c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});

В приведенном выше коде клиент регистрируется с:

  • базовым адресом https://api.github.com/;
  • двумя заголовками, необходимыми для работы с API GitHub.

CreateClient

При каждом вызове CreateClient:

  • создается новый экземпляр HttpClient;
  • вызывается действие настройки.

Чтобы создать именованный клиент, передайте его имя в CreateClient:

public class NamedClientModel : PageModel
{
    private readonly IHttpClientFactory _clientFactory;

    public IEnumerable<GitHubPullRequest> PullRequests { get; private set; }

    public bool GetPullRequestsError { get; private set; }

    public bool HasPullRequests => PullRequests.Any();

    public NamedClientModel(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }

    public async Task OnGet()
    {
        var request = new HttpRequestMessage(HttpMethod.Get,
            "repos/dotnet/AspNetCore.Docs/pulls");

        var client = _clientFactory.CreateClient("github");

        var response = await client.SendAsync(request);

        if (response.IsSuccessStatusCode)
        {
            using var responseStream = await response.Content.ReadAsStreamAsync();
            PullRequests = await JsonSerializer.DeserializeAsync
                    <IEnumerable<GitHubPullRequest>>(responseStream);
        }
        else
        {
            GetPullRequestsError = true;
            PullRequests = Array.Empty<GitHubPullRequest>();
        }
    }
}

В приведенном выше коде в запросе не требуется указывать имя узла. Достаточно передать только путь, так как используется базовый адрес, заданный для клиента.

Типизированные клиенты

Типизированные клиенты:

  • предоставляют те же возможности, что и именованные клиенты, без необходимости использовать строки в качестве ключей.
  • Это помогает IntelliSense и компилятору при использовании клиентов.
  • Они предоставляют единое расположение для настройки и взаимодействия с конкретным клиентом HttpClient. Например, можно использовать один типизированный клиент:
    • для одной серверной конечной точки;
    • для инкапсуляции всей логики, связанной с конечной точкой.
  • Поддерживаются работа с внедрением зависимостей и возможность вставки в нужное место в приложении.

Типизированный клиент принимает параметр HttpClient в конструкторе:

public class GitHubService
{
    public HttpClient Client { get; }

    public GitHubService(HttpClient client)
    {
        client.BaseAddress = new Uri("https://api.github.com/");
        // GitHub API versioning
        client.DefaultRequestHeaders.Add("Accept",
            "application/vnd.github.v3+json");
        // GitHub requires a user-agent
        client.DefaultRequestHeaders.Add("User-Agent",
            "HttpClientFactory-Sample");

        Client = client;
    }

    public async Task<IEnumerable<GitHubIssue>> GetAspNetDocsIssues()
    {
        var response = await Client.GetAsync(
            "/repos/dotnet/AspNetCore.Docs/issues?state=open&sort=created&direction=desc");

        response.EnsureSuccessStatusCode();

        using var responseStream = await response.Content.ReadAsStreamAsync();
        return await JsonSerializer.DeserializeAsync
            <IEnumerable<GitHubIssue>>(responseStream);
    }
}

Если вы хотите увидеть комментарии к коду, переведенные на языки, отличные от английского, сообщите нам на странице обсуждения этой проблемы на сайте GitHub.

В предыдущем коде:

  • Конфигурация перемещается в типизированный клиент.
  • Объект HttpClient предоставляется в виде открытого свойства.

Можно создать связанные с API методы, которые предоставляют функциональные возможности HttpClient. Например, метод GetAspNetDocsIssues инкапсулирует код для получения открытых вопросов.

Следующий код вызывает AddHttpClient в Startup.ConfigureServices для регистрации класса типизированного клиента:

services.AddHttpClient<GitHubService>();

Типизированный клиент регистрируется во внедрении зависимостей как временный. В приведенном выше коде AddHttpClient регистрирует GitHubService как временную службу. Эта регистрация использует фабричный метод для следующих задач:

  1. Создайте экземпляр HttpClient.
  2. Создайте экземпляр GitHubService, передав его конструктору экземпляр HttpClient.

Типизированный клиент можно внедрить и использовать напрямую:

public class TypedClientModel : PageModel
{
    private readonly GitHubService _gitHubService;

    public IEnumerable<GitHubIssue> LatestIssues { get; private set; }

    public bool HasIssue => LatestIssues.Any();

    public bool GetIssuesError { get; private set; }

    public TypedClientModel(GitHubService gitHubService)
    {
        _gitHubService = gitHubService;
    }

    public async Task OnGet()
    {
        try
        {
            LatestIssues = await _gitHubService.GetAspNetDocsIssues();
        }
        catch(HttpRequestException)
        {
            GetIssuesError = true;
            LatestIssues = Array.Empty<GitHubIssue>();
        }
    }
}

Конфигурацию для типизированного клиента можно указать во время регистрации в Startup.ConfigureServices, а не в конструкторе типизированного клиента:

services.AddHttpClient<RepoService>(c =>
{
    c.BaseAddress = new Uri("https://api.github.com/");
    c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
    c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});

HttpClient может быть инкапсулирован в типизированном клиенте. Вместо предоставления его как свойства определите метод для внутреннего вызова экземпляра HttpClient:

public class RepoService
{
    // _httpClient isn't exposed publicly
    private readonly HttpClient _httpClient;

    public RepoService(HttpClient client)
    {
        _httpClient = client;
    }

    public async Task<IEnumerable<string>> GetRepos()
    {
        var response = await _httpClient.GetAsync("aspnet/repos");

        response.EnsureSuccessStatusCode();

        using var responseStream = await response.Content.ReadAsStreamAsync();
        return await JsonSerializer.DeserializeAsync
            <IEnumerable<string>>(responseStream);
    }
}

В приведенном выше коде HttpClient хранится в закрытом поле. Доступ к HttpClient осуществляется с помощью общедоступного метода GetRepos.

Созданные клиенты

IHttpClientFactory можно использовать в сочетании с библиотеками сторонних разработчиков, например Refit. Refit — это библиотека REST для .NET. Она преобразует REST API в динамические интерфейсы. Реализация интерфейса формируется динамически с помощью RestService с использованием HttpClient для совершения внешних вызовов HTTP.

Для представления внешнего API и его ответа определяются интерфейс и ответ:

public interface IHelloClient
{
    [Get("/helloworld")]
    Task<Reply> GetMessageAsync();
}

public class Reply
{
    public string Message { get; set; }
}

Можно добавить типизированный клиент, используя Refit для создания реализации:

public void ConfigureServices(IServiceCollection services)
{
    services.AddHttpClient("hello", c =>
    {
        c.BaseAddress = new Uri("http://localhost:5000");
    })
    .AddTypedClient(c => Refit.RestService.For<IHelloClient>(c));

    services.AddControllers();
}

При необходимости можно использовать определенный интерфейс с реализацией, предоставленной внедрением зависимостей и Refit:

[ApiController]
public class ValuesController : ControllerBase
{
    private readonly IHelloClient _client;

    public ValuesController(IHelloClient client)
    {
        _client = client;
    }

    [HttpGet("/")]
    public async Task<ActionResult<Reply>> Index()
    {
        return await _client.GetMessageAsync();
    }
}

Выполнение запросов POST, PUT и DELETE

В предыдущих примерах все HTTP-запросы используют HTTP-команду GET. HttpClient также поддерживает другие HTTP-команды, в том числе:

  • POST
  • PUT
  • DELETE
  • PATCH

Полный список поддерживаемых HTTP-команд см. в статье HttpMethod.

В следующем примере показано, как выполнить HTTP-запрос POST:

public async Task CreateItemAsync(TodoItem todoItem)
{
    var todoItemJson = new StringContent(
        JsonSerializer.Serialize(todoItem, _jsonSerializerOptions),
        Encoding.UTF8,
        "application/json");

    using var httpResponse =
        await _httpClient.PostAsync("/api/TodoItems", todoItemJson);

    httpResponse.EnsureSuccessStatusCode();
}

В приведенном выше коде метод CreateItemAsync выполняет следующие задачи:

  • сериализует параметр TodoItem в JSON с помощью System.Text.Json. Для настройки процесса сериализации используется экземпляр JsonSerializerOptions.
  • создает экземпляр StringContent для упаковки сериализованного JSON для отправки в тексте HTTP-запроса.
  • вызывает метод PostAsync для отправки содержимого JSON по указанному URL-адресу. Это относительный URL-адрес, который добавляется в свойство HttpClient.BaseAddress.
  • вызывает метод EnsureSuccessStatusCode, чтобы создавать исключение, если код состояния ответа означает неудачное выполнение.

HttpClient также поддерживает другие типы содержимого. Например, MultipartContent и StreamContent. Полный список поддерживаемого содержимого см. в статье HttpContent.

Ниже приводится пример HTTP-запроса PUT.

public async Task SaveItemAsync(TodoItem todoItem)
{
    var todoItemJson = new StringContent(
        JsonSerializer.Serialize(todoItem),
        Encoding.UTF8,
        "application/json");

    using var httpResponse =
        await _httpClient.PutAsync($"/api/TodoItems/{todoItem.Id}", todoItemJson);

    httpResponse.EnsureSuccessStatusCode();
}

Приведенный выше код практически аналогичен коду в примере с POST. Метод SaveItemAsync вызывает PutAsync вместо PostAsync.

Ниже приводится пример HTTP-запроса DELETE.

public async Task DeleteItemAsync(long itemId)
{
    using var httpResponse =
        await _httpClient.DeleteAsync($"/api/TodoItems/{itemId}");

    httpResponse.EnsureSuccessStatusCode();
}

В приведенном выше коде метод DeleteItemAsync вызывает DeleteAsync. Поскольку HTTP-запросы DELETE обычно не содержат текст, метод DeleteAsync не предоставляет перегрузку, которая принимает экземпляр HttpContent.

Дополнительные сведения об использовании различных HTTP-команд с HttpClient см. в статье HttpClient.

ПО промежуточного слоя для исходящих запросов

В HttpClient существует концепция делегирования обработчиков, которые можно связать друг с другом для исходящих HTTP-запросов. IHttpClientFactory:

  • Упрощает определение обработчиков для применения к каждому именованному клиенту.
  • Поддерживает регистрацию и объединение в цепочки нескольких обработчиков для создания конвейера ПО промежуточного слоя для исходящих запросов. Каждый из этих обработчиков может выполнять работу до и после исходящего запроса. Этот шаблон:
    • похож на входящий конвейер ПО промежуточного слоя в ASP.NET Core;
    • Предоставляет механизм для управления перекрестными проблемами в отношении HTTP-запросов, таких как:
      • кэширование
      • обработка ошибок
      • последовательное упорядочение
      • Ведение журналов

Чтобы создать делегированный обработчик, сделайте следующее:

  • Создайте объект, производный от DelegatingHandler.
  • Переопределите SendAsync. Выполните код до передачи запроса следующему обработчику в конвейере:
public class ValidateHeaderHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        if (!request.Headers.Contains("X-API-KEY"))
        {
            return new HttpResponseMessage(HttpStatusCode.BadRequest)
            {
                Content = new StringContent(
                    "You must supply an API key header called X-API-KEY")
            };
        }

        return await base.SendAsync(request, cancellationToken);
    }
}

Предыдущий код проверяет, находится ли заголовок X-API-KEY в запросе. Если X-API-KEY отсутствует, возвращается BadRequest.

Можно добавить сразу несколько обработчиков в конфигурацию для HttpClient с использованием Microsoft.Extensions.DependencyInjection.HttpClientBuilderExtensions.AddHttpMessageHandler:

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<ValidateHeaderHandler>();

    services.AddHttpClient("externalservice", c =>
    {
        // Assume this is an "external" service which requires an API KEY
        c.BaseAddress = new Uri("https://localhost:5001/");
    })
    .AddHttpMessageHandler<ValidateHeaderHandler>();

    // Remaining code deleted for brevity.

В приведенном выше коде ValidateHeaderHandler регистрируется с помощью внедрения зависимостей. После регистрации можно вызвать AddHttpMessageHandler, передав тип обработчика.

Можно зарегистрировать несколько обработчиков в порядке, в котором они должны выполняться. Каждый обработчик содержит следующий обработчик, пока последний HttpClientHandler не выполнит запрос:

services.AddTransient<SecureRequestHandler>();
services.AddTransient<RequestDataHandler>();

services.AddHttpClient("clientwithhandlers")
    // This handler is on the outside and called first during the 
    // request, last during the response.
    .AddHttpMessageHandler<SecureRequestHandler>()
    // This handler is on the inside, closest to the request being 
    // sent.
    .AddHttpMessageHandler<RequestDataHandler>();

Использование внедрения зависимостей в ПО промежуточного слоя для исходящих запросов

Когда IHttpClientFactory создает новый делегированный обработчик, он использует внедрение зависимостей для выполнения параметров конструктора обработчика. IHttpClientFactory создает отдельную область внедрения зависимостей для каждого обработчика, что может стать причиной аномального поведения, когда обработчик использует службу с назначенной областью.

Например, рассмотрим следующий интерфейс и его реализацию, которая представляет задачу в виде операции с идентификатором OperationId:

public interface IOperationScoped 
{
    string OperationId { get; }
}

public class OperationScoped : IOperationScoped
{
    public string OperationId { get; } = Guid.NewGuid().ToString()[^4..];
}

Как можно понять из названия, IOperationScoped регистрируется с помощью внедрения зависимостей с использованием времени существования с назначенной областью:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<TodoContext>(options =>
        options.UseInMemoryDatabase("TodoItems"));

    services.AddHttpContextAccessor();

    services.AddHttpClient<TodoClient>((sp, httpClient) =>
    {
        var httpRequest = sp.GetRequiredService<IHttpContextAccessor>().HttpContext.Request;

        // For sample purposes, assume TodoClient is used in the context of an incoming request.
        httpClient.BaseAddress = new Uri(UriHelper.BuildAbsolute(httpRequest.Scheme,
                                         httpRequest.Host, httpRequest.PathBase));
        httpClient.Timeout = TimeSpan.FromSeconds(5);
    });

    services.AddScoped<IOperationScoped, OperationScoped>();
    
    services.AddTransient<OperationHandler>();
    services.AddTransient<OperationResponseHandler>();

    services.AddHttpClient("Operation")
        .AddHttpMessageHandler<OperationHandler>()
        .AddHttpMessageHandler<OperationResponseHandler>()
        .SetHandlerLifetime(TimeSpan.FromSeconds(5));

    services.AddControllers();
    services.AddRazorPages();
}

Следующий делегирующий обработчик принимает и использует IOperationScoped для задания заголовка X-OPERATION-ID для исходящего запроса:

public class OperationHandler : DelegatingHandler
{
    private readonly IOperationScoped _operationService;

    public OperationHandler(IOperationScoped operationScoped)
    {
        _operationService = operationScoped;
    }

    protected async override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        request.Headers.Add("X-OPERATION-ID", _operationService.OperationId);

        return await base.SendAsync(request, cancellationToken);
    }
}

В скачиваемом ресурсе HttpRequestsSample] перейдите к /Operation и обновите страницу. Значение области запроса изменяется с каждым запросом, но значение области обработчика изменяется только каждые 5 секунд.

Обработчики могут зависеть от служб из любой области. Службы, которые зависят от обработчиков, удаляются при удалении обработчика.

Используйте один из следующих методов для предоставления общего доступа к состоянию отдельных запросов с помощью обработчиков сообщений:

  • Передайте данные в обработчик с помощью HttpRequestMessage.Properties.
  • Используйте IHttpContextAccessor для доступа к текущему запросу.
  • Создайте пользовательский объект хранилища AsyncLocal<T> для передачи данных.

Использование обработчиков на основе Polly

IHttpClientFactory интегрируется с библиотекой сторонних разработчиков Polly. Polly — это комплексная библиотека, обеспечивающая отказоустойчивость и обработку временных сбоев в .NET. Она позволяет разработчикам выражать политики, например политику повтора, размыкателя цепи, времени ожидания, изоляции отсеков и отката, более эффективным и потокобезопасным образом.

Для использования политик Polly с настроенными экземплярами HttpClient предоставляются методы расширения. Расширения Polly поддерживают добавление обработчиков на основе Polly клиентам. Polly нужен пакет NuGet Microsoft.Extensions.Http.Polly.

Обработка временных сбоев

Чаще всего ошибки происходят, когда внешние вызовы HTTP являются временными. AddTransientHttpErrorPolicy позволяет определить политику для обработки временных ошибок. Политики, настроенные с помощью AddTransientHttpErrorPolicy, обрабатывают следующие ответы:

AddTransientHttpErrorPolicy предоставляет доступ к объекту PolicyBuilder, настроенному для обработки ошибок, представляющих возможный временный сбой:

public void ConfigureServices(IServiceCollection services)
{           
    services.AddHttpClient<UnreliableEndpointCallerService>()
        .AddTransientHttpErrorPolicy(p => 
            p.WaitAndRetryAsync(3, _ => TimeSpan.FromMilliseconds(600)));

    // Remaining code deleted for brevity.

В приведенном выше коде определена политика WaitAndRetryAsync. Неудачные запросы повторяются до трех раз с задержкой 600 мс между попытками.

Динамический выбор политик

Предоставляются методы расширения для добавления обработчиков на основе Polly, например AddPolicyHandler. Следующая перегрузка AddPolicyHandler проверяет запрос для определения применимой политики:

var timeout = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(10));
var longTimeout = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(30));

services.AddHttpClient("conditionalpolicy")
// Run some code to select a policy based on the request
    .AddPolicyHandler(request => 
        request.Method == HttpMethod.Get ? timeout : longTimeout);

Если в приведенном выше коде исходящий запрос является запросом HTTP GET, применяется время ожидания 10 секунд. Для остальных методов HTTP время ожидания — 30 секунд.

Добавление нескольких обработчиков Polly

Общепринятой практикой является вложение политик Polly:

services.AddHttpClient("multiplepolicies")
    .AddTransientHttpErrorPolicy(p => p.RetryAsync(3))
    .AddTransientHttpErrorPolicy(
        p => p.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));

В предыдущем примере:

  • Добавляются два обработчика.
  • Первый обработчик использует AddTransientHttpErrorPolicy, чтобы добавить политику повтора. Неудачные запросы выполняются повторно до трех раз.
  • Второй вызов AddTransientHttpErrorPolicy добавляет политику размыкателя цепи. Дополнительные внешние запросы блокируются в течение 30 секунд в случае пяти неудачных попыток подряд. Политики размыкателя цепи отслеживают состояние. Все вызовы через этот клиент имеют одинаковое состояние цепи.

Добавление политик из реестра Polly

Подход к управлению регулярно используемыми политиками заключается в их однократном определении и регистрации с помощью PolicyRegistry.

В приведенном ниже коде выполняется следующее:

  • Добавляются политики regular и long.
  • AddPolicyHandlerFromRegistry добавляет политики regular и long из реестра.
public void ConfigureServices(IServiceCollection services)
{           
    var timeout = Policy.TimeoutAsync<HttpResponseMessage>(
        TimeSpan.FromSeconds(10));
    var longTimeout = Policy.TimeoutAsync<HttpResponseMessage>(
        TimeSpan.FromSeconds(30));
    
    var registry = services.AddPolicyRegistry();

    registry.Add("regular", timeout);
    registry.Add("long", longTimeout);
    
    services.AddHttpClient("regularTimeoutHandler")
        .AddPolicyHandlerFromRegistry("regular");

    services.AddHttpClient("longTimeoutHandler")
       .AddPolicyHandlerFromRegistry("long");

    // Remaining code deleted for brevity.

Дополнительные сведения о IHttpClientFactory и интеграции Polly см. на вики-сайте Polly.

Управление HttpClient и временем существования

При каждом вызове CreateClient в IHttpClientFactory возвращается новый экземпляр HttpClient. HttpMessageHandler создается для каждого именованного клиента. Фабрика обеспечивает управление временем существования экземпляров HttpMessageHandler.

IHttpClientFactory объединяет в пул все экземпляры HttpMessageHandler, созданные фабрикой, чтобы уменьшить потребление ресурсов. Экземпляр HttpMessageHandler можно использовать повторно из пула при создании экземпляра HttpClient, если его время существования еще не истекло.

Создавать пулы обработчиков желательно, так как каждый обработчик обычно управляет собственными базовыми HTTP-подключениями. Создание лишних обработчиков может привести к задержке подключения. Некоторые обработчики поддерживают подключения открытыми в течение неопределенного периода, что может помешать обработчику отреагировать на изменения службы доменных имен (DNS).

Время существования обработчика по умолчанию — две минуты. Значение по умолчанию можно переопределить для каждого именованного клиента:

public void ConfigureServices(IServiceCollection services)
{           
    services.AddHttpClient("extendedhandlerlifetime")
        .SetHandlerLifetime(TimeSpan.FromMinutes(5));

    // Remaining code deleted for brevity.

Экземпляры HttpClient обычно можно рассматривать как объекты .NET, не требующие освобождения. Высвобождение отменяет исходящие запросы и гарантирует, что указанный экземпляр HttpClient не может использоваться после вызова Dispose. IHttpClientFactory отслеживает и высвобождает ресурсы, используемые экземплярами HttpClient.

До появления IHttpClientFactory один экземпляр HttpClient часто сохраняли в активном состоянии в течение длительного времени. После перехода на IHttpClientFactory это уже не нужно.

Альтернативы интерфейсу IHttpClientFactory

Использование IHttpClientFactory в приложении с внедрением зависимостей позволяет:

  • предотвращать проблемы нехватки ресурсов путем объединения экземпляров HttpMessageHandler в пулы;
  • предотвращать проблемы устаревания записей DNS путем регулярной утилизации экземпляров HttpMessageHandler.

Существуют альтернативные способы решения указанных выше проблем с помощью долгосрочного экземпляра SocketsHttpHandler.

  • Создайте экземпляр SocketsHttpHandler при запуске приложения и используйте его в течение всего жизненного цикла приложения.
  • Присвойте PooledConnectionLifetime соответствующее значение в соответствии со временем обновления записей DNS.
  • По мере необходимости создавайте экземпляры HttpClient с помощью new HttpClient(handler, disposeHandler: false).

Описанные выше подходы решают проблемы, связанные с управлением ресурсами, которые в IHttpClientFactory решаются сходным образом.

  • SocketsHttpHandler обеспечивает совместное использование подключений экземплярами HttpClient. Этот позволяет предотвратить нехватку сокетов.
  • SocketsHttpHandler уничтожает подключения в соответствии со значением PooledConnectionLifetime, чтобы предотвратить проблемы устаревания записей DNS.

Файлы cookie

Объединение экземпляров HttpMessageHandler в пул приводит к совместному использованию объектов CookieContainer. Непредвиденное совместное использование объектов CookieContainer часто приводит к ошибкам в коде. Для приложений, которым требуются файлы cookie, рекомендуется один из следующих подходов:

  • отключите автоматическую обработку файлов cookie;
  • не используйте IHttpClientFactory.

Чтобы отключить автоматическую обработку файлов ConfigurePrimaryHttpMessageHandler, вызовите cookie:

services.AddHttpClient("configured-disable-automatic-cookies")
    .ConfigurePrimaryHttpMessageHandler(() =>
    {
        return new HttpClientHandler()
        {
            UseCookies = false,
        };
    });

Ведение журнала

Клиенты, созданные через IHttpClientFactory, записывают сообщения журнала для всех запросов. Установите соответствующий уровень информации в конфигурации ведения журнала, чтобы просматривать сообщения журнала по умолчанию. Дополнительное ведение журнала, например запись заголовков запросов, включено только на уровне трассировки.

Категория журнала для каждого клиента включает в себя имя клиента. Клиент с именем MyNamedClient, например, записывает в журнал сообщения с категорией "System.Net.Http.HttpClient.MyNamedClient.LogicalHandler". Сообщения с суффиксом LogicalHandler создаются за пределами конвейера обработчиков запросов. Во время запроса сообщения записываются в журнал до обработки запроса другими обработчиками в конвейере. Во время ответа сообщения записываются в журнал после получения ответа другими обработчиками в конвейере.

Кроме того, журнал ведется в конвейере обработчиков запросов. В примере MyNamedClient эти сообщения регистрируются с категорией журнала "System.Net.Http.HttpClient.MyNamedClient.ClientHandler". Во время запроса это происходит после выполнения всех обработчиков и непосредственно перед отправкой запроса. Во время ответа в журнале записывается состояние ответа перед его передачей обратно по конвейеру обработчиков.

Включив ведение журнала в конвейере и за его пределами, можно выполнять проверку изменений, внесенных другими обработчиками конвейера. Сюда входят изменения заголовков запросов или кода состояния ответов.

Включение имени клиента в категорию журнала позволяет фильтровать журналы по именованным клиентам.

Настройка HttpMessageHandler

Иногда необходимо контролировать конфигурацию внутреннего обработчика HttpMessageHandler, используемого клиентом.

При добавлении именованного или типизированного клиента возвращается IHttpClientBuilder. Для определения делегата можно использовать метод расширения ConfigurePrimaryHttpMessageHandler. Делегат используется для создания и настройки основного обработчика HttpMessageHandler, используемого этим клиентом:

public void ConfigureServices(IServiceCollection services)
{            
    services.AddHttpClient("configured-inner-handler")
        .ConfigurePrimaryHttpMessageHandler(() =>
        {
            return new HttpClientHandler()
            {
                AllowAutoRedirect = false,
                UseDefaultCredentials = true
            };
        });

    // Remaining code deleted for brevity.

Использование IHttpClientFactory в консольном приложении

В консольном приложении добавьте в проект следующие ссылки на пакеты:

В следующем примере :

  • IHttpClientFactory регистрируется в контейнере службы универсального узла:
  • MyService создает экземпляр фабрики клиента из службы, который используется для создания HttpClient. HttpClient используется для получения веб-страницы.
  • Main создает область для выполнения метода GetPage службы и вывода первых 500 символов содержимого веб-страницы на консоль.
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
class Program
{
    static async Task<int> Main(string[] args)
    {
        var builder = new HostBuilder()
            .ConfigureServices((hostContext, services) =>
            {
                services.AddHttpClient();
                services.AddTransient<IMyService, MyService>();
            }).UseConsoleLifetime();

        var host = builder.Build();

        try
        {
            var myService = host.Services.GetRequiredService<IMyService>();
            var pageContent = await myService.GetPage();

            Console.WriteLine(pageContent.Substring(0, 500));
        }
        catch (Exception ex)
        {
            var logger = host.Services.GetRequiredService<ILogger<Program>>();

            logger.LogError(ex, "An error occurred.");
        }

        return 0;
    }

    public interface IMyService
    {
        Task<string> GetPage();
    }

    public class MyService : IMyService
    {
        private readonly IHttpClientFactory _clientFactory;

        public MyService(IHttpClientFactory clientFactory)
        {
            _clientFactory = clientFactory;
        }

        public async Task<string> GetPage()
        {
            // Content from BBC One: Dr. Who website (©BBC)
            var request = new HttpRequestMessage(HttpMethod.Get,
                "https://www.bbc.co.uk/programmes/b006q2x0");
            var client = _clientFactory.CreateClient();
            var response = await client.SendAsync(request);

            if (response.IsSuccessStatusCode)
            {
                return await response.Content.ReadAsStringAsync();
            }
            else
            {
                return $"StatusCode: {response.StatusCode}";
            }
        }
    }
}

ПО промежуточного слоя для распространения заголовков

Header propagation — это ПО промежуточного слоя ASP.NET Core для распространения HTTP-заголовков из входящего запроса на исходящие запросы HTTP-клиентов. Чтобы использовать распространение заголовков, сделайте следующее:

  • Укажите ссылку на пакет Microsoft.AspNetCore.HeaderPropagation.

  • Настройте ПО промежуточного слоя и HttpClient в Startup:

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();
    
        services.AddHttpClient("MyForwardingClient").AddHeaderPropagation();
        services.AddHeaderPropagation(options =>
        {
            options.Headers.Add("X-TraceId");
        });
    }
    
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
    
        app.UseHttpsRedirection();
    
        app.UseHeaderPropagation();
    
        app.UseRouting();
    
        app.UseAuthorization();
    
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }
    
  • Клиент включает настроенные заголовки в исходящие запросы:

    var client = clientFactory.CreateClient("MyForwardingClient");
    var response = client.GetAsync(...);
    

Дополнительные ресурсы

Авторы: Гленн Кондрон (Glenn Condron), Райан Новак (Ryan Nowak) и Стив Гордон (Steve Gordon)

IHttpClientFactory можно зарегистрировать и использовать для настройки и создания экземпляров HttpClient в приложении. Так вы получите следующие преимущества:

  • Центральное расположение для именования и настройки логических экземпляров HttpClient. Например, можно зарегистрировать и настроить клиент github для доступа к GitHub. Можно зарегистрировать клиент по умолчанию для других целей.
  • Кодификация концепции исходящего ПО промежуточного слоя путем делегирования обработчиков в HttpClient и предоставление расширений для ПО промежуточного слоя на основе Polly для использования этой возможности.
  • Управление созданием пулов и временем существования базовых экземпляров HttpClientMessageHandler с целью избежать обычных проблем с DNS, которые возникают при управлении временем существования HttpClient вручную.
  • Настройка параметров ведения журнала (через ILogger) для всех запросов, отправленных через клиентов, созданных фабрикой.

Просмотреть или скачать образец кода (описание загрузки)

Необходимые компоненты

Для проектов, предназначенных для .NET Framework, необходимо установить пакет NuGet Microsoft.Extensions.Http. Пакет Microsoft.Extensions.Http уже включен в проекты, предназначенные для .NET Core и ссылающиеся на метапакет Microsoft.AspNetCore.App.

Шаблоны потребления

Существует несколько способов использования IHttpClientFactory в приложении:

Все способы равноценны. Оптимальный подход зависит от ограничений приложения.

Базовое использование

IHttpClientFactory можно зарегистрировать путем вызова метода расширения AddHttpClient в IServiceCollection внутри метода Startup.ConfigureServices.

services.AddHttpClient();

После регистрации код может принимать IHttpClientFactory в любом месте, куда можно внедрить службу с помощью внедрения зависимостей (DI). IHttpClientFactory можно использовать для создания экземпляра HttpClient:

public class BasicUsageModel : PageModel
{
    private readonly IHttpClientFactory _clientFactory;

    public IEnumerable<GitHubBranch> Branches { get; private set; }

    public bool GetBranchesError { get; private set; }

    public BasicUsageModel(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }

    public async Task OnGet()
    {
        var request = new HttpRequestMessage(HttpMethod.Get, 
            "https://api.github.com/repos/dotnet/AspNetCore.Docs/branches");
        request.Headers.Add("Accept", "application/vnd.github.v3+json");
        request.Headers.Add("User-Agent", "HttpClientFactory-Sample");

        var client = _clientFactory.CreateClient();

        var response = await client.SendAsync(request);

        if (response.IsSuccessStatusCode)
        {
            Branches = await response.Content
                .ReadAsAsync<IEnumerable<GitHubBranch>>();
        }
        else
        {
            GetBranchesError = true;
            Branches = Array.Empty<GitHubBranch>();
        }                               
    }
}

Подобное использование IHttpClientFactory — это отличный способ рефакторинга имеющегося приложения. Он не оказывает влияния на использование HttpClient. Там, где в данный момент создаются экземпляры HttpClient, используйте вызов к CreateClient.

Именованные клиенты

Если для приложения предполагаются разные способы использования HttpClient, каждый со своей конфигурацией, можно применять именованные клиенты. Конфигурацию для именованного клиента HttpClient можно указать во время регистрации в Startup.ConfigureServices.

services.AddHttpClient("github", c =>
{
    c.BaseAddress = new Uri("https://api.github.com/");
    // Github API versioning
    c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
    // Github requires a user-agent
    c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});

В приведенном выше коде вызывается клиент AddHttpClient, предоставляющий имя github. У клиента есть некоторые настройки по умолчанию, а именно: базовый адрес и два заголовка, необходимые для работы с API GitHub.

При каждом вызове CreateClient создается новый экземпляр HttpClient и вызывается действие конфигурации.

Для использования именованного клиента можно передать строковый параметр в CreateClient. Укажите имя создаваемого клиента:

public class NamedClientModel : PageModel
{
    private readonly IHttpClientFactory _clientFactory;

    public IEnumerable<GitHubPullRequest> PullRequests { get; private set; }

    public bool GetPullRequestsError { get; private set; }

    public bool HasPullRequests => PullRequests.Any();

    public NamedClientModel(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }

    public async Task OnGet()
    {
        var request = new HttpRequestMessage(HttpMethod.Get, 
            "repos/dotnet/AspNetCore.Docs/pulls");

        var client = _clientFactory.CreateClient("github");

        var response = await client.SendAsync(request);

        if (response.IsSuccessStatusCode)
        {
            PullRequests = await response.Content
                .ReadAsAsync<IEnumerable<GitHubPullRequest>>();
        }
        else
        {
            GetPullRequestsError = true;
            PullRequests = Array.Empty<GitHubPullRequest>();
        }
    }
}

В приведенном выше коде в запросе не требуется указывать имя узла. Достаточно передать только путь, так как используется базовый адрес, заданный для клиента.

Типизированные клиенты

Типизированные клиенты:

  • предоставляют те же возможности, что и именованные клиенты, без необходимости использовать строки в качестве ключей.
  • Это помогает IntelliSense и компилятору при использовании клиентов.
  • Они предоставляют единое расположение для настройки и взаимодействия с конкретным клиентом HttpClient. Например, для конечной точки серверной части можно использовать один типизированный клиент, который будет содержать всю логику работы с этой конечной точкой.
  • Поддерживаются работа с внедрением зависимостей и возможность вставки в нужное место в приложении.

Типизированный клиент принимает параметр HttpClient в конструкторе:

public class GitHubService
{
    public HttpClient Client { get; }

    public GitHubService(HttpClient client)
    {
        client.BaseAddress = new Uri("https://api.github.com/");
        // GitHub API versioning
        client.DefaultRequestHeaders.Add("Accept", 
            "application/vnd.github.v3+json");
        // GitHub requires a user-agent
        client.DefaultRequestHeaders.Add("User-Agent", 
            "HttpClientFactory-Sample");

        Client = client;
    }

    public async Task<IEnumerable<GitHubIssue>> GetAspNetDocsIssues()
    {
        var response = await Client.GetAsync(
            "/repos/dotnet/AspNetCore.Docs/issues?state=open&sort=created&direction=desc");

        response.EnsureSuccessStatusCode();

        var result = await response.Content
            .ReadAsAsync<IEnumerable<GitHubIssue>>();

        return result;
    }
}

В приведенном выше коде конфигурация перемещается в типизированный клиент. Объект HttpClient предоставляется в виде открытого свойства. Можно определить связанные с API методы, которые предоставляют функциональные возможности HttpClient. Метод GetAspNetDocsIssues инкапсулирует код, необходимый для запроса и анализа последнего открытого выпуска из репозитория GitHub.

Для регистрации типизированного клиента можно использовать универсальный метод расширения AddHttpClient в Startup.ConfigureServices, указав класс типизированного клиента:

services.AddHttpClient<GitHubService>();

Типизированный клиент регистрируется во внедрении зависимостей как временный. Типизированный клиент можно внедрить и использовать напрямую:

public class TypedClientModel : PageModel
{
    private readonly GitHubService _gitHubService;

    public IEnumerable<GitHubIssue> LatestIssues { get; private set; }

    public bool HasIssue => LatestIssues.Any();

    public bool GetIssuesError { get; private set; }

    public TypedClientModel(GitHubService gitHubService)
    {
        _gitHubService = gitHubService;
    }

    public async Task OnGet()
    {
        try
        {
            LatestIssues = await _gitHubService.GetAspNetDocsIssues();
        }
        catch(HttpRequestException)
        {
            GetIssuesError = true;
            LatestIssues = Array.Empty<GitHubIssue>();
        }
    }
}

При желании конфигурацию для типизированного клиента можно указать во время регистрации в Startup.ConfigureServices, а не в конструкторе типизированного клиента:

services.AddHttpClient<RepoService>(c =>
{
    c.BaseAddress = new Uri("https://api.github.com/");
    c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
    c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});

Можно полностью инкапсулировать HttpClient внутри типизированного клиента. Вместо предоставления его как свойства можно использовать открытые методы для внутреннего вызова экземпляра HttpClient.

public class RepoService
{
    // _httpClient isn't exposed publicly
    private readonly HttpClient _httpClient;

    public RepoService(HttpClient client)
    {
        _httpClient = client;
    }

    public async Task<IEnumerable<string>> GetRepos()
    {
        var response = await _httpClient.GetAsync("aspnet/repos");

        response.EnsureSuccessStatusCode();

        var result = await response.Content
            .ReadAsAsync<IEnumerable<string>>();

        return result;
    }
}

В приведенном выше коде HttpClient хранится как закрытое поле. Любой доступ для совершения внешних вызовов осуществляется через метод GetRepos.

Созданные клиенты

IHttpClientFactory можно использовать в сочетании с другими библиотеками сторонних разработчиков, например Refit. Refit — это библиотека REST для .NET. Она преобразует REST API в динамические интерфейсы. Реализация интерфейса формируется динамически с помощью RestService с использованием HttpClient для совершения внешних вызовов HTTP.

Для представления внешнего API и его ответа определяются интерфейс и ответ:

public interface IHelloClient
{
    [Get("/helloworld")]
    Task<Reply> GetMessageAsync();
}

public class Reply
{
    public string Message { get; set; }
}

Можно добавить типизированный клиент, используя Refit для создания реализации:

public void ConfigureServices(IServiceCollection services)
{
    services.AddHttpClient("hello", c =>
    {
        c.BaseAddress = new Uri("http://localhost:5000");
    })
    .AddTypedClient(c => Refit.RestService.For<IHelloClient>(c));

    services.AddMvc();
}

При необходимости можно использовать определенный интерфейс с реализацией, предоставленной внедрением зависимостей и Refit:

[ApiController]
public class ValuesController : ControllerBase
{
    private readonly IHelloClient _client;

    public ValuesController(IHelloClient client)
    {
        _client = client;
    }

    [HttpGet("/")]
    public async Task<ActionResult<Reply>> Index()
    {
        return await _client.GetMessageAsync();
    }
}

ПО промежуточного слоя для исходящих запросов

В HttpClient уже существует концепция делегирования обработчиков, которые можно связать друг с другом для исходящих HTTP-запросов. Класс IHttpClientFactory упрощает определение обработчиков для применения к каждому именованному клиенту. Он поддерживает регистрацию и объединение в цепочки нескольких обработчиков для создания конвейера ПО промежуточного слоя для исходящих запросов. Каждый из этих обработчиков может выполнять работу до и после исходящего запроса. Этот шаблон похож на входящий конвейер ПО промежуточного слоя в ASP.NET Core. Шаблон предоставляет механизм управления сквозной функциональностью HTTP-запросов, включая кэширование, обработку ошибок, сериализацию и ведение журнала.

Чтобы создать обработчик, необходимо определить класс, производный от DelegatingHandler. Переопределите метод SendAsync для выполнения кода до передачи запросов следующему обработчику в конвейере:

public class ValidateHeaderHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        if (!request.Headers.Contains("X-API-KEY"))
        {
            return new HttpResponseMessage(HttpStatusCode.BadRequest)
            {
                Content = new StringContent(
                    "You must supply an API key header called X-API-KEY")
            };
        }

        return await base.SendAsync(request, cancellationToken);
    }
}

В предыдущем коде определяется базовый обработчик. Он проверяет, включен ли в запрос заголовок X-API-KEY. Если заголовок отсутствует, он может избежать вызовов HTTP и вернуть подходящий ответ.

Во время регистрации можно добавить один или несколько обработчиков в конфигурацию для HttpClient. Эта задача выполняется через методы расширения в IHttpClientBuilder.

services.AddTransient<ValidateHeaderHandler>();

services.AddHttpClient("externalservice", c =>
{
    // Assume this is an "external" service which requires an API KEY
    c.BaseAddress = new Uri("https://localhost:5000/");
})
.AddHttpMessageHandler<ValidateHeaderHandler>();

В приведенном выше коде ValidateHeaderHandler регистрируется с помощью внедрения зависимостей. Обработчик должен регистрироваться во внедрении зависимостей как временная служба, а не ограниченная. Если обработчик зарегистрирован в качестве службы с областью действия и все службы, от которых зависит этот обработчик, освобождаются:

  • Службы обработчика могли быть удалены, прежде чем обработчик вышел из области действия.
  • Освобожденные службы обработчика приводят к его сбою.

После регистрации можно вызвать AddHttpMessageHandler, передав тип обработчика.

Можно зарегистрировать несколько обработчиков в порядке, в котором они должны выполняться. Каждый обработчик содержит следующий обработчик, пока последний HttpClientHandler не выполнит запрос:

services.AddTransient<SecureRequestHandler>();
services.AddTransient<RequestDataHandler>();

services.AddHttpClient("clientwithhandlers")
    // This handler is on the outside and called first during the 
    // request, last during the response.
    .AddHttpMessageHandler<SecureRequestHandler>()
    // This handler is on the inside, closest to the request being 
    // sent.
    .AddHttpMessageHandler<RequestDataHandler>();

Используйте один из следующих методов для предоставления общего доступа к состоянию отдельных запросов с помощью обработчиков сообщений:

  • Передайте данные в обработчик с помощью HttpRequestMessage.Properties.
  • Используйте IHttpContextAccessor для доступа к текущему запросу.
  • Создайте пользовательский объект хранилища AsyncLocal для передачи данных.

Использование обработчиков на основе Polly

IHttpClientFactory интегрируется с популярной библиотекой сторонних разработчиков под названием Polly. Polly — это комплексная библиотека, обеспечивающая отказоустойчивость и обработку временных сбоев в .NET. Она позволяет разработчикам выражать политики, например политику повтора, размыкателя цепи, времени ожидания, изоляции отсеков и отката, более эффективным и потокобезопасным образом.

Для использования политик Polly с настроенными экземплярами HttpClient предоставляются методы расширения. Расширения Polly:

  • Поддерживает добавление обработчиков на основе Polly клиентам.
  • Можно использовать после установки пакета NuGet Microsoft.Extensions.Http.Polly. Пакет не включен в общую платформу ASP.NET Core.

Обработка временных сбоев

Чаще всего ошибки происходят, когда внешние вызовы HTTP являются временными. Используется удобный метод расширения AddTransientHttpErrorPolicy, который позволяет определить политику для обработки временных ошибок. Политики, заданные с помощью этого метода расширения, обрабатывают HttpRequestException, ответы HTTP 5xx и ответы HTTP 408.

Расширение AddTransientHttpErrorPolicy может быть использовано в Startup.ConfigureServices. Данное расширение предоставляет доступ к объекту PolicyBuilder, настроенному для обработки ошибок, представляющих возможный временный сбой:

services.AddHttpClient<UnreliableEndpointCallerService>()
    .AddTransientHttpErrorPolicy(p => 
        p.WaitAndRetryAsync(3, _ => TimeSpan.FromMilliseconds(600)));

В приведенном выше коде определена политика WaitAndRetryAsync. Неудачные запросы повторяются до трех раз с задержкой 600 мс между попытками.

Динамический выбор политик

Существуют дополнительные методы расширения, которые можно использовать для добавления обработчиков на основе Polly. Одним из таких расширений является AddPolicyHandler с несколькими перегрузками. Одна перегрузка разрешает проверку запроса для определения необходимой политики:

var timeout = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(10));
var longTimeout = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(30));

services.AddHttpClient("conditionalpolicy")
// Run some code to select a policy based on the request
    .AddPolicyHandler(request => 
        request.Method == HttpMethod.Get ? timeout : longTimeout);

Если в приведенном выше коде исходящий запрос является запросом HTTP GET, применяется время ожидания 10 секунд. Для остальных методов HTTP время ожидания — 30 секунд.

Добавление нескольких обработчиков Polly

Общепринятой практикой является вложение политик Polly для предоставления расширенной функциональности:

services.AddHttpClient("multiplepolicies")
    .AddTransientHttpErrorPolicy(p => p.RetryAsync(3))
    .AddTransientHttpErrorPolicy(
        p => p.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));

В приведенном выше примере добавляются два обработчика. Первый использует расширение AddTransientHttpErrorPolicy, чтобы добавить политику повтора. Неудачные запросы выполняются повторно до трех раз. Второй вызов к AddTransientHttpErrorPolicy добавляет политику размыкателя цепи. Дополнительные внешние запросы блокируются в течение 30 секунд в случае пяти неудачных попыток подряд. Политики размыкателя цепи отслеживают состояние. Все вызовы через этот клиент имеют одинаковое состояние цепи.

Добавление политик из реестра Polly

Подход к управлению регулярно используемыми политиками заключается в их однократном определении и регистрации с помощью PolicyRegistry. Предоставляется метод расширения, разрешающий добавление обработчика с помощью политики из реестра:

var registry = services.AddPolicyRegistry();

registry.Add("regular", timeout);
registry.Add("long", longTimeout);

services.AddHttpClient("regulartimeouthandler")
    .AddPolicyHandlerFromRegistry("regular");

В приведенном выше коде, когда PolicyRegistry добавляется в ServiceCollection, регистрируются две политики. Чтобы использовать политику из реестра, применяется метод AddPolicyHandlerFromRegistry, который передает имя необходимой политики.

Дополнительные сведения об интеграции IHttpClientFactory и Polly см. на вики-сайте Polly.

Управление HttpClient и временем существования

При каждом вызове CreateClient в IHttpClientFactory возвращается новый экземпляр HttpClient. Для каждого названного клиента существует HttpMessageHandler. Фабрика обеспечивает управление временем существования экземпляров HttpMessageHandler.

IHttpClientFactory объединяет в пул все экземпляры HttpMessageHandler, созданные фабрикой, чтобы уменьшить потребление ресурсов. Экземпляр HttpMessageHandler можно использовать повторно из пула при создании экземпляра HttpClient, если его время существования еще не истекло.

Создавать пулы обработчиков желательно, так как каждый обработчик обычно управляет собственными базовыми HTTP-подключениями. Создание лишних обработчиков может привести к задержке подключения. Некоторые обработчики поддерживают подключения открытыми в течение неопределенного периода, что может помешать обработчику отреагировать на изменения DNS.

Время существования обработчика по умолчанию — две минуты. Значение по умолчанию можно переопределить для каждого именованного клиента. Чтобы переопределить это значение, вызовите SetHandlerLifetime в IHttpClientBuilder, который возвращается при создании клиента:

services.AddHttpClient("extendedhandlerlifetime")
    .SetHandlerLifetime(TimeSpan.FromMinutes(5));

Высвобождать клиент не требуется. Высвобождение отменяет исходящие запросы и гарантирует, что указанный экземпляр HttpClient не может использоваться после вызова Dispose. IHttpClientFactory отслеживает и высвобождает ресурсы, используемые экземплярами HttpClient. Экземпляры HttpClient обычно можно рассматривать как объекты .NET, не требующие высвобождения.

До появления IHttpClientFactory один экземпляр HttpClient часто сохраняли в активном состоянии в течение длительного времени. После перехода на IHttpClientFactory это уже не нужно.

Альтернативы интерфейсу IHttpClientFactory

Использование IHttpClientFactory в приложении с внедрением зависимостей позволяет:

  • предотвращать проблемы нехватки ресурсов путем объединения экземпляров HttpMessageHandler в пулы;
  • предотвращать проблемы устаревания записей DNS путем регулярной утилизации экземпляров HttpMessageHandler.

Существуют альтернативные способы решения указанных выше проблем с помощью долгосрочного экземпляра SocketsHttpHandler.

  • Создайте экземпляр SocketsHttpHandler при запуске приложения и используйте его в течение всего жизненного цикла приложения.
  • Присвойте PooledConnectionLifetime соответствующее значение в соответствии со временем обновления записей DNS.
  • По мере необходимости создавайте экземпляры HttpClient с помощью new HttpClient(handler, disposeHandler: false).

Описанные выше подходы решают проблемы, связанные с управлением ресурсами, которые в IHttpClientFactory решаются сходным образом.

  • SocketsHttpHandler обеспечивает совместное использование подключений экземплярами HttpClient. Этот позволяет предотвратить нехватку сокетов.
  • SocketsHttpHandler уничтожает подключения в соответствии со значением PooledConnectionLifetime, чтобы предотвратить проблемы устаревания записей DNS.

Файлы cookie

Объединение экземпляров HttpMessageHandler в пул приводит к совместному использованию объектов CookieContainer. Непредвиденное совместное использование объектов CookieContainer часто приводит к ошибкам в коде. Для приложений, которым требуются файлы cookie, рекомендуется один из следующих подходов:

  • отключите автоматическую обработку файлов cookie;
  • не используйте IHttpClientFactory.

Чтобы отключить автоматическую обработку файлов ConfigurePrimaryHttpMessageHandler, вызовите cookie:

services.AddHttpClient("configured-disable-automatic-cookies")
    .ConfigurePrimaryHttpMessageHandler(() =>
    {
        return new HttpClientHandler()
        {
            UseCookies = false,
        };
    });

Ведение журнала

Клиенты, созданные через IHttpClientFactory, записывают сообщения журнала для всех запросов. Установите соответствующий уровень информации в конфигурации ведения журнала, чтобы просматривать сообщения журнала по умолчанию. Дополнительное ведение журнала, например запись заголовков запросов, включено только на уровне трассировки.

Категория журнала для каждого клиента включает в себя имя клиента. Клиент с именем MyNamedClient, например, записывает в журнал сообщения с категорией System.Net.Http.HttpClient.MyNamedClient.LogicalHandler. Сообщения с суффиксом LogicalHandler создаются за пределами конвейера обработчиков запросов. Во время запроса сообщения записываются в журнал до обработки запроса другими обработчиками в конвейере. Во время ответа сообщения записываются в журнал после получения ответа другими обработчиками в конвейере.

Кроме того, журнал ведется в конвейере обработчиков запросов. В примере MyNamedClient эти сообщения вносятся в журнал по категории журнала System.Net.Http.HttpClient.MyNamedClient.ClientHandler. Во время запроса это происходит после выполнения всех обработчиков и непосредственно перед отправкой запроса по сети. Во время ответа в журнале записывается состояние ответа перед его передачей обратно по конвейеру обработчиков.

Включив ведение журнала в конвейере и за его пределами, можно выполнять проверку изменений, внесенных другими обработчиками конвейера. Сюда входят, например, изменения заголовков запросов или кода состояния ответов.

Включение имени клиента в категорию журнала позволяет фильтровать журналы по именованным клиентам при необходимости.

Настройка HttpMessageHandler

Иногда необходимо контролировать конфигурацию внутреннего обработчика HttpMessageHandler, используемого клиентом.

При добавлении именованного или типизированного клиента возвращается IHttpClientBuilder. Для определения делегата можно использовать метод расширения ConfigurePrimaryHttpMessageHandler. Делегат используется для создания и настройки основного обработчика HttpMessageHandler, используемого этим клиентом:

services.AddHttpClient("configured-inner-handler")
    .ConfigurePrimaryHttpMessageHandler(() =>
    {
        return new HttpClientHandler()
        {
            AllowAutoRedirect = false,
            UseDefaultCredentials = true
        };
    });

Использование IHttpClientFactory в консольном приложении

В консольном приложении добавьте в проект следующие ссылки на пакеты:

В следующем примере :

  • IHttpClientFactory регистрируется в контейнере службы универсального узла:
  • MyService создает экземпляр фабрики клиента из службы, который используется для создания HttpClient. HttpClient используется для получения веб-страницы.
  • Метод GetPage службы выполняется для записи первых 500 символов содержимого веб-страницы в консоль. Дополнительные сведения о вызове служб из Program.Main см. в разделе Внедрение зависимостей в ASP.NET Core.
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
class Program
{
    static async Task<int> Main(string[] args)
    {
        var builder = new HostBuilder()
            .ConfigureServices((hostContext, services) =>
            {
                services.AddHttpClient();
                services.AddTransient<IMyService, MyService>();
            }).UseConsoleLifetime();

        var host = builder.Build();

        try
        {
            var myService = host.Services.GetRequiredService<IMyService>();
            var pageContent = await myService.GetPage();

            Console.WriteLine(pageContent.Substring(0, 500));
        }
        catch (Exception ex)
        {
            var logger = host.Services.GetRequiredService<ILogger<Program>>();

            logger.LogError(ex, "An error occurred.");
        }

        return 0;
    }

    public interface IMyService
    {
        Task<string> GetPage();
    }

    public class MyService : IMyService
    {
        private readonly IHttpClientFactory _clientFactory;

        public MyService(IHttpClientFactory clientFactory)
        {
            _clientFactory = clientFactory;
        }

        public async Task<string> GetPage()
        {
            // Content from BBC One: Dr. Who website (©BBC)
            var request = new HttpRequestMessage(HttpMethod.Get,
                "https://www.bbc.co.uk/programmes/b006q2x0");
            var client = _clientFactory.CreateClient();
            var response = await client.SendAsync(request);

            if (response.IsSuccessStatusCode)
            {
                return await response.Content.ReadAsStringAsync();
            }
            else
            {
                return $"StatusCode: {response.StatusCode}";
            }
        }
    }
}

ПО промежуточного слоя для распространения заголовков

Header propagation — это поддерживаемое сообществом ПО промежуточного слоя для распространения HTTP-заголовков из входящего запроса на исходящие запросы HTTP-клиентов. Чтобы использовать распространение заголовков, сделайте следующее:

  • Укажите ссылку на поддерживаемый сообществом порт пакета HeaderPropagation. ASP.NET Core 3.1 и более поздних версий поддерживает Microsoft.AspNetCore.HeaderPropagation.

  • Настройте ПО промежуточного слоя и HttpClient в Startup:

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
    
        services.AddHttpClient("MyForwardingClient").AddHeaderPropagation();
        services.AddHeaderPropagation(options =>
        {
            options.Headers.Add("X-TraceId");
        });
    }
    
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseHsts();
        }
    
        app.UseHttpsRedirection();
    
        app.UseHeaderPropagation();
    
        app.UseMvc();
    }
    
  • Клиент включает настроенные заголовки в исходящие запросы:

    var client = clientFactory.CreateClient("MyForwardingClient");
    var response = client.GetAsync(...);
    

Дополнительные ресурсы