Ringleader 3.0.0-rc

This is a prerelease version of Ringleader.
There is a newer version of this package available.
See the version list below for details.
dotnet add package Ringleader --version 3.0.0-rc                
NuGet\Install-Package Ringleader -Version 3.0.0-rc                
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Ringleader" Version="3.0.0-rc" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add Ringleader --version 3.0.0-rc                
#r "nuget: Ringleader, 3.0.0-rc"                
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install Ringleader as a Cake Addin
#addin nuget:?package=Ringleader&version=3.0.0-rc&prerelease

// Install Ringleader as a Cake Tool
#tool nuget:?package=Ringleader&version=3.0.0-rc&prerelease                

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 return an HttpMessageHandler with the appropriate configuration.

public interface IContextualHttpClientFactory
{
    TClient CreateClient<TClient>(string handlerContext);
    HttpClient CreateClient(string clientName, string handlerContext);
}

public interface IPrimaryHandlerFactory
{
    HttpMessageHandler CreateHandler(string 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, or alternatively supply a function instead that optionally returns a customized handler.

using System.Net.Http;

// Program.cs services registration ...

builder.Services.AddHttpClient<ExampleTypedClient>();

builder.Services.AddContextualHttpClientFactory((client, context) =>
{
    if (client == typeof(ExampleTypedClient).Name)
    {
        var handler = new SocketsHttpHandler();
        if (context == "certificate-one")
        {
            // your customizations here
            handler.SslOptions = new System.Net.Security.SslClientAuthenticationOptions()
            {
                ClientCertificates = new X509Certificate2Collection()
            };
        }
        return handler;
    }

    return null;
});
//...

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.

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

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.

Product Compatible and additional computed target framework versions.
.NET net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
3.0.0 75 11/25/2024
3.0.0-rc 63 11/18/2024
2.0.1 556 3/3/2024
1.0.2 7,722 3/7/2020