In modern ASP.NET applications—especially with the advent of minimal APIs in .NET 9, configuring HTTPClients for external calls is both powerful and flexible. One of the key strengths is the ability to create named HTTPClients and extend their behavior using message handlers. In this post, we'll explore two types of message handlers:
- PrimaryMessageHandlers: Provide control over the underlying transport (e.g., enabling TLS1.3, customizing SSL certificate validation).
- DelegatingHandlers: Allow you to modify or inspect HTTP requests and responses at the application level (e.g., setting HTTP version, logging, custom headers).
Below, we’ll see how each can be used in minimal API ASP.NET services along with code examples.
Named HTTPClients in Minimal API Services
ASP.NET’s dependency injection makes it simple to register named HTTPClients. You can configure different clients with different behaviors, and later retrieve them using the IHttpClientFactory
.
Example Registration:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient("MyClient");
// Add other named clients
var app = builder.Build();
app.MapGet("/", async (IHttpClientFactory clientFactory) =>
{
var client = clientFactory.CreateClient("MyClient");
var response = await client.GetAsync("https://example.com");
return await response.Content.ReadAsStringAsync();
});
app.Run();
PrimaryMessageHandlers: Transport-level Customization
The PrimaryMessageHandler is the underlying handler that deals with network transport. By configuring it, you can control low-level details like TLS protocols and SSL certificate validation. This is especially useful when you need to enforce specific security requirements.
Example: Configuring TLS1.3 and Custom SSL Certificate Validation
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient("MyPrimaryClient")
.ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
{
SslOptions = new SslClientAuthenticationOptions
{
// Enforce TLS1.3 for secure communication
EnabledSslProtocols = System.Security.Authentication.SslProtocols.Tls13,
RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) =>
{
// Custom logic to validate SSL certificate can be added here
Console.WriteLine("Primary handler: Validating SSL certificate.");
return true; // Or implement proper validation logic
}
}
});
// Configure other named clients
var app = builder.Build();
app.MapGet("/primary", async (IHttpClientFactory clientFactory) =>
{
var client = clientFactory.CreateClient("MyPrimaryClient");
var response = await client.GetAsync("https://example.com");
return await response.Content.ReadAsStringAsync();
});
app.Run();
DelegatingHandlers: Application-level Customization
DelegatingHandlers sit in the HTTPClient pipeline and allow you to modify the request or response. They are ideal for tasks such as logging, adding custom headers, or enforcing application-level protocols like HTTP/2.
Example: Custom Delegating Handler to Enforce HTTP/2
public class CustomDelegatingHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
// Enforce HTTP/2 for the outgoing request
request.Version = HttpVersion.Version20;
Console.WriteLine("CustomDelegatingHandler: Request is set to use HTTP/2.");
return await base.SendAsync(request, cancellationToken);
}
}
Lesson Learned
Trying to use the lean modular approach of DelegatingHandlers
to apply transport-level configuration does not work. Modify the InnerHandler
of a DelegatingHandler
results an exception:
System.InvalidOperationException: The 'InnerHandler' property must be null. 'DelegatingHandler' instances provided to 'HttpMessageHandlerBuilder' must not be reused or cached.
This is due to the fact that modifying the InnerHandler
would break the handler pipeline. Wrapping each DelegatingHandler
's request in a new HttpMessageInvoker
will workaround the exception, but still break the chain.
TL;DR: Don't use DelegatingHandlers
for transport-level.
Registering the Handler with a Named HTTPClient
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddTransient<CustomDelegatingHandler>();
builder.Services.AddHttpClient("MyDelegatingClient")
.AddHttpMessageHandler<CustomDelegatingHandler>();
var app = builder.Build();
app.MapGet("/delegating", async (IHttpClientFactory clientFactory) =>
{
var client = clientFactory.CreateClient("MyDelegatingClient");
var response = await client.GetAsync("https://example.com");
return await response.Content.ReadAsStringAsync();
});
app.Run();
Conclusion
By leveraging both DelegatingHandlers and PrimaryMessageHandlers, you can finely control HTTP communication in your ASP.NET Core minimal API services:
- Named HTTP Clients provide a modular way of configuring HTTP clients for different targets.
- PrimaryMessageHandlers provide a gateway to transport-level customizations (e.g., enforcing TLS1.3, managing SSL certificate validations).
- DelegatingHandlers allow manipulation of application-level behaviors (e.g., enforcing HTTP/2, adding headers).
This layered approach enables robust, secure, and flexible HTTP interactions tailored to the specific needs of your application. Happy coding!