Для чего нужен HttpClient.BaseAddress и почему я не могу его изменить после первого запроса

Итак, большинство из нас, вероятно, читали, что мы должны повторно использовать экземпляры HttpClient вместо использования using и создания новых. Это означает, что я могу просто создать единственный экземпляр HttpClient в своей программе и вызвать GetAsync, используя полную строку uri для каждого запроса. Это приводит меня к свойству BaseAddress HttpClient. Рассмотрим следующий код:

HttpClient microsoftClient = new HttpClient() { BaseAddress = new Uri("https://www.microsoft.com/") };
HttpClient stackoverflowClient = new HttpClient() { BaseAddress = new Uri("https://stackoverflow.com/") };

var response = microsoftClient.GetAsync("about").Result;
Console.WriteLine($"I {((response.IsSuccessStatusCode) ? "can" : "cannot")} access microsoft.com/about from the microsoft client");

response = microsoftClient.GetAsync("trademarks").Result;
Console.WriteLine($"I {((response.IsSuccessStatusCode) ? "can" : "cannot")} access microsoft.com/trademarks from the microsoft client");

response = stackoverflowClient.GetAsync("company/about").Result;
Console.WriteLine($"I {((response.IsSuccessStatusCode) ? "can" : "cannot")} access stackoverflow.com/company/about from the stackoverflow client");

response = stackoverflowClient.GetAsync("https://www.microsoft.com/about").Result;
Console.WriteLine($"I {((response.IsSuccessStatusCode) ? "can" : "cannot")} access microsoft.com/about from the stackoverflow client");

microsoftClient.BaseAddress = new Uri("https://stackoverflow.com");
response = microsoftClient.GetAsync("company/about").Result;
Console.WriteLine($"I {((response.IsSuccessStatusCode) ? "can" : "cannot")} access stackoverflow.com/company/about from the microsoft client, after changing the BaseAddress");

Вплоть до последнего блока этот код работает нормально, даже при использовании клиента с stackoverflow BaseAddress для доступа к Microsoft. Однако этот код выдает InvalidOperationException в начале последнего блока при переназначении BaseAddress, указывая

'This instance has already started one or more requests. Properties can only be modified before sending the first request.'

Это подводит меня к следующим вопросам:

  1. Какая вообще польза от использования BaseAddress? Я всегда мог использовать полный адрес в моем GetAsync звонке. Это просто для удобства / производительности, так как не нужно создавать полную строку запроса? Я предполагал, что он создаст только один ServicePoint внутри, как описано в первом абзаце это сообщение в блоге (или что-то подобное, поскольку сообщение довольно старое).
  2. Что происходит внутри нас, когда мы не можем изменить свойство HttpClient, особенно BaseAddress, после отправки первого запроса? Это кажется довольно неудобным, если использование этого свойства действительно приносит пользу.

person Jerome Reinländer    schedule 13.07.2018    source источник


Ответы (2)


Для (1) типичным вариантом использования будет клиент, который взаимодействует ровно с одним сервером. Возможно, это внутренний API, для использования которого был создан этот клиент. Точные данные будут храниться в файле конфигурации, который клиент читает во время запуска.

Мы могли бы засорять наш код прямым доступом к конфигурации или вставлять строку, прочитанную из конфигурации, во все места, где необходимо создать полный URL-адрес. Или мы могли бы просто настроить BaseAddress HttpClient, который мы помещаем в наш контейнер для внедрения зависимостей, и просто позволить потребляющим местоположениям внедрить этот объект. Для меня это несколько ожидаемый вариант использования.

Для (2) я не думаю, что существует техническое ограничение. Я думаю, это больше для того, чтобы спасти людей от самих себя. Поскольку установка BaseAddress и вызов фактического запроса отправляется, например, через GetAsync - это отдельные действия, было бы небезопасно, если бы два отдельных фрагмента кода выполняли это одновременно - вы легко можете получить гонки. Так что проще рассуждать о многопоточных программах, которые могут совместно использовать один экземпляр HttpClient, если такие гонки вообще не разрешены.

person Damien_The_Unbeliever    schedule 13.07.2018
comment
Это определенно хороший ответ и подчеркивает аспекты, о которых я даже не думал. Я приму это, если никто не сможет придумать лучшее (то есть техническое) объяснение. - person Jerome Reinländer; 13.07.2018
comment
Согласно вашему ответу, в ситуации, когда у вас много запросов к нескольким базовым адресам (т.е. 5), было бы целесообразно иметь 5 экземпляров HttpClient с установленным BaseAddress вместо одного для всех запросов? - person Jerome Reinländer; 13.07.2018
comment
@ JeromeReinländer - если бы они все примерно одинакового веса, я бы, наверное, так и поступил. Итак, у нас есть установка, в которой есть клиентская служба REST, служба Notes REST и служба REST коллеги. Каждая служба включает в себя множество разнообразных внутренних ссылок, и все они являются относительными, а не абсолютными URL-адресами. Таким образом, клиенту, использующему все эти службы, будет предоставлено 3 экземпляра HttpClient, настроенных с базовыми адресами для соответствующей комбинации среды / службы, и он сможет легко взаимодействовать с каждой службой по мере необходимости. - person Damien_The_Unbeliever; 13.07.2018

2 цели:

  1. Удобство. Если вы вызываете множество конечных точек с одного хоста и управляете базовым адресом и сегментами конечной точки как отдельными строками (очень часто), это помогает избежать некрасивой конкатенации строк при каждом вызове.

  2. Поощрение передового опыта. Хотя выполнение вызовов через GetAsync и т. Д. Является потокобезопасным, HttpClient имеет несколько свойств в дополнение к BaseAddress, например DefaultRequestHeaders, которые не являются. Как правило, вы хотите, чтобы они были одинаковыми для вызовов на один и тот же хост, но не для вызовов на разные. По этой причине HttpClient экземпляров на каждый вызываемый хост на самом деле является очень хорошей практикой. Если вы не звоните на тысячи разных хостов, вам не нужно беспокоиться о печально известная проблема нехватки сокетов здесь. (И даже если вы использовали синглтон, базовому сетевому стеку все равно нужно было бы открыть другой сокет для каждого хоста.)

Так почему же вообще можно указать полный адрес в HttpClient вызове? Опять же удобство. Адрес может поступать из внешнего источника или вводиться пользователем, и вам не нужно разбивать его на части, чтобы использовать. Но в этом случае вы находитесь на крючке обеспечения безопасности потоков, и этих свойств, не связанных с потоками, вероятно, следует полностью избегать.

person Todd Menier    schedule 13.07.2018