HttpClient.BaseAddress的目的是什么,为什么我不能在第一次请求后更改它

所以我们大多数人可能已经读过我们应该重用HttpClient实例,而不是using和创建新的实例。 这意味着我可以在我的程序中创建一个HttpClient实例,并使用每个请求使用完整的uri字符串调用GetAsync 。 这导致我进入HttpClientBaseAddress属性。 请考虑以下代码:

 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也是如此。 但是,当重新分配BaseAddress ,此代码会在最后一个块的开头抛出InvalidOperationException ,并说明

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

这引出了以下问题:

  1. 完全使用BaseAddress什么好处? 我总是可以在GetAsync调用中使用完整地址。 是否只是为了方便/性能而不必构建完整的请求字符串? 我的猜测是,它只会在内部创建一个ServicePoint ,如本博文第一段所述(或类似post相当陈旧)。
  2. 内部发生的事情是,在发送第一个请求后,我们无法更改HttpClient的属性,尤其是BaseAddress ? 如果使用此属性实际产生好处,这似乎非常不方便。

对于(1),常见的用例是与一个服务器交互的客户端。 也许这是该客户端构建使用的后端API。 确切的详细信息将存储在客户端在启动期间读取的配置文件中。

我们可以通过直接访问配置来丢弃我们的代码,或者将从config读取的字符串注入到需要构建完整URL的每个地方。 或者我们可以配置HttpClient的BaseAddress ,我们将它们放入我们的Dependency Injection容器中,只是让消费位置注入该对象。 对我来说这是一个有点预期的用例。

对于(2),我认为没有技术限制。 我认为这更能帮助人们摆脱困境。 由于设置BaseAddress并导致实际请求通过例如GetAsync是单独的操作,因此两个独立的代码片段同时执行此类操作将是不安全的 – 您可以轻松地获得比赛。 因此,如果首先不允许这样的比赛,可能更容易推理可能共享HttpClient的单个实例的multithreading程序。

2目的:

  1. 方便。 如果您在单个主机上调用许多端点,并且您将基地址和端点段作为单独的字符串(非常常见)进行管理,则可以帮助您避免在每次调用时进行丑陋的字符串连接。

  2. 鼓励最佳实践。 尽管通过GetAsync等进行调用是线程安全的,但HttpClient除了BaseAddress之外还有几个属性,例如DefaultRequestHeaders ,而不是。 通常,您希望这些调用对于同一主机的调用是相同的,而不是对不同调用的调用。 出于这个原因, 每个被调用主机的HttpClient实例实际上是一个非常好的做法。 除非你要调用数千个不同的主机,否则你不必担心这里臭名昭着的套接字耗尽问题 。 (即使您使用的是单例,底层网络堆栈也需要为每个主机打开一个不同的套接字。)

那么为什么在HttpClient调用上指定一个完整的地址甚至可以工作呢? 再次,方便。 地址可能来自外部源或用户输入,您不希望为了使用它而将其分解成碎片。 但在这种情况下,您处于线程安全的困境,并且应该完全避免那些非线程安全的属性。