Поделиться через


IHttpClientFactory в .NET

В этой статье вы узнаете, как использовать IHttpClientFactory интерфейс для создания HttpClient типов с различными основами .NET, такими как внедрение зависимостей (DI), ведение журнала и конфигурация. Тип HttpClient впервые появился в .NET Framework 4.5 в 2012 году. Иными словами, он используется уже довольно давно. HttpClient используется для выполнения HTTP-запросов и обработки ответов HTTP из веб-ресурсов, определенных Uri. При передаче интернет-трафика в большинстве случаев используется протокол HTTP.

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

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

Если приложению требуются файлы cookie, рекомендуется избежать использования IHttpClientFactory. Объединение экземпляров HttpMessageHandler приводит к совместному использованию объектов CookieContainer. Непреднамеренное совместное использование CookieContainer может привести к утечке куки между несвязанными частями приложения. Кроме того, когда HandlerLifetime срок действия истекает, обработчик перезапускается, что означает, что все файлы cookie, хранящиеся в ней CookieContainer , теряются. Альтернативные способы управления клиентами см. в рекомендациях по использованию HTTP-клиентов.

Внимание

Управление временем существования экземпляров, созданных HttpClient, совершенно отличается от управления временем существования экземпляров, созданных вручную. Стратегии предназначены для использования кратковременных клиентов, созданных IHttpClientFactory, или долгосрочных клиентов с PooledConnectionLifetime настройками. Дополнительные сведения см. в разделе "Управление временем существования HttpClient" и "Рекомендации по использованию HTTP-клиентов".

Тип IHttpClientFactory.

Для всех примеров исходного кода, предоставленных в этой статье, требуется установка пакета Microsoft.Extensions.Http NuGet. Кроме того, в примерах кода демонстрируется использование HTTP-запросов GET для получения объектов пользовательских Todo из бесплатного API JSON Placeholder.

При вызове любого из методов расширения AddHttpClient вы добавляете IHttpClientFactory и связанные службы в IServiceCollection. Тип IHttpClientFactory предоставляет следующие преимущества:

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

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

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

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

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

Чтобы зарегистрировать IHttpClientFactory, вызовите AddHttpClient:

using Shared;
using BasicHttp.Example;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddHttpClient();
builder.Services.AddTransient<TodoService>();

using IHost host = builder.Build();

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

using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Shared;

namespace BasicHttp.Example;

public sealed class TodoService(
    IHttpClientFactory httpClientFactory,
    ILogger<TodoService> logger)
{
    public async Task<Todo[]> GetUserTodosAsync(int userId)
    {
        // Create the client
        HttpClient client = httpClientFactory.CreateClient();
        
        try
        {
            // Make HTTP GET request
            // Parse JSON response deserialize into Todo types
            Todo[]? todos = await client.GetFromJsonAsync<Todo[]>(
                $"https://jsonplaceholder.typicode.com/todos?userId={userId}",
                new JsonSerializerOptions(JsonSerializerDefaults.Web));

            return todos ?? [];
        }
        catch (Exception ex)
        {
            logger.LogError("Error getting something fun to say: {Error}", ex);
        }

        return [];
    }
}

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

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

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

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

Конфигурацию именованного HttpClient можно указать во время регистрации в IServiceCollection:

using Shared;
using NamedHttp.Example;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

string? httpClientName = builder.Configuration["TodoHttpClientName"];
ArgumentException.ThrowIfNullOrEmpty(httpClientName);

builder.Services.AddHttpClient(
    httpClientName,
    client =>
    {
        // Set the base address of the named client.
        client.BaseAddress = new Uri("https://jsonplaceholder.typicode.com/");

        // Add a user-agent default request header.
        client.DefaultRequestHeaders.UserAgent.ParseAdd("dotnet-docs");
    });

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

  • Имя, которое извлекается из конфигурации под "TodoHttpClientName".
  • Основной адрес https://jsonplaceholder.typicode.com/;
  • Заголовок "User-Agent".

Вы можете использовать конфигурацию для указания имен HTTP-клиентов. Это помогает избежать ошибок в именах клиентов при их добавлении и создании. В этом примере для настройки имени HTTP-клиента используется файл appsettings.json:

{
    "TodoHttpClientName": "JsonPlaceholderApi"
}

Вы можете легко расширить эту конфигурацию и сохранить дополнительные сведения о том, как будет работать клиент HTTP. Дополнительные сведения см. в статье Конфигурация в .NET.

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

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

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

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

using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Shared;

namespace NamedHttp.Example;

public sealed class TodoService
{
    private readonly IHttpClientFactory _httpClientFactory = null!;
    private readonly IConfiguration _configuration = null!;
    private readonly ILogger<TodoService> _logger = null!;

    public TodoService(
        IHttpClientFactory httpClientFactory,
        IConfiguration configuration,
        ILogger<TodoService> logger) =>
        (_httpClientFactory, _configuration, _logger) =
            (httpClientFactory, configuration, logger);

    public async Task<Todo[]> GetUserTodosAsync(int userId)
    {
        // Create the client
        string? httpClientName = _configuration["TodoHttpClientName"];
        HttpClient client = _httpClientFactory.CreateClient(httpClientName ?? "");

        try
        {
            // Make HTTP GET request
            // Parse JSON response deserialize into Todo type
            Todo[]? todos = await client.GetFromJsonAsync<Todo[]>(
                $"todos?userId={userId}",
                new JsonSerializerOptions(JsonSerializerDefaults.Web));

            return todos ?? [];
        }
        catch (Exception ex)
        {
            _logger.LogError("Error getting something fun to say: {Error}", ex);
        }

        return [];
    }
}

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

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

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

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

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

using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Shared;

namespace TypedHttp.Example;

public sealed class TodoService(
    HttpClient httpClient,
    ILogger<TodoService> logger)
{
    public async Task<Todo[]> GetUserTodosAsync(int userId)
    {
        try
        {
            // Make HTTP GET request
            // Parse JSON response deserialize into Todo type
            Todo[]? todos = await httpClient.GetFromJsonAsync<Todo[]>(
                $"todos?userId={userId}",
                new JsonSerializerOptions(JsonSerializerDefaults.Web));

            return todos ?? [];
        }
        catch (Exception ex)
        {
            logger.LogError("Error getting something fun to say: {Error}", ex);
        }

        return [];
    }
}

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

  • Конфигурация задается при добавлении типизированного клиента в коллекцию служб.
  • HttpClient назначается как переменная (поле) с областью видимости класса и используется с общедоступными API.

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

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

using Shared;
using TypedHttp.Example;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddHttpClient<TodoService>(
    client =>
    {
        // Set the base address of the typed client.
        client.BaseAddress = new Uri("https://jsonplaceholder.typicode.com/");

        // Add a user-agent default request header.
        client.DefaultRequestHeaders.UserAgent.ParseAdd("dotnet-docs");
    });

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

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

Внимание

Использование типизированных клиентов в одноэлементных службах может быть опасным. Дополнительные сведения см. в разделе Избегайте типизированных клиентов в службах-одиночках.

Примечание.

При регистрации типизированного клиента с помощью метода AddHttpClient<TClient>, тип TClient должен иметь конструктор, который принимает HttpClient в качестве параметра. Кроме того, TClient тип не должен быть зарегистрирован в контейнере DI отдельно, так как это приведет к тому, что последующая регистрация перезапишет предыдущую.

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

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

Рассмотрим следующий record тип:

namespace Shared;

public record class Todo(
    int UserId,
    int Id,
    string Title,
    bool Completed);

В следующем примере используется пакет NuGet Refit.HttpClientFactory, и это простой интерфейс.

using Refit;
using Shared;

namespace GeneratedHttp.Example;

public interface ITodoService
{
    [Get("/todos?userId={userId}")]
    Task<Todo[]> GetUserTodosAsync(int userId);
}

Предыдущий интерфейс C#:

  • Определяет метод с именем GetUserTodosAsync, который возвращает экземпляр Task<Todo[]>.
  • Объявляет для внешнего API атрибут Refit.GetAttribute с путем и строкой запроса.

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

using GeneratedHttp.Example;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Refit;
using Shared;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddRefitClient<ITodoService>()
    .ConfigureHttpClient(client =>
    {
        // Set the base address of the named client.
        client.BaseAddress = new Uri("https://jsonplaceholder.typicode.com/");

        // Add a user-agent default request header.
        client.DefaultRequestHeaders.UserAgent.ParseAdd("dotnet-docs");
    });

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

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

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

  • POST
  • PUT
  • DELETE
  • PATCH

Полный список поддерживаемых HTTP-команд см. в статье HttpMethod. Дополнительные сведения о выполнении HTTP-запросов см. в статье "Отправка запроса с помощью HttpClient".

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

public async Task CreateItemAsync(Item item)
{
    using StringContent json = new(
        JsonSerializer.Serialize(item, new JsonSerializerOptions(JsonSerializerDefaults.Web)),
        Encoding.UTF8,
        MediaTypeNames.Application.Json);

    using HttpResponseMessage httpResponse =
        await httpClient.PostAsync("/api/items", json);

    httpResponse.EnsureSuccessStatusCode();
}

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

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

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

В следующем примере показан HTTP-запрос PUT :

public async Task UpdateItemAsync(Item item)
{
    using StringContent json = new(
        JsonSerializer.Serialize(item, new JsonSerializerOptions(JsonSerializerDefaults.Web)),
        Encoding.UTF8,
        MediaTypeNames.Application.Json);

    using HttpResponseMessage httpResponse =
        await httpClient.PutAsync($"/api/items/{item.Id}", json);

    httpResponse.EnsureSuccessStatusCode();
}

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

В следующем примере показан HTTP-запрос DELETE :

public async Task DeleteItemAsync(Guid id)
{
    using HttpResponseMessage httpResponse =
        await httpClient.DeleteAsync($"/api/items/{id}");

    httpResponse.EnsureSuccessStatusCode();
}

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

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

Управление жизненным циклом HttpClient

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

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

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

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

services.AddHttpClient("Named.Client")
    .SetHandlerLifetime(TimeSpan.FromMinutes(5));

Внимание

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

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

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

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

Настройте HttpMessageHandler

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

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

.ConfigurePrimaryHttpMessageHandler(() =>
{
    return new HttpClientHandler
    {
        AllowAutoRedirect = false,
        UseDefaultCredentials = true
    };
});

Настройка HttClientHandler позволяет указать прокси-сервер для HttpClient экземпляра среди различных других свойств обработчика. Дополнительные сведения см. в разделе "Прокси-сервер для каждого клиента".

Дополнительная настройка

Существует несколько дополнительных вариантов настройки для управления IHttpClientHandler:

Метод Описание
AddHttpMessageHandler Добавляет дополнительный обработчик сообщений для именованного объекта HttpClient.
AddTypedClient Настраивает привязку между TClient и именованным объектом HttpClient, связанным с IHttpClientBuilder.
ConfigureHttpClient Добавляет делегат, который будет использоваться для настройки именованного HttpClient.
ConfigurePrimaryHttpMessageHandler Настраивает основной HttpMessageHandler из контейнера внедрения зависимостей для именованного компонента HttpClient.
RedactLoggedHeaders Задает коллекцию имен заголовков HTTP, для которых значения должны быть скрыты перед занесением в журнал.
SetHandlerLifetime Задает период времени, в течение которого экземпляр HttpMessageHandler может использоваться повторно. Для каждого именованного клиента можно задать собственное значение времени жизни обработчика.
UseSocketsHttpHandler Настраивает новый или ранее добавленный экземпляр SocketsHttpHandler из контейнера внедрения зависимостей, который будет использоваться как основной обработчик для именованного HttpClient. (только .NET 5+ )

Использование IHttpClientFactory вместе с SocketsHttpHandler

Реализация SocketsHttpHandler была добавлена в .NET Core 2.1, что позволяет настроить HttpMessageHandler. Этот параметр используется для обеспечения реагирования обработчика на изменения DNS, поэтому использование SocketsHttpHandler считается альтернативой использованию IHttpClientFactory. Дополнительные сведения см. в руководстве по использованию HTTP-клиентов.

Тем не менее, SocketsHttpHandler и IHttpClientFactory можно использовать вместе, чтобы улучшить настраиваемость. Используя оба этих API, можно воспользоваться возможностью настройки на низком уровне (например, при LocalCertificateSelectionCallback использовании динамического выбора сертификатов) и на высоком уровне (например, с использованием интеграции DI и нескольких конфигураций клиента).

Чтобы использовать оба API:

  1. Укажите SocketsHttpHandler как PrimaryHandler через ConfigurePrimaryHttpMessageHandlerили UseSocketsHttpHandler (только .NET 5+).
  2. Настройте SocketsHttpHandler.PooledConnectionLifetime в зависимости от интервала, в котором вы ожидаете обновления DNS; например, установите значение, которое было ранее в HandlerLifetime.
  3. (Необязательно) Поскольку SocketsHttpHandler будет управлять пулом подключений и рециркуляцией, больше нет необходимости в рециркуляции обработчика на уровне IHttpClientFactory. Ее можно отключить, задав для параметра HandlerLifetime значение Timeout.InfiniteTimeSpan.
services.AddHttpClient(name)
    .UseSocketsHttpHandler((handler, _) =>
        handler.PooledConnectionLifetime = TimeSpan.FromMinutes(2)) // Recreate connection every 2 minutes
    .SetHandlerLifetime(Timeout.InfiniteTimeSpan); // Disable rotation, as it is handled by PooledConnectionLifetime

В приведенном выше примере 2 минуты были выбраны произвольно в целях иллюстрации, что соответствует значению по умолчанию HandlerLifetime. Вы должны выбрать значение на основе ожидаемой частоты ИЗМЕНЕНИЙ DNS или других сетевых изменений. Дополнительные сведения см. в разделе «Поведение DNS» в рекомендациях , а также в разделе «Примечания» в документации по API HttpClient.

Избегайте типизированных клиентов в одноэлементных службах

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

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

Внимание

Ожидается, что типизированные клиенты будут недолговечными в том же смысле, что и HttpClient экземпляры, создаваемые IHttpClientFactory (для получения дополнительной информации см. раздел HttpClient"Управление временем существования"). После создания типизированного экземпляра клиента, IHttpClientFactory теряет контроль над ним. Если типизированный экземпляр клиента сохраняется в одиночном экземпляре, это может мешать реагированию на изменения DNS, нарушив одну из целей IHttpClientFactory.

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

  • Вместо этого используйте подход с именованным клиентом, внедряя в одиночный сервис и повторно создавая IHttpClientFactory экземпляры при необходимости.
  • Если вам требуется типизированный клиент, настройте SocketsHttpHandler с помощью PooledConnectionLifetime в качестве основного обработчика. Дополнительные сведения об использовании SocketsHttpHandler с IHttpClientFactory см. в разделе Использование IHttpClientFactory вместе с SocketsHttpHandler.

Области обработчика сообщений в IHttpClientFactory

IHttpClientFactory создает отдельную область DI для каждого HttpMessageHandler экземпляра. Эти области DI отделены от областей DI приложения (например, области входящих запросов в ASP.NET или вручную созданной пользователем области DI), поэтому они не будут совместно использовать экземпляры служб с ограниченной областью действия. Области обработчика сообщений привязаны к времени существования обработчика и могут выходить за пределы областей приложений, что может привести к повторному использовании одного HttpMessageHandler экземпляра с одинаковыми внедренными зависимостями области между несколькими входящими запросами.

Схема с двумя областями di приложения и отдельной областью обработчика сообщений

Пользователям настоятельно рекомендуется не кэшировать сведения , связанные с областью действия (например, данные из HttpContext) в HttpMessageHandler экземплярах и использовать зависимости с областью действия с осторожностью, чтобы избежать утечки конфиденциальной информации.

Если вам требуется доступ к области DI приложения из обработчика сообщений, например, для аутентификации, вы инкапсулируете логику, поддерживающую области, в отдельный временный DelegatingHandler, и обернёте её вокруг экземпляра HttpMessageHandler из кэша IHttpClientFactory. Чтобы вызвать обработчик для любого зарегистрированного IHttpMessageHandlerFactory.CreateHandler, используйте . В этом случае вы самостоятельно создадите экземпляр HttpClient, используя обработчик, который вы создали.

Схема получения доступа к областям di приложения с помощью отдельного обработчика временных сообщений и IHttpMessageHandlerFactory

В следующем примере показано создание HttpClient, учитывающего область DelegatingHandler.

if (scopeAwareHandlerType != null)
{
    if (!typeof(DelegatingHandler).IsAssignableFrom(scopeAwareHandlerType))
    {
        throw new ArgumentException($"""
            Scope aware HttpHandler {scopeAwareHandlerType.Name} should
            be assignable to DelegatingHandler
            """);
    }

    // Create top-most delegating handler with scoped dependencies
    scopeAwareHandler = (DelegatingHandler)_scopeServiceProvider.GetRequiredService(scopeAwareHandlerType); // should be transient
    if (scopeAwareHandler.InnerHandler != null)
    {
        throw new ArgumentException($"""
            Inner handler of a delegating handler {scopeAwareHandlerType.Name} should be null.
            Scope aware HttpHandler should be registered as Transient.
            """);
    }
}

// Get or create HttpMessageHandler from HttpClientFactory
HttpMessageHandler handler = _httpMessageHandlerFactory.CreateHandler(name);

if (scopeAwareHandler != null)
{
    scopeAwareHandler.InnerHandler = handler;
    handler = scopeAwareHandler;
}

HttpClient client = new(handler);

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

public static IHttpClientBuilder AddScopeAwareHttpHandler<THandler>(
    this IHttpClientBuilder builder) where THandler : DelegatingHandler
{
    builder.Services.TryAddTransient<THandler>();
    if (!builder.Services.Any(sd => sd.ImplementationType == typeof(ScopeAwareHttpClientFactory)))
    {
        // Override default IHttpClientFactory registration
        builder.Services.AddTransient<IHttpClientFactory, ScopeAwareHttpClientFactory>();
    }

    builder.Services.Configure<ScopeAwareHttpClientFactoryOptions>(
        builder.Name, options => options.HttpHandlerType = typeof(THandler));

    return builder;
}

Дополнительные сведения см. в полном примере.

Не полагайтесь на основного обработчика с заводскими настройками по умолчанию.

В этом разделе термин "основной обработчик по умолчанию" относится к основному обработчику, который назначается реализацией по умолчанию IHttpClientFactory (или, более точно, реализацией HttpMessageHandlerBuilder по умолчанию), если он никак не настроен.

Примечание.

Основной обработчик по настройкам по умолчанию завода — это деталь реализации и может измениться. ❌ ИЗБЕГАЙТЕ зависимости от конкретной реализации, которая используется как "значение по умолчанию" (например, HttpClientHandler).

Существуют случаи, когда необходимо знать конкретный тип первичного обработчика, особенно при работе с библиотекой классов. При сохранении конфигурации конечного пользователя вам может потребоваться, например, обновить специфические свойства HttpClientHandler, такие как ClientCertificates, UseCookiesи UseProxy. Может быть заманчиво преобразовать основной обработчик в HttpClientHandler, что случайно работало, когда HttpClientHandler использовался в качестве основного обработчика по умолчанию, установленного производителем. Но как и любой код, зависящий от деталей реализации, такой обходной путь хрупкий и склонен к сбоям.

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

// Contract with the end-user: Only HttpClientHandler is supported.

// --- "Pre-configure" stage ---
// The default is fixed as HttpClientHandler to avoid depending on the "factory-default"
// Primary Handler.
services.ConfigureHttpClientDefaults(b =>
    b.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler() { UseCookies = false }));

// --- "End-user" stage ---
// IHttpClientBuilder builder = services.AddHttpClient("test", /* ... */);
// ...

// --- "Post-configure" stage ---
// The code can rely on the contract, and cast to HttpClientHandler only.
builder.ConfigurePrimaryHttpMessageHandler((handler, provider) =>
    {
        if (handler is not HttpClientHandler h)
        {
            throw new InvalidOperationException("Only HttpClientHandler is supported");
        }

        h.ClientCertificates.Add(GetClientCert(provider, builder.Name));

        //X509Certificate2 GetClientCert(IServiceProvider p, string name) { ... }
    });

Кроме того, можно рассмотреть возможность проверки типа основного обработчика и настройки таких параметров, как клиентские сертификаты, только в общепринятых поддерживаемых типах (скорее всего, HttpClientHandler и SocketsHttpHandler).

// --- "End-user" stage ---
// IHttpClientBuilder builder = services.AddHttpClient("test", /* ... */);
// ...

// --- "Post-configure" stage ---
// No contract is in place. Trying to configure main handler types supporting client
// certs, logging and skipping otherwise.
builder.ConfigurePrimaryHttpMessageHandler((handler, provider) =>
    {
        if (handler is HttpClientHandler h)
        {
            h.ClientCertificates.Add(GetClientCert(provider, builder.Name));
        }
        else if (handler is SocketsHttpHandler s)
        {
            s.SslOptions ??= new System.Net.Security.SslClientAuthenticationOptions();
            s.SslOptions.ClientCertificates ??= new X509CertificateCollection();
            s.SslOptions.ClientCertificates!.Add(GetClientCert(provider, builder.Name));
        }
        else
        {
            // Log warning
        }

        //X509Certificate2 GetClientCert(IServiceProvider p, string name) { ... }
    });

См. также