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/JsonContentand 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
DelegatingHandlerif you have a dynamic header you always need settingUse
DefaultRequestHeaderswhen you have a static header you always need a setting but only set this once!