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

What is the purpose of HttpClient.BaseAddress and why can't I change it after the first request

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

HttpClient microsoftClient = new HttpClient() { BaseAddress = new Uri("https://www.microsoft.com/") };
HttpClient WhosebugClient = new HttpClient() { BaseAddress = new Uri("https://whosebug.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 = WhosebugClient.GetAsync("company/about").Result;
Console.WriteLine($"I {((response.IsSuccessStatusCode) ? "can" : "cannot")} access whosebug.com/company/about from the Whosebug client");

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

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

直到最后一个块,此代码运行良好,即使使用带有 Whosebug 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 电话中使用完整地址。它只是为了 convenience/performance 不必构建完整的请求字符串吗?我的猜测是,它只会在内部创建一个 ServicePoint,如 this blog post 的第一段所述(或类似于 post 的东西很旧)。
  2. 在发送第一个请求后,我们无法更改 HttpClient 的 属性,尤其是 BaseAddress,内部发生了什么?如果使用这个 属性 实际上会产生好处,这似乎很不方便。

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

我们可以直接访问配置,或者将从配置中读取的字符串注入每个需要构建完整 URL 的地方。或者我们可以只配置要放入依赖注入容器的 HttpClient 的 BaseAddress,然后让消费位置注入该对象。这对我来说是一个有点意料之中的用例。

对于 (2),我认为没有 技术 限制。我认为这更多是为了拯救人们自己。由于设置 BaseAddress 并导致实际请求通过例如发出GetAsync 是单独的操作,两段单独的代码同时做这样的事情是不安全的——你可以很容易地进行比赛。因此,如果首先不允许此类竞争,则更容易推断出可能共享 HttpClient 的单个实例的多线程程序。

2 个用途:

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

  2. 鼓励最佳实践。虽然通过 GetAsync 等进行调用是线程安全的,但 HttpClient 除了 BaseAddress 之外还有几个属性,例如 DefaultRequestHeaders,但它们不是。通常,您希望这些对于同一主机的调用是相同的,但对于不同主机的调用则不同。出于这个原因,一个 HttpClient 每个主机被调用的实例 实际上是一个很好的做法。除非你正在调用成千上万个不同的主机,否则你不需要担心这里的 the infamous socket exhaustion problem。 (即使您使用单例,底层网络堆栈也需要为每个主机打开不同的套接字。)

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