Skip to content

Extends the .NET DefaultHttpClientFactory to support contextually appropriate primary handlers (i.e. unique certificates for different endpoints) and per-request cookie scope while retaining the benefits of handler pooling and typed client pipelines

License

Notifications You must be signed in to change notification settings

agertenbach/Ringleader

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Ringleader

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

How do I use it?

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.

Ringleader for HttpClientFactory

What is the problem?

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.

How does Ringleader help?

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.

Registering IContextualHttpClientFactory at startup

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 the client parameter to enable consistent conversions and comparisons with the string format used by DefaultHttpClientFactory. This type supports implicit conversion to and from a normal string, but allows explicit matching with client.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;
});
//...

New in 3.x: Typed clients with interfaces

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);

Using IContextualHttpClientFactory in your application

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();
            ...
        }
    }

Questions / FAQ / Notes

Does this break IHttpClientFactory usage outside of Ringleader?

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.

Ringleader for Cookies

What is the problem?

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.

How does Ringleader help?

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.

Registering contextual cookie support at startup

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>();

Applying cookie context on HttpRequestMessage for a registered client

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"));

Questions / FAQ / Notes

I have other actions that modify the primary handler for my client. Is this a problem?

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.

What happens if I try to use the extensions without opting the client in?

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.

Does this work with the Ringleader IHttpClientFactory extensions?

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.

Couldn't I just use something like Flurl?

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.

About

Extends the .NET DefaultHttpClientFactory to support contextually appropriate primary handlers (i.e. unique certificates for different endpoints) and per-request cookie scope while retaining the benefits of handler pooling and typed client pipelines

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages