Я знаю, что при использовании контейнера внедрения зависимостей Microsoft для обработки экземпляров HttpClient рекомендуется использовать интерфейс IHttpClientFactory, предоставляемый пакетом nuget Microsoft.Extensions.Http.
Правильно.
К сожалению, классы, реализующие интерфейс IHttpClientFactory, не являются общедоступными (как вы можете проверить здесь), поэтому единственный способ использовать этот шаблон - использовать контейнер внедрения зависимостей Microsoft (по крайней мере, это единственныйтот, который я знаю). Иногда мне нужно поддерживать старые приложения, используя другой контейнер, поэтому мне нужно найти лучшие практики, даже если подход IHttpClientFactory не может быть использован.
Microsoft.Extensions.DependencyInjection
("MEDI") следует подумать(упрощенной) абстракции над несколькими системами DI - так уж получилось, что он поставляется с собственным базовым контейнером DI. Вы можете использовать MEDI в качестве интерфейса для Unity, SimpleInject, Ninject и др.
Как объяснено в этой известной статье и подтверждено также в документах Microsoft, класс HttpClient
предназначен для одного экземпляра. за время жизни приложения и повторно используется в нескольких вызовах HTTP.
Не совсем.
- Вам не нужно, чтобы singleton
HttpClient
использовался всемипотребители HttpClient
в вашем приложении, потому что разные потребители могут иметь разные предположения относительно (как вы позже укажете) DefaultRequestHeaders
и других HttpClient
состояний. Некоторый код может также предполагать, что HttpClient
также не использует экземпляры DelegatingHandler
. - Вам также не нужны экземпляры
HttpClient
(созданные с использованием собственного конструктора без параметров) с неограниченным временем жизни, потому чтоо том, как его внутренняя HttpClientHandler
по умолчанию обрабатывает (или, скорее, не обрабатывает) изменения DNS. Следовательно, по умолчанию IHttpClientFactory
накладывает ограничение продолжительности жизни 2 минуты для каждого экземпляра HttpClientHandler
.
Это открывает вопрос: что произойдет, если у меня будет одноэлементный экземпляр HttpClient и где-нибудь вмой код Я использую свойство DefaultRequestHeaders, чтобы установить некоторые распространенные заголовки HTTP-запроса, полезные для вызова одного из хостов, с которыми должно взаимодействовать мое приложение?
Что происходит? То, что происходит, - это то, что вы можете ожидать: разные потребители одного и того же экземпляра HttpClient
действуют на неверную информацию - например, отправляют неправильный заголовок Authorization
на неправильный BaseAddress
. Вот почему HttpClient
экземпляры не должны быть общими.
Это потенциально опасно, потому что разные хосты могут требовать разные значения для одного и того же заголовка запроса (например, аутентификация может быть примером). Кроме того, изменение DefaultRequestHeaders одновременно из двух потоков может потенциально испортить внутреннее состояние экземпляра HttpClient из-за отсутствия гарантий безопасности потоков.
Это не обязательно проблема «безопасности потоков» -у вас может быть однопоточное приложение, которое таким образом злоупотребляет HttpClient
и все еще имеет ту же проблему. Реальная проблема заключается в том, что различные объекты (потребители HttpClient
) предполагают, что они являются владельцем HttpClient
, когда они не являются.
К сожалению, C # и .NETу нас нет встроенного способа объявить и утвердить владение или время жизни объекта (следовательно, почему IDisposable
сегодня немного беспорядок), поэтому нам нужно прибегнуть к различным альтернативам.
createодно хранилище HttpClient для каждого хоста, с которым приложение должно взаимодействовать. Каждый вызов одного конкретного хоста будет использовать один и тот же экземпляр HttpClient. Параллельные вызовы к одному и тому же хосту безопасны из-за задокументированной безопасности потоков, используемых для выполнения вызовов.
(Под «хостом» я предполагаю, что вы имеете в виду HTTP «происхождение»). Это наивно и не будет работать, если вы отправляете разные запросы одному и тому же сервису с разными токенами доступа (если токены доступа хранятся в DefaultRequestHeaders
).
создать одну службу для каждого хоста, с которым приложение должно взаимодействовать. Экземпляр HttpClient внедряется внутри этого сервиса, а сам сервис используется как приложение в приложении. Этот сервис используется для отвлечения доступа к хосту, с которым он связан. Подобные классы полностью тестируемы, как показано здесь.
Опять же, не думайте о сервисах HTTP в терминах "хостов" - в противном случае это имеет ту же проблему, что и выше.
единственной точкой, где создаются и настраиваются экземпляры HttpClient, является корень композиции приложения. Код в корне композиции является однопоточным, поэтому для настройки экземпляров HttpClient безопасно использовать такие свойства, как DefaultRequestHeaders.
Я тоже не уверен, как это поможет. Ваши потребители могут быть в состоянии.
В любом случае, реальным решением, imo, является реализация вашего собственного IHttpClientFactory
(это также может быть ваш собственный интерфейс!). Чтобы упростить задачу, конструкторы ваших потребителей не примут экземпляр HttpClient
, а вместо этого примут IHttpClientFactory
и вызовут его метод CreateClient
, чтобы получить собственный частный и сохраняющий состояние экземпляр HttpClient
, который затем использует пул общих и не сохраняющих состояние экземпляров HttpClientHandler
.
Используя этот подход:
- Каждый потребитель получает свой собственный частный экземпляриз
HttpClient
, которые они могут изменять по своему усмотрению - не стоит беспокоиться о том, что объекты изменяют экземпляры, которыми они не владеют. HttpClient
экземпляр каждого потребителя не нуждается в утилизации - вы можете спокойно игнорировать тот факт, что они реализуют IDisposable
.
- Без обработчиков из пула каждый экземпляр
HttpClient
имеет свой собственный обработчик, который должен быть утилизирован. - Нос помощью обработчиков пула, как и при таком подходе, пул управляет временем жизни и очисткой обработчика, а не экземплярами
HttpClient
. - Ваш код может вызывать
HttpClient.Dispose()
, если он действительно хочет (или тыя хочу отключить FxCop), но он ничего не сделает: базовый HttpMessageHandler
(PooledHttpClientHandler
) имеет метод удаления NOOP.
Управление временем жизни HttpClient
не имеет значения, поскольку каждый HttpClient
имеет только свое собственное изменяемое состояние, такое как DefaultRequestHeaders
и BaseAddress
- так что вы можете иметь переходные процессы,экземпляры с длительным сроком действия или единичные HttpClient
, и это нормально, потому что все они погружаются в пул HttpClientHandler
экземпляров только тогда, когда они действительно отправляют запрос.
Вот так:
/// <summary>This service should be registered as a singleton, or otherwise have an unbounded lifetime.</summary>
public QuickAndDirtyHttpClientFactory : IHttpClientFactory // `IHttpClientFactory ` can be your own interface. You do NOT need to use `Microsoft.Extensions.Http`.
{
private readonly HttpClientHandlerPool pool = new HttpClientHandlerPool();
public HttpClient CreateClient( String name )
{
PooledHttpClientHandler pooledHandler = new PooledHttpClientHandler( name, this.pool );
return new HttpClient( pooledHandler );
}
// Alternative, which allows consumers to set up their own DelegatingHandler chains without needing to configure them during DI setup.
public HttpClient CreateClient( String name, Func<HttpMessageHandler, DelegatingHandler> createHandlerChain )
{
PooledHttpClientHandler pooledHandler = new PooledHttpClientHandler( name, this.pool );
DelegatingHandler chain = createHandlerChain( pooledHandler );
return new HttpClient( chain );
}
}
internal class HttpClientHandlerPool
{
public HttpClientHandler BorrowHandler( String name )
{
// Implementing this is an exercise for the reader.
// Alternatively, I'm available as a consultant for a very high hourly rate :D
}
public void ReleaseHandler( String name, HttpClientHandler handler )
{
// Implementing this is an exercise for the reader.
}
}
internal class PooledHttpClientHandler : HttpMessageHandler
{
private readonly String name;
private readonly HttpClientHandlerPool pool;
public PooledHttpClientHandler( String name, HttpClientHandlerPool pool )
{
this.name = name;
this.pool = pool ?? throw new ArgumentNullException(nameof(pool));
}
protected override async Task<HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken cancellationToken )
{
HttpClientHandler handler = this.pool.BorrowHandler( this.name );
try
{
return await handler.SendAsync( request, cancellationToken ).ConfigureAwait(false);
}
finally
{
this.pool.ReleaseHandler( this.name, handler );
}
}
// Don't override `Dispose(Bool)` - don't need to.
}
Тогда каждый потребитель может использовать его так:
public class Turboencabulator : IEncabulator
{
private readonly HttpClient httpClient;
public Turboencabulator( IHttpClientFactory hcf )
{
this.httpClient = hcf.CreateClient();
this.httpClient.DefaultRequestHeaders.Add( "Authorization", "my-secret-bearer-token" );
this.httpClient.BaseAddress = "https://api1.example.com";
}
public async InverseReactiveCurrent( UnilateralPhaseDetractor upd )
{
await this.httpClient.GetAsync( etc )
}
}
public class SecretelyDivertDataToTheNsaEncabulator : IEncabulator
{
private readonly HttpClient httpClientReal;
private readonly HttpClient httpClientNsa;
public SecretNsaClientService( IHttpClientFactory hcf )
{
this.httpClientReal = hcf.CreateClient();
this.httpClientReal.DefaultRequestHeaders.Add( "Authorization", "a-different-secret-bearer-token" );
this.httpClientReal.BaseAddress = "https://api1.example.com";
this.httpClientNsa = hcf.CreateClient();
this.httpClientNsa.DefaultRequestHeaders.Add( "Authorization", "TODO: it's on a postit note on my desk viewable from outside the building" );
this.httpClientNsa.BaseAddress = "https://totallylegit.nsa.gov";
}
public async InverseReactiveCurrent( UnilateralPhaseDetractor upd )
{
await this.httpClientNsa.GetAsync( etc )
await this.httpClientReal.GetAsync( etc )
}
}