Ringleader 2.0.1
dotnet add package Ringleader --version 2.0.1
NuGet\Install-Package Ringleader -Version 2.0.1
<PackageReference Include="Ringleader" Version="2.0.1" />
paket add Ringleader --version 2.0.1
#r "nuget: Ringleader, 2.0.1"
// Install Ringleader as a Cake Addin #addin nuget:?package=Ringleader&version=2.0.1 // Install Ringleader as a Cake Tool #tool nuget:?package=Ringleader&version=2.0.1
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.
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.
Product | Versions 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. |
-
net6.0
- Microsoft.Extensions.Http (>= 6.0.0)
- Microsoft.Extensions.Logging (>= 6.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.