Extending HttpClient With Delegating Handlers in ASP.NET Core
Tags: aspnetcore, dotnet, api, httpclient
Published: 2024-01-16 18:53:10

---
title: Extending HttpClient With Delegating Handlers in ASP.NET Core
published: true
date: 2024-01-13 00:00:00 UTC
tags: aspnetcore,dotnet,apis,httpclient
canonical_url: https://www.milanjovanovic.tech/blog/extending-httpclient-with-delegating-handlers-in-aspnetcore
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/44du8yi83ayej8piew89.png
---
[Delegating handlers](https://learn.microsoft.com/en-us/dotnet/api/system.net.http.delegatinghandler?view=net-8.0) are like [ASP.NET Core middleware](https://www.milanjovanovic.tech/blog/3-ways-to-create-middleware-in-asp-net-core). Except they work with the [`HttpClient`](https://www.milanjovanovic.tech/blog/the-right-way-to-use-httpclient-in-dotnet). The ASP.NET Core request pipeline allows you to introduce custom behavior with [middleware.](https://www.milanjovanovic.tech/blog/3-ways-to-create-middleware-in-asp-net-core)You can solve many cross-cutting concerns using middleware — logging, tracing, validation, authentication, authorization, etc.

But, an important aspect here is that middleware works with incoming HTTP requests to your API. Delegating handlers work with outgoing requests.

[`HttpClient`](https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpclient?view=net-8.0) is my preferred way of sending HTTP requests in ASP.NET Core. It's straightforward to use and solves most of my use cases. You can use delegating handlers to extend the `HttpClient` with behavior before or after sending an HTTP request.

Today, I want to show you how to use a [`DelegatingHandler`](https://learn.microsoft.com/en-us/dotnet/api/system.net.http.delegatinghandler?view=net-8.0) to introduce:

- Logging
- Resiliency
- Authentication

## Configuring an HttpClient

Here's a very simple application that:

- Configures the `GitHubService` class as a typed HTTP client
- Sets the `HttpClient.BaseAddress` to point to the GitHub API
- Exposes an endpoint that retrieves a GitHub user by their username

We're going to extend the `GitHubService` behavior using delegating handlers.

```csharp
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpClient<GitHubService>(httpClient =>
{
    httpClient.BaseAddress = new Uri("https://api.github.com");
});

var app = builder.Build();

app.MapGet("api/users/{username}", async (
    string username,
    GitHubService gitHubService) =>
{
    var content = await gitHubService.GetByUsernameAsync(username);

    return Results.Ok(content);
});

app.Run();
```

The `GitHubService` class is a [typed client](https://www.milanjovanovic.tech/blog/the-right-way-to-use-httpclient-in-dotnet#replacing-named-clients-with-typed-clients) implementation. Typed clients allow you to expose a strongly typed API and hide the `HttpClient`. The runtime takes care of providing a configured `HttpClient` instance through dependency injection. You also don't have to think about disposing of the `HttpClient`. It's resolved from an underlying `IHttpClientFactory` that manages the `HttpClient` lifetime.

```csharp
public class GitHubService(HttpClient client)
{
    public async Task<GitHubUser?> GetByUsernameAsync(string username)
    {
        var url = $"users/{username}";

        return await client.GetFromJsonAsync<GitHubUser>(url);
    }
}
```

## Logging HTTP Requests Using Delegating Handlers

Let's start with a simple example. We will add logging before and after sending an HTTP request. For this, we will to create a custom delegating handler - `LoggingDelegatingHandler`.

The custom delegating handler implements the `DelegatingHandler` base class. Then, you can override the `SendAsync` method to introduce additional behavior.

```csharp
public class LoggingDelegatingHandler(ILogger<LoggingDelegatingHandler> logger)
    : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        try
        {
            logger.LogInformation("Before HTTP request");

            var result = await base.SendAsync(request, cancellationToken);

            result.EnsureSuccessStatusCode();

            logger.LogInformation("After HTTP request");

            return result;
        }
        catch (Exception e)
        {
            logger.LogError(e, "HTTP request failed");

            throw;
        }
    }
}
```

You also need to register the `LoggingDelegatingHandler` with dependency injection. Delegating handlers must be registered as **transient** services.

The `AddHttpMessageHandler` method adds the `LoggingDelegatingHandler` as a delegating handler for the `GitHubService`. Any HTTP request sent using the `GitHubService` will first go through the `LoggingDelegatingHandler`.

```csharp
builder.Services.AddTransient<LoggingDelegatingHandler>();

builder.Services.AddHttpClient<GitHubService>(httpClient =>
{
    httpClient.BaseAddress = new Uri("https://api.github.com");
})
.AddHttpMessageHandler<LoggingDelegatingHandler>();
```

Let's see what else we can do.

## Adding Resiliency With Delegating Handlers

Building [resilient](https://learn.microsoft.com/en-us/dotnet/core/resilience/?tabs=dotnet-cli) applications is an important requirement for cloud development.

The `RetryDelegatingHandler` class uses [Polly](https://github.com/App-vNext/Polly) to create an `AsyncRetryPolicy`. The retry policy wraps the HTTP request and retries it in case of a transient failure.

```csharp
public class RetryDelegatingHandler : DelegatingHandler
{
    private readonly AsyncRetryPolicy<HttpResponseMessage> _retryPolicy =
        Policy<HttpResponseMessage>
            .Handle<HttpRequestException>()
            .RetryAsync(2);

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        var policyResult = await _retryPolicy.ExecuteAndCaptureAsync(
            () => base.SendAsync(request, cancellationToken));

        if (policyResult.Outcome == OutcomeType.Failure)
        {
            throw new HttpRequestException(
                "Something went wrong",
                policyResult.FinalException);
        }

        return policyResult.Result;
    }
}
```

You also need to register the `RetryDelegatingHandler` with dependency injection. Also, remember to configure it as a message handler. In this example, I'm chaining two delegating handlers together, and they will run one after another.

```csharp
builder.Services.AddTransient<RetryDelegatingHandler>();

builder.Services.AddHttpClient<GitHubService>(httpClient =>
{
    httpClient.BaseAddress = new Uri("https://api.github.com");
})
.AddHttpMessageHandler<LoggingDelegatingHandler>()
.AddHttpMessageHandler<RetryDelegatingHandler>();
```

## Solving Authentication With Delegating Handlers

Authentication is a cross-cutting concern you will have to solve in any microservices application. A common use case for delegating handlers is adding the `Authorization` header before sending an HTTP request.

For example, the GitHub API requires an access token to be present for authenticating incoming requests. The `AuthenticationDelegatingHandler` class adds the `Authorization` header value from the `GitHubOptions`. Another requirement is specifying the `User-Agent` header, which is set from the app configuration.

```csharp
public class AuthenticationDelegatingHandler(IOptions<GitHubOptions> options)
    : DelegatingHandler
{
    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        request.Headers.Add("Authorization", options.Value.AccessToken);
        request.Headers.Add("User-Agent", options.Value.UserAgent);

        return base.SendAsync(request, cancellationToken);
    }
}
```

Don't forget to configure the `AuthenticationDelegatingHandler` with the `GitHubService`:

```csharp
builder.Services.AddTransient<AuthenticationDelegatingHandler>();

builder.Services.AddHttpClient<GitHubService>(httpClient =>
{
    httpClient.BaseAddress = new Uri("https://api.github.com");
})
.AddHttpMessageHandler<LoggingDelegatingHandler>()
.AddHttpMessageHandler<RetryDelegatingHandler>()
.AddHttpMessageHandler<AuthenticationDelegatingHandler>();
```

Here's a more involved authentication example using the `KeyCloakAuthorizationDelegatingHandler`. This is a delegating handler that acquires the access token from [Keycloak](https://www.keycloak.org/). Keycloak is an open-source identity and access management service.

I used Keycloak as the identity provider in my [Pragmatic Clean Architecture](https://www.milanjovanovic.tech/pragmatic-clean-architecture) course.

The delegating handler in this example uses an [OAuth 2.0](https://oauth.net/2/) [client credentials](https://www.oauth.com/oauth2-servers/access-tokens/client-credentials/) grant flow to obtain an access token. This grant is used when applications request an access token to access their own resources, not on behalf of a user.

```csharp
public class KeyCloakAuthorizationDelegatingHandler(
    IOptions<KeycloakOptions> keycloakOptions)
    : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        var authToken = await GetAccessTokenAsync();

        request.Headers.Authorization = new AuthenticationHeaderValue(
            JwtBearerDefaults.AuthenticationScheme,
            authToken.AccessToken);

        var httpResponseMessage = await base.SendAsync(
            request,
            cancellationToken);

        httpResponseMessage.EnsureSuccessStatusCode();

        return httpResponseMessage;
    }

    private async Task<AuthToken> GetAccessTokenAsync()
    {
        var params = new KeyValuePair<string, string>[]
        {
            new("client_id", _keycloakOptions.Value.AdminClientId),
            new("client_secret", _keycloakOptions.Value.AdminClientSecret),
            new("scope", "openid email"),
            new("grant_type", "client_credentials")
        };

        var content = new FormUrlEncodedContent(params);

        var authRequest = new HttpRequestMessage(
            HttpMethod.Post,
            new Uri(_keycloakOptions.TokenUrl))
        {
            Content = content
        };

        var response = await base.SendAsync(authRequest, cancellationToken);

        response.EnsureSuccessStatusCode();

        return await response.Content.ReadFromJsonAsync<AuthToken>() ??
               throw new ApplicationException();
    }
}
```

## Takeaway

Delegating handlers give you a powerful mechanism to extend the behavior when sending requests with an `HttpClient`. You can use delegating handlers to solve cross-cutting concerns, similar to how you would use middleware.

Here are a few ideas on how you could use delegating handlers:

- Logging before and after sending HTTP requests
- Introducing resilience policies (retry, fallback)
- Validating the HTTP request content
- Authenticating with an external API

I'm sure you can come up with a few use cases yourself.

I made a video showing how to [implement delegating handlers](https://youtu.be/_u6v4D6qgDI), and you can [watch it here.](https://youtu.be/_u6v4D6qgDI)

Thanks for reading, and stay awesome!

* * *

P.S. Whenever you're ready, there are 2 ways I can help you:

1. [**Pragmatic Clean Architecture:**](https://www.milanjovanovic.tech/pragmatic-clean-architecture?utm_source=dev.to&utm_medium=website&utm_campaign=cross-posting) This comprehensive course will teach you the system I use to ship production-ready applications using Clean Architecture. Learn how to apply the best practices of modern software architecture. [**Join 1,900+ students here.**](https://www.milanjovanovic.tech/pragmatic-clean-architecture?utm_source=dev.to&utm_medium=website&utm_campaign=cross-posting)

2. [**Patreon Community:**](https://www.patreon.com/milanjovanovic) Think like a senior software engineer with **access to the source code** I use in my YouTube videos and **exclusive discounts** for my courses. [**Join 980+ engineers here.**](https://www.patreon.com/milanjovanovic)