Ringleader includes extensions, handler builder filters, and interfaces that extend the DefaultHttpClientFactory
implementation to make customizing primary handler and cookie behavior for typed/named HTTP clients easier without losing the pooling and handler pipeline benefits of IHttpClientFactory
Ringleader is available from NuGet, or can be built from this source along with a sample project and XUnit tests. It includes extensions for registering your classes to the ASP NET Core DI service container during startup.
The .NET DefaultHttpClientFactory
implementation offers a number of benefits in terms of managing HttpClient
instances, including managed reuse and disposal of primary handlers and adding handler pipelines and policies using named or typed clients, as described at https://docs.microsoft.com/en-us/dotnet/architecture/microservices/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests
All instances of a named or typed client will use the same primary handler configuration, with a single handler shared in the pool for each type. For most per-request settings, this may not be an issue, but as the primary handler cannot be reconfigured per request and is not exposed once the HttpClient
is returned, there is no way to customize handler-level properties like certificates based on some contextual element of the request.
Suppose that we have a typed client called CommerceHttpClient
with a well-defined set of calls and a robust delegating handler and retry pipeline. The service behind our typed client authenticates via certificates, and there are maybe 4 or 5 different certificates needed depending on the subdomain in the URL of a given request. Under the DefaultHttpClientFactory
implementation, you would need to register a different typed client and pipeline for each subdomain so that the primary handler delegate configures the certificate correctly, and moreso you would have to perform this registration for all known iterations of the sites at the composition root. Bummer.
A quick web search for "change certificate per request httpclientfactory" shows that this is not an uncommon problem, and most of the answers are less than ideal, summing up to "create your HttpClient
manually," which means you may lose several of the benefits of IHttpClientFactory
.
By adding in a few additional classes and components that wrap the existing DefaultHttpClientFactory
implementation, we can establish a pattern for requesting a typed client that has a primary handler partitioned for a given string-based context. That could be part of your request URL, your logged in user, the current date, whatever. Best of all, we keep all the base functionality and benefits that IHttpClientFactory
can offer.
Under the hood, Ringleader uses a decorator and custom builder filter that takes advantage of the consistent use of IOptionsMonitor<HttpClientFactoryOptions>
within the DefaultHttpClientFactory
implementation to intercept and split client configuration and pool entry naming behavior to resolve unique primary handlers in the pool specific to not only the typed client, but the passed context, as well. They will be managed and recycled just like any other handlers, and should not interfere with any handlers generated by other clients that are generated using the standard IHttpClientFactory
approach.
Ringleader exposes an interface called IContextualHttpClientFactory
that resembles IHttpClientFactory
and allows resolving typed or named clients, but adds a second parameter for partitioning the primary handler by a specified context.
In order to enable the supplied context to provision a handler, a second interface IPrimaryHandlerFactory
is used that accepts the client name and context to optionally return a customized HttpMessageHandler
with the appropriate configuration.
public interface IContextualHttpClientFactory
{
TClient CreateClient<TClient>(string handlerContext) where TClient : class;
HttpClient CreateClient(string clientName, string handlerContext);
}
public interface IPrimaryHandlerFactory
{
// TypedClientSignature is a convenience wrapper implicitly convertable to and from a string
HttpMessageHandler? CreateHandler(TypedClientSignature clientName, string handlerContext);
}
In order to register the Ringleader HttpClientFactory interfaces, use the extensions during startup in addition to your normal use of AddHttpClient()
to set up named or typed clients. You may register the primary handler factory as a singleton implementation for more advanced logic that requires additional dependency injection, or alternatively supply a function that optionally returns a customized handler directly at registration.
New in 3.x: A convenience type called
TypedClientSignature
wraps theclient
parameter to enable consistent conversions and comparisons with the string format used byDefaultHttpClientFactory
. This type supports implicit conversion to and from a normal string, but allows explicit matching withclient.IsTypedClient<T>()
using System.Net.Http;
// Program.cs services registration ...
builder.Services.AddHttpClient<ExampleTypedClient>();
builder.Services.AddContextualHttpClientFactory((client, context) =>
{
// This replaces 2.x: if (typeof(ExampleTypedClient).Name == client)
if (client.IsTypedClient<ExampleTypedClient>())
{
var handler = new SocketsHttpHandler();
if (context == "certificate-one")
{
// Your handler customization here
handler.SslOptions = new System.Net.Security.SslClientAuthenticationOptions()
{
ClientCertificates = new X509Certificate2Collection()
};
}
return handler;
}
// The default implementation will be used if you return null
return null;
});
//...
Previously, Ringleader was not able to correctly resolve contextual typed clients that were registered and resolved against an interface. Due to the way that DefaultHttpClientFactory
resolves interface-registered typed client resolution from the service provider, interface-registered typed clients are now supported by Ringleader but require an explicit addition during services registration.
using System.Net.Http;
// Program.cs services registration ...
builder.Services.AddHttpClient<IExampleTypedClient, ExampleTypedClient>();
builder.Services.AddContextualHttpClientFactory<MyPrimaryHandlerFactory>()
.WithTypedClientImplementation<IExampleTypedClient, ExampleTypedClient>();
//...
// This now works
var client = _clientFactory.CreateClient<IExampleTypedClient>(context);
Inject IContextualHttpClientFactory
into your controllers and classes. Named and typed clients generated by the factory will have the delegating handler pipeline and policies in place as if they were fetched normally, but handlers will be partitioned by the context you supply and customized based on the primary handler factory behavior you registered.
public class ExampleController : ControllerBase
{
private readonly IContextualHttpClientFactory _clientFactory;
public ExampleController(IContextualHttpClientFactory clientFactory)
{
_clientFactory = clientFactory;
}
public async Task MakeHttpCall(Uri uri)
{
string context = uri.Host == "something" ? "certificate-one": "no-certificate";
var client = _clientFactory.CreateClient<ExampleTypedClient>(context);
await client.MyMethodHere();
...
}
}
Using the DefaultHttpClientFactory
implementation up through .NET 8, the decorated behavior is consistent such that normal usage of IHttpClientFactory
should be unaffected by the partitioning method applied by IContextualHttpClientFactory
. You should review any libraries, extensions, or other customizations that add or modify the list of registered IHttpMessageHandlerBuilderFilter
implementations for compatibility as this may cause unexpected effects if they attempt to use the unparsed value passed via Builder.Name
.
In .NET, the CookieContainer
that applies cookie state across multiple requests is attached to the primary message handler of an HttpClient
and not the client itself. This means that handler pooling mechanisms introduced with IHttpClientFactory
can make cookie state difficult to use as handlers are frequently recycled as clients are instantiated. Furthermore, there is no straightforward interface for grouping cookie management within a specific client based on context, for example multiple requests made using one client but on behalf of different credentials.
Using a combination of a handler builder filter, a delegating handler, and HttpRequestMessage
options, Ringleader makes it easier to disable cookie management at the primary handler level for named or typed clients, opting instead to manage cookie state using containers applied on a per-request basis. These containers are provisioned and resolved using an interface that allows you to create custom implementations for persistence instead, negating ambiguity of cookie state supplied when handlers are recycled or disposed.
In order to opt a named or typed client into per-request cookie behaviors at startup, use the following extension when registering the client:
builder.Services
.AddHttpClient<ExampleTypedClient>()
.UseContextualCookies();
The ICookieContainerCache
implementation used can be optionally customized. If not called, a basic concurrent dictionary and cloning approach will be added by default. Due to scoping behaviors, custom implementations should be a singleton, and you should ensure containers are copied/cloned or freshly instantiated before adding to or retrieving from the cache.
builder.Services.AddCookieContainerCache<MyCustomContainerCache>();
In the typed client implementation you opted in, cookie context can be set for any underlying HttpClient
request that is made using an HttpRequestMessage
:
var request = new HttpRequestMessage(HttpMethod.Get, "https://www.example.com");
request.SetCookieContext("cookie-container-name");
return _httpClient.SendAsync(request, cancellationToken);
You may access a copy of the most recent cookie container state through the ICookieContainerCache
interface, as well as update the cached copy using AddOrUpdate()
.
var cookieContainer = await _cookieContainerCache.GetOrAdd<ExampleTypedClient>("cookie-container-name", token);
string cookieHeader = cookieContainer.GetCookieHeader(new Uri("https://www.example.com"));
The filter builders attempt to toggle the UseCookies
flag of the primary handler as late in the pipeline as possible so that it is not impacted by changes to handler instantiation or other modifications. That said, you should test thoroughly if you modify primary handler behavior.
It will (probably) use normal cookie behavior instead. The opt-in customizes returned primary handlers so that the UseCookies
flag is toggled to false and adds delegating handlers to apply the customized cookie scoping behavior. If these are not present, the options set with the SetCookieContext()
extension will be ignored.
Yes. The handler builder filter for the cookies component has been designed to work within the contraints of the Ringleader IHttpClientFactory
extensions regarding handler builder filter behavior.
You bet! These extensions were designed to enable better control over cookies within the .NET HttpClient
and IHttpClientFactory
ecosystem, should you choose (or need) to use them.