In this article, we’ll explore HttpClient’s delegating handlers in ASP.NET Core.
These handlers act as a middleware pipeline for HTTP requests and responses, allowing us to intercept, process, and modify HTTP traffic before it reaches external applications.
What Are Delegating Handlers?
Delegating Handlers in HttpClient
closely resembles ASP.NET Core’s middleware architecture. Each handler processes incoming HTTP requests and outgoing responses sequentially, similar to the chain of responsibility pattern. This design helps us implement modular, extensible request and response handling.
HttpClient
and middleware in ASP.NET Core, then check out our articles HttpClient with ASP.NET Core Tutorial and ASP.NET Core Middleware – Creating Flexible Application Flows.Benefits of Using Delegating Handlers
Delegating Handlers promote modularity, allowing for separation of concerns, such as logging, authentication, or error handling, simplifying development and maintenance.
They are reusable across different applications or parts of the same application. Once written, handlers can be leveraged across various components, reducing duplication efforts and promoting code reuse in line with the DRY (Don’t Repeat Yourself) principle.
Adjusting the order or chain of handlers is straightforward. This flexibility enables quick adaptation to changing requirements or the introduction of new processing logic without significant code modifications.
Handlers are independently testable, simplifying unit testing.
Handler order and logic provide precise control over HTTP request and response flow. This level of control is useful in scenarios like error recovery or conditional processing, where specific actions need to be taken based on the request or response characteristics.
Delegating Handlers can enforce consistent security measures across all HTTP calls. This includes applying standard headers, enforcing authentication and authorization, or logging security-related information.
Moving forward, let’s explore how to create and use a DelegatingHandler
.
Creating a Basic Delegating Handler
Let’s start by defining a SimpleHandler
that inherits from DelegatingHandler
and overrides the SendAsync()
method:
public class SimpleHandler(ILogger<SimpleHandler> logger) : DelegatingHandler { protected override async Task<HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { logger.LogInformation("Hello from SimpleHandler"); var response = await base.SendAsync(request, cancellationToken); logger.LogInformation("Goodbye from SimpleHandler"); return response; } }
In this handler, we inject an ILogger<SimpleHandler>
to log messages before and after processing each HTTP request.
Next, we register this handler in .NET Core’s DI container as transient. Once registered, we add it to our named HttpClient
:
builder.Services.AddTransient<SimpleHandler>(); builder.Services.AddHttpClient("ExtendedClient", (httpClient) => { httpClient.BaseAddress = new Uri("https://localhost:7133"); }) .AddHttpMessageHandler<SimpleHandler>();
This setup ensures SimpleHandler
logs messages before and after every HTTP call made by ExtendedClient
.
To demonstrate this in action, let’s create a minimal API that internally consumes the client:
app.MapGet("/api/simple-handler-demo", async (IHttpClientFactory clientFactory) => { using var httpClient = clientFactory.CreateClient("ExtendedClient"); var response = await httpClient.GetAsync("/api/external-service"); return Results.Ok(response); });
Upon calling this API, we observe log messages from SimpleHandler
in the console output:
info: HttpClientDelegatingHandlersInAspNetCore.DelegatingHandlers.SimpleHandler[0] Hello from SimpleHandler info: HttpClientDelegatingHandlersInAspNetCore.DelegatingHandlers.SimpleHandler[0] Goodbye from SimpleHandler
Chaining Multiple Delegating Handlers Together
We can chain multiple handlers to manage different aspects of HTTP requests.
Let’s demonstrate this with AuthorizationHandler
and MetricsHandler
.
AuthorizationHandler
Here, our AuthorizationHandler
class generates a random Guid
for demonstration purposes, and sets the Authorization
header of the outgoing HTTP request to a Bearer token with this Guid value:
public class AuthorizationHandler(ILogger<AuthorizationHandler> logger) : DelegatingHandler { protected override async Task<HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { logger.LogInformation("Hello from AuthorizationHandler"); var token = Guid.NewGuid().ToString(); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); var response = await base.SendAsync(request, cancellationToken); logger.LogInformation("Goodbye from AuthorizationHandler"); return response; } }
After adding the token to the Authorization
header, the request continues down the pipeline.
MetricsHandler
Our MetricsHandler
class, meanwhile, tracks how long each request takes to complete:
public class MetricsHandler(ILogger<MetricsHandler> logger) : DelegatingHandler { protected override async Task<HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { logger.LogInformation("Hello from MetricsHandler"); var stopwatch = Stopwatch.StartNew(); var response = await base.SendAsync(request, cancellationToken); stopwatch.Stop(); logger.LogInformation("Request duration for {uriPath}: {elapsedMs}ms", request.RequestUri.AbsoluteUri, stopwatch.ElapsedMilliseconds); logger.LogInformation("Goodbye from MetricsHandler"); return response; } }
Here, we use a Stopwatch
to measure request duration, but typically, we’d use more precise tools.
Registering and Using the Handlers
Let’s first register these handlers in the DI container:
builder.Services .AddTransient<AuthorizationHandler>() .AddTransient<MetricsHandler>();
Next, we add them to the HttpClient
:
builder.Services.AddHttpClient("ChainedClient", (httpClient) => { httpClient.BaseAddress = new Uri("https://localhost:7133"); }) .AddHttpMessageHandler<AuthorizationHandler>() .AddHttpMessageHandler<MetricsHandler>();
The order of execution depends on the order of registration. In this setup, AuthorizationHandler
runs first, followed by MetricsHandler
.
To demonstrate this, let’s create another minimal API:
app.MapGet("/api/chained-handlers-demo", async (IHttpClientFactory clientFactory) => { using var httpClient = clientFactory.CreateClient("ChainedClient"); var response = await httpClient.GetAsync("/api/external-service"); return Results.Ok(response); });
Here, we retrieve an instance of our ChainedClient
from the IHttpClientFactory
interface.
Upon calling this API, we observe the sequence of log messages:
info: HttpClientDelegatingHandlersInAspNetCore.DelegatingHandlers.AuthorizationHandler[0] Hello from AuthorizationHandler info: HttpClientDelegatingHandlersInAspNetCore.DelegatingHandlers.MetricsHandler[0] Hello from MetricsHandler info: HttpClientDelegatingHandlersInAspNetCore.DelegatingHandlers.MetricsHandler[0] Request duration for https://localhost:7133/api/external-service: 168ms info: HttpClientDelegatingHandlersInAspNetCore.DelegatingHandlers.MetricsHandler[0] Goodbye from MetricsHandler info: HttpClientDelegatingHandlersInAspNetCore.DelegatingHandlers.AuthorizationHandler[0] Goodbye from AuthorizationHandler
As expected, the first log message comes from the AuthorizationHandler
, followed by the MetricsHandler
logging before sending the HTTP request. The third message shows the request duration. The fourth message is from the MetricsHandler
again, and finally, the AuthorizationHandler
logs before the chain exits.
Recommendations for Delegating Handlers
Always remember that handlers are invoked in the same order they are added to the client through the AddHttpMessageHandler()
method.
It’s best to register delegating handlers as transient to ensure a new instance for each HTTP request.
However, exercise caution because named and typed clients pool and reuse handlers. Despite being registered as transient, their management by IHttpClientFactory
might cause confusion if not carefully managed.
Regardless of the registration method, named and typed clients keep handlers for a standard two-minute lifetime. New instances are only created after this period to enhance resource efficiency and performance, although this interval can be modified at the time of handler registration with SetHandlerLifetime()
.
For instance, let’s consider the scenario where we create a TransientIdentifiableHandler
, which receives a unique ID each time a new instance is generated:
public class TransientIdentifiableHandler(ILogger<TransientIdentifiableHandler> logger) : DelegatingHandler { private readonly Guid _id = Guid.NewGuid(); protected override async Task<HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { logger.LogInformation("Request made from handler with Id: {id}", _id); return await base.SendAsync(request, cancellationToken); } }
First, we register it as transient, and then we add it to a named HttpClient
:
builder.Services .AddTransient<TransientIdentifiableHandler>(); builder.Services.AddHttpClient("HandlerLifeTimeDemoClient", (httpClient) => { httpClient.BaseAddress = new Uri("https://localhost:7133"); }) .AddHttpMessageHandler<TransientIdentifiableHandler>();
Subsequently, in a minimal API setup, we create a client, invoke an HTTP request, wait for two seconds, and then create another client to send another request:
app.MapGet("/api/handler-lifetime-demo", async (IHttpClientFactory clientFactory) => { using var httpClient1 = clientFactory.CreateClient("HandlerLifeTimeDemoClient"); var response1 = await httpClient1.GetAsync("/api/external-service"); await Task.Delay(TimeSpan.FromSeconds(2)); using var httpClient2 = clientFactory.CreateClient("HandlerLifeTimeDemoClient"); var response2 = await httpClient2.GetAsync("/api/external-service"); return Results.Ok(new[] { response1, response2 }); });
Observing the console output, we notice that the same handler processed both requests:
info: HttpClientDelegatingHandlersInAspNetCore.DelegatingHandlers.TransientIdentifiableHandler[0] Request made from handler with Id: 7d6e1e16-6583-4181-ba75-103578ffab9b info: HttpClientDelegatingHandlersInAspNetCore.DelegatingHandlers.TransientIdentifiableHandler[0] Request made from handler with Id: 7d6e1e16-6583-4181-ba75-103578ffab9b
However, with just a single line added during client registration, setting the handler lifetime to 1 second:
builder.Services.AddHttpClient("HandlerLifeTimeDemoClient", (httpClient) => { httpClient.BaseAddress = new Uri("http://localhost:7133"); }) .AddHttpMessageHandler<TransientIdentifiableHandler>() .SetHandlerLifetime(TimeSpan.FromSeconds(1));
The console output changes significantly, indicating that two different handlers processed the requests from the two clients:
info: HttpClientDelegatingHandlersInAspNetCore.DelegatingHandlers.TransientIdentifiableHandler[0] Request made from handler with Id: e8d04682-4440-47ea-9ef5-27a1c797fbc9 info: HttpClientDelegatingHandlersInAspNetCore.DelegatingHandlers.TransientIdentifiableHandler[0] Request made from handler with Id: e033c753-6ce9-4c64-be4d-088635af23c8
It’s important to note that there’s no one-size-fits-all solution for determining the appropriate handler lifetime. We must carefully evaluate the value if we need to adjust it, considering factors such as performance, resource utilization, and application requirements.
Conclusion
HttpClient’s Delegating Handlers offer a flexible and modular approach to handling HTTP communications in .NET. By leveraging these handlers, we can create more robust, scalable, and maintainable applications.