In this article, we’ll explore HttpClient’s delegating handlers in ASP.NET Core.

To download the source code for this article, you can visit our GitHub repository.

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.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!
If you want to know more about 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. 

To use Delegating Handlers for attaching a Bearer token to the HTTP request, check out our article How to Add a BearerToken to an HttpClient Request.

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().

If you want to know more about dependency injection lifetime, then check out our article Dependency Injection Lifetimes in ASP.NET Core.

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.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!