HoangTuBienXanh.Ringleader
2.0.1
dotnet add package HoangTuBienXanh.Ringleader --version 2.0.1
NuGet\Install-Package HoangTuBienXanh.Ringleader -Version 2.0.1
<PackageReference Include="HoangTuBienXanh.Ringleader" Version="2.0.1" />
paket add HoangTuBienXanh.Ringleader --version 2.0.1
#r "nuget: HoangTuBienXanh.Ringleader, 2.0.1"
// Install HoangTuBienXanh.Ringleader as a Cake Addin #addin nuget:?package=HoangTuBienXanh.Ringleader&version=2.0.1 // Install HoangTuBienXanh.Ringleader as a Cake Tool #tool nuget:?package=HoangTuBienXanh.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
Changes from agertenbach/Ringleader
Modifications:
- Register the delegating handler as a transient service, see AddHttpMessageHandler
- Add
IServiceProvider
parameter to theAddContextualHttpClientFactory
method. - Update TargetFramework to .NET 8
builder.Services.AddContextualHttpClientFactory((client, context, serviceProvider) =>
{
if (client == typeof(ExampleTypedClient).Name)
{
var handler = new SocketsHttpHandler();
if (context == "certificate-one")
{
// Customizations for the specific context
handler.SslOptions = new System.Net.Security.SslClientAuthenticationOptions()
{
ClientCertificates = new X509Certificate2Collection()
};
}
return handler;
}
return null;
});
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 | net8.0 is compatible. 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. |
-
net8.0
- Microsoft.Extensions.Http (>= 8.0.0)
- Microsoft.Extensions.Logging (>= 8.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.
Version | Downloads | Last updated |
---|---|---|
2.0.1 | 116 | 9/6/2024 |