Verification: a143cc29221c9be0

Php check string contains substring

Содержание

Принципы использования

Существует несколько способов использования 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 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
                >(responseStream);
        }
        else
        {
            GetBranchesError = true;
            Branches = Array.Empty();
        }
    }
}

Подобное использование 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 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
                    >(responseStream);
        }
        else
        {
            GetPullRequestsError = true;
            PullRequests = Array.Empty();
        }
    }
}

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

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

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

  • предоставляют те же возможности, что и именованные клиенты, без необходимости использовать строки в качестве ключей.
  • Это помогает 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> GetAspNetDocsIssues()
    {
        return await Client.GetFromJsonAsync>(
          "/repos/aspnet/AspNetCore.Docs/issues?state=open&sort=created&direction=desc");
    }
}

В приведенном выше коде:

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

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

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

services.AddHttpClient();

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

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

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

public class TypedClientModel : PageModel
{
    private readonly GitHubService _gitHubService;

    public IEnumerable 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();
        }
    }
}

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

services.AddHttpClient(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> GetRepos()
    {
        var response = await _httpClient.GetAsync("aspnet/repos");

        response.EnsureSuccessStatusCode();

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

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

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

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

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

public interface IHelloClient
{
    [Get("/helloworld")]
    Task 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(c));

    services.AddControllers();
}

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

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

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

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

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

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

  • ПОМЕСТИТЬ
  • ОТПРАВКА
  • 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 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();

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

    // Remaining code deleted for brevity.

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

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

services.AddTransient();
services.AddTransient();

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

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

Когда 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(options =>
        options.UseInMemoryDatabase("TodoItems"));

    services.AddHttpContextAccessor();

    services.AddHttpClient((sp, httpClient) =>
    {
        var httpRequest = sp.GetRequiredService().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();
    
    services.AddTransient();
    services.AddTransient();

    services.AddHttpClient("Operation")
        .AddHttpMessageHandler()
        .AddHttpMessageHandler()
        .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 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 для передачи данных.

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

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

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

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

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

  • HttpRequestException
  • HTTP 5xx
  • HTTP 408

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

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

    // Remaining code deleted for brevity.

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

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

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

var timeout = Policy.TimeoutAsync(
    TimeSpan.FromSeconds(10));
var longTimeout = Policy.TimeoutAsync(
    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(
        TimeSpan.FromSeconds(10));
    var longTimeout = Policy.TimeoutAsync(
        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.

Cookies

Объединение экземпляров 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 в консольном приложении

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

  • Microsoft.Extensions.Hosting
  • Microsoft.Extensions.Http

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

  • 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 Main(string[] args)
    {
        var builder = new HostBuilder()
            .ConfigureServices((hostContext, services) =>
            {
                services.AddHttpClient();
                services.AddTransient();
            }).UseConsoleLifetime();

        var host = builder.Build();

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

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

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

        return 0;
    }

    public interface IMyService
    {
        Task GetPage();
    }

    public class MyService : IMyService
    {
        private readonly IHttpClientFactory _clientFactory;

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

        public async Task 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(...);
    

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

  • Использование HttpClientFactory для реализации устойчивых HTTP-запросов
  • Реализация повторных попыток вызова HTTP с экспоненциальной задержкой с помощью HttpClientFactory и политик Polly
  • Реализация шаблона размыкателя цепи
  • Сериализация и десериализация JSON в .NET

Принципы использования

Существует несколько способов использования 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 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
                >(responseStream);
        }
        else
        {
            GetBranchesError = true;
            Branches = Array.Empty();
        }
    }
}

Подобное использование 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 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
                    >(responseStream);
        }
        else
        {
            GetPullRequestsError = true;
            PullRequests = Array.Empty();
        }
    }
}

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

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

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

  • предоставляют те же возможности, что и именованные клиенты, без необходимости использовать строки в качестве ключей.
  • Это помогает 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> 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
            >(responseStream);
    }
}

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

В приведенном выше коде:

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

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

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

services.AddHttpClient();

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

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

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

public class TypedClientModel : PageModel
{
    private readonly GitHubService _gitHubService;

    public IEnumerable 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();
        }
    }
}

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

services.AddHttpClient(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> GetRepos()
    {
        var response = await _httpClient.GetAsync("aspnet/repos");

        response.EnsureSuccessStatusCode();

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

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

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

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

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

public interface IHelloClient
{
    [Get("/helloworld")]
    Task 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(c));

    services.AddControllers();
}

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

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

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

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

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

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

  • ПОМЕСТИТЬ
  • ОТПРАВКА
  • 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 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();

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

    // Remaining code deleted for brevity.

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

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

services.AddTransient();
services.AddTransient();

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

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

Когда 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(options =>
        options.UseInMemoryDatabase("TodoItems"));

    services.AddHttpContextAccessor();

    services.AddHttpClient((sp, httpClient) =>
    {
        var httpRequest = sp.GetRequiredService().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();
    
    services.AddTransient();
    services.AddTransient();

    services.AddHttpClient("Operation")
        .AddHttpMessageHandler()
        .AddHttpMessageHandler()
        .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 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 для передачи данных.

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

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

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

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

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

  • HttpRequestException
  • HTTP 5xx
  • HTTP 408

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

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

    // Remaining code deleted for brevity.

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

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

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

var timeout = Policy.TimeoutAsync(
    TimeSpan.FromSeconds(10));
var longTimeout = Policy.TimeoutAsync(
    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(
        TimeSpan.FromSeconds(10));
    var longTimeout = Policy.TimeoutAsync(
        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.

Cookies

Объединение экземпляров 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 в консольном приложении

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

  • Microsoft.Extensions.Hosting
  • Microsoft.Extensions.Http

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

  • 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 Main(string[] args)
    {
        var builder = new HostBuilder()
            .ConfigureServices((hostContext, services) =>
            {
                services.AddHttpClient();
                services.AddTransient();
            }).UseConsoleLifetime();

        var host = builder.Build();

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

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

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

        return 0;
    }

    public interface IMyService
    {
        Task GetPage();
    }

    public class MyService : IMyService
    {
        private readonly IHttpClientFactory _clientFactory;

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

        public async Task 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(...);
    

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

  • Использование HttpClientFactory для реализации устойчивых HTTP-запросов
  • Реализация повторных попыток вызова HTTP с экспоненциальной задержкой с помощью HttpClientFactory и политик Polly
  • Реализация шаблона размыкателя цепи
  • Сериализация и десериализация JSON в .NET

Предварительные требования

Для проектов, предназначенных для .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 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>();
        }
        else
        {
            GetBranchesError = true;
            Branches = Array.Empty();
        }                               
    }
}

Подобное использование 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 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>();
        }
        else
        {
            GetPullRequestsError = true;
            PullRequests = Array.Empty();
        }
    }
}

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

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

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

  • предоставляют те же возможности, что и именованные клиенты, без необходимости использовать строки в качестве ключей.
  • Это помогает 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> 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>();

        return result;
    }
}

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

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

services.AddHttpClient();

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

public class TypedClientModel : PageModel
{
    private readonly GitHubService _gitHubService;

    public IEnumerable 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();
        }
    }
}

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

services.AddHttpClient(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> GetRepos()
    {
        var response = await _httpClient.GetAsync("aspnet/repos");

        response.EnsureSuccessStatusCode();

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

        return result;
    }
}

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

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

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

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

public interface IHelloClient
{
    [Get("/helloworld")]
    Task 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(c));

    services.AddMvc();
}

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

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

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

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

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

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

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

public class ValidateHeaderHandler : DelegatingHandler
{
    protected override async Task 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();

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

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

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

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

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

services.AddTransient();
services.AddTransient();

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

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

  • Передайте данные в обработчик с помощью 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()
    .AddTransientHttpErrorPolicy(p => 
        p.WaitAndRetryAsync(3, _ => TimeSpan.FromMilliseconds(600)));

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

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

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

var timeout = Policy.TimeoutAsync(
    TimeSpan.FromSeconds(10));
var longTimeout = Policy.TimeoutAsync(
    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.

Cookies

Объединение экземпляров 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 в консольном приложении

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

  • Microsoft.Extensions.Hosting
  • Microsoft.Extensions.Http

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

  • 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 Main(string[] args)
    {
        var builder = new HostBuilder()
            .ConfigureServices((hostContext, services) =>
            {
                services.AddHttpClient();
                services.AddTransient();
            }).UseConsoleLifetime();

        var host = builder.Build();

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

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

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

        return 0;
    }

    public interface IMyService
    {
        Task GetPage();
    }

    public class MyService : IMyService
    {
        private readonly IHttpClientFactory _clientFactory;

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

        public async Task 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(...);
    

Example 1.

The following code will evaluate to true because the main string $str contains the substring ‘This‘ in it. This will print “true”.

$str = 'This is Main String';

if (strpos($str, 'This') !== false) {

    echo 'true';

}

?>

Output: true

Example 2.

The following code will evaluate to false because the main string $str doesn’t contains the substring ‘Hello‘ in it. This will nothing print.

$str = 'This is Main String';

$substr = "Hello";

if (strpos($str, $substr) !== false) {

    echo 'true';

}

?>

Output: none

Example 3.

The following code will check if a String contains a substring at start. The following code will evaluate to true because the main string $str contains the substring ‘This‘ at start.

$str = 'This is Main String';

if (strpos($str, 'This') === 0 ) {

    echo 'true';

}

?>

Output: true

Use the strpos() Function to Check if a String Contains a Substring in PHP

We will use the built-in function strpos() to check if a string contains a substring. This command checks the occurrence of a passed substring; the correct syntax to execute this is as follows:

strpos($originalString, $checkString, $offset);

The built-in function strpos() has three parameters, which are discussed below:

Parameters Description
$originalString mandatory It’s the string that we want to check for the substring.
$checkString mandatory It’s the string that we want to check in the $checkString.
$offset optional It’s the start position of the search process; if the offset is negative, the search process will start from the end.

This function returns the position of the substring relative to the position of the original string. The program below shows how we can use the strpos() function to check if a string contains a substring.

Output:

True

If we pass the $offset parameter, the function will start the search from the specified location.

Output:

False

The output has shown False because the string that we searched was before the offset.

Use the preg_match() Function to Check if a String Contains a Substring in PHP

In PHP, we can also use the preg_match() function to check if a string contains a specific word. This function uses regular expressions to match patterns. The correct syntax to use this function is as follows:

preg_match($pattern, $inputString, $matches, $flag, $offset);

The function preg_match() accepts five parameters. The detail of its parameters is as follows

Parameters Description
$regexPattern mandatory It is the pattern which we will search in the original string.
$string mandatory It is the string that we will search in the original string.
$matches optional This parameter stores the result of the matching process in it.
$flag optional This parameter specifies the flags. We have two flags for this parameter: PREG_OFFSET_CAPTURE and PREG_UNMATCHED_AS_NULL.
$offset optional This parameter tells about the start position of the matching process.

We will use the pattern /{$search}/i to check for a specific word. The program that checks if a string contains a specific word is as follows:

Output:

True

Contribute

DelftStack is a collective effort contributed by software geeks like you. If you like the article and would like to contribute to DelftStack by writing paid articles, you can check the write for us page.