Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Распространенные
В этой статье вы узнаете о некоторых наиболее распространенных проблемах, с которыми можно столкнуться при использовании IHttpClientFactory для создания HttpClient экземпляров.
IHttpClientFactory — удобный способ настроить несколько HttpClient конфигураций в контейнере DI, настроить ведение журнала, настроить стратегии устойчивости и многое другое.
IHttpClientFactory также инкапсулирует управление жизненным циклом экземпляров HttpClient и HttpMessageHandler, чтобы предотвратить такие проблемы, как исчерпание сокетов и потеря изменений DNS. Общие сведения об использовании IHttpClientFactory в приложении .NET см. в разделе IHttpClientFactory с .NET.
В связи с комплексной природой интеграции IHttpClientFactory с DI вы можете столкнуться с некоторыми проблемами, которые будет трудно выявить и устранить. В сценариях, перечисленных в этой статье, также содержатся рекомендации, которые можно применять заранее, чтобы избежать потенциальных проблем.
HttpClient не учитывает Scoped время жизни
Если вам потребуется получить доступ к любой службе с областью действия, например HttpContext или к какому-либо кэшу с областью действия, из HttpMessageHandler. Данные, сохраненные там, могут либо «исчезнуть», либо, наоборот, «сохраниться», когда этого не должно происходить. Это вызвано несоответствием области внедрения зависимостей (DI) между контекстом приложения и экземпляром обработчика, и это известное ограничение.IHttpClientFactory
IHttpClientFactory создает отдельную область DI для каждого HttpMessageHandler экземпляра. Эти области обработчика отличаются от областей контекста приложения (например, области входящих запросов в ASP.NET Core или созданной пользователем области DI), поэтому они не будут использовать совместные экземпляры служб с областью действия.
В результате этого ограничения:
- Любые данные, кэшированные "внешне" в службе с ограниченной областью действия, не будут доступны в
HttpMessageHandlerпределах. - Все данные, кэшированные внутри
HttpMessageHandlerили его зависимостей , могут наблюдаться из нескольких областей di приложения (например, из разных входящих запросов), так как они могут совместно использовать один и тот же обработчик.
Рассмотрим следующие рекомендации, которые помогут устранить это известное ограничение:
❌ НЕ кэшируйте любую информацию, связанную с областью действия (например, данные из HttpContext) в HttpMessageHandler экземплярах или их зависимостях, чтобы предотвратить утечку конфиденциальной информации.
❌ Не используйте файлы cookie, так как CookieContainer они будут совместно использоваться обработчиком.
✔️ Подумайте о том, чтобы не хранить информацию или передавать ее только в экземпляре HttpRequestMessage.
Для передачи произвольных сведений вместе с HttpRequestMessage можно использовать свойство HttpRequestMessage.Options.
✔️ Рассмотрите возможность инкапсулировать всю логику, связанную с областью (например, аутентификацию) в отдельном DelegatingHandler объекте, который не создан, IHttpClientFactoryи используйте его для упаковки созданного IHttpClientFactoryобработчика.
Чтобы создать только HttpClient экземпляр самостоятельно с помощью объединенного обработчика. Вы можете найти полный пример запуска для этого обходного решения на сайте GitHub.
Дополнительные сведения см. в разделе Области обработчика сообщений в IHttpClientFactory в IHttpClientFactory рекомендациях.
HttpClient не учитывает изменения DNS
Даже если IHttpClientFactory используется, проблема с устаревшим DNS по-прежнему возможна. Обычно это может произойти, если HttpClient экземпляр захватывается в Singleton службе или, как правило, хранится где-то дольше, чем указанный промежуток времени HandlerLifetime.
HttpClient захватывается также, если соответствующий типизированный клиент захватывается одиночным экземпляром.
❌ НЕ кэшируйте HttpClient экземпляры, созданные IHttpClientFactory в течение длительного периода времени.
❌ НЕ внедряйте типизированные экземпляры клиента в Singleton службы.
✔️ Рассмотрите возможность запросить клиента из IHttpClientFactory своевременно или в каждый момент, когда это необходимо. Созданные фабрикой программные клиенты безопасны для уничтожения.
HttpClient экземпляры, созданные с помощью IHttpClientFactory, предназначены для краткосрочного использования.
Переработка и воссоздание
HttpMessageHandlerпо истечении срока их службы являются важными дляIHttpClientFactoryобеспечения того, чтобы обработчики реагировали на изменения DNS.HttpClientпривязан к конкретному экземпляру обработчика при его создании, поэтому новыеHttpClientэкземпляры должны быть своевременно запрошены, чтобы клиент получил обновленный обработчик.Удаление таких
HttpClientэкземпляров, созданных фабрикой, не приведет к исчерпанию сокета, так как его удаление не активирует удалениеHttpMessageHandler.IHttpClientFactoryотслеживает и удаляет ресурсы, используемые для созданияHttpClientэкземпляров, в частностиHttpMessageHandlerэкземпляров, как только срок их существования истекает, и они больше неHttpClientиспользуются.
Типизированные клиенты предназначены для краткосрочного использования, так как экземпляр внедряется в конструктор, поэтому он будет совместно использовать срок жизни типизированного клиента.
Дополнительные сведения см. в разделах HttpClient по управлению временем существования и избегайте типизированных клиентов в разделах служб singleton в IHttpClientFactory рекомендациях.
HttpClient использует слишком много сокетов
Даже при использовании IHttpClientFactory, все равно может возникнуть проблема с исчерпанием сокета при определенном сценарии использования. По умолчанию HttpClient не ограничивает количество одновременных запросов. Если одновременно запускается большое количество запросов HTTP/1.1, каждое из них в конечном итоге активирует новую попытку HTTP-подключения, так как в пуле нет свободного подключения и ограничения не задано.
❌ Не запускайте большое количество запросов HTTP/1.1 одновременно без указания ограничений.
✔️ Установите HttpClientHandler.MaxConnectionsPerServer (или SocketsHttpHandler.MaxConnectionsPerServer, если вы используете его в качестве основного обработчика) на разумное значение. Обратите внимание, что эти ограничения применяются только к конкретному экземпляру обработчика.
✔️ Рекомендуется использовать ПРОТОКОЛ HTTP/2, который позволяет мультиплексирование запросов через одно TCP-подключение.
Типизированному клиенту неправильно внедрили HttpClient
Могут возникнуть различные ситуации, в которых возможно неожиданное внедрение HttpClient в типизированного клиента. В большинстве случаев основная причина будет находиться в ошибочной конфигурации, так как, по проектированию DI, любая последующая регистрация службы переопределяет предыдущую.
Типизированные клиенты используют именованные клиенты "под капотом": добавление типизированного клиента неявно регистрирует и связывает его с именованным клиентом. Имя клиента, если явно не указано, будет установлено как имя типа TClient. Это будет первый из TClient,TImplementation пары, если используются AddHttpClient<TClient,TImplementation> перегрузки.
Таким образом, регистрация типизированного клиента выполняет две отдельные задачи:
- Регистрирует именованного клиента (в простейшем случае по умолчанию это имя
typeof(TClient).Name). - Регистрирует службу, используя
TClientилиTClient,TImplementation.
Следующие два выражения технически одинаковы:
services.AddHttpClient<ExampleClient>(c => c.BaseAddress = new Uri("http://example.com"));
// -OR-
services.AddHttpClient(nameof(ExampleClient), c => c.BaseAddress = new Uri("http://example.com")) // register named client
.AddTypedClient<ExampleClient>(); // link the named client to a typed client
В простом случае это также будет похоже на следующее:
services.AddHttpClient(nameof(ExampleClient), c => c.BaseAddress = new Uri("http://example.com")); // register named client
// register plain Transient service and link it to the named client
services.AddTransient<ExampleClient>(s =>
new ExampleClient(
s.GetRequiredService<IHttpClientFactory>().CreateClient(nameof(ExampleClient))));
Рассмотрим следующие примеры того, как связь между типизированными и именованными клиентами может быть нарушена.
Типизированный клиент регистрируется во второй раз
❌ НЕ регистрируйте типизированный клиент отдельно— он уже зарегистрирован автоматически с помощью AddHttpClient<T> вызова.
Если типизированный клиент ошибочно регистрируется во второй раз в качестве обычной транзитной службы, это перезаписывает регистрацию, добавленную с помощью IHttpClientFactory, разрывая связь с именованным клиентом. Он будет проявляться так, словно HttpClient потеряла конфигурацию, так как вместо этого ненастроенная HttpClient будет внедрена в типизированный клиент.
Это может вызвать путаницу, что вместо того, чтобы вызвать исключение, используется «неверный» HttpClient. Это происходит потому, что "по умолчанию не настроенный" HttpClient — клиент с именем Options.DefaultName (string.Empty) зарегистрирован как простой сквозной сервис, чтобы обеспечить самый базовый сценарий использования IHttpClientFactory. Именно поэтому, после того как ссылка будет нарушена и типизированный клиент станет обычной службой, этот "по умолчанию" HttpClient естественным образом будет внедрен в соответствующий параметр конструктора.
Разные типизированные клиенты регистрируются в общем интерфейсе
Если два разных типизированных клиента зарегистрированы в общем интерфейсе, они оба будут повторно использовать один и тот же именованный клиент. Это может казаться так, как будто первый типизированный клиент получает второго именованного клиента, «неправильно» внедренного.
❌ НЕ регистрируйте несколько типизированных клиентов в одном интерфейсе без явного указания имени.
✔️ РЕКОМЕНДУЕТСЯ зарегистрировать и настроить именованный клиент отдельно, а затем связать его с одним или несколькими типизированными клиентами, указав имя в AddHttpClient<T> вызове или вызвав AddTypedClient во время настройки именованного клиента.
По задумке, регистрация и настройка именованного клиента с тем же именем несколько раз просто добавляет действия конфигурации в список уже существующих. Это поведение IHttpClientFactory может быть не очевидным, но это тот же подход, который используется шаблоном параметров и API конфигурации, напримерConfigure.
Это в основном полезно для расширенных конфигураций обработчика, например, добавления пользовательского обработчика в именованный клиент, определенный внешним образом, или создания имитации основного обработчика для тестов, но это также работает и для HttpClient конфигурации экземпляра. Например, три следующих примера приводят к HttpClient настройке таким же образом (оба BaseAddress и DefaultRequestHeaders задано):
// one configuration callback
services.AddHttpClient("example", c =>
{
c.BaseAddress = new Uri("http://example.com");
c.DefaultRequestHeaders.UserAgent.ParseAdd("HttpClient/8.0");
});
// -OR-
// two configuration callbacks
services.AddHttpClient("example", c => c.BaseAddress = new Uri("http://example.com"))
.ConfigureHttpClient(c => c.DefaultRequestHeaders.UserAgent.ParseAdd("HttpClient/8.0"));
// -OR-
// two configuration callbacks in separate AddHttpClient calls
services.AddHttpClient("example", c => c.BaseAddress = new Uri("http://example.com"));
services.AddHttpClient("example")
.ConfigureHttpClient(c => c.DefaultRequestHeaders.UserAgent.ParseAdd("HttpClient/8.0"));
Это позволяет связать типизированный клиент с уже определенным именованным клиентом, а также связать несколько типизированных клиентов с одним именованным клиентом. Более очевидно, если используются перегрузки с параметром name :
services.AddHttpClient("LogClient", c => c.BaseAddress = new Uri(LogServerAddress));
services.AddHttpClient<FooLogger>("LogClient");
services.AddHttpClient<BarLogger>("LogClient");
То же самое можно также достичь путем вызова AddTypedClient во время конфигурации именованного клиента:
services.AddHttpClient("LogClient", c => c.BaseAddress = new Uri(LogServerAddress))
.AddTypedClient<FooLogger>()
.AddTypedClient<BarLogger>();
Однако если вы не хотите повторно использовать тот же именованный клиент, но вы по-прежнему хотите зарегистрировать клиенты в одном интерфейсе, это можно сделать, явно указав для них разные имена:
services.AddHttpClient<ITypedClient, ExampleClient>(nameof(ExampleClient),
c => c.BaseAddress = new Uri("http://example.com"));
services.AddHttpClient<ITypedClient, GithubClient>(nameof(GithubClient),
c => c.BaseAddress = new Uri("https://github.com"));