At the heart of every distributed .NET application lies the humble and unassuming HttpClient
class. Arguably one of the most important implementations in the base class library, the HttpClient
allows developers to communicate with external HTTP services and connect applications conveniently. A class that negotiates the intricacies of the HTTP protocol for you, what more could you want?
Well, .NET developers always want more, and in this post, we’ll discuss how at Duende, we use DelegatingHandler
implementations in our free open-source libraries to give you more flexibility, convenience, and power.
What is an HttpClient?
As the name suggests, the HttpClient
is an HTTP client that executes HTTP protocol requests and handles responses. It abstracts away the complexities of HTTP into a straightforward request and response pattern. Let’s take a look at an example use for the HttpClient
.
var client = new HttpClient();
var response =
await client
.GetAsync("https://duendesoftware.com");
var html =
await response
.Content
.ReadAsStringAsync();
Console.WriteLine(html);
In the example, we issue a GET
request to read the HTML content of the Duende website. What’s not immediately apparent in the example is that the HttpClient
, and more specifically its default message handler, does so much more for us:
- Validate the request URI.
- Negotiate the Secure Socket Layer handshake and verify the validity of the certificate.
- Initiate the
GET
request according to the HTTP protocol. - Parse the HTTP response, including headers and status code.
- Stream the content from the remote server to the client.
- Manage and throw exception states on failed requests.
That’s a lot happening in two lines of code, and we get all that and more when using HttpClient
. But what if we want to do more than the default HttpClient
does? That’s where the DelegatingHandler
comes in, as the HttpClient
is a pluggable wrapper for delegating handlers.
What is a Delegating Handler?
The DelegatingHandler
class allows developers to intercept the invocation of a request before it is issued and handle the event when the target returns a response. During the lifecycle of the handler, you can execute any code to complete the request. Some everyday use cases for writing delegating handlers include:
- Exception handling and retry resiliency
- Appending additional headers
- Serialization handling of requests and responses
- Telemetry and logging
- Caching
Let’s look at our example again, but this time, we’ll write a DelegatingHandler
that adds a request header to all outgoing requests.
var handler = new HelloHandler();
var client = new HttpClient(handler);
var response =
await client
.GetAsync("http://duendesoftware.com/");
var html =
await response
.Content
.ReadAsStringAsync();
Console.WriteLine(html);
public class HelloHandler : DelegatingHandler
{
public HelloHandler()
{
InnerHandler = new HttpClientHandler();
}
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
request.Headers.Add("X-Hello", "World");
return base.SendAsync(request, cancellationToken);
}
}
An important note about the HelloHandler
class is that you must set the InnerHandler
to a protocol or other delegating handler. In most cases, you want the terminating handler to be a SocketsHttpHandler
. Now, on each use of our HttpClient
, we’ll add the X-Hello
header with a value of World
.
The example shows how a single HttpClient
can have a different InnerHandler
, but what do you do in an ASP.NET Core application when you require multiple delegating handlers for an HTTP client
?
DelegatingHandlers in ASP.NET Core
ASP.NET Core and its dependency injection provide developers with an IHttpClientFactory
abstraction that allows them to configure one or more HttpClient
instances for use within their web applications. The dependency injection APIs enable you to create named client instances and configure the internal behavior of each instance, including what DelegatingHandler
implementations are part of the request/response lifecycle.
Let’s look at an example that adds multiple delegating handlers and then issues a request.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient("MyClient")
.AddHttpMessageHandler(svc => new Countdown(3, svc.GetRequiredService<ILogger<Countdown>>()))
.AddHttpMessageHandler(svc => new Countdown(2, svc.GetRequiredService<ILogger<Countdown>>()))
.AddHttpMessageHandler(svc => new Countdown(1, svc.GetRequiredService<ILogger<Countdown>>()))
.AddHttpMessageHandler(svc => new Countdown(0, svc.GetRequiredService<ILogger<Countdown>>()));
var app = builder.Build();
app.MapGet("/", async (IHttpClientFactory clientFactory) =>
{
var response = await clientFactory
.CreateClient("MyClient")
.PostAsync(
"http://localhost:5217/hello",
new FormUrlEncodedContent([])
);
var content = await response.Content.ReadAsStringAsync();
return $"Hello World! {content}";
});
var count = 0;
app.MapPost("/hello", () => ++count);
app.Run();
public class Countdown(int number, ILogger<Countdown> logger) : DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
logger.LogInformation(number == 0 ? "🚀 Blast Off!" : $"{number}...");
return base.SendAsync(request, cancellationToken);
}
}
Each call to AddHttpMessageHandler
adds a new delegating handler instance that executes in the registration order. Looking at the output, we see the following:
info: System.Net.Http.HttpClient.MyClient.LogicalHandler[100]
Start processing HTTP request POST http://localhost:5217/hello
info: Countdown[0]
3...
info: Countdown[0]
2...
info: Countdown[0]
1...
info: Countdown[0]
🚀 Blast Off!
info: System.Net.Http.HttpClient.MyClient.ClientHandler[100]
Sending HTTP request POST http://localhost:5217/hello
info: System.Net.Http.HttpClient.MyClient.ClientHandler[101]
Received HTTP response headers after 17.7313ms - 200
info: System.Net.Http.HttpClient.MyClient.LogicalHandler[101]
End processing HTTP request after 25.5661ms - 200
While the HttpClient
executes each registered handler in order of registration, the client will only perform the web request once. The execution behavior is because, as we saw previously, we register each handler as an InnerHandler
of the previous one, using a Russian doll model approach. The innermost handler is the final stop in the call chain and is responsible for the low-level networking call.
As you can imagine, this adds many capabilities and options to existing ASP.NET Core applications without the headache of removing, reregistering, and manipulating existing delegating handlers.
Now that you’ve seen how the HttpClient
and DelegatingHandler
classes work generally, how does Duende use this abstraction to deliver security features to .NET developers?
Delegating Handlers in Duende.AccessTokenManagement
Duende.AccessTokenManagement
library provides automatic token management to ASP.NET Core and worker service by allowing developers to register several delegating handlers. These delegating handlers include the RefreshTokenDelegatingHandler
, ProofTokenMessageHandler
, and several derived types from AccessTokenHandler
. To use the library, you must first install the NuGet package in your existing project.
dotnet add package Duende.AccessTokenManagement
Then, register the OpenIdConnectUserAccessTokenHandler
as part of your dependency injection pipeline.
// registers HTTP client that uses the managed user access token
builder.Services.AddUserAccessTokenHttpClient("invoices",
configureClient: client => { client.BaseAddress = new Uri("https://api.company.com/invoices/"); });
The handler implementation uses all the features provided by the NuGet package.
// Copyright (c) Duende Software. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
using Microsoft.Extensions.Logging;
namespace Duende.AccessTokenManagement.OpenIdConnect;
/// <summary>
/// Delegating handler that injects the current access token into an outgoing request
/// </summary>
public class OpenIdConnectUserAccessTokenHandler : AccessTokenHandler
{
private readonly IUserAccessor _userAccessor;
private readonly IUserTokenManagementService _userTokenManagement;
private readonly UserTokenRequestParameters _parameters;
/// <summary>
/// ctor
/// </summary>
/// <param name="dPoPProofService"></param>
/// <param name="dPoPNonceStore"></param>
/// <param name="userAccessor"></param>
/// <param name="userTokenManagement"></param>
/// <param name="logger"></param>
/// <param name="parameters"></param>
public OpenIdConnectUserAccessTokenHandler(
IDPoPProofService dPoPProofService,
IDPoPNonceStore dPoPNonceStore,
IUserAccessor userAccessor,
IUserTokenManagementService userTokenManagement,
ILogger<OpenIdConnectClientAccessTokenHandler> logger,
UserTokenRequestParameters? parameters = null)
: base(dPoPProofService, dPoPNonceStore, logger)
{
_userAccessor = userAccessor;
_userTokenManagement = userTokenManagement;
_parameters = parameters ?? new UserTokenRequestParameters();
}
/// <inheritdoc/>
protected override async Task<ClientCredentialsToken> GetAccessTokenAsync(bool forceRenewal, CancellationToken cancellationToken)
{
var parameters = new UserTokenRequestParameters
{
SignInScheme = _parameters.SignInScheme,
ChallengeScheme = _parameters.ChallengeScheme,
Resource = _parameters.Resource,
Context = _parameters.Context,
ForceRenewal = forceRenewal,
};
var user = await _userAccessor.GetCurrentUserAsync();
return await _userTokenManagement.GetAccessTokenAsync(user, parameters, cancellationToken).ConfigureAwait(false);
}
}
You can also add the handlers to any typed HttpClient
.
// registers a typed HTTP client with token management support
builder.Services.AddHttpClient<InvoiceClient>(client =>
{
client.BaseAddress = new Uri("https://api.company.com/invoices/");
})
.AddUserAccessTokenHandler();
From here, the Duende.AccessTokenManagement
library provides automatic access token management features for .NET worker and ASP.NET Core web applications:
- automatic acquisition and lifetime management of client credentials based access tokens for machine-to-machine communication
- automatic access token lifetime management using a refresh token for API calls on behalf of the currently logged-in user
- Revocation of access tokens
That’s functionality that users adopting OAuth and OpenID Connect require for their applications, but seldom want to manage or think about themselves. Luckily for .NET developers, we’ve done all the hard work for you.
To learn more about the Duende.AccessTokenManagement
library, read through our documentation for in-depth details and samples.
Conclusion
The HttpClient
and DelegatingHandler
classes provide significant flexibility and power when working with HTTP in .NET applications. While HttpClient
handles the fundamental aspects of making requests and receiving responses, DelegatingHandlers
enable developers to intercept and modify these requests and responses, extending the base functionality. These handlers are useful for exception handling, header manipulation, logging, and security features like token management.
By effectively understanding and utilizing HttpClient
and DelegatingHandlers
, .NET developers can enhance their applications' reliability, security, and overall functionality.
As always, we’d love to hear from you in the comments section.