It’s common for APIs to communicate with each other using HTTP requests.
The most straightforward way to handle this in .NET is with HttpClient, which is native and easy to get started with.
However, working with HttpClient can be a real drag if you’re not familiar with the right patterns and practices.
Delegating handlers bring flexibility to HttpClient, making common tasks easier to implement and maintain.
Delegating Handlers
A delegating handler is a component that sits in the request/response pipeline of an HttpClient.
Delegating handlers function in a way similar to API middleware, except that they're built for HttpClient.
They are especially useful for centralizing cross-cutting concerns such as logging, resiliency and more.
Getting Started
Before adding delegating handlers, let’s set up a simple application that integrates with an external service via HTTP requests:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddHttpClient<CryptoApiClient>(client =>
client.BaseAddress = new Uri("https://cex.io/api/"));
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.MapGet("crypto/limits", async (CryptoApiClient cryptoApi, CancellationToken cancellationToken) =>
{
var response = await cryptoApi.GetLimits(cancellationToken);
return Results.Ok(response);
});
app.Run();
For this example, we’ll create a simple client that fetches data from a crypto API:
public class CryptoApiClient(HttpClient client)
{
public async Task<CurrencyLimitResponse?> GetLimits(CancellationToken cancellationToken = default) =>
await client.GetFromJsonAsync<CurrencyLimitResponse>(
"currency_limits",
cancellationToken);
}
Adding Delegating Handler
With the setup complete, we can now add a delegating handler:
public class LoggingDelegatingHandler(ILogger<LoggingDelegatingHandler> logger) : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
logger.LogInformation("Sending HTTP {Method} request to {Url}", request.Method, request.RequestUri);
try
{
var response = await base.SendAsync(request, cancellationToken);
logger.LogInformation("Received {StatusCode} from {Url}", response.StatusCode, request.RequestUri);
return response;
}
catch (Exception e)
{
logger.LogError(e, "Request to {Url} failed with exception", request.RequestUri);
throw;
}
}
}
To define a delegating handler, we use DelegatingHandler, which is a base class that allows you to intercept, process, or modify HTTP requests and responses within the HttpClient pipeline.
Next, register the handler with dependency injection:
builder.Services.AddTransient<LoggingDelegatingHandler>();
Each delegating handler should be registered as transient. Once registered, you can attach it to your HttpClient:
builder.Services.AddHttpClient<CryptoApiClient>(client =>
client.BaseAddress = new Uri("https://cex.io/api/"))
.AddHttpMessageHandler<LoggingDelegatingHandler>();
NOTE: If you register multiple handlers, they’ll be executed in the same order as they were added.
Now, whenever the CryptoApiClient sends a request, it will first pass through the LoggingDelegatingHandler.
Conclusion
Delegating handlers bring the flexibility of middleware to your HttpClient pipeline.
They make it easy to centralize and manage common concerns like logging, error handling, retries and more.
By registering and attaching them properly, you can keep your HTTP integrations clean, consistent and easier to maintain.
If you want to check out examples I created, you can find the source code here:
Source CodeI hope you enjoyed it, subscribe and get a notification when a new blog is up!
