So most of us have probably read that we are supposed to reuse instances of HttpClient
instead of using using
and creating new ones. That means I can just create a single instance of HttpClient
in my program and call GetAsync
using a full uri string per request. This leads me to the BaseAddress
property of HttpClient
. Consider the following code:
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");
Up until the last block this code runs fine, even when using the client with the stackoverflow BaseAddress
to access Microsoft. However this code throws an InvalidOperationException
at the beginning of the last block, when reassigning the BaseAddress
, stating
'This instance has already started one or more requests. Properties can only be modified before sending the first request.'
This leads me to the following questions:
BaseAddress
at all? I could just always use the full address in my GetAsync
call. Is it just for the convenience/performance of not having to build the the full request string? My guess was that it would only create a single ServicePoint
internally as described in the first paragraph of this blog post (or something similar as the post is quite old).HttpClient
, especially BaseAddress
, after sending the first request? This seems quite inconvenient if using this property actually yields benefits.Gets or sets the base address of Uniform Resource Identifier (URI) of the Internet resource used when sending requests. public: property Uri ^ BaseAddress { Uri ^ get(); void set(Uri ^ value); };
All you have to do is set the BaseAddress on the HttpClient: var httpClient = new HttpClient(); httpClient. BaseAddress = new Uri("https://peterdaugaardrasmussen.com/"); var response = await httpClient.
GetAsync(Uri) Send a GET request to the specified Uri as an asynchronous operation. GetAsync(String, HttpCompletionOption) Send a GET request to the specified Uri with an HTTP completion option as an asynchronous operation.
For (1), a common use case would be a client which interacts with exactly one server. Maybe it's the back-end API that this client was built to use. The exact details will be stored in a config file that the client reads during startup.
We could litter our code with direct accesses to the config, or inject the string read from config into every place that needs to construct a full URL. Or we could just configure the BaseAddress
of the HttpClient we're putting into our Dependency Injection container and just let the consuming locations have that object injected. That for me is a somewhat expected use case.
For (2), I don't think there's a technical limitation. I think this is more there to save people from themselves. Since setting a BaseAddress
and causing an actual request to go out via e.g. GetAsync
are separate actions, it would be unsafe for two separate pieces of code to be doing such a thing at the same time - you could easily get races. So it's easier to reason about multi-threaded programs that may be sharing a single instance of HttpClient
if such races aren't allowed in the first place.
2 purposes:
Convenience. If you're calling many endpoints off a single host, and you're managing the base address and endpoint segments as separate strings (very common), it helps you avoid doing ugly string concatenation on every call.
Encouraging best practices. Although making calls via GetAsync
, etc. is thread safe, HttpClient
has several properties in addition to BaseAddress
, such as DefaultRequestHeaders
, that are not. Very typically, you want these to be the same for calls to the same host but not for calls to different ones. For this reason, an HttpClient
instance per host being called is actually a very good practice. Unless you're calling thousands of different hosts, you don't need to worry about the infamous socket exhaustion problem here. (And even if you were using a singleton, the underlying network stack would need to open a different socket per host anyway.)
So why does specifying a full address on an HttpClient
call even work at all? Again, convenience. The address might be coming from an external source or user input, and you wouldn't want to have to break it into pieces in order to use it. But in this case, you are on the hook for thread safety, and those non-thread-safe properties should probably just be avoided entirely.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With