Как правильно инжектировать HttpClient в бизнес-логику: создавать новый каждый раз или использовать фабрики и DI контейнер?
Что проверяют
Знаете ли вы обе классические ловушки HttpClient, а не одну, и умеете ли спрятать HTTP за абстракцией, чтобы бизнес-логика от него не зависела. Следом спрашивают, как это тестировать и с каким временем жизни регистрируется typed client.
Как отвечать
Сначала две ошибки, между которыми идёт выбор:
- новый клиент на каждый запрос:
using var client = new HttpClient()закрывает соединения, но сокеты остаются вTIME_WAIT, и под нагрузкой заканчиваются порты. Плюс каждый раз заново TCP и TLS-рукопожатие; - один статический клиент навсегда: соединения в пуле живут бесконечно, и смена DNS-записи сервиса не будет замечена.
Штатных решений два:
IHttpClientFactory:builder.Services.AddHttpClient<IPaymentGateway, PaymentGateway>(...). Фабрика держит пулHttpMessageHandlerи по умолчанию пересоздаёт обработчик раз в две минуты (SetHandlerLifetime), а самHttpClientпри этом лёгкий. Именованные клиенты:AddHttpClient("name")иCreateClient("name");- singleton с
SocketsHttpHandler: если DI нет, один клиент сPooledConnectionLifetime, например в две минуты, решает и сокеты, и DNS.
Для бизнес-логики важнее другое: сервис заказов зависит не от HttpClient, а от интерфейса вроде IPaymentGateway. Его реализация и есть typed client, в ней живут адреса, сериализация и разбор кодов ответа. Повторы и таймауты добавляют через Microsoft.Extensions.Http.Resilience и AddStandardResilienceHandler(), а не циклами в коде. В тестах бизнес-логики мокают интерфейс, а сам typed client проверяют с подменённым HttpMessageHandler.
Где спотыкаются
Внедряют typed client в singleton. Typed client регистрируется как transient, а захваченный навсегда держит один обработчик и теряет ротацию, то есть возвращается проблема с DNS.
Пытаются замокать HttpClient напрямую. GetAsync не виртуальный, а PostAsJsonAsync вообще метод расширения, Moq их не перехватит. Подменять нужно обработчик или интерфейс уровнем выше.
Регистрируют вручную services.AddScoped(_ => new HttpClient()). Это тот же новый клиент на каждый запрос, только спрятанный в контейнер, и конфигурация из Startup.cs старых шаблонов тут ничего не меняет.