Skip to main content

Extending HttpClient With Delegating Handlers in ASP.NET Core

About 4 minC#DotNetArticle(s)blogmilanjovanovic.techcsc#csharpdotnet

Extending HttpClient With Delegating Handlers in ASP.NET Core 관련

C# > Article(s)

Article(s)

Extending HttpClient With Delegating Handlers in ASP.NET Core
Delegating handlers are like ASP.NET Core middleware. Except they work with the HttpClient. I'll show you how to work with delegating handlers

Delegating handlersopen in new window are like ASP.NET Core middleware. Except they work with the HttpClient. The ASP.NET Core request pipeline allows you to introduce custom behavior with middleware. 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.

HttpClientopen in new window 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 DelegatingHandleropen in new window 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.

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

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.

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.

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 resilientopen in new window applications is an important requirement for cloud development.

The RetryDelegatingHandler class uses Polly (App-vNext/Polly)open in new window to create an AsyncRetryPolicy. The retry policy wraps the HTTP request and retries it in case of a transient failure.

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.

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.

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:

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 Keycloakopen in new window. Keycloak is an open-source identity and access management service.

I used Keycloak as the identity provider in my Pragmatic Clean Architecture course.

The delegating handler in this example uses an OAuth 2.0open in new window client credentialsopen in new window 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.

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 handlersopen in new window, and you can watch it here.open in new window

Thanks for reading, and stay awesome!


이찬희 (MarkiiimarK)
Never Stop Learning.