PostAsJsonAsync - Now with headers
The issue
I feel like I've run into this particular problem a few times now.
How can I use those lovely extensions in HttpClientJsonExtensions
but send them with some additional custom headers? Or, are there any other options?
If you're unfamiliar with the extensions, they essentially allow you to do the following
// This...
var message = new HttpRequestMessage
{
Content = JsonContent.Create(new { Foo = "foo" }),
Method = HttpMethod.Post,
RequestUri = new Uri("https://example.com/some-endpoint"),
};
var response = await _httpClient.SendAsync(message);
// Or this...
var content = JsonContent.Create(new { Foo = "foo" });
var response = await _httpClient.PostAsync(
"https://example.com/some-endpoint",
content);
// Turns into...
var response = _httpClient.PostAsJsonAsync(
"https://example.com/some-endpoint",
new { Foo = "foo" });
Much simpler, right?
There is a whole raft of overloads on PostAsJsonAsync
which you can look over here but most of the time, you would probably just end up using something similar to the above.
But what do we do if we want some custom headers? That's not part of the extensions.
We have three options at this point:
Go back to hand-crafting the
HttpResponseMessage
/JsonContent
and adding the headers there.Create some extensions ourselves
Use delegating handlers
Let's just go through what that might look like.
Option 1 - Hand-crafting
Arguably the simplest, we just go back to what we were doing in the first example.
// Using the less verbose syntax...
var content = JsonContent.Create(new { Foo = "foo" });
content.Headers.Add("Bar-Header", "Bar");
var response = await _httpClient.PostAsync(
"https://example.com/some-endpoint",
content);
Why would you go for this? I would start here if I only needed to do this <=3 times. The code maintenance overhead is not that much and if you needed to add a second header then it would not be too cumbersome to refactor. You could even add a helper method accepting HttpContent
which added those headers on whilst leaving the rest of the code untouched.
This means we are not using the extensions but in this case, keeping it simple is fine.
Option 2 - Extension methods
Our first stop is to see how the originals work under the hood, and they are quite simple for the most part.
public static Task<HttpResponseMessage> PostAsJsonAsync<TValue>(
this HttpClient client,
[StringSyntax(StringSyntaxAttribute.Uri)] string? requestUri,
TValue value,
JsonSerializerOptions? options = null,
CancellationToken cancellationToken = default)
{
if (client is null)
{
throw new ArgumentNullException(nameof(client));
}
JsonContent content = JsonContent.Create(value, mediaType: null, options);
return client.PostAsync(requestUri, content, cancellationToken);
}
Simple enough, right?
If we wanted to add headers, we could just make our own extension class
public static class HttpClientExtensions
{
public static Task<HttpResponseMessage> PostAsJsonAsync<TValue>(
this HttpClient client,
[StringSyntax(StringSyntaxAttribute.Uri)] string? requestUri,
TValue value,
IReadOnlyDictionary<string, string> headers, // Add to the parameters
JsonSerializerOptions? options = null,
CancellationToken cancellationToken = default)
{
if (client is null)
{
throw new ArgumentNullException(nameof(client));
}
JsonContent content = JsonContent.Create(value, mediaType: null, options);
foreach (var header in headers)
{
content.Headers.Remove(header.Key); // Just in case it was added elsewhere
content.Headers.Add(header.Key, header.Value);
}
return client.PostAsync(requestUri, content, cancellationToken);
}
}
// And then we use it much like in the first example
var response = _httpClient.PostAsJsonAsync(
"https://example.com/some-endpoint",
new { Foo = "foo" },
new Dictionary<string, string>
{
["Bar-Header"] = "Bar"
});
We now have a reusable method for anywhere in our codebase that needs this sort of thing. This is an excellent choice for adding simple ad-hoc headers to requests.
But what if we had something that we needed to add to all requests for a particular client but that value could change over time?
Option 3 - Delegating Handlers
If you have not come across these before, DelegatingHandlers
in C# are like filters or middleware for your HTTP requests and responses when attached to an HttpClient
.
These are great for when you want to do something with every request and response. In our case, let's imagine that we need our Bar-Header
to change on every request. We could still do that with our other two options but it would be great if we could forget that we even needed to add that header in every request.
Here is what a simple DelegatingHandler
would look like for adding a header.
public class BarHandler : DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
// Assume GenerateBarHeader() returns some dynamic string
request.Headers.Add("Bar-Header", GenerateBarHeader());
return base.SendAsync(request, cancellationToken);
}
}
Very simple.
And then to hook this up, assuming you are using DI to inject your HttpClient
.
builder.Services.AddTransient<BarHandler>();
builder.Services.AddHttpClient().AddHttpMessageHandler<BarHandler>()
HttpClient
instances we inject. You probably do not want this. Typed or named clients are far more flexible which is covered in the documentation here.Now whenever you send a request out, our header will be added to the collection of headers, even when using PostAsJsonAsync
.
The most common use for this is adding an Authorization
header for all requests, but it can also be used for doing things like custom logging.
Honourable mention - DefaultRequestHeaders
Leaving this until last as I have seen this horribly abused. If we go back to our original scenario of adding Bar-Header: Bar
to all requests, you could do the following
httpClient.DefaultRequestHeaders.Add("Bar-Header", "Bar");
Very simple, you can do this when registering with the DI container as well. You can do it whenever you have access to the HttpClient
and that is where the problem comes in.
I imagine some or most have you have come across something like this
var content = JsonContent.Create(new { Foo = "foo" });
_httpClient.DefaultRequestHeaders.Add("Bar-Header", "Bar");
var response = await _httpClient.PostAsync(
"https://example.com/some-endpoint",
content);
// And then sometimes you see this...
_httpClient.DefaultRequestHeaders.Remove("Bar-Header");
// Or worse...
_httpClient.DefaultRequestHeaders.Clear();
Please, do not do this.
Will it work? Yes, assuming that HttpClient
is not being used in other threads and DefaultRequestHeaders
is empty before you add anything. These are fairly big assumptions and hinder refactoring. If you want to use DefaultRequestHeaders
only do it when you first create the client and then leave it alone after that.
Which should I use?
In my opinion
Handcraft the requests if you only have a few places that need an additional header
Create the extension(s) if you have more than three places that have an additional header, or if you have different classes that need this shared functionality
Use
DelegatingHandler
if you have a dynamic header you always need settingUse
DefaultRequestHeaders
when you have a static header you always need a setting but only set this once!