Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Note
Это не последняя версия этой статьи. Текущий выпуск можно найти в версии этой статьи о .NET 10.
Warning
Эта версия ASP.NET Core больше не поддерживается. Для получения дополнительной информации см. Политику поддержки .NET и .NET Core. Текущий выпуск можно найти в версии этой статьи о .NET 10.
Авторы Кирк Ларкин (Kirk Larkin), Стив Гордон (Steve Gordon), Гленн Кондрон (Glenn Condron) и Райан Новак (Ryan Nowak).
Его можно зарегистрировать IHttpClientFactory и использовать для создания и настройки экземпляров HttpClient в приложении. Интерфейс IHttpClientFactory предлагает следующие преимущества:
Обеспечивает централизованное расположение для именования и настройки логических экземпляров
HttpClient. Например, клиент с именем github можно зарегистрировать и настроить для доступа к GitHub. Можно зарегистрировать клиент по умолчанию для общего доступа.Кодифицирует концепцию исходящего промежуточного ПО посредством делегирования обработчиков в экземпляре
HttpClient. Предоставляет расширения для ПО промежуточного слоя на основе Polly для использования делегирующих обработчиков вHttpClient.Управляет пулом и временем существования базовых экземпляров
HttpClientMessageHandler. Автоматическое управление позволяет избежать обычных проблем со службой доменных имен (DNS), которые возникают при управлении временем существованияHttpClientвручную.Добавляет настраиваемый интерфейс ведения журнала (через
ILogger) для всех запросов, отправленных через клиенты, созданные фабрикой.
В этой статье описывается, как выполнять HTTP-запросы с помощью IHttpClientFactory в приложениях ASP.NET Core. Пример кода использует System.Text.Json для десериализации содержимого JSON, возвращаемого в ответах HTTP. Для примеров, использующих Json.NET и ReadAsAsync<T>, используйте селектор браузера Version для выбора версии 2.x этой статьи.
Просмотр шаблонов потребления
Существует несколько способов использования IHttpClientFactory в приложении:
Оптимальный подход зависит от требований приложения.
Базовое использование
Зарегистрировать IHttpClientFactory вызовом метода AddHttpClient в файле Program.cs:
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddHttpClient();
Запрос IHttpClientFactory можно запросить с помощью внедрения зависимостей (DI). Следующий код использует IHttpClientFactory для создания экземпляра HttpClient:
public class BasicModel : PageModel
{
private readonly IHttpClientFactory _httpClientFactory;
public BasicModel(IHttpClientFactory httpClientFactory) =>
_httpClientFactory = httpClientFactory;
public IEnumerable<GitHubBranch>? GitHubBranches { get; set; }
public async Task OnGet()
{
var httpRequestMessage = new HttpRequestMessage(
HttpMethod.Get,
"https://api.github.com/repos/dotnet/AspNetCore.Docs/branches")
{
Headers =
{
{ HeaderNames.Accept, "application/vnd.github.v3+json" },
{ HeaderNames.UserAgent, "HttpRequestsSample" }
}
};
var httpClient = _httpClientFactory.CreateClient();
var httpResponseMessage = await httpClient.SendAsync(httpRequestMessage);
if (httpResponseMessage.IsSuccessStatusCode)
{
using var contentStream =
await httpResponseMessage.Content.ReadAsStreamAsync();
GitHubBranches = await JsonSerializer.DeserializeAsync
<IEnumerable<GitHubBranch>>(contentStream);
}
}
}
В примере вызов IHttpClientFactory является хорошим способом для рефакторинга существующего приложения. Этот подход не влияет на использование HttpClient.
В сценариях, когда HttpClient экземпляры создаются в существующем приложении, замените вхождения вызовами CreateClient.
Именованные клиенты
Именованные клиенты подходят для следующих сценариев:
- Приложение требует много отдельных использований
HttpClient. - Существует множество
HttpClientэкземпляров с различными конфигурациями.
Укажите конфигурацию именованного HttpClient экземпляра во время регистрации в файле Program.cs :
builder.Services.AddHttpClient("GitHub", httpClient =>
{
httpClient.BaseAddress = new Uri("https://api.github.com/");
// using Microsoft.Net.Http.Headers;
// The GitHub API requires two headers.
httpClient.DefaultRequestHeaders.Add(
HeaderNames.Accept, "application/vnd.github.v3+json");
httpClient.DefaultRequestHeaders.Add(
HeaderNames.UserAgent, "HttpRequestsSample");
});
В фрагменте кода клиент настроен следующим образом:
- Базовый адрес
https://api.github.com/. - Два заголовка, необходимые для работы с API GitHub.
Метод CreateClient
Для каждого вызова CreateClient:
- Создается новый экземпляр
HttpClient. - вызывается действие настройки.
Чтобы создать именованный клиент, передайте имя клиента в CreateClient метод:
public class NamedClientModel : PageModel
{
private readonly IHttpClientFactory _httpClientFactory;
public NamedClientModel(IHttpClientFactory httpClientFactory) =>
_httpClientFactory = httpClientFactory;
public IEnumerable<GitHubBranch>? GitHubBranches { get; set; }
public async Task OnGet()
{
var httpClient = _httpClientFactory.CreateClient("GitHub");
var httpResponseMessage = await httpClient.GetAsync(
"repos/dotnet/AspNetCore.Docs/branches");
if (httpResponseMessage.IsSuccessStatusCode)
{
using var contentStream =
await httpResponseMessage.Content.ReadAsStreamAsync();
GitHubBranches = await JsonSerializer.DeserializeAsync
<IEnumerable<GitHubBranch>>(contentStream);
}
}
}
В фрагменте кода запрос не должен указывать имя узла. Код может передать путь только потому, что используется базовый адрес, настроенный для клиента.
Типизированные клиенты
Типизированные клиенты предоставляют несколько преимуществ:
- Доступ к таким же возможностям, что и именованные клиенты без необходимости использовать строки в качестве ключей.
- IntelliSense и компилятор помогают при использовании клиентских приложений.
- Возможность определить одно расположение для настройки и взаимодействия с конкретным
HttpClientэкземпляром. Например, один типизированный клиент может использоваться для одной серверной конечной точки или инкапсулировать всю логику, связанную с конечной точкой. - Поддержка di и внедрения, в которых приложение требует функциональных возможностей.
Типизированный клиент принимает параметр HttpClient в конструкторе:
public class GitHubService
{
private readonly HttpClient _httpClient;
public GitHubService(HttpClient httpClient)
{
_httpClient = httpClient;
_httpClient.BaseAddress = new Uri("https://api.github.com/");
// using Microsoft.Net.Http.Headers;
// The GitHub API requires two headers.
_httpClient.DefaultRequestHeaders.Add(
HeaderNames.Accept, "application/vnd.github.v3+json");
_httpClient.DefaultRequestHeaders.Add(
HeaderNames.UserAgent, "HttpRequestsSample");
}
public async Task<IEnumerable<GitHubBranch>?> GetAspNetCoreDocsBranchesAsync() =>
await _httpClient.GetFromJsonAsync<IEnumerable<GitHubBranch>>(
"repos/dotnet/AspNetCore.Docs/branches");
}
В фрагменте кода:
- Конфигурация перемещается в типизированный клиент.
- Предоставленный экземпляр
HttpClientсохраняется в виде закрытого поля.
Можно создать связанные с API методы, которые предоставляют функциональные возможности HttpClient. Например, метод GetAspNetCoreDocsBranches инкапсулирует код для получения документов из веток GitHub.
Следующий код вызывает AddHttpClient в файле Program.cs для регистрации GitHubService типизированного класса клиента:
builder.Services.AddHttpClient<GitHubService>();
Типизированный клиент регистрируется в системе внедрения зависимостей (Dependency Injection) как объект с кратковременным сроком службы. В этом фрагменте метод AddHttpClient регистрирует GitHubService как временный сервис. Эта регистрация использует фабричный метод для следующих задач:
- Создайте экземпляр
HttpClient. - Создайте экземпляр
GitHubServiceи передайте экземплярHttpClientв его конструктор.
Типизированный клиент можно внедрить и использовать напрямую:
public class TypedClientModel : PageModel
{
private readonly GitHubService _gitHubService;
public TypedClientModel(GitHubService gitHubService) =>
_gitHubService = gitHubService;
public IEnumerable<GitHubBranch>? GitHubBranches { get; set; }
public async Task OnGet()
{
try
{
GitHubBranches = await _gitHubService.GetAspNetCoreDocsBranchesAsync();
}
catch (HttpRequestException)
{
// ...
}
}
}
Конфигурацию для типизированного клиента также можно указать во время регистрации в файле Program.cs , а не в конструкторе типизированного клиента:
builder.Services.AddHttpClient<GitHubService>(httpClient =>
{
httpClient.BaseAddress = new Uri("https://api.github.com/");
// ...
});
Созданные клиенты
Интерфейс IHttpClientFactory можно использовать в сочетании с сторонними библиотеками, такими как Refit. Refit — это библиотека REST для .NET, которая преобразует API-интерфейсы REST в динамические интерфейсы.
AddRefitClient Вызовите метод для создания динамической реализации интерфейса. Метод используется HttpClient для выполнения внешних http-вызовов.
Настраиваемый интерфейс представляет внешний API:
public interface IGitHubClient
{
[Get("/repos/dotnet/AspNetCore.Docs/branches")]
Task<IEnumerable<GitHubBranch>> GetAspNetCoreDocsBranchesAsync();
}
Сначала вызовите AddRefitClient, чтобы создать динамическую реализацию, а затем вызовите метод ConfigureHttpClient, чтобы настроить базовый экземпляр HttpClient.
builder.Services.AddRefitClient<IGitHubClient>()
.ConfigureHttpClient(httpClient =>
{
httpClient.BaseAddress = new Uri("https://api.github.com/");
// using Microsoft.Net.Http.Headers;
// The GitHub API requires two headers.
httpClient.DefaultRequestHeaders.Add(
HeaderNames.Accept, "application/vnd.github.v3+json");
httpClient.DefaultRequestHeaders.Add(
HeaderNames.UserAgent, "HttpRequestsSample");
});
Используйте DI для доступа к динамической реализации IGitHubClient:
public class RefitModel : PageModel
{
private readonly IGitHubClient _gitHubClient;
public RefitModel(IGitHubClient gitHubClient) =>
_gitHubClient = gitHubClient;
public IEnumerable<GitHubBranch>? GitHubBranches { get; set; }
public async Task OnGet()
{
try
{
GitHubBranches = await _gitHubClient.GetAspNetCoreDocsBranchesAsync();
}
catch (ApiException)
{
// ...
}
}
}
Выполнение запросов POST, PUT и DELETE
В предыдущих примерах все HTTP-запросы используют HTTP-команду GET.
HttpClient также поддерживает другие HTTP-команды, в том числе:
- POST
- PUT
- DELETE
- PATCH
Полный список поддерживаемых HTTP-команд см. в разделе HttpMethod.
В следующем примере показано, как выполнить HTTP-запрос POST:
public async Task CreateItemAsync(TodoItem todoItem)
{
var todoItemJson = new StringContent(
JsonSerializer.Serialize(todoItem),
Encoding.UTF8,
Application.Json); // using static System.Net.Mime.MediaTypeNames;
using var httpResponseMessage =
await _httpClient.PostAsync("/api/TodoItems", todoItemJson);
httpResponseMessage.EnsureSuccessStatusCode();
}
В приведенном выше коде метод CreateItemAsync выполняет следующие задачи:
- Сериализует параметр
TodoItemв JSON с помощьюSystem.Text.Json. - Создает экземпляр StringContent для упаковки сериализованного JSON для отправки в теле HTTP-запроса.
- Вызывает PostAsync для отправки содержимого JSON на указанный URL-адрес. Это относительный URL-адрес, добавляемый в httpClient.BaseAddress.
- Вызывает EnsureSuccessStatusCode для выброса исключения, если код состояния ответа не указывает на успех.
HttpClient также поддерживает другие типы содержимого. Например, MultipartContent и StreamContent. Полный список поддерживаемого содержимого см. в статье HttpContent.
Ниже приводится пример HTTP-запроса PUT.
public async Task SaveItemAsync(TodoItem todoItem)
{
var todoItemJson = new StringContent(
JsonSerializer.Serialize(todoItem),
Encoding.UTF8,
Application.Json);
using var httpResponseMessage =
await _httpClient.PutAsync($"/api/TodoItems/{todoItem.Id}", todoItemJson);
httpResponseMessage.EnsureSuccessStatusCode();
}
Фрагмент кода аналогичен примеру POST. Метод SaveItemAsync вызывает PutAsync вместо PostAsync.
Ниже приводится пример HTTP-запроса DELETE.
public async Task DeleteItemAsync(long itemId)
{
using var httpResponseMessage =
await _httpClient.DeleteAsync($"/api/TodoItems/{itemId}");
httpResponseMessage.EnsureSuccessStatusCode();
}
В фрагменте кода метод DeleteItemAsync вызывает DeleteAsync. Поскольку HTTP-запросы DELETE обычно не содержат текст, метод DeleteAsync не предоставляет перегрузку, которая принимает экземпляр HttpContent.
Дополнительные сведения об использовании различных HTTP-команд с HttpClient см. в статье HttpClient.
С помощью средств промежуточной обработки исходящих запросов
В HttpClient существует концепция делегирования обработчиков, которые можно связать друг с другом для исходящих HTTP-запросов. Интерфейс IHttpClientFactory предоставляет следующие преимущества:
- Определите обработчики, применяемые для каждого именованного клиента, с помощью упрощенного процесса.
- Зарегистрируйте и объедините несколько обработчиков в конвейер посреднического слоя для обработки исходящего запроса. Каждый обработчик может выполнять работу до и после исходящего запроса. Этот шаблон аналогичен входящему конвейеру посреднического ПО в ASP.NET Core.
- Управление перекрестными проблемами в отношении HTTP-запросов, таких как кэширование, обработка ошибок, сериализация и ведение журнала.
Чтобы создать делегированный обработчик, сделайте следующее:
- Производный от DelegatingHandler.
- Переопределите SendAsync. Выполните код до передачи запроса следующему обработчику в конвейере:
public class ValidateHeaderHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
if (!request.Headers.Contains("X-API-KEY"))
{
return new HttpResponseMessage(HttpStatusCode.BadRequest)
{
Content = new StringContent(
"The API key header X-API-KEY is required.")
};
}
return await base.SendAsync(request, cancellationToken);
}
}
Фрагмент кода проверяет, находится ли X-API-KEY заголовок в запросе. Если X-API-KEY отсутствует, код возвращает статус BadRequest.
Несколько обработчиков можно добавить в конфигурацию для экземпляра HttpClient с помощью AddHttpMessageHandler:
builder.Services.AddTransient<ValidateHeaderHandler>();
builder.Services.AddHttpClient("HttpMessageHandler")
.AddHttpMessageHandler<ValidateHeaderHandler>();
В фрагменте кода ValidateHeaderHandler регистрируется в DI. После регистрации AddHttpMessageHandler можно вызвать, передав в качестве параметра тип обработчика.
Несколько обработчиков можно зарегистрировать в том порядке, в котором они должны выполняться. Каждый обработчик оборачивает следующий обработчик, и в конце последний HttpClientHandler выполняет запрос.
builder.Services.AddTransient<SampleHandler1>();
builder.Services.AddTransient<SampleHandler2>();
builder.Services.AddHttpClient("MultipleHttpMessageHandlers")
.AddHttpMessageHandler<SampleHandler1>()
.AddHttpMessageHandler<SampleHandler2>();
Сначала во фрагменте кода выполняется SampleHandler1, затем SampleHandler2.
Использование dependency injection в промежуточном программном обеспечении для исходящих запросов
Когда IHttpClientFactory создает новый делегирующий обработчик, он использует механизм внедрения зависимостей для выполнения параметров конструктора обработчика.
IHttpClientFactory создает отдельную область внедрения зависимостей для каждого обработчика, что может стать причиной аномального поведения, когда обработчик использует службу с назначенной областью.
Рассмотрим следующий интерфейс и его реализацию, которая представляет задачу как операцию с идентификатором: OperationId
public interface IOperationScoped
{
string OperationId { get; }
}
public class OperationScoped : IOperationScoped
{
public string OperationId { get; } = Guid.NewGuid().ToString()[^4..];
}
Как следует из названия, IOperationScoped регистрируется в DI с помощью ограниченной области действия:
builder.Services.AddScoped<IOperationScoped, OperationScoped>();
Следующий делегирующий обработчик принимает и использует IOperationScoped для задания заголовка X-OPERATION-ID для исходящего запроса:
public class OperationHandler : DelegatingHandler
{
private readonly IOperationScoped _operationScoped;
public OperationHandler(IOperationScoped operationScoped) =>
_operationScoped = operationScoped;
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
request.Headers.Add("X-OPERATION-ID", _operationScoped.OperationId);
return await base.SendAsync(request, cancellationToken);
}
}
В примере HttpRequestsSample запустите приложение, перейдите на страницу /Operation, а затем обновите страницу. Значение области запроса изменяется с каждым запросом, но значение области обработчика изменяется только каждые 5 секунд.
Обработчики могут зависеть от служб из любой области. Службы, которые зависят от обработчиков, удаляются при удалении обработчика.
Используйте один из следующих методов для предоставления общего доступа к состоянию отдельных запросов с помощью обработчиков сообщений:
- Передайте данные в обработчик с помощью метода Options.
- Используйте IHttpContextAccessor для доступа к текущему запросу.
- Создайте пользовательский AsyncLocal<T> объект хранилища для передачи данных.
Использование обработчиков на основе 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, настроенному для обработки ошибок, представляющих возможный временный сбой:
builder.Services.AddHttpClient("PollyWaitAndRetry")
.AddTransientHttpErrorPolicy(policyBuilder =>
policyBuilder.WaitAndRetryAsync(
3, retryNumber => TimeSpan.FromMilliseconds(600)));
В фрагменте WaitAndRetryAsync кода определяется политика. Неудачные запросы повторяются до трех раз с задержкой 600 мс между попытками.
Динамический выбор политик
Методы расширения предоставляются для добавления обработчиков на основе Polly, например AddPolicyHandler. Следующая перегрузка AddPolicyHandler проверяет запрос для определения применимой политики:
var timeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(
TimeSpan.FromSeconds(10));
var longTimeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(
TimeSpan.FromSeconds(30));
builder.Services.AddHttpClient("PollyDynamic")
.AddPolicyHandler(httpRequestMessage =>
httpRequestMessage.Method == HttpMethod.Get ? timeoutPolicy : longTimeoutPolicy);
В фрагменте кода, если исходящий запрос является HTTP GET, применяется 10-секундное время ожидания. Для остальных методов HTTP время ожидания — 30 секунд.
Добавьте несколько обработчиков Polly
Обычно рекомендуется вложить политики Polly:
builder.Services.AddHttpClient("PollyMultiple")
.AddTransientHttpErrorPolicy(policyBuilder =>
policyBuilder.RetryAsync(3))
.AddTransientHttpErrorPolicy(policyBuilder =>
policyBuilder.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));
В этом примере добавляются два обработчика:
Первый обработчик использует AddTransientHttpErrorPolicy для добавления политики повторных попыток. Неудачные запросы выполняются повторно до трех раз.
Второй вызов
AddTransientHttpErrorPolicyдобавляет политику автоматического выключателя. Дополнительные внешние запросы блокируются в течение 30 секунд в случае пяти неудачных попыток подряд. Политики размыкателя цепи отслеживают состояние. Все вызовы через этот клиент имеют одинаковое состояние цепи.
Добавление политик из реестра Polly
Подход к управлению регулярно используемыми политиками заключается в их однократном определении и регистрации с помощью PolicyRegistry. Рассмотрим пример.
var timeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(
TimeSpan.FromSeconds(10));
var longTimeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(
TimeSpan.FromSeconds(30));
var policyRegistry = builder.Services.AddPolicyRegistry();
policyRegistry.Add("Regular", timeoutPolicy);
policyRegistry.Add("Long", longTimeoutPolicy);
builder.Services.AddHttpClient("PollyRegistryRegular")
.AddPolicyHandlerFromRegistry("Regular");
builder.Services.AddHttpClient("PollyRegistryLong")
.AddPolicyHandlerFromRegistry("Long");
Фрагмент кода добавляет две политики в реестр Polly: Regular и Long. Код вызывает AddPolicyHandlerFromRegistry для настройки отдельных именованных клиентов с использованием этих политик из реестра Polly.
Дополнительные сведения о IHttpClientFactory и интеграции Polly см. на вики-сайте Polly.
Управление HttpClient и временем существования
При каждом вызове HttpClient на CreateClient возвращается новый экземпляр IHttpClientFactory. Создается HttpMessageHandler для каждого именованного клиента. Фабрика управляет циклом жизни экземпляров HttpMessageHandler.
IHttpClientFactory объединяет в общий пул экземпляры HttpMessageHandler, созданные фабрикой, чтобы уменьшить потребление ресурсов. Экземпляр HttpMessageHandler может быть повторно использован из пула при создании нового HttpClient экземпляра, если срок его существования не истек.
Создавать пулы обработчиков желательно, так как каждый обработчик обычно управляет собственными базовыми HTTP-подключениями. Создание лишних обработчиков может привести к задержке подключения. Некоторые обработчики поддерживают подключения открытыми в течение неопределенного периода, что может помешать обработчику отреагировать на изменения DNS.
Время существования обработчика по умолчанию составляет 2 минуты. Значение по умолчанию можно переопределить для каждого именованного клиента.
builder.Services.AddHttpClient("HandlerLifetime")
.SetHandlerLifetime(TimeSpan.FromMinutes(5));
экземпляры HttpClient обычно могут рассматриваться как .NET объекты, которые не требуют удаления. Удаление отменяет исходящие запросы и гарантирует, что экземпляр HttpClient больше не может быть использован после вызова Dispose.
IHttpClientFactory отслеживает и высвобождает ресурсы, используемые экземплярами HttpClient.
До появления HttpClient было принято поддерживать один экземпляр IHttpClientFactory в активном состоянии в течение длительного времени. После перехода на IHttpClientFactory это уже не нужно.
Изучите альтернативы IHttpClientFactory
Использование IHttpClientFactory в приложении с внедрением зависимостей избегает:
- Проблемы истощения ресурсов из-за объединения экземпляров
HttpMessageHandlerв пулы. - предотвращать проблемы устаревших записей DNS путем периодического перезапуска экземпляров
HttpMessageHandler.
Существуют альтернативные способы решения предыдущих проблем с помощью долго существующего экземпляра SocketsHttpHandler.
- Создайте экземпляр
SocketsHttpHandlerпри запуске приложения и используйте его в течение всего жизненного цикла приложения. - Присвойте PooledConnectionLifetime соответствующее значение в соответствии со временем обновления записей DNS.
- Создайте
HttpClientэкземпляры с помощьюnew HttpClient(handler, disposeHandler: false)по мере необходимости.
Альтернативные подходы решают проблемы управления ресурсами так же, как решает IHttpClientFactory.
-
SocketsHttpHandlerделится подключениями между инстанциямиHttpClient. Этот способ позволяет предотвратить нехватку сокетов. -
SocketsHttpHandlerобрабатывает подключения в соответствии сPooledConnectionLifetime, чтобы избежать проблем с устареванием записей DNS.
Журнал сообщений и состояния ответа
Клиенты, созданные через IHttpClientFactory, записывают сообщения журнала для всех запросов. Включите соответствующий уровень информации в конфигурации ведения журнала, чтобы просмотреть сообщения журнала по умолчанию. Остальные виды ведения журнала, такие как ведение заголовков запросов, включаются только на уровне трассировки.
Категория журнала для каждого клиента включает в себя имя клиента. Клиент с именем MyNamedClient, например, регистрирует сообщения с категорией "System.Net.Http.HttpClient.MyNamedClient.LogicalHandler". Сообщения с суффиксом LogicalHandler происходят за пределами конвейера обработчика запросов. По запросу сообщения регистрируются до обработки всеми другими обработчиками в процессе данных конвейера. В ответе сообщения регистрируются после получения ответа другими обработчиками конвейера.
Кроме того, журнал ведется в конвейере обработчиков запросов. В примере MyNamedClient эти сообщения регистрируются с категорией журнала System.Net.Http.HttpClient. MyNamedClient. ClientHandler". Для запроса ведение журнала происходит после выполнения всех других обработчиков и непосредственно перед отправкой запроса. В ответе логирование включает состояние ответа, перед тем как он передается через конвейер обработчика.
Включив ведение журнала в конвейере и за его пределами, можно выполнять проверку изменений, внесенных другими обработчиками конвейера. Ведение журнала может включать изменения в заголовки запросов или код состояния ответа.
Включение имени клиента в категорию журнала позволяет фильтровать журналы по именованным клиентам.
Настройка HttpMessageHandler
Может потребоваться управлять конфигурацией внутреннего HttpMessageHandler, используемого клиентом.
При добавлении именованного или типизированного клиента возвращается IHttpClientBuilder. Для определения делегата можно использовать метод расширения ConfigurePrimaryHttpMessageHandler. Делегат используется для создания и настройки основного компонента HttpMessageHandler, который используется этим клиентом:
builder.Services.AddHttpClient("ConfiguredHttpMessageHandler")
.ConfigurePrimaryHttpMessageHandler(() =>
new HttpClientHandler
{
AllowAutoRedirect = true,
UseDefaultCredentials = true
});
Работа с файлами cookie
Объединение экземпляров HttpMessageHandler в пул приводит к совместному использованию объектов CookieContainer. Непредвиденное совместное использование объектов CookieContainer часто приводит к ошибкам в коде.
Для приложений, требующих файлов cookie, рассмотрите возможность:
- отключите автоматическую обработку cookie;
- не используйте
IHttpClientFactory.
Вызовите ConfigurePrimaryHttpMessageHandler, чтобы отключить автоматическую обработку cookie.
builder.Services.AddHttpClient("NoAutomaticCookies")
.ConfigurePrimaryHttpMessageHandler(() =>
new HttpClientHandler
{
UseCookies = false
});
Использование IHttpClientFactory в консольном приложении
В консольном приложении добавьте в проект следующие ссылки на пакеты:
В следующем примере :
-
IHttpClientFactory и
GitHubServiceзарегистрированы в контейнере службы Generic Host. -
GitHubServiceзапрашивается у контейнера DI, который, в свою очередь, запрашивает экземплярIHttpClientFactory. -
GitHubServiceиспользуетIHttpClientFactoryдля создания экземпляраHttpClient, с помощью которого он получает ветви документации GitHub.
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
var host = new HostBuilder()
.ConfigureServices(services =>
{
services.AddHttpClient();
services.AddTransient<GitHubService>();
})
.Build();
try
{
var gitHubService = host.Services.GetRequiredService<GitHubService>();
var gitHubBranches = await gitHubService.GetAspNetCoreDocsBranchesAsync();
Console.WriteLine($"{gitHubBranches?.Count() ?? 0} GitHub Branches");
if (gitHubBranches is not null)
{
foreach (var gitHubBranch in gitHubBranches)
{
Console.WriteLine($"- {gitHubBranch.Name}");
}
}
}
catch (Exception ex)
{
host.Services.GetRequiredService<ILogger<Program>>()
.LogError(ex, "Unable to load branches from GitHub.");
}
public class GitHubService
{
private readonly IHttpClientFactory _httpClientFactory;
public GitHubService(IHttpClientFactory httpClientFactory) =>
_httpClientFactory = httpClientFactory;
public async Task<IEnumerable<GitHubBranch>?> GetAspNetCoreDocsBranchesAsync()
{
var httpRequestMessage = new HttpRequestMessage(
HttpMethod.Get,
"https://api.github.com/repos/dotnet/AspNetCore.Docs/branches")
{
Headers =
{
{ "Accept", "application/vnd.github.v3+json" },
{ "User-Agent", "HttpRequestsConsoleSample" }
}
};
var httpClient = _httpClientFactory.CreateClient();
var httpResponseMessage = await httpClient.SendAsync(httpRequestMessage);
httpResponseMessage.EnsureSuccessStatusCode();
using var contentStream =
await httpResponseMessage.Content.ReadAsStreamAsync();
return await JsonSerializer.DeserializeAsync
<IEnumerable<GitHubBranch>>(contentStream);
}
}
public record GitHubBranch(
[property: JsonPropertyName("name")] string Name);
Используйте промежуточное ПО для распространения заголовков
Распространение заголовков — это ПО промежуточного слоя ASP.NET Core для распространения заголовков HTTP из входящего запроса на исходящие запросы HttpClient. Чтобы использовать распространение заголовков, сделайте следующее:
Установите пакет Microsoft.AspNetCore.HeaderPropagation.
Настройте экземпляр
HttpClientи конвейер промежуточного ПО в файле Program.cs:// Add services to the container. builder.Services.AddControllers(); builder.Services.AddHttpClient("PropagateHeaders") .AddHeaderPropagation(); builder.Services.AddHeaderPropagation(options => { options.Headers.Add("X-TraceId"); }); var app = builder.Build(); // Configure the HTTP request pipeline. app.UseHttpsRedirection(); app.UseHeaderPropagation(); app.MapControllers();Выполняйте исходящие запросы с помощью настроенного экземпляра
HttpClient, который включает в себя добавленные заголовки.
Авторы Кирк Ларкин (Kirk Larkin), Стив Гордон (Steve Gordon), Гленн Кондрон (Glenn Condron) и Райан Новак (Ryan Nowak).
IHttpClientFactory можно зарегистрировать и использовать для настройки и создания экземпляров HttpClient в приложении.
IHttpClientFactory предоставляет следующие преимущества:
- Обеспечивает централизованное расположение для именования и настройки логических экземпляров
HttpClient. Например, клиент с именем github можно зарегистрировать и настроить для доступа к GitHub. Можно зарегистрировать клиент по умолчанию для общего доступа. - Кодификация концепции исходящего промежуточного ПО через делегирование обработчиков в
HttpClient. Предоставляет расширения для ПО промежуточного слоя на основе Polly для использования делегирующих обработчиков вHttpClient. - Управляет пулом и временем существования базовых экземпляров
HttpClientMessageHandler. Автоматическое управление позволяет избежать обычных проблем со службой доменных имен (DNS), которые возникают при управлении временем существованияHttpClientвручную. - Добавляет возможность настройки ведения журнала (через
ILogger) для всех запросов, отправленных через клиентов, созданных фабрикой.
Просмотреть или скачать пример кода (описание скачивания).
Пример кода в этой версии раздела использует System.Text.Json для десериализации содержимого JSON, возвращаемого в ответах HTTP. Используйте селектор версий для выбора версии 2.x этой статьи для примеров, использующих Json.NET и ReadAsAsync<T>.
Шаблоны потребления
Существует несколько способов использования IHttpClientFactory в приложении:
Оптимальный подход зависит от требований приложения.
Базовое использование
IHttpClientFactory можно зарегистрировать, вызвав AddHttpClient:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpClient();
// Remaining code deleted for brevity.
IHttpClientFactory можно запросить с помощью внедрения зависимостей (DI). Следующий код использует IHttpClientFactory для создания экземпляра HttpClient:
public class BasicUsageModel : PageModel
{
private readonly IHttpClientFactory _clientFactory;
public IEnumerable<GitHubBranch> Branches { get; private set; }
public bool GetBranchesError { get; private set; }
public BasicUsageModel(IHttpClientFactory clientFactory)
{
_clientFactory = clientFactory;
}
public async Task OnGet()
{
var request = new HttpRequestMessage(HttpMethod.Get,
"https://api.github.com/repos/dotnet/AspNetCore.Docs/branches");
request.Headers.Add("Accept", "application/vnd.github.v3+json");
request.Headers.Add("User-Agent", "HttpClientFactory-Sample");
var client = _clientFactory.CreateClient();
var response = await client.SendAsync(request);
if (response.IsSuccessStatusCode)
{
using var responseStream = await response.Content.ReadAsStreamAsync();
Branches = await JsonSerializer.DeserializeAsync
<IEnumerable<GitHubBranch>>(responseStream);
}
else
{
GetBranchesError = true;
Branches = Array.Empty<GitHubBranch>();
}
}
}
Подобное использование IHttpClientFactory — это хороший способ рефакторинга имеющегося приложения. Он не влияет на то, как HttpClient используется. Там, где в существующем приложении создаются экземпляры HttpClient, замените их вызовами CreateClient.
Именованные клиенты
Именованные клиенты являются хорошим выбором в следующих случаях:
- Приложение требует много отдельных использований
HttpClient. - Многие
HttpClientимеют другую конфигурацию.
Конфигурацию для именованного HttpClient можно указать во время регистрации в Startup.ConfigureServices:
services.AddHttpClient("github", c =>
{
c.BaseAddress = new Uri("https://api.github.com/");
// Github API versioning
c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
// Github requires a user-agent
c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});
В предыдущем коде клиент настраивается следующим образом:
- Базовый адрес
https://api.github.com/. - Два заголовка, необходимые для работы с API GitHub.
CreateClient
При каждом вызове CreateClient:
- Создается новый экземпляр
HttpClient. - вызывается действие настройки.
Чтобы создать именованный клиент, передайте его имя в CreateClient:
public class NamedClientModel : PageModel
{
private readonly IHttpClientFactory _clientFactory;
public IEnumerable<GitHubPullRequest> PullRequests { get; private set; }
public bool GetPullRequestsError { get; private set; }
public bool HasPullRequests => PullRequests.Any();
public NamedClientModel(IHttpClientFactory clientFactory)
{
_clientFactory = clientFactory;
}
public async Task OnGet()
{
var request = new HttpRequestMessage(HttpMethod.Get,
"repos/dotnet/AspNetCore.Docs/pulls");
var client = _clientFactory.CreateClient("github");
var response = await client.SendAsync(request);
if (response.IsSuccessStatusCode)
{
using var responseStream = await response.Content.ReadAsStreamAsync();
PullRequests = await JsonSerializer.DeserializeAsync
<IEnumerable<GitHubPullRequest>>(responseStream);
}
else
{
GetPullRequestsError = true;
PullRequests = Array.Empty<GitHubPullRequest>();
}
}
}
В приведенном выше коде в запросе не требуется указывать имя узла. Достаточно передать только путь, так как используется базовый адрес, заданный для клиента.
Типизированные клиенты
Типизированные клиенты:
- предоставляют те же возможности, что и именованные клиенты, без необходимости использовать строки в качестве ключей.
- Это обеспечивает поддержку IntelliSense и компилятора при работе с клиентскими приложениями.
- Предоставьте единое расположение для настройки и взаимодействия с определённым
HttpClient. Например, можно использовать один типизированный клиент:- для одной серверной конечной точки;
- для инкапсуляции всей логики, связанной с конечной точкой.
- Поддерживаются работа с внедрением зависимостей и возможность вставки в нужное место в приложении.
Типизированный клиент принимает параметр HttpClient в конструкторе:
public class GitHubService
{
public HttpClient Client { get; }
public GitHubService(HttpClient client)
{
client.BaseAddress = new Uri("https://api.github.com/");
// GitHub API versioning
client.DefaultRequestHeaders.Add("Accept",
"application/vnd.github.v3+json");
// GitHub requires a user-agent
client.DefaultRequestHeaders.Add("User-Agent",
"HttpClientFactory-Sample");
Client = client;
}
public async Task<IEnumerable<GitHubIssue>> GetAspNetDocsIssues()
{
return await Client.GetFromJsonAsync<IEnumerable<GitHubIssue>>(
"/repos/aspnet/AspNetCore.Docs/issues?state=open&sort=created&direction=desc");
}
}
В предыдущем коде:
- Конфигурация перемещается в типизированный клиент.
- Объект
HttpClientпредоставляется какpublicсвойство.
Можно создать связанные с API методы, которые предоставляют функциональные возможности HttpClient. Например, метод GetAspNetDocsIssues инкапсулирует код для получения открытых вопросов.
Следующий код вызывает AddHttpClient в Startup.ConfigureServices для регистрации класса типизированного клиента:
services.AddHttpClient<GitHubService>();
Типизированный клиент регистрируется в системе внедрения зависимостей (Dependency Injection) как объект с кратковременным сроком службы. В приведенном выше коде AddHttpClient регистрирует GitHubService как временную службу. Эта регистрация использует фабричный метод для следующих задач:
- Создайте экземпляр
HttpClient. - Создайте экземпляр
GitHubService, передав в его конструктор экземплярHttpClient.
Типизированный клиент можно внедрить и использовать напрямую:
public class TypedClientModel : PageModel
{
private readonly GitHubService _gitHubService;
public IEnumerable<GitHubIssue> LatestIssues { get; private set; }
public bool HasIssue => LatestIssues.Any();
public bool GetIssuesError { get; private set; }
public TypedClientModel(GitHubService gitHubService)
{
_gitHubService = gitHubService;
}
public async Task OnGet()
{
try
{
LatestIssues = await _gitHubService.GetAspNetDocsIssues();
}
catch(HttpRequestException)
{
GetIssuesError = true;
LatestIssues = Array.Empty<GitHubIssue>();
}
}
}
Конфигурацию для типизированного клиента можно указать во время регистрации в Startup.ConfigureServices, а не в конструкторе типизированного клиента:
services.AddHttpClient<RepoService>(c =>
{
c.BaseAddress = new Uri("https://api.github.com/");
c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});
HttpClient может быть инкапсулирован в типизированном клиенте. Вместо того, чтобы предоставлять его как свойство, определите метод, который вызывает HttpClient экземпляр внутри:
public class RepoService
{
// _httpClient isn't exposed publicly
private readonly HttpClient _httpClient;
public RepoService(HttpClient client)
{
_httpClient = client;
}
public async Task<IEnumerable<string>> GetRepos()
{
var response = await _httpClient.GetAsync("aspnet/repos");
response.EnsureSuccessStatusCode();
using var responseStream = await response.Content.ReadAsStreamAsync();
return await JsonSerializer.DeserializeAsync
<IEnumerable<string>>(responseStream);
}
}
В приведенном выше коде HttpClient хранится в закрытом поле. Доступ к HttpClient осуществляется с помощью общедоступного метода GetRepos.
Созданные клиенты
IHttpClientFactory можно использовать в сочетании с библиотеками сторонних разработчиков, например Refit. Refit — это библиотека REST для .NET. Она преобразует REST API в динамические интерфейсы. Реализация интерфейса формируется динамически с помощью RestService с использованием HttpClient для совершения внешних вызовов HTTP.
Для представления внешнего API и его ответа определяются интерфейс и ответ:
public interface IHelloClient
{
[Get("/helloworld")]
Task<Reply> GetMessageAsync();
}
public class Reply
{
public string Message { get; set; }
}
Можно добавить типизированный клиент, используя Refit для создания реализации:
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpClient("hello", c =>
{
c.BaseAddress = new Uri("http://localhost:5000");
})
.AddTypedClient(c => Refit.RestService.For<IHelloClient>(c));
services.AddControllers();
}
При необходимости можно использовать определенный интерфейс с реализацией, предоставленной внедрением зависимостей и Refit:
[ApiController]
public class ValuesController : ControllerBase
{
private readonly IHelloClient _client;
public ValuesController(IHelloClient client)
{
_client = client;
}
[HttpGet("/")]
public async Task<ActionResult<Reply>> Index()
{
return await _client.GetMessageAsync();
}
}
Выполнение запросов POST, PUT и DELETE
В предыдущих примерах все HTTP-запросы используют HTTP-команду GET.
HttpClient также поддерживает другие HTTP-команды, в том числе:
- POST
- PUT
- DELETE
- PATCH
Полный список поддерживаемых HTTP-команд см. в статье HttpMethod.
В следующем примере показано, как выполнить HTTP-запрос POST:
public async Task CreateItemAsync(TodoItem todoItem)
{
var todoItemJson = new StringContent(
JsonSerializer.Serialize(todoItem, _jsonSerializerOptions),
Encoding.UTF8,
"application/json");
using var httpResponse =
await _httpClient.PostAsync("/api/TodoItems", todoItemJson);
httpResponse.EnsureSuccessStatusCode();
}
В приведенном выше коде метод CreateItemAsync выполняет следующие задачи:
- сериализует параметр
TodoItemв JSON с помощьюSystem.Text.Json. Для настройки процесса сериализации используется экземпляр JsonSerializerOptions. - Создает экземпляр StringContent для упаковки сериализованного JSON для отправки в теле HTTP-запроса.
- вызывает метод PostAsync для отправки содержимого JSON по указанному URL-адресу. Это относительный URL-адрес, который добавляется в свойство HttpClient.BaseAddress.
- Вызывает EnsureSuccessStatusCode для выдачи исключения, если код состояния ответа не указывает на успешное выполнение.
HttpClient также поддерживает другие типы содержимого. Например, MultipartContent и StreamContent. Полный список поддерживаемого содержимого см. в статье HttpContent.
Ниже приводится пример HTTP-запроса PUT.
public async Task SaveItemAsync(TodoItem todoItem)
{
var todoItemJson = new StringContent(
JsonSerializer.Serialize(todoItem),
Encoding.UTF8,
"application/json");
using var httpResponse =
await _httpClient.PutAsync($"/api/TodoItems/{todoItem.Id}", todoItemJson);
httpResponse.EnsureSuccessStatusCode();
}
Приведенный выше код похож на код в примере с POST. Метод SaveItemAsync вызывает PutAsync вместо PostAsync.
Ниже приводится пример HTTP-запроса DELETE.
public async Task DeleteItemAsync(long itemId)
{
using var httpResponse =
await _httpClient.DeleteAsync($"/api/TodoItems/{itemId}");
httpResponse.EnsureSuccessStatusCode();
}
В приведенном выше коде метод DeleteItemAsync вызывает DeleteAsync. Поскольку HTTP-запросы DELETE обычно не содержат текст, метод DeleteAsync не предоставляет перегрузку, которая принимает экземпляр HttpContent.
Дополнительные сведения об использовании различных HTTP-команд с HttpClient см. в статье HttpClient.
Промежуточное программное обеспечение для обработки исходящих запросов
В HttpClient существует концепция делегирования обработчиков, которые можно связать друг с другом для исходящих HTTP-запросов.
IHttpClientFactory:
- Упрощает определение обработчиков для применения к каждому именованному клиенту.
- Поддерживает регистрацию и объединение нескольких обработчиков в цепочке для построения конвейера промежуточного ПО для исходящих запросов. Каждый из этих обработчиков может выполнять работу до и после исходящего запроса. Этот шаблон:
- Аналогично конвейеру входящего ПО промежуточного слоя в ASP.NET Core.
- Предоставляет механизм для управления перекрестными проблемами в отношении HTTP-запросов, таких как:
- caching
- error handling
- serialization
- logging
Чтобы создать делегированный обработчик, сделайте следующее:
- Создайте объект, производный от DelegatingHandler.
- Переопределите SendAsync. Выполните код до передачи запроса следующему обработчику в конвейере:
public class ValidateHeaderHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
if (!request.Headers.Contains("X-API-KEY"))
{
return new HttpResponseMessage(HttpStatusCode.BadRequest)
{
Content = new StringContent(
"You must supply an API key header called X-API-KEY")
};
}
return await base.SendAsync(request, cancellationToken);
}
}
Предыдущий код проверяет, находится ли заголовок X-API-KEY в запросе. Если X-API-KEY отсутствует, возвращается BadRequest.
В конфигурацию для HttpClient с Microsoft.Extensions.DependencyInjection.HttpClientBuilderExtensions.AddHttpMessageHandler можно добавить несколько обработчиков:
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<ValidateHeaderHandler>();
services.AddHttpClient("externalservice", c =>
{
// Assume this is an "external" service which requires an API KEY
c.BaseAddress = new Uri("https://localhost:5001/");
})
.AddHttpMessageHandler<ValidateHeaderHandler>();
// Remaining code deleted for brevity.
В приведенном выше коде ValidateHeaderHandler зарегистрирован с DI. После регистрации можно вызвать AddHttpMessageHandler, передав тип обработчика.
Можно зарегистрировать несколько обработчиков в порядке, в котором они должны выполняться. Каждый обработчик оборачивает следующий обработчик, и в конце последний HttpClientHandler выполняет запрос.
services.AddTransient<SecureRequestHandler>();
services.AddTransient<RequestDataHandler>();
services.AddHttpClient("clientwithhandlers")
// This handler is on the outside and called first during the
// request, last during the response.
.AddHttpMessageHandler<SecureRequestHandler>()
// This handler is on the inside, closest to the request being
// sent.
.AddHttpMessageHandler<RequestDataHandler>();
Использование dependency injection в промежуточном программном обеспечении для исходящих запросов
Когда IHttpClientFactory создает новый делегирующий обработчик, он использует механизм внедрения зависимостей для выполнения параметров конструктора обработчика.
IHttpClientFactory создает отдельную область внедрения зависимостей для каждого обработчика, что может стать причиной аномального поведения, когда обработчик использует службу с назначенной областью.
Например, рассмотрим следующий интерфейс и его реализацию, которая представляет задачу в виде операции с идентификатором OperationId:
public interface IOperationScoped
{
string OperationId { get; }
}
public class OperationScoped : IOperationScoped
{
public string OperationId { get; } = Guid.NewGuid().ToString()[^4..];
}
Как можно понять из названия, IOperationScoped регистрируется в DI с ограниченным временем жизни (scoped lifetime):
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<TodoContext>(options =>
options.UseInMemoryDatabase("TodoItems"));
services.AddHttpContextAccessor();
services.AddHttpClient<TodoClient>((sp, httpClient) =>
{
var httpRequest = sp.GetRequiredService<IHttpContextAccessor>().HttpContext.Request;
// For sample purposes, assume TodoClient is used in the context of an incoming request.
httpClient.BaseAddress = new Uri(UriHelper.BuildAbsolute(httpRequest.Scheme,
httpRequest.Host, httpRequest.PathBase));
httpClient.Timeout = TimeSpan.FromSeconds(5);
});
services.AddScoped<IOperationScoped, OperationScoped>();
services.AddTransient<OperationHandler>();
services.AddTransient<OperationResponseHandler>();
services.AddHttpClient("Operation")
.AddHttpMessageHandler<OperationHandler>()
.AddHttpMessageHandler<OperationResponseHandler>()
.SetHandlerLifetime(TimeSpan.FromSeconds(5));
services.AddControllers();
services.AddRazorPages();
}
Следующий делегирующий обработчик принимает и использует IOperationScoped для задания заголовка X-OPERATION-ID для исходящего запроса:
public class OperationHandler : DelegatingHandler
{
private readonly IOperationScoped _operationService;
public OperationHandler(IOperationScoped operationScoped)
{
_operationService = operationScoped;
}
protected async override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
request.Headers.Add("X-OPERATION-ID", _operationService.OperationId);
return await base.SendAsync(request, cancellationToken);
}
}
В HttpRequestsSample загружаемом разделе перейдите к /Operation и обновите страницу. Значение области запроса изменяется с каждым запросом, но значение области обработчика изменяется только каждые 5 секунд.
Обработчики могут зависеть от служб из любой области. Службы, которые зависят от обработчиков, удаляются при удалении обработчика.
Используйте один из следующих методов для предоставления общего доступа к состоянию отдельных запросов с помощью обработчиков сообщений:
- Передайте данные в обработчик с помощью HttpRequestMessage.Options.
- Используйте IHttpContextAccessor для доступа к текущему запросу.
- Создайте пользовательский объект хранилища AsyncLocal<T> для передачи данных.
Использование обработчиков на основе Polly
IHttpClientFactory интегрируется с библиотекой сторонних разработчиков Polly. Polly — это комплексная библиотека устойчивости и временной обработки ошибок для .NET. Она позволяет разработчикам выражать политики, такие как политика повтора, автоматический выключатель, время ожидания, изоляция отсеков и резервный механизм, в понятной и потокобезопасной манере.
Для использования политик Polly с настроенными экземплярами HttpClient предоставляются методы расширения. Расширения Polly поддерживают добавление клиентам обработчиков на основе Polly. Для Polly требуется пакет NuGet Microsoft.Extensions.Http.Polly.
Обработка временных сбоев
Чаще всего ошибки происходят, когда внешние вызовы HTTP являются временными.
AddTransientHttpErrorPolicy позволяет определить политику для обработки временных ошибок. Политики, настроенные с помощью AddTransientHttpErrorPolicy, обрабатывают следующие ответы:
- HttpRequestException
- HTTP 5xx
- HTTP 408
AddTransientHttpErrorPolicy предоставляет доступ к объекту PolicyBuilder, настроенному для обработки ошибок, представляющих возможный временный сбой:
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpClient<UnreliableEndpointCallerService>()
.AddTransientHttpErrorPolicy(p =>
p.WaitAndRetryAsync(3, _ => TimeSpan.FromMilliseconds(600)));
// Remaining code deleted for brevity.
В приведенном выше коде определена политика WaitAndRetryAsync. Неудачные запросы повторяются до трех раз с задержкой 600 мс между попытками.
Динамический выбор политик
Предоставляются методы расширения для добавления обработчиков на основе Polly, например AddPolicyHandler. Следующая перегрузка AddPolicyHandler проверяет запрос для определения применимой политики:
var timeout = Policy.TimeoutAsync<HttpResponseMessage>(
TimeSpan.FromSeconds(10));
var longTimeout = Policy.TimeoutAsync<HttpResponseMessage>(
TimeSpan.FromSeconds(30));
services.AddHttpClient("conditionalpolicy")
// Run some code to select a policy based on the request
.AddPolicyHandler(request =>
request.Method == HttpMethod.Get ? timeout : longTimeout);
Если в приведенном выше коде исходящий запрос является запросом HTTP GET, применяется время ожидания 10 секунд. Для остальных методов HTTP время ожидания — 30 секунд.
Добавьте несколько обработчиков Polly
Общепринятой практикой является вложение политик Polly:
services.AddHttpClient("multiplepolicies")
.AddTransientHttpErrorPolicy(p => p.RetryAsync(3))
.AddTransientHttpErrorPolicy(
p => p.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));
В предыдущем примере:
- Добавляются два обработчика.
- Первый обработчик использует AddTransientHttpErrorPolicy, чтобы добавить политику повтора. Неудачные запросы выполняются повторно до трех раз.
- Второй вызов
AddTransientHttpErrorPolicyдобавляет политику автоматического выключателя. Дополнительные внешние запросы блокируются в течение 30 секунд в случае пяти неудачных попыток подряд. Политики размыкателя цепи отслеживают состояние. Все вызовы через этот клиент имеют одинаковое состояние цепи.
Добавление политик из реестра Polly
Подход к управлению регулярно используемыми политиками заключается в их однократном определении и регистрации с помощью PolicyRegistry.
В приведенном ниже коде выполняется следующее:
- Добавляются политики "regular" и "long".
- AddPolicyHandlerFromRegistry добавляет политики regular и long из реестра.
public void ConfigureServices(IServiceCollection services)
{
var timeout = Policy.TimeoutAsync<HttpResponseMessage>(
TimeSpan.FromSeconds(10));
var longTimeout = Policy.TimeoutAsync<HttpResponseMessage>(
TimeSpan.FromSeconds(30));
var registry = services.AddPolicyRegistry();
registry.Add("regular", timeout);
registry.Add("long", longTimeout);
services.AddHttpClient("regularTimeoutHandler")
.AddPolicyHandlerFromRegistry("regular");
services.AddHttpClient("longTimeoutHandler")
.AddPolicyHandlerFromRegistry("long");
// Remaining code deleted for brevity.
Дополнительные сведения о IHttpClientFactory и интеграции Polly см. на вики-сайте Polly.
Управление HttpClient и временем существования
При каждом вызове HttpClient на CreateClient возвращается новый экземпляр IHttpClientFactory. Создается 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.
До появления HttpClient было принято поддерживать один экземпляр IHttpClientFactory в активном состоянии в течение длительного времени. После перехода на 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,
};
});
Logging
Клиенты, созданные через IHttpClientFactory, записывают сообщения журнала для всех запросов. Установите соответствующий уровень информации в конфигурации ведения журнала, чтобы просматривать сообщения журнала по умолчанию. Другие виды журналирования, такие как логирование заголовков запросов, выполняется только на уровне трассировки.
Категория журнала для каждого клиента включает в себя имя клиента. Клиент с именем MyNamedClient, например, фиксирует сообщения с категорией "System.Net.Http.HttpClient.MyNamedClient.ЛогическийОбработчик". Сообщения с суффиксом ЛогическийОбработчик происходят за пределами конвейера обработчика запросов. Во время запроса сообщения записываются в журнал до обработки запроса другими обработчиками в конвейере. Во время ответа сообщения записываются в журнал после получения ответа другими обработчиками в конвейере.
Кроме того, журнал ведется в конвейере обработчиков запросов. В примере MyNamedClient эти сообщения регистрируются с категорией журнала System.Net.Http.HttpClient. MyNamedClient. ClientHandler". Для запроса это происходит после выполнения всех других обработчиков и непосредственно перед отправкой запроса. Во время ответа в журнале записывается состояние ответа перед его передачей обратно по конвейеру обработчиков.
Включив ведение журнала в конвейере и за его пределами, можно выполнять проверку изменений, внесенных другими обработчиками конвейера. Ведение журнала может включать изменения в заголовки запросов или код состояния ответа.
Включение имени клиента в категорию журнала позволяет фильтровать журналы по именованным клиентам.
Настройка HttpMessageHandler
Может потребоваться управлять конфигурацией внутреннего HttpMessageHandler, который используется клиентом.
При добавлении именованного или типизированного клиента возвращается IHttpClientBuilder. Для определения делегата можно использовать метод расширения ConfigurePrimaryHttpMessageHandler. Делегат используется для создания и настройки основного компонента HttpMessageHandler, который используется этим клиентом:
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpClient("configured-inner-handler")
.ConfigurePrimaryHttpMessageHandler(() =>
{
return new HttpClientHandler()
{
AllowAutoRedirect = false,
UseDefaultCredentials = true
};
});
// Remaining code deleted for brevity.
Использование IHttpClientFactory в консольном приложении
В консольном приложении добавьте в проект следующие ссылки на пакеты:
В следующем примере :
- IHttpClientFactory зарегистрирован в контейнере службы Generic Host.
-
MyServiceсоздает экземпляр фабрики клиента из службы, который используется для созданияHttpClient.HttpClientиспользуется для получения веб-страницы. -
Mainсоздает область для выполнения методаGetPageслужбы и вывода первых 500 символов содержимого веб-страницы на консоль.
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
class Program
{
static async Task<int> Main(string[] args)
{
var builder = new HostBuilder()
.ConfigureServices((hostContext, services) =>
{
services.AddHttpClient();
services.AddTransient<IMyService, MyService>();
}).UseConsoleLifetime();
var host = builder.Build();
try
{
var myService = host.Services.GetRequiredService<IMyService>();
var pageContent = await myService.GetPage();
Console.WriteLine(pageContent.Substring(0, 500));
}
catch (Exception ex)
{
var logger = host.Services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred.");
}
return 0;
}
public interface IMyService
{
Task<string> GetPage();
}
public class MyService : IMyService
{
private readonly IHttpClientFactory _clientFactory;
public MyService(IHttpClientFactory clientFactory)
{
_clientFactory = clientFactory;
}
public async Task<string> GetPage()
{
// Content from BBC One: Dr. Who website (©BBC)
var request = new HttpRequestMessage(HttpMethod.Get,
"https://www.bbc.co.uk/programmes/b006q2x0");
var client = _clientFactory.CreateClient();
var response = await client.SendAsync(request);
if (response.IsSuccessStatusCode)
{
return await response.Content.ReadAsStringAsync();
}
else
{
return $"StatusCode: {response.StatusCode}";
}
}
}
}
Промежуточное ПО для распространения заголовков
Распространение заголовков — это ASP.NET Core ПО промежуточного слоя для распространения заголовков HTTP из входящего запроса на исходящие HTTP-запросы клиента. Чтобы использовать распространение заголовков, сделайте следующее:
Сослаться на пакет Microsoft.AspNetCore.HeaderPropagation.
Настройте посредническое ПО и
HttpClientвStartup:public void ConfigureServices(IServiceCollection services) { services.AddControllers(); services.AddHttpClient("MyForwardingClient").AddHeaderPropagation(); services.AddHeaderPropagation(options => { options.Headers.Add("X-TraceId"); }); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseHttpsRedirection(); app.UseHeaderPropagation(); app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); }Клиент включает настроенные заголовки в исходящие запросы:
var client = clientFactory.CreateClient("MyForwardingClient"); var response = client.GetAsync(...);
Авторы Кирк Ларкин (Kirk Larkin), Стив Гордон (Steve Gordon), Гленн Кондрон (Glenn Condron) и Райан Новак (Ryan Nowak).
IHttpClientFactory можно зарегистрировать и использовать для настройки и создания экземпляров HttpClient в приложении.
IHttpClientFactory предоставляет следующие преимущества:
- Обеспечивает централизованное расположение для именования и настройки логических экземпляров
HttpClient. Например, клиент с именем github можно зарегистрировать и настроить для доступа к GitHub. Можно зарегистрировать клиент по умолчанию для общего доступа. - Кодификация концепции исходящего промежуточного ПО через делегирование обработчиков в
HttpClient. Предоставляет расширения для ПО промежуточного слоя на основе Polly для использования делегирующих обработчиков вHttpClient. - Управляет пулом и временем существования базовых экземпляров
HttpClientMessageHandler. Автоматическое управление позволяет избежать обычных проблем со службой доменных имен (DNS), которые возникают при управлении временем существованияHttpClientвручную. - Добавляет возможность настройки ведения журнала (через
ILogger) для всех запросов, отправленных через клиентов, созданных фабрикой.
Просмотреть или скачать пример кода (описание скачивания).
Пример кода в этой версии раздела использует System.Text.Json для десериализации содержимого JSON, возвращаемого в ответах HTTP. Используйте селектор версий для выбора версии 2.x этой статьи для примеров, использующих Json.NET и ReadAsAsync<T>.
Шаблоны потребления
Существует несколько способов использования IHttpClientFactory в приложении:
Оптимальный подход зависит от требований приложения.
Базовое использование
IHttpClientFactory можно зарегистрировать, вызвав AddHttpClient:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpClient();
// Remaining code deleted for brevity.
IHttpClientFactory можно запросить с помощью внедрения зависимостей (DI). Следующий код использует IHttpClientFactory для создания экземпляра HttpClient:
public class BasicUsageModel : PageModel
{
private readonly IHttpClientFactory _clientFactory;
public IEnumerable<GitHubBranch> Branches { get; private set; }
public bool GetBranchesError { get; private set; }
public BasicUsageModel(IHttpClientFactory clientFactory)
{
_clientFactory = clientFactory;
}
public async Task OnGet()
{
var request = new HttpRequestMessage(HttpMethod.Get,
"https://api.github.com/repos/dotnet/AspNetCore.Docs/branches");
request.Headers.Add("Accept", "application/vnd.github.v3+json");
request.Headers.Add("User-Agent", "HttpClientFactory-Sample");
var client = _clientFactory.CreateClient();
var response = await client.SendAsync(request);
if (response.IsSuccessStatusCode)
{
using var responseStream = await response.Content.ReadAsStreamAsync();
Branches = await JsonSerializer.DeserializeAsync
<IEnumerable<GitHubBranch>>(responseStream);
}
else
{
GetBranchesError = true;
Branches = Array.Empty<GitHubBranch>();
}
}
}
Подобное использование IHttpClientFactory — это хороший способ рефакторинга имеющегося приложения. Он не влияет на то, как HttpClient используется. Там, где в существующем приложении создаются экземпляры HttpClient, замените их вызовами CreateClient.
Именованные клиенты
Именованные клиенты являются хорошим выбором в следующих случаях:
- Приложение требует много отдельных использований
HttpClient. - Многие
HttpClientимеют другую конфигурацию.
Конфигурацию для именованного HttpClient можно указать во время регистрации в Startup.ConfigureServices:
services.AddHttpClient("github", c =>
{
c.BaseAddress = new Uri("https://api.github.com/");
// Github API versioning
c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
// Github requires a user-agent
c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});
В предыдущем коде клиент настраивается следующим образом:
- Базовый адрес
https://api.github.com/. - Два заголовка, необходимые для работы с API GitHub.
CreateClient
При каждом вызове CreateClient:
- Создается новый экземпляр
HttpClient. - вызывается действие настройки.
Чтобы создать именованный клиент, передайте его имя в CreateClient:
public class NamedClientModel : PageModel
{
private readonly IHttpClientFactory _clientFactory;
public IEnumerable<GitHubPullRequest> PullRequests { get; private set; }
public bool GetPullRequestsError { get; private set; }
public bool HasPullRequests => PullRequests.Any();
public NamedClientModel(IHttpClientFactory clientFactory)
{
_clientFactory = clientFactory;
}
public async Task OnGet()
{
var request = new HttpRequestMessage(HttpMethod.Get,
"repos/dotnet/AspNetCore.Docs/pulls");
var client = _clientFactory.CreateClient("github");
var response = await client.SendAsync(request);
if (response.IsSuccessStatusCode)
{
using var responseStream = await response.Content.ReadAsStreamAsync();
PullRequests = await JsonSerializer.DeserializeAsync
<IEnumerable<GitHubPullRequest>>(responseStream);
}
else
{
GetPullRequestsError = true;
PullRequests = Array.Empty<GitHubPullRequest>();
}
}
}
В приведенном выше коде в запросе не требуется указывать имя узла. Достаточно передать только путь, так как используется базовый адрес, заданный для клиента.
Типизированные клиенты
Типизированные клиенты:
- предоставляют те же возможности, что и именованные клиенты, без необходимости использовать строки в качестве ключей.
- Это обеспечивает поддержку IntelliSense и компилятора при работе с клиентскими приложениями.
- Предоставьте единое расположение для настройки и взаимодействия с определённым
HttpClient. Например, можно использовать один типизированный клиент:- для одной серверной конечной точки;
- для инкапсуляции всей логики, связанной с конечной точкой.
- Поддерживаются работа с внедрением зависимостей и возможность вставки в нужное место в приложении.
Типизированный клиент принимает параметр HttpClient в конструкторе:
public class GitHubService
{
public HttpClient Client { get; }
public GitHubService(HttpClient client)
{
client.BaseAddress = new Uri("https://api.github.com/");
// GitHub API versioning
client.DefaultRequestHeaders.Add("Accept",
"application/vnd.github.v3+json");
// GitHub requires a user-agent
client.DefaultRequestHeaders.Add("User-Agent",
"HttpClientFactory-Sample");
Client = client;
}
public async Task<IEnumerable<GitHubIssue>> GetAspNetDocsIssues()
{
var response = await Client.GetAsync(
"/repos/dotnet/AspNetCore.Docs/issues?state=open&sort=created&direction=desc");
response.EnsureSuccessStatusCode();
using var responseStream = await response.Content.ReadAsStreamAsync();
return await JsonSerializer.DeserializeAsync
<IEnumerable<GitHubIssue>>(responseStream);
}
}
В предыдущем коде:
- Конфигурация перемещается в типизированный клиент.
- Объект
HttpClientпредоставляется какpublicсвойство.
Можно создать связанные с API методы, которые предоставляют функциональные возможности HttpClient. Например, метод GetAspNetDocsIssues инкапсулирует код для получения открытых вопросов.
Следующий код вызывает AddHttpClient в Startup.ConfigureServices для регистрации класса типизированного клиента:
services.AddHttpClient<GitHubService>();
Типизированный клиент регистрируется в системе внедрения зависимостей (Dependency Injection) как объект с кратковременным сроком службы. В приведенном выше коде AddHttpClient регистрирует GitHubService как временную службу. Эта регистрация использует фабричный метод для следующих задач:
- Создайте экземпляр
HttpClient. - Создайте экземпляр
GitHubService, передав в его конструктор экземплярHttpClient.
Типизированный клиент можно внедрить и использовать напрямую:
public class TypedClientModel : PageModel
{
private readonly GitHubService _gitHubService;
public IEnumerable<GitHubIssue> LatestIssues { get; private set; }
public bool HasIssue => LatestIssues.Any();
public bool GetIssuesError { get; private set; }
public TypedClientModel(GitHubService gitHubService)
{
_gitHubService = gitHubService;
}
public async Task OnGet()
{
try
{
LatestIssues = await _gitHubService.GetAspNetDocsIssues();
}
catch(HttpRequestException)
{
GetIssuesError = true;
LatestIssues = Array.Empty<GitHubIssue>();
}
}
}
Конфигурацию для типизированного клиента можно указать во время регистрации в Startup.ConfigureServices, а не в конструкторе типизированного клиента:
services.AddHttpClient<RepoService>(c =>
{
c.BaseAddress = new Uri("https://api.github.com/");
c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});
HttpClient может быть инкапсулирован в типизированном клиенте. Вместо того, чтобы предоставлять его как свойство, определите метод, который вызывает HttpClient экземпляр внутри:
public class RepoService
{
// _httpClient isn't exposed publicly
private readonly HttpClient _httpClient;
public RepoService(HttpClient client)
{
_httpClient = client;
}
public async Task<IEnumerable<string>> GetRepos()
{
var response = await _httpClient.GetAsync("aspnet/repos");
response.EnsureSuccessStatusCode();
using var responseStream = await response.Content.ReadAsStreamAsync();
return await JsonSerializer.DeserializeAsync
<IEnumerable<string>>(responseStream);
}
}
В приведенном выше коде HttpClient хранится в закрытом поле. Доступ к HttpClient осуществляется с помощью общедоступного метода GetRepos.
Созданные клиенты
IHttpClientFactory можно использовать в сочетании с библиотеками сторонних разработчиков, например Refit. Refit — это библиотека REST для .NET. Она преобразует REST API в динамические интерфейсы. Реализация интерфейса формируется динамически с помощью RestService с использованием HttpClient для совершения внешних вызовов HTTP.
Для представления внешнего API и его ответа определяются интерфейс и ответ:
public interface IHelloClient
{
[Get("/helloworld")]
Task<Reply> GetMessageAsync();
}
public class Reply
{
public string Message { get; set; }
}
Можно добавить типизированный клиент, используя Refit для создания реализации:
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpClient("hello", c =>
{
c.BaseAddress = new Uri("http://localhost:5000");
})
.AddTypedClient(c => Refit.RestService.For<IHelloClient>(c));
services.AddControllers();
}
При необходимости можно использовать определенный интерфейс с реализацией, предоставленной внедрением зависимостей и Refit:
[ApiController]
public class ValuesController : ControllerBase
{
private readonly IHelloClient _client;
public ValuesController(IHelloClient client)
{
_client = client;
}
[HttpGet("/")]
public async Task<ActionResult<Reply>> Index()
{
return await _client.GetMessageAsync();
}
}
Выполнение запросов POST, PUT и DELETE
В предыдущих примерах все HTTP-запросы используют HTTP-команду GET.
HttpClient также поддерживает другие HTTP-команды, в том числе:
- POST
- PUT
- DELETE
- PATCH
Полный список поддерживаемых HTTP-команд см. в статье HttpMethod.
В следующем примере показано, как выполнить HTTP-запрос POST:
public async Task CreateItemAsync(TodoItem todoItem)
{
var todoItemJson = new StringContent(
JsonSerializer.Serialize(todoItem, _jsonSerializerOptions),
Encoding.UTF8,
"application/json");
using var httpResponse =
await _httpClient.PostAsync("/api/TodoItems", todoItemJson);
httpResponse.EnsureSuccessStatusCode();
}
В приведенном выше коде метод CreateItemAsync выполняет следующие задачи:
- сериализует параметр
TodoItemв JSON с помощьюSystem.Text.Json. Для настройки процесса сериализации используется экземпляр JsonSerializerOptions. - Создает экземпляр StringContent для упаковки сериализованного JSON для отправки в теле HTTP-запроса.
- вызывает метод PostAsync для отправки содержимого JSON по указанному URL-адресу. Это относительный URL-адрес, который добавляется в свойство HttpClient.BaseAddress.
- Вызывает EnsureSuccessStatusCode для выдачи исключения, если код состояния ответа не указывает на успешное выполнение.
HttpClient также поддерживает другие типы содержимого. Например, MultipartContent и StreamContent. Полный список поддерживаемого содержимого см. в статье HttpContent.
Ниже приводится пример HTTP-запроса PUT.
public async Task SaveItemAsync(TodoItem todoItem)
{
var todoItemJson = new StringContent(
JsonSerializer.Serialize(todoItem),
Encoding.UTF8,
"application/json");
using var httpResponse =
await _httpClient.PutAsync($"/api/TodoItems/{todoItem.Id}", todoItemJson);
httpResponse.EnsureSuccessStatusCode();
}
Приведенный выше код похож на код в примере с POST. Метод SaveItemAsync вызывает PutAsync вместо PostAsync.
Ниже приводится пример HTTP-запроса DELETE.
public async Task DeleteItemAsync(long itemId)
{
using var httpResponse =
await _httpClient.DeleteAsync($"/api/TodoItems/{itemId}");
httpResponse.EnsureSuccessStatusCode();
}
В приведенном выше коде метод DeleteItemAsync вызывает DeleteAsync. Поскольку HTTP-запросы DELETE обычно не содержат текст, метод DeleteAsync не предоставляет перегрузку, которая принимает экземпляр HttpContent.
Дополнительные сведения об использовании различных HTTP-команд с HttpClient см. в статье HttpClient.
Промежуточное программное обеспечение для обработки исходящих запросов
В HttpClient существует концепция делегирования обработчиков, которые можно связать друг с другом для исходящих HTTP-запросов.
IHttpClientFactory:
- Упрощает определение обработчиков для применения к каждому именованному клиенту.
- Поддерживает регистрацию и объединение нескольких обработчиков в цепочке для построения конвейера промежуточного ПО для исходящих запросов. Каждый из этих обработчиков может выполнять работу до и после исходящего запроса. Этот шаблон:
- Аналогично конвейеру входящего ПО промежуточного слоя в ASP.NET Core.
- Предоставляет механизм для управления перекрестными проблемами в отношении HTTP-запросов, таких как:
- caching
- error handling
- serialization
- logging
Чтобы создать делегированный обработчик, сделайте следующее:
- Создайте объект, производный от DelegatingHandler.
- Переопределите SendAsync. Выполните код до передачи запроса следующему обработчику в конвейере:
public class ValidateHeaderHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
if (!request.Headers.Contains("X-API-KEY"))
{
return new HttpResponseMessage(HttpStatusCode.BadRequest)
{
Content = new StringContent(
"You must supply an API key header called X-API-KEY")
};
}
return await base.SendAsync(request, cancellationToken);
}
}
Предыдущий код проверяет, находится ли заголовок X-API-KEY в запросе. Если X-API-KEY отсутствует, возвращается BadRequest.
В конфигурацию для HttpClient с Microsoft.Extensions.DependencyInjection.HttpClientBuilderExtensions.AddHttpMessageHandler можно добавить несколько обработчиков:
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<ValidateHeaderHandler>();
services.AddHttpClient("externalservice", c =>
{
// Assume this is an "external" service which requires an API KEY
c.BaseAddress = new Uri("https://localhost:5001/");
})
.AddHttpMessageHandler<ValidateHeaderHandler>();
// Remaining code deleted for brevity.
В приведенном выше коде ValidateHeaderHandler зарегистрирован с DI. После регистрации можно вызвать AddHttpMessageHandler, передав тип обработчика.
Можно зарегистрировать несколько обработчиков в порядке, в котором они должны выполняться. Каждый обработчик оборачивает следующий обработчик, и в конце последний HttpClientHandler выполняет запрос.
services.AddTransient<SecureRequestHandler>();
services.AddTransient<RequestDataHandler>();
services.AddHttpClient("clientwithhandlers")
// This handler is on the outside and called first during the
// request, last during the response.
.AddHttpMessageHandler<SecureRequestHandler>()
// This handler is on the inside, closest to the request being
// sent.
.AddHttpMessageHandler<RequestDataHandler>();
Использование dependency injection в промежуточном программном обеспечении для исходящих запросов
Когда IHttpClientFactory создает новый делегирующий обработчик, он использует механизм внедрения зависимостей для выполнения параметров конструктора обработчика.
IHttpClientFactory создает отдельную область внедрения зависимостей для каждого обработчика, что может стать причиной аномального поведения, когда обработчик использует службу с назначенной областью.
Например, рассмотрим следующий интерфейс и его реализацию, которая представляет задачу в виде операции с идентификатором OperationId:
public interface IOperationScoped
{
string OperationId { get; }
}
public class OperationScoped : IOperationScoped
{
public string OperationId { get; } = Guid.NewGuid().ToString()[^4..];
}
Как можно понять из названия, IOperationScoped регистрируется в DI с ограниченным временем жизни (scoped lifetime):
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<TodoContext>(options =>
options.UseInMemoryDatabase("TodoItems"));
services.AddHttpContextAccessor();
services.AddHttpClient<TodoClient>((sp, httpClient) =>
{
var httpRequest = sp.GetRequiredService<IHttpContextAccessor>().HttpContext.Request;
// For sample purposes, assume TodoClient is used in the context of an incoming request.
httpClient.BaseAddress = new Uri(UriHelper.BuildAbsolute(httpRequest.Scheme,
httpRequest.Host, httpRequest.PathBase));
httpClient.Timeout = TimeSpan.FromSeconds(5);
});
services.AddScoped<IOperationScoped, OperationScoped>();
services.AddTransient<OperationHandler>();
services.AddTransient<OperationResponseHandler>();
services.AddHttpClient("Operation")
.AddHttpMessageHandler<OperationHandler>()
.AddHttpMessageHandler<OperationResponseHandler>()
.SetHandlerLifetime(TimeSpan.FromSeconds(5));
services.AddControllers();
services.AddRazorPages();
}
Следующий делегирующий обработчик принимает и использует IOperationScoped для задания заголовка X-OPERATION-ID для исходящего запроса:
public class OperationHandler : DelegatingHandler
{
private readonly IOperationScoped _operationService;
public OperationHandler(IOperationScoped operationScoped)
{
_operationService = operationScoped;
}
protected async override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
request.Headers.Add("X-OPERATION-ID", _operationService.OperationId);
return await base.SendAsync(request, cancellationToken);
}
}
В HttpRequestsSample загружаемом разделе перейдите к /Operation и обновите страницу. Значение области запроса изменяется с каждым запросом, но значение области обработчика изменяется только каждые 5 секунд.
Обработчики могут зависеть от служб из любой области. Службы, которые зависят от обработчиков, удаляются при удалении обработчика.
Используйте один из следующих методов для предоставления общего доступа к состоянию отдельных запросов с помощью обработчиков сообщений:
- Передайте данные в обработчик с помощью HttpRequestMessage.Properties.
- Используйте IHttpContextAccessor для доступа к текущему запросу.
- Создайте пользовательский объект хранилища AsyncLocal<T> для передачи данных.
Использование обработчиков на основе Polly
IHttpClientFactory интегрируется с библиотекой сторонних разработчиков Polly. Polly — это комплексная библиотека устойчивости и временной обработки ошибок для .NET. Она позволяет разработчикам выражать политики, такие как политика повтора, автоматический выключатель, время ожидания, изоляция отсеков и резервный механизм, в понятной и потокобезопасной манере.
Для использования политик Polly с настроенными экземплярами HttpClient предоставляются методы расширения. Расширения Polly поддерживают добавление клиентам обработчиков на основе Polly. Для Polly требуется пакет NuGet Microsoft.Extensions.Http.Polly.
Обработка временных сбоев
Чаще всего ошибки происходят, когда внешние вызовы HTTP являются временными.
AddTransientHttpErrorPolicy позволяет определить политику для обработки временных ошибок. Политики, настроенные с помощью AddTransientHttpErrorPolicy, обрабатывают следующие ответы:
- HttpRequestException
- HTTP 5xx
- HTTP 408
AddTransientHttpErrorPolicy предоставляет доступ к объекту PolicyBuilder, настроенному для обработки ошибок, представляющих возможный временный сбой:
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpClient<UnreliableEndpointCallerService>()
.AddTransientHttpErrorPolicy(p =>
p.WaitAndRetryAsync(3, _ => TimeSpan.FromMilliseconds(600)));
// Remaining code deleted for brevity.
В приведенном выше коде определена политика WaitAndRetryAsync. Неудачные запросы повторяются до трех раз с задержкой 600 мс между попытками.
Динамический выбор политик
Предоставляются методы расширения для добавления обработчиков на основе Polly, например AddPolicyHandler. Следующая перегрузка AddPolicyHandler проверяет запрос для определения применимой политики:
var timeout = Policy.TimeoutAsync<HttpResponseMessage>(
TimeSpan.FromSeconds(10));
var longTimeout = Policy.TimeoutAsync<HttpResponseMessage>(
TimeSpan.FromSeconds(30));
services.AddHttpClient("conditionalpolicy")
// Run some code to select a policy based on the request
.AddPolicyHandler(request =>
request.Method == HttpMethod.Get ? timeout : longTimeout);
Если в приведенном выше коде исходящий запрос является запросом HTTP GET, применяется время ожидания 10 секунд. Для остальных методов HTTP время ожидания — 30 секунд.
Добавьте несколько обработчиков Polly
Общепринятой практикой является вложение политик Polly:
services.AddHttpClient("multiplepolicies")
.AddTransientHttpErrorPolicy(p => p.RetryAsync(3))
.AddTransientHttpErrorPolicy(
p => p.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));
В предыдущем примере:
- Добавляются два обработчика.
- Первый обработчик использует AddTransientHttpErrorPolicy, чтобы добавить политику повтора. Неудачные запросы выполняются повторно до трех раз.
- Второй вызов
AddTransientHttpErrorPolicyдобавляет политику автоматического выключателя. Дополнительные внешние запросы блокируются в течение 30 секунд в случае пяти неудачных попыток подряд. Политики размыкателя цепи отслеживают состояние. Все вызовы через этот клиент имеют одинаковое состояние цепи.
Добавление политик из реестра Polly
Подход к управлению регулярно используемыми политиками заключается в их однократном определении и регистрации с помощью PolicyRegistry.
В приведенном ниже коде выполняется следующее:
- Добавляются политики "regular" и "long".
- AddPolicyHandlerFromRegistry добавляет политики regular и long из реестра.
public void ConfigureServices(IServiceCollection services)
{
var timeout = Policy.TimeoutAsync<HttpResponseMessage>(
TimeSpan.FromSeconds(10));
var longTimeout = Policy.TimeoutAsync<HttpResponseMessage>(
TimeSpan.FromSeconds(30));
var registry = services.AddPolicyRegistry();
registry.Add("regular", timeout);
registry.Add("long", longTimeout);
services.AddHttpClient("regularTimeoutHandler")
.AddPolicyHandlerFromRegistry("regular");
services.AddHttpClient("longTimeoutHandler")
.AddPolicyHandlerFromRegistry("long");
// Remaining code deleted for brevity.
Дополнительные сведения о IHttpClientFactory и интеграции Polly см. на вики-сайте Polly.
Управление HttpClient и временем существования
При каждом вызове HttpClient на CreateClient возвращается новый экземпляр IHttpClientFactory. Создается 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.
До появления HttpClient было принято поддерживать один экземпляр IHttpClientFactory в активном состоянии в течение длительного времени. После перехода на 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,
};
});
Logging
Клиенты, созданные через IHttpClientFactory, записывают сообщения журнала для всех запросов. Установите соответствующий уровень информации в конфигурации ведения журнала, чтобы просматривать сообщения журнала по умолчанию. Другие виды логирования, такие как логирование заголовков запросов, включаются только на уровне трассировки.
Категория журнала для каждого клиента включает в себя имя клиента. Клиент с именем MyNamedClient, например, регистрирует сообщения с категорией "System.Net.Http.HttpClient.MyNamedClient.LogicalHandler". Сообщения с суффиксом LogicalHandler происходят за пределами конвейера обработчика запросов. Сообщения регистрируются по запросу перед обработкой другими системами в процессе конвейера. В процессе отправки ответа сообщения логируются после обработки ответа другими обработчиками конвейера.
Кроме того, журнал ведется в конвейере обработчиков запросов. В примере MyNamedClient эти сообщения регистрируются с категорией журнала System.Net.Http.HttpClient. MyNamedClient. ClientHandler". Для запроса это происходит после выполнения всех других обработчиков и непосредственно перед отправкой запроса. Во время ответа в журнале записывается состояние ответа перед его передачей обратно по конвейеру обработчиков.
Включив ведение журнала в конвейере и за его пределами, можно выполнять проверку изменений, внесенных другими обработчиками конвейера. Ведение журнала может включать изменения в заголовки запросов или код состояния ответа.
Включение имени клиента в категорию журнала позволяет фильтровать журналы по именованным клиентам.
Настройка HttpMessageHandler
Может потребоваться управлять конфигурацией внутреннего HttpMessageHandler, который используется клиентом.
При добавлении именованного или типизированного клиента возвращается IHttpClientBuilder. Для определения делегата можно использовать метод расширения ConfigurePrimaryHttpMessageHandler. Делегат используется для создания и настройки основного компонента HttpMessageHandler, который используется этим клиентом:
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpClient("configured-inner-handler")
.ConfigurePrimaryHttpMessageHandler(() =>
{
return new HttpClientHandler()
{
AllowAutoRedirect = false,
UseDefaultCredentials = true
};
});
// Remaining code deleted for brevity.
Использование IHttpClientFactory в консольном приложении
В консольном приложении добавьте в проект следующие ссылки на пакеты:
В следующем примере :
- IHttpClientFactory зарегистрирован в контейнере службы Generic Host.
-
MyServiceсоздает экземпляр фабрики клиента из службы, который используется для созданияHttpClient.HttpClientиспользуется для получения веб-страницы. -
Mainсоздает область для выполнения методаGetPageслужбы и вывода первых 500 символов содержимого веб-страницы на консоль.
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
class Program
{
static async Task<int> Main(string[] args)
{
var builder = new HostBuilder()
.ConfigureServices((hostContext, services) =>
{
services.AddHttpClient();
services.AddTransient<IMyService, MyService>();
}).UseConsoleLifetime();
var host = builder.Build();
try
{
var myService = host.Services.GetRequiredService<IMyService>();
var pageContent = await myService.GetPage();
Console.WriteLine(pageContent.Substring(0, 500));
}
catch (Exception ex)
{
var logger = host.Services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred.");
}
return 0;
}
public interface IMyService
{
Task<string> GetPage();
}
public class MyService : IMyService
{
private readonly IHttpClientFactory _clientFactory;
public MyService(IHttpClientFactory clientFactory)
{
_clientFactory = clientFactory;
}
public async Task<string> GetPage()
{
// Content from BBC One: Dr. Who website (©BBC)
var request = new HttpRequestMessage(HttpMethod.Get,
"https://www.bbc.co.uk/programmes/b006q2x0");
var client = _clientFactory.CreateClient();
var response = await client.SendAsync(request);
if (response.IsSuccessStatusCode)
{
return await response.Content.ReadAsStringAsync();
}
else
{
return $"StatusCode: {response.StatusCode}";
}
}
}
}
Промежуточное ПО для распространения заголовков
Распространение заголовков — это ASP.NET Core ПО промежуточного слоя для распространения заголовков HTTP из входящего запроса на исходящие HTTP-запросы клиента. Чтобы использовать распространение заголовков, сделайте следующее:
Сослаться на пакет Microsoft.AspNetCore.HeaderPropagation.
Настройте посредническое ПО и
HttpClientвStartup:public void ConfigureServices(IServiceCollection services) { services.AddControllers(); services.AddHttpClient("MyForwardingClient").AddHeaderPropagation(); services.AddHeaderPropagation(options => { options.Headers.Add("X-TraceId"); }); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseHttpsRedirection(); app.UseHeaderPropagation(); app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); }Клиент включает настроенные заголовки в исходящие запросы:
var client = clientFactory.CreateClient("MyForwardingClient"); var response = client.GetAsync(...);
Авторы: Гленн Кондрон (Glenn Condron), Райан Новак (Ryan Nowak) и Стив Гордон (Steve Gordon)
IHttpClientFactory можно зарегистрировать и использовать для настройки и создания экземпляров HttpClient в приложении. Так вы получите следующие преимущества:
- Обеспечивает централизованное расположение для именования и настройки логических экземпляров
HttpClient. Например, клиент github можно зарегистрировать и настроить для доступа к GitHub. Можно зарегистрировать клиент по умолчанию для других целей. - Кодифицирует концепцию исходящего промежуточного ПО через делегирование обработчиков в
HttpClientи предоставляет расширения для промежуточного ПО на основе Polly, чтобы использовать эту возможность. - Управляет пулом и временем существования базовых экземпляров
HttpClientMessageHandlerдля предотвращения типичных проблем с DNS, возникающих при ручном управлении временем существованияHttpClient. - Добавляет возможность настройки ведения журнала (через
ILogger) для всех запросов, отправленных через клиентов, созданных фабрикой.
Просмотреть или скачать образец кода (описание загрузки)
Prerequisites
Для проектов, предназначенных для платформы .NET Framework, требуется установка Microsoft. Extensions.Http пакет NuGet. Проекты, предназначенные для .NET Core и ссылающиеся на метапакет Microsoft.AspNetCore.App уже включают пакет Microsoft.Extensions.Http.
Шаблоны потребления
Существует несколько способов использования IHttpClientFactory в приложении:
Ни один из них не является строго лучшим по сравнению с другим. Оптимальный подход зависит от ограничений приложения.
Базовое использование
IHttpClientFactory можно зарегистрировать путем вызова метода расширения AddHttpClient в IServiceCollection внутри метода Startup.ConfigureServices.
services.AddHttpClient();
После регистрации, код может принимать IHttpClientFactory в любом месте, где можно внедрять службы с помощью внедрения зависимостей (DI).
IHttpClientFactory можно использовать для создания экземпляра HttpClient:
public class BasicUsageModel : PageModel
{
private readonly IHttpClientFactory _clientFactory;
public IEnumerable<GitHubBranch> Branches { get; private set; }
public bool GetBranchesError { get; private set; }
public BasicUsageModel(IHttpClientFactory clientFactory)
{
_clientFactory = clientFactory;
}
public async Task OnGet()
{
var request = new HttpRequestMessage(HttpMethod.Get,
"https://api.github.com/repos/dotnet/AspNetCore.Docs/branches");
request.Headers.Add("Accept", "application/vnd.github.v3+json");
request.Headers.Add("User-Agent", "HttpClientFactory-Sample");
var client = _clientFactory.CreateClient();
var response = await client.SendAsync(request);
if (response.IsSuccessStatusCode)
{
Branches = await response.Content
.ReadAsAsync<IEnumerable<GitHubBranch>>();
}
else
{
GetBranchesError = true;
Branches = Array.Empty<GitHubBranch>();
}
}
}
Подобное использование IHttpClientFactory — это отличный способ рефакторинга имеющегося приложения. Он не влияет на использование HttpClient. Там, где в данный момент создаются экземпляры HttpClient, замените их вызовом к CreateClient.
Именованные клиенты
Если приложению требуется множество различных вариантов использования HttpClient, каждый с уникальной конфигурацией, можно использовать именованные клиенты. Конфигурацию для именованного HttpClient можно указать во время регистрации в Startup.ConfigureServices.
services.AddHttpClient("github", c =>
{
c.BaseAddress = new Uri("https://api.github.com/");
// Github API versioning
c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
// Github requires a user-agent
c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});
В приведенном выше коде вызывается AddHttpClient, предоставляющий имя github. Этот клиент применяет определенную конфигурацию по умолчанию— а именно базовый адрес и два заголовка, необходимые для работы с API GitHub.
При каждом вызове CreateClient создается новый экземпляр HttpClient и вызывается действие конфигурации.
Для использования именованного клиента можно передать строковый параметр в CreateClient. Укажите имя создаваемого клиента:
public class NamedClientModel : PageModel
{
private readonly IHttpClientFactory _clientFactory;
public IEnumerable<GitHubPullRequest> PullRequests { get; private set; }
public bool GetPullRequestsError { get; private set; }
public bool HasPullRequests => PullRequests.Any();
public NamedClientModel(IHttpClientFactory clientFactory)
{
_clientFactory = clientFactory;
}
public async Task OnGet()
{
var request = new HttpRequestMessage(HttpMethod.Get,
"repos/dotnet/AspNetCore.Docs/pulls");
var client = _clientFactory.CreateClient("github");
var response = await client.SendAsync(request);
if (response.IsSuccessStatusCode)
{
PullRequests = await response.Content
.ReadAsAsync<IEnumerable<GitHubPullRequest>>();
}
else
{
GetPullRequestsError = true;
PullRequests = Array.Empty<GitHubPullRequest>();
}
}
}
В приведенном выше коде в запросе не требуется указывать имя узла. Достаточно передать только путь, так как используется базовый адрес, заданный для клиента.
Типизированные клиенты
Типизированные клиенты:
- предоставляют те же возможности, что и именованные клиенты, без необходимости использовать строки в качестве ключей.
- Это обеспечивает поддержку IntelliSense и компилятора при работе с клиентскими приложениями.
- Предоставьте единое расположение для настройки и взаимодействия с определённым
HttpClient. Например, для конечной точки серверной части можно использовать один типизированный клиент, который будет содержать всю логику работы с этой конечной точкой. - Работа с внедрением зависимостей (DI) и их возможностью встраивания в нужные места вашего приложения.
Типизированный клиент принимает параметр HttpClient в конструкторе:
public class GitHubService
{
public HttpClient Client { get; }
public GitHubService(HttpClient client)
{
client.BaseAddress = new Uri("https://api.github.com/");
// GitHub API versioning
client.DefaultRequestHeaders.Add("Accept",
"application/vnd.github.v3+json");
// GitHub requires a user-agent
client.DefaultRequestHeaders.Add("User-Agent",
"HttpClientFactory-Sample");
Client = client;
}
public async Task<IEnumerable<GitHubIssue>> GetAspNetDocsIssues()
{
var response = await Client.GetAsync(
"/repos/dotnet/AspNetCore.Docs/issues?state=open&sort=created&direction=desc");
response.EnsureSuccessStatusCode();
var result = await response.Content
.ReadAsAsync<IEnumerable<GitHubIssue>>();
return result;
}
}
В приведенном выше коде конфигурация перемещается в типизированный клиент. Объект HttpClient предоставляется как public свойство. Можно определить связанные с API методы, которые предоставляют функциональные возможности HttpClient. Метод GetAspNetDocsIssues инкапсулирует код, необходимый для запроса и анализа последних открытых проблем из репозитория GitHub.
Для регистрации типизированного клиента можно использовать универсальный метод расширения AddHttpClient в Startup.ConfigureServices, указав класс типизированного клиента:
services.AddHttpClient<GitHubService>();
Типизированный клиент регистрируется в системе внедрения зависимостей (Dependency Injection) как объект с кратковременным сроком службы. Типизированный клиент можно внедрить и использовать напрямую:
public class TypedClientModel : PageModel
{
private readonly GitHubService _gitHubService;
public IEnumerable<GitHubIssue> LatestIssues { get; private set; }
public bool HasIssue => LatestIssues.Any();
public bool GetIssuesError { get; private set; }
public TypedClientModel(GitHubService gitHubService)
{
_gitHubService = gitHubService;
}
public async Task OnGet()
{
try
{
LatestIssues = await _gitHubService.GetAspNetDocsIssues();
}
catch(HttpRequestException)
{
GetIssuesError = true;
LatestIssues = Array.Empty<GitHubIssue>();
}
}
}
При желании конфигурацию для типизированного клиента можно указать во время регистрации в Startup.ConfigureServices, а не в конструкторе типизированного клиента:
services.AddHttpClient<RepoService>(c =>
{
c.BaseAddress = new Uri("https://api.github.com/");
c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});
Возможно полностью инкапсулировать HttpClient внутри типизированного клиента. Вместо предоставления его как свойства можно использовать открытые методы для внутреннего вызова экземпляра HttpClient.
public class RepoService
{
// _httpClient isn't exposed publicly
private readonly HttpClient _httpClient;
public RepoService(HttpClient client)
{
_httpClient = client;
}
public async Task<IEnumerable<string>> GetRepos()
{
var response = await _httpClient.GetAsync("aspnet/repos");
response.EnsureSuccessStatusCode();
var result = await response.Content
.ReadAsAsync<IEnumerable<string>>();
return result;
}
}
В приведенном выше коде HttpClient хранится как закрытое поле. Любой доступ для совершения внешних вызовов осуществляется через метод GetRepos.
Созданные клиенты
IHttpClientFactory можно использовать в сочетании с другими библиотеками сторонних разработчиков, например Refit. Refit — это библиотека REST для .NET. Она преобразует REST API в динамические интерфейсы. Реализация интерфейса формируется динамически с помощью RestService с использованием HttpClient для совершения внешних вызовов HTTP.
Для представления внешнего API и его ответа определяются интерфейс и ответ:
public interface IHelloClient
{
[Get("/helloworld")]
Task<Reply> GetMessageAsync();
}
public class Reply
{
public string Message { get; set; }
}
Можно добавить типизированный клиент, используя Refit для создания реализации:
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpClient("hello", c =>
{
c.BaseAddress = new Uri("http://localhost:5000");
})
.AddTypedClient(c => Refit.RestService.For<IHelloClient>(c));
services.AddMvc();
}
При необходимости можно использовать определенный интерфейс с реализацией, предоставленной внедрением зависимостей и Refit:
[ApiController]
public class ValuesController : ControllerBase
{
private readonly IHelloClient _client;
public ValuesController(IHelloClient client)
{
_client = client;
}
[HttpGet("/")]
public async Task<ActionResult<Reply>> Index()
{
return await _client.GetMessageAsync();
}
}
Промежуточное программное обеспечение для обработки исходящих запросов
В HttpClient уже существует концепция делегирования обработчиков, которые можно связать друг с другом для исходящих HTTP-запросов. Класс IHttpClientFactory упрощает определение обработчиков для применения к каждому именованному клиенту. Он поддерживает регистрацию и связывание нескольких обработчиков в цепочки для создания конвейера промежуточного ПО для исходящих запросов. Каждый из этих обработчиков может выполнять работу до и после исходящего запроса. Этот шаблон аналогичен входящему конвейеру посреднического ПО в ASP.NET Core. Шаблон предоставляет механизм управления перекрестными задачами для HTTP-запросов, включая кэширование, обработку ошибок, сериализацию и логирование.
Чтобы создать обработчик, необходимо определить класс, производный от DelegatingHandler. Переопределите метод SendAsync для выполнения кода до передачи запросов следующему обработчику в конвейере:
public class ValidateHeaderHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
if (!request.Headers.Contains("X-API-KEY"))
{
return new HttpResponseMessage(HttpStatusCode.BadRequest)
{
Content = new StringContent(
"You must supply an API key header called X-API-KEY")
};
}
return await base.SendAsync(request, cancellationToken);
}
}
В предыдущем коде определяется базовый обработчик. Он проверяет, включен ли в запрос заголовок X-API-KEY. Если заголовок отсутствует, он может избежать вызовов HTTP и вернуть подходящий ответ.
Во время регистрации можно добавить один или несколько обработчиков в конфигурацию для элемента HttpClient. Эта задача выполняется через методы расширения в IHttpClientBuilder.
services.AddTransient<ValidateHeaderHandler>();
services.AddHttpClient("externalservice", c =>
{
// Assume this is an "external" service which requires an API KEY
c.BaseAddress = new Uri("https://localhost:5000/");
})
.AddHttpMessageHandler<ValidateHeaderHandler>();
В приведенном выше коде ValidateHeaderHandler зарегистрирован с DI. Обработчик должен регистрироваться в DI как временный сервис, а не область. Если обработчик зарегистрирован как служба в пределах области, и любые службы, от которых зависит обработчик, подлежат удалению:
- Службы обработчика могли быть удалены, прежде чем обработчик вышел из области действия.
- Снятые с использования службы обработчика приводят к сбою обработчика.
После регистрации можно вызвать AddHttpMessageHandler, передав тип обработчика.
Можно зарегистрировать несколько обработчиков в порядке, в котором они должны выполняться. Каждый обработчик оборачивает следующий обработчик, и в конце последний HttpClientHandler выполняет запрос.
services.AddTransient<SecureRequestHandler>();
services.AddTransient<RequestDataHandler>();
services.AddHttpClient("clientwithhandlers")
// This handler is on the outside and called first during the
// request, last during the response.
.AddHttpMessageHandler<SecureRequestHandler>()
// This handler is on the inside, closest to the request being
// sent.
.AddHttpMessageHandler<RequestDataHandler>();
Используйте один из следующих методов для предоставления общего доступа к состоянию отдельных запросов с помощью обработчиков сообщений:
- Передайте данные в обработчик с помощью
HttpRequestMessage.Properties. - Используйте
IHttpContextAccessorдля доступа к текущему запросу. - Создайте пользовательский объект хранилища
AsyncLocalдля передачи данных.
Использование обработчиков на основе Polly
IHttpClientFactory интегрируется с популярной библиотекой сторонних разработчиков под названием Polly. Polly — это комплексная библиотека устойчивости и временной обработки ошибок для .NET. Она позволяет разработчикам выражать политики, такие как политика повтора, автоматический выключатель, время ожидания, изоляция отсеков и резервный механизм, в понятной и потокобезопасной манере.
Для использования политик Polly с настроенными экземплярами HttpClient предоставляются методы расширения. Расширения Polly:
- Поддерживает добавление обработчиков на основе Polly клиентам.
- Можно использовать после установки пакета NuGet Microsoft.Extensions.Http.Polly. Пакет не включен в общую платформу ASP.NET Core.
Обработка временных сбоев
Чаще всего ошибки происходят, когда внешние вызовы HTTP являются временными. Используется удобный метод расширения AddTransientHttpErrorPolicy, который позволяет определить политику для обработки временных ошибок. Политики, настроенные с использованием этого метода расширения, обрабатывают HttpRequestException, ответы HTTP 5xx и ответы HTTP 408.
Расширение AddTransientHttpErrorPolicy может быть использовано в Startup.ConfigureServices. Данное расширение предоставляет доступ к объекту PolicyBuilder, настроенному для обработки ошибок, представляющих возможный временный сбой:
services.AddHttpClient<UnreliableEndpointCallerService>()
.AddTransientHttpErrorPolicy(p =>
p.WaitAndRetryAsync(3, _ => TimeSpan.FromMilliseconds(600)));
В приведенном выше коде определена политика WaitAndRetryAsync. Неудачные запросы повторяются до трех раз с задержкой 600 мс между попытками.
Динамический выбор политик
Существуют другие методы расширения, которые можно использовать для добавления обработчиков, использующих Polly. Одним из таких расширений является AddPolicyHandler, который имеет несколько перегрузок. Одна перегрузка разрешает проверку запроса для определения необходимой политики:
var timeout = Policy.TimeoutAsync<HttpResponseMessage>(
TimeSpan.FromSeconds(10));
var longTimeout = Policy.TimeoutAsync<HttpResponseMessage>(
TimeSpan.FromSeconds(30));
services.AddHttpClient("conditionalpolicy")
// Run some code to select a policy based on the request
.AddPolicyHandler(request =>
request.Method == HttpMethod.Get ? timeout : longTimeout);
Если в приведенном выше коде исходящий запрос является запросом HTTP GET, применяется время ожидания 10 секунд. Для остальных методов HTTP время ожидания — 30 секунд.
Добавьте несколько обработчиков Polly
Общепринятой практикой является вложение политик Polly для предоставления расширенной функциональности:
services.AddHttpClient("multiplepolicies")
.AddTransientHttpErrorPolicy(p => p.RetryAsync(3))
.AddTransientHttpErrorPolicy(
p => p.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));
В приведенном выше примере добавляются два обработчика. Первый использует расширение AddTransientHttpErrorPolicy, чтобы добавить политику повтора. Неудачные запросы выполняются повторно до трех раз. Второй вызов к AddTransientHttpErrorPolicy добавляет политику автоматического выключателя. Дополнительные внешние запросы блокируются в течение 30 секунд в случае пяти неудачных попыток подряд. Политики размыкателя цепи отслеживают состояние. Все вызовы через этот клиент имеют одинаковое состояние цепи.
Добавление политик из реестра Polly
Подход к управлению регулярно используемыми политиками заключается в их однократном определении и регистрации с помощью PolicyRegistry. Предоставляется метод расширения, разрешающий добавление обработчика с помощью политики из реестра:
var registry = services.AddPolicyRegistry();
registry.Add("regular", timeout);
registry.Add("long", longTimeout);
services.AddHttpClient("regulartimeouthandler")
.AddPolicyHandlerFromRegistry("regular");
В приведенном выше коде, когда PolicyRegistry добавляется в ServiceCollection, регистрируются две политики. Чтобы использовать политику из реестра, применяется метод AddPolicyHandlerFromRegistry, который передает имя необходимой политики.
Дополнительные сведения об интеграции IHttpClientFactory и Polly см. на вики-сайте Polly.
Управление HttpClient и временем существования
При каждом вызове HttpClient на CreateClient возвращается новый экземпляр IHttpClientFactory. Для каждого именованного клиента существует HttpMessageHandler. Фабрика управляет циклом жизни экземпляров HttpMessageHandler.
IHttpClientFactory объединяет в общий пул экземпляры HttpMessageHandler, созданные фабрикой, чтобы уменьшить потребление ресурсов. Экземпляр HttpMessageHandler может быть повторно использован из пула при создании нового HttpClient экземпляра, если срок его существования не истек.
Создавать пулы обработчиков желательно, так как каждый обработчик обычно управляет собственными базовыми HTTP-подключениями. Создание лишних обработчиков может привести к задержке подключения. Некоторые обработчики поддерживают подключения открытыми в течение неопределенного периода, что может помешать обработчику отреагировать на изменения DNS.
Время существования обработчика по умолчанию — две минуты. Значение по умолчанию можно переопределить для каждого именованного клиента. Чтобы переопределить это значение, вызовите SetHandlerLifetime в IHttpClientBuilder, который возвращается при создании клиента:
services.AddHttpClient("extendedhandlerlifetime")
.SetHandlerLifetime(TimeSpan.FromMinutes(5));
Высвобождать клиент не требуется. Удаление отменяет исходящие запросы и гарантирует, что экземпляр HttpClient больше не может быть использован после вызова Dispose.
IHttpClientFactory отслеживает и высвобождает ресурсы, используемые экземплярами HttpClient. Как правило, экземпляры HttpClient могут рассматриваться как .NET объекты, не требующие удаления.
До появления HttpClient было принято поддерживать один экземпляр IHttpClientFactory в активном состоянии в течение длительного времени. После перехода на 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,
};
});
Logging
Клиенты, созданные через IHttpClientFactory, записывают сообщения журнала для всех запросов. Установите соответствующий уровень информации в конфигурации ведения журнала, чтобы просматривать сообщения журнала по умолчанию. Другие виды ведения журнала, такие как логирование заголовков запросов, включаются только на уровне трассировки.
Категория журнала для каждого клиента включает в себя имя клиента. Клиент с именем MyNamedClient, например, записывает в журнал сообщения с категорией System.Net.Http.HttpClient.MyNamedClient.LogicalHandler. Сообщения с суффиксом LogicalHandler создаются за пределами конвейера обработчиков запросов. Во время запроса сообщения записываются в журнал до обработки запроса другими обработчиками в конвейере. Во время ответа сообщения записываются в журнал после получения ответа другими обработчиками в конвейере.
Кроме того, журнал ведется в конвейере обработчиков запросов. В примере MyNamedClient эти сообщения вносятся в журнал по категории журнала System.Net.Http.HttpClient.MyNamedClient.ClientHandler. Запрос обрабатывается после выполнения всех других обработчиков и непосредственно перед отправкой его по сети. Во время ответа в журнале записывается состояние ответа перед его передачей обратно по конвейеру обработчиков.
Включив ведение журнала в конвейере и за его пределами, можно выполнять проверку изменений, внесенных другими обработчиками конвейера. Ведение журнала может включать изменения в заголовки запросов, например, или код состояния ответа.
Включение имени клиента в категорию журнала позволяет фильтровать журналы по именованным клиентам при необходимости.
Настройка HttpMessageHandler
Может потребоваться управлять конфигурацией внутреннего элемента HttpMessageHandler, используемого клиентом.
При добавлении именованного или типизированного клиента возвращается IHttpClientBuilder. Для определения делегата можно использовать метод расширения ConfigurePrimaryHttpMessageHandler. Делегат используется для создания и настройки основного компонента HttpMessageHandler, который используется этим клиентом:
services.AddHttpClient("configured-inner-handler")
.ConfigurePrimaryHttpMessageHandler(() =>
{
return new HttpClientHandler()
{
AllowAutoRedirect = false,
UseDefaultCredentials = true
};
});
Использование IHttpClientFactory в консольном приложении
В консольном приложении добавьте в проект следующие ссылки на пакеты:
В следующем примере :
- IHttpClientFactory зарегистрирован в контейнере службы Generic Host.
-
MyServiceсоздает экземпляр фабрики клиента из службы, который используется для созданияHttpClient.HttpClientиспользуется для получения веб-страницы. - Метод
GetPageслужбы выполняется для записи первых 500 символов содержимого веб-страницы в консоль. Дополнительные сведения о вызове служб изProgram.Mainсм. в разделе Внедрение зависимостей в ASP.NET Core.
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
class Program
{
static async Task<int> Main(string[] args)
{
var builder = new HostBuilder()
.ConfigureServices((hostContext, services) =>
{
services.AddHttpClient();
services.AddTransient<IMyService, MyService>();
}).UseConsoleLifetime();
var host = builder.Build();
try
{
var myService = host.Services.GetRequiredService<IMyService>();
var pageContent = await myService.GetPage();
Console.WriteLine(pageContent.Substring(0, 500));
}
catch (Exception ex)
{
var logger = host.Services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred.");
}
return 0;
}
public interface IMyService
{
Task<string> GetPage();
}
public class MyService : IMyService
{
private readonly IHttpClientFactory _clientFactory;
public MyService(IHttpClientFactory clientFactory)
{
_clientFactory = clientFactory;
}
public async Task<string> GetPage()
{
// Content from BBC One: Dr. Who website (©BBC)
var request = new HttpRequestMessage(HttpMethod.Get,
"https://www.bbc.co.uk/programmes/b006q2x0");
var client = _clientFactory.CreateClient();
var response = await client.SendAsync(request);
if (response.IsSuccessStatusCode)
{
return await response.Content.ReadAsStringAsync();
}
else
{
return $"StatusCode: {response.StatusCode}";
}
}
}
}
Промежуточное ПО для распространения заголовков
Header propagation — это поддерживаемое сообществом промежуточное ПО для распространения HTTP-заголовков из входящего запроса на исходящие запросы HTTP-клиенту. Чтобы использовать распространение заголовков, сделайте следующее:
Укажите ссылку на поддерживаемый сообществом порт пакета HeaderPropagation. ASP.NET Core 3.1 или более поздней версии поддерживает Microsoft. AspNetCore.HeaderPropagation.
Настройте посредническое ПО и
HttpClientвStartup:public void ConfigureServices(IServiceCollection services) { services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); services.AddHttpClient("MyForwardingClient").AddHeaderPropagation(); services.AddHeaderPropagation(options => { options.Headers.Add("X-TraceId"); }); } public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseHsts(); } app.UseHttpsRedirection(); app.UseHeaderPropagation(); app.UseMvc(); }Клиент включает настроенные заголовки в исходящие запросы:
var client = clientFactory.CreateClient("MyForwardingClient"); var response = client.GetAsync(...);
Связанный контент
ASP.NET Core