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:

  1. Go back to hand-crafting the HttpResponseMessage / JsonContent and adding the headers there.

  2. Create some extensions ourselves

  3. 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);
}

Taken from - https://github.com/dotnet/runtime/blob/main/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Post.cs

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"
    });
💡
As mentioned before, there are a lot of overloads you could potentially add header support to. If going down this route, just add them as you need them.

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.

💡
Microsoft's documentation on the subject is fairly thorough and I'd recommend reading it - link here. Worth noting that it talks about some now fairly old ASP.NET WebAPI bits which can be ignored.

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>()
💡
A quick note here - this would add our header to all 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 setting

  • Use DefaultRequestHeaders when you have a static header you always need a setting but only set this once!

Did you find this article valuable?

Support Daniel Edwards by becoming a sponsor. Any amount is appreciated!