Поддержка зависимостей с ключами (keyed DI) в IHttpClientFactory
Из этой статьи вы узнаете, как интегрировать IHttpClientFactory
с keyed Services.
Keyed Services (также называемый Keyed DI) — это функция внедрения зависимостей (DI), которая позволяет удобно работать с несколькими реализациями одной службы. При регистрации можно связать разные ключи службы с конкретными реализациями. Во время выполнения этот ключ используется в поиске в сочетании с типом службы, что означает, что вы можете получить определенную реализацию, передав соответствующий ключ. Дополнительные сведения о службах Keyed Services и DI в целом см. в внедрение зависимостей .NET.
Для получения сведений об использовании IHttpClientFactory
в приложении .NET смотрите в разделе IHttpClientFactory с .NET.
Предыстория
IHttpClientFactory
и именованные экземпляры HttpClient
, неудивительно, хорошо соответствуют идее ключевых сервисов. Исторически, среди прочего, IHttpClientFactory
был способом преодолеть отсутствие этой давней возможности DI. Но обычные именованные клиенты требуют получения, хранения и запроса экземпляра IHttpClientFactory
вместо внедрения настроенной HttpClient
, что может оказаться неудобным. Хотя типизированные клиенты пытаются упростить такую часть, она поставляется с перехватом: типизированные клиенты легко неправильно настроить и неправильно использовать, а поддерживающая инфраструктура также может быть существенной нагрузкой в определенных сценариях (например, на мобильных платформах).
Начиная с .NET 9 (пакетыMicrosoft.Extensions.Http
и Microsoft.Extensions.DependencyInjection
версии 9.0.0+
), IHttpClientFactory
могут напрямую использовать Keyed DI, вводя новый подход использования ключевого DI (в отличие от подходов "Именованный" и "Типизированный"). "Подход с использованием ключей DI связывает удобные, высоконастраиваемые регистрации HttpClient
с простым внедрением конкретных настроенных экземпляров HttpClient
."
Базовое использование
По состоянию на .NET 9 необходимо принять участие в функции, вызвав метод расширения AddAsKeyed. Если выбрано, именованный клиент, применяющий конфигурацию, добавляется в контейнер DI в качестве службы Keyed HttpClient
, используя имя клиента в качестве ключа службы. Это позволяет использовать стандартные API Keyed Services (например, FromKeyedServicesAttribute) для получения необходимых экземпляров Именованных HttpClient
(созданных и настроенных IHttpClientFactory
). По умолчанию клиенты регистрируются с областью действия и временем жизни.
Следующий код иллюстрирует интеграцию между IHttpClientFactory
, keyed DI и ASP.NET Core 9.0 Min API:
var builder = WebApplication.CreateBuilder(args);
// --- (1) Registration ---
builder.Services.AddHttpClient("github", c =>
{
c.BaseAddress = new Uri("https://api.github.com/");
c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
c.DefaultRequestHeaders.Add("User-Agent", "dotnet");
})
.AddAsKeyed(); // Add HttpClient as a Keyed Scoped service for key="github"
var app = builder.Build();
// --- (2) Obtaining HttpClient instance ---
// Directly inject the Keyed HttpClient by its name
app.MapGet("/", ([FromKeyedServices("github")] HttpClient httpClient) =>
// --- (3) Using HttpClient instance ---
httpClient.GetFromJsonAsync<Repo>("/repos/dotnet/runtime"));
app.Run();
record Repo(string Name, string Url);
Ответ конечной точки:
> ~ curl http://localhost:5000/
{"name":"runtime","url":"https://api.github.com/repos/dotnet/runtime"}
В этом примере настроенная HttpClient
внедряется в обработчик запросов через стандартную инфраструктуру keyed DI, которая интегрирована в привязку параметров ASP.NET Core. Дополнительные сведения о службах с указанием ключей в ASP.NET Core см. в разделе внедрение зависимостей в ASP.NET Core.
Сравнение подходов Keyed, Named и Typed
Рассмотрим только код, связанный с IHttpClientFactory
, из примера базового использования .
services.AddHttpClient("github", /* ... */).AddAsKeyed(); // (1)
app.MapGet("/", ([FromKeyedServices("github")] HttpClient httpClient) => // (2)
//httpClient.Get.... // (3)
В этом фрагменте кода показано, как может выглядеть регистрация (1)
, получение настроенного экземпляра HttpClient
(2)
и использование полученного экземпляра клиента по мере необходимости (3)
при использовании подхода Keyed DI.
Сравните, как эти же шаги выполняются с двумя "старыми" подходами.
Во-первых, с именованным подходом:
services.AddHttpClient("github", /* ... */); // (1)
app.MapGet("/github", (IHttpClientFactory httpClientFactory) =>
{
HttpClient httpClient = httpClientFactory.CreateClient("github"); // (2)
//return httpClient.Get.... // (3)
});
Во-вторых, при типизированном подходе:
services.AddHttpClient<GitHubClient>(/* ... */); // (1)
app.MapGet("/github", (GitHubClient gitHubClient) =>
gitHubClient.GetRepoAsync());
public class GitHubClient(HttpClient httpClient) // (2)
{
private readonly HttpClient _httpClient = httpClient;
public Task<Repo> GetRepoAsync() =>
//_httpClient.Get.... // (3)
}
Из трех подход keyed DI предлагает наиболее краткий способ достичь того же поведения.
Встроенная проверка контейнера DI
Если вы включили регистрацию с использованием ключей для конкретного именованного клиента, вы можете получить к ней доступ с помощью любых существующих API, DI с использованием ключей. Но если вы ошибочно попытаетесь использовать имя, которое еще не включено, вы получите стандартное исключение Keyed DI:
services.AddHttpClient("keyed").AddAsKeyed();
services.AddHttpClient("not-keyed");
provider.GetRequiredKeyedService<HttpClient>("keyed"); // OK
// Throws: No service for type 'System.Net.Http.HttpClient' has been registered.
provider.GetRequiredKeyedService<HttpClient>("not-keyed");
Кроме того, время существования клиентов с ограниченной областью может помочь перехватывать случаи приватных зависимостей:
services.AddHttpClient("scoped").AddAsKeyed();
services.AddSingleton<CapturingSingleton>();
// Throws: Cannot resolve scoped service 'System.Net.Http.HttpClient' from root provider.
rootProvider.GetRequiredKeyedService<HttpClient>("scoped");
using var scope = provider.CreateScope();
scope.ServiceProvider.GetRequiredKeyedService<HttpClient>("scoped"); // OK
// Throws: Cannot consume scoped service 'System.Net.Http.HttpClient' from singleton 'CapturingSingleton'.
public class CapturingSingleton([FromKeyedServices("scoped")] HttpClient httpClient)
//{ ...
Выбор времени существования службы
По умолчанию AddAsKeyed()
регистрирует HttpClient
как службу с ключом и с областью видимости. Можно также явно указать время существования, передав параметр ServiceLifetime
методу AddAsKeyed()
:
services.AddHttpClient("explicit-scoped")
.AddAsKeyed(ServiceLifetime.Scoped);
services.AddHttpClient("singleton")
.AddAsKeyed(ServiceLifetime.Singleton);
При вызове AddAsKeyed()
в регистрации типизированного клиента регистрируется только базовый именованный клиент как Keyed. Типизированный клиент по-прежнему регистрируется как обычный временный сервис.
Избегайте временной утечки памяти в HttpClient
Важный
HttpClient
— это IDisposable
, поэтому настоятельно рекомендуется избегать временного срока существования для экземпляров с ключами HttpClient
.
Регистрация клиента в качестве временной службы keyed приводит к HttpClient
и HttpMessageHandler
экземплярам, которые фиксируются контейнером DI, как и реализация IDisposable
. Это может привести к утечке памяти , если клиент инициализируется несколько раз в рамках служб Singleton.
Избегайте зависимостей в плену
Важный
Если HttpClient
зарегистрировано либо:
- как ключ Singleton, -OR-
- как ключевой с областью видимости или временныйи внедряется в длительное (дольше
HandlerLifetime
) приложение области действия, -ИЛИ- - как временный keyedи внедренный в службу Singleton,
— экземпляр HttpClient
становится пленникоми, скорее всего, переживет ожидаемый срок службы HandlerLifetime
.
IHttpClientFactory
не имеет контроля над зависимыми клиентами, они не в состоянии участвовать в ротации обработчиков, и это может привести к потере из-за изменений DNS. Аналогичная проблема уже существует для типизированных клиентов, которые регистрируются как временные сервисы.
В случаях, когда долговечность клиента не может быть избегнута или если она сознательно требуется, например, для *keyed Singleton*, рекомендуется использовать SocketsHttpHandler
, установив PooledConnectionLifetime
разумное значение.
services.AddHttpClient("shared")
.AddAsKeyed(ServiceLifetime.Singleton) // explicit singleton
.UseSocketsHttpHandler((h, _) => h.PooledConnectionLifetime = TimeSpan.FromMinutes(2))
.SetHandlerLifetime(Timeout.InfiniteTimeSpan); // disable rotation
services.AddSingleton<MySingleton>();
public class MySingleton([FromKeyedServices("shared")] HttpClient shared) // { ...
Остерегайтесь несовпадения масштаба
Хотя срок существования объекта с областью видимости гораздо менее проблематичен для именованных HttpClient
(по сравнению с проблемами Singleton и Transient), у него есть своя собственная особенность.
Важный
Время существования заданной области определенного экземпляра HttpClient
привязано к обычной области приложения (например, области входящих запросов), из которой она была разрешена. Однако это не относится к базовой цепочке обработчиков сообщений, которая по-прежнему управляется IHttpClientFactory
, так же, как это происходит для именованных клиентов, создаваемых непосредственно на фабрике.
HttpClient
с одним и тем же именем, но разрешённым (в рамках HandlerLifetime
временного интервала) в двух разных областях (например, два параллельных запроса к одной и той же конечной точке), могут повторно использовать один и тот же экземплярHttpMessageHandler
. Этот экземпляр, в свою очередь, имеет собственную отдельную область, как показано в области обработчика сообщений .
Заметка
Проблема несоответствия области является неприятным и давним, и по состоянию на .NET 9 по-прежнему остается нераскрытым. От службы, внедренной через обычную инфраструктуру di, вы ожидаете, что все зависимости будут удовлетворены из той же области, но для экземпляров с заданной областью ключа HttpClient
, это, к сожалению, не так.
Цепочка обработчиков сообщений с ключами
Для некоторых сложных сценариев может потребоваться напрямую получить доступ к цепочке HttpMessageHandler
вместо объекта HttpClient
.
IHttpClientFactory
предоставляет интерфейс IHttpMessageHandlerFactory
для создания обработчиков; и если включить Keyed DI, то не только HttpClient
, но и соответствующая цепочка HttpMessageHandler
регистрируется как служба Keyed:
services.AddHttpClient("keyed-handler").AddAsKeyed();
var handler = provider.GetRequiredKeyedService<HttpMessageHandler>("keyed-handler");
var invoker = new HttpMessageInvoker(handler, disposeHandler: false);
Практическое руководство. Переход с типизированного подхода к keyed DI
Заметка
В настоящее время мы рекомендуем использовать подход keyed DI вместо типизированных клиентов.
Минимальное изменение от существующего клиента с типизацией к зависимости с ключами можно реализовать следующим образом:
- services.AddHttpClient<Service>( // (1) Typed client
+ services.AddHttpClient(nameof(Service), // (1) Named client
c => { /* ... */ } // HttpClient configuration
//).Configure....
- );
+ ).AddAsKeyed(); // (1) + Keyed DI opt-in
+ services.AddTransient<Service>(); // (1) Plain Transient service
public class Service(
- // (2) "Hidden" Named dependency
+ [FromKeyedServices(nameof(Service))] // (2) Explicit Keyed dependency
HttpClient httpClient) // { ...
В примере:
- Регистрация типизированного клиента
Service
разделена на:- Регистрация именованного клиента
nameof(Service)
с той же конфигурациейHttpClient
и согласие на DI с использованием ключей. - Обычная временная услуга
Service
.
- Регистрация именованного клиента
-
HttpClient
зависимость вService
явно привязана к ключевой службе с ключомnameof(Service)
.
Имя не обязательно должно быть nameof(Service)
, но это пример, цель которого — минимизация изменений в поведении. Во внутренней среде типизированные клиенты используют именованные клиенты, и по умолчанию такие "скрытые" именованные клиенты идут по имени типа связанного типизированного клиента. В этом случае "скрытое" имя было nameof(Service)
, поэтому пример сохранил его.
Технически, пример "распаковывает" типизированный клиент, чтобы ранее "скрытый" именованный клиент стал "открытым", и зависимость удовлетворяется через инфраструктуру Keyed DI вместо типизированной инфраструктуры клиента.
Практическое руководство. Как включить Keyed DI по умолчанию
Вам не нужно вызывать AddAsKeyed для каждого отдельного клиента— вы можете легко выбрать "глобально" (для любого имени клиента) через ConfigureHttpClientDefaults. С точки зрения Keyed Services это приводит к регистрации KeyedService.AnyKey.
services.ConfigureHttpClientDefaults(b => b.AddAsKeyed());
services.AddHttpClient("first", /* ... */);
services.AddHttpClient("second", /* ... */);
services.AddHttpClient("third", /* ... */);
public class MyController(
[FromKeyedServices("first")] HttpClient first,
[FromKeyedServices("second")] HttpClient second,
[FromKeyedServices("third")] HttpClient third)
//{ ...
Остерегайтесь "неизвестных" клиентов
Заметка
KeyedService.AnyKey
регистрации определяют сопоставление от любого ключевого значения к некоторому экземпляру службы. Однако в результате проверка контейнера не применяется, и ошибочное значение ключанезаметно приводит к неправильному экземпляру, который внедряется.
Важный
Для ключевых элементов HttpClient
, ошибка в указании имени клиента может привести к случайной инъекции "неизвестного" клиента, то есть клиента, имя которого никогда не было зарегистрировано.
То же самое верно для простых именованных клиентов: IHttpClientFactory
не требует явной регистрации имени клиента, что соответствует способу работы шаблона параметров . Фабрика предоставляет ненастроенные (или, точнее, установленные по умолчанию)HttpClient
для любого неизвестного имени.
Заметка
Поэтому важно помнить: подход "Ключ по умолчанию" охватывает не только все зарегистрированныеHttpClient
, но и все клиенты, которые IHttpClientFactory
, могут создавать.
services.ConfigureHttpClientDefaults(b => b.AddAsKeyed());
services.AddHttpClient("known", /* ... */);
provider.GetRequiredKeyedService<HttpClient>("known"); // OK
provider.GetRequiredKeyedService<HttpClient>("unknown"); // OK (unconfigured instance)
Соображения по стратегии "Активного согласия"
Несмотря на то, что "глобальное" согласие является однострочным, жаль, что функция по-прежнему требует его, а не работает просто "из коробки". Полный контекст и обоснование этого решения см. в разделе dotnet/runtime#89755 и dotnet/runtime#104943. Короче говоря, основным препятствием для включения 'по умолчанию' является ServiceLifetime
"противоречие": в текущем (9.0.0
) состоянии9.0.0
и IHttpClientFactory
реализации, не существует такого ServiceLifetime
, который был бы достаточно безопасным для всех HttpClient
во всех возможных ситуациях. Однако есть намерение решить ограничения в предстоящих выпусках и изменить стратегию с "согласия" на "отказ".
Практическое руководство. Отказ от регистрации с ключами
Вы можете явно отказаться от Keyed DI для HttpClient
-s, вызвав метод расширения RemoveAsKeyed отдельно для каждого имени клиента.
services.ConfigureHttpClientDefaults(b => b.AddAsKeyed()); // opt IN by default
services.AddHttpClient("keyed", /* ... */);
services.AddHttpClient("not-keyed", /* ... */).RemoveAsKeyed(); // opt OUT per name
provider.GetRequiredKeyedService<HttpClient>("keyed"); // OK
provider.GetRequiredKeyedService<HttpClient>("not-keyed"); // Throws: No service for type 'System.Net.Http.HttpClient' has been registered.
provider.GetRequiredKeyedService<HttpClient>("unknown"); // OK (unconfigured instance)
Или "глобально" с ConfigureHttpClientDefaults:
services.ConfigureHttpClientDefaults(b => b.RemoveAsKeyed()); // opt OUT by default
services.AddHttpClient("keyed", /* ... */).AddAsKeyed(); // opt IN per name
services.AddHttpClient("not-keyed", /* ... */);
provider.GetRequiredKeyedService<HttpClient>("keyed"); // OK
provider.GetRequiredKeyedService<HttpClient>("not-keyed"); // Throws: No service for type 'System.Net.Http.HttpClient' has been registered.
provider.GetRequiredKeyedService<HttpClient>("unknown"); // Throws: No service for type 'System.Net.Http.HttpClient' has been registered.
Порядок приоритета
Если они вызываются вместе или отдельно несколько раз, AddAsKeyed()
и RemoveAsKeyed()
обычно следуют правилам конфигураций IHttpClientFactory
и регистрации DI.
- При вызове с тем же именем последние настройки имеют приоритет: срок действия из последней
AddAsKeyed()
используется для создания ключевой регистрации (если последним был вызванRemoveAsKeyed()
, в этом случае имя будет исключено). - Если используется только в
ConfigureHttpClientDefaults
, последний параметр выигрывает. - Если использовались оба
ConfigureHttpClientDefaults
и конкретное имя клиента, все настройки по умолчанию считаются применяющимися до всех параметров каждого имени. Таким образом, значения по умолчанию можно игнорировать, а последний параметр для каждого имени становится приоритетным.
См. также
- IHttpClientFactory с .NET
- внедрение зависимостей в .NET
- IHttpClientFactory
-
Распространенные проблемы
IHttpClientFactory
использования