Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v3.0.0-rc #5

Merged
merged 7 commits into from
Nov 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/dotnetcore.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v1
uses: actions/setup-dotnet@v4
with:
dotnet-version: '6.0.x'
- name: Build with dotnet
Expand Down
40 changes: 32 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

# 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`

Expand All @@ -24,22 +25,25 @@ Under the hood, Ringleader uses a decorator and custom builder filter that takes

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.
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.

```csharp
public interface IContextualHttpClientFactory
{
TClient CreateClient<TClient>(string handlerContext);
TClient CreateClient<TClient>(string handlerContext) where TClient : class;
HttpClient CreateClient(string clientName, string handlerContext);
}

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

```csharp
using System.Net.Http;
Expand All @@ -50,25 +54,45 @@ builder.Services.AddHttpClient<ExampleTypedClient>();

builder.Services.AddContextualHttpClientFactory((client, context) =>
{
if (client == typeof(ExampleTypedClient).Name)
// This replaces 2.x: if (typeof(ExampleTypedClient).Name == client)
if (client.IsTypedClient<ExampleTypedClient>())
{
var handler = new SocketsHttpHandler();
if (context == "certificate-one")
{
// your customizations here
// 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.

```csharp
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.
Expand Down Expand Up @@ -149,4 +173,4 @@ It will (probably) use normal cookie behavior instead. The opt-in customizes ret
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.
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.
6 changes: 6 additions & 0 deletions Ringleader.sln
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RingleaderTests", "Ringlead
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebExample", "WebExample\WebExample.csproj", "{219EDA92-D634-40C0-A71A-7B49275FF086}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{95D2B26C-D658-4663-9121-13604107D090}"
ProjectSection(SolutionItems) = preProject
LICENSE = LICENSE
README.md = README.md
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down
54 changes: 54 additions & 0 deletions Ringleader/HttpClientFactory/ContextualClientFactoryOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http;
using Ringleader.Shared;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;

namespace Ringleader.HttpClientFactory
{
public class ContextualClientFactoryOptions
{
public Dictionary<TypedClientSignature, Func<IServiceProvider, HttpClient, object>> _typedClientResolvers = new();

/// <summary>
/// Register a map between a typed client interface <typeparamref name="TInterface"/> and implementation <typeparamref name="TImplementation"/>
/// </summary>
/// <typeparam name="TInterface"></typeparam>
/// <typeparam name="TImplementation"></typeparam>
public void SetTypedClientImplementation<TInterface, TImplementation>() where TInterface : class where TImplementation: class, TInterface
{
_typedClientResolvers.Add(TypedClientSignature.For<TInterface>(), DefaultResolver<TInterface, TImplementation>());
}

/// <summary>
/// Resolve a typed client implementation for <typeparamref name="T"/> using the specified service provider and <see cref="HttpClient"/>
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="sp"></param>
/// <param name="client"></param>
/// <returns></returns>
public T ResolveTypedClientImplementation<T>(IServiceProvider sp, HttpClient client) where T : class
{
if(_typedClientResolvers.TryGetValue(TypedClientSignature.For<T>(), out var resolver))
{
object t = resolver.Invoke(sp, client);
return (T)t;
}
return DefaultResolver<T, T>().Invoke(sp, client);
}

/// <summary>
/// Default resolver for a typed client of <typeparamref name="TImplementation"/> for <typeparamref name="TInterface"/>
/// </summary>
/// <typeparam name="TInterface"></typeparam>
/// <typeparam name="TImplementation"></typeparam>
/// <returns></returns>
private static Func<IServiceProvider, HttpClient, TInterface> DefaultResolver<TInterface, TImplementation>() where TInterface : class where TImplementation : class, TInterface
=> (sp, client)
=> sp.GetRequiredService<ITypedHttpClientFactory<TImplementation>>().CreateClient(client);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public static class ContextualHttpClientFactoryExtensions
/// <param name="handlerContext">A string-based context for primary handler resolution and partitioning</param>
/// <param name="handlerContextResolver">A resolver function for identifying a string-based context - if not specified, ToString() will be called</param>
/// <returns></returns>
public static TClient CreateClient<TClient, TContext>(this IContextualHttpClientFactory factory, TContext handlerContext, Func<TContext, string>? handlerContextResolver = null)
public static TClient CreateClient<TClient, TContext>(this IContextualHttpClientFactory factory, TContext handlerContext, Func<TContext, string>? handlerContextResolver = null) where TClient : class
=> factory.CreateClient<TClient>(
handlerContextResolver is null
? (handlerContext?.ToString() ?? string.Empty)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.Extensions.Options;
using Ringleader.Shared;
using System;
using System.Collections.Generic;
using System.Net.Http;
Expand All @@ -11,7 +12,7 @@ public class DefaultActionedPrimaryHandlerFactoryOptions
/// <summary>
/// Function to optionally resolve an <see cref="HttpMessageHandler"/> based on a specified client and/or context
/// </summary>
public Func<string, string, HttpMessageHandler?> HandlerFactory { get; set; }
public Func<TypedClientSignature, string, HttpMessageHandler?> HandlerFactory { get; set; }
= (client, context) => null;
}

Expand All @@ -27,7 +28,7 @@ public DefaultActionedPrimaryHandlerFactory(IOptionsMonitor<DefaultActionedPrima
_options = options ?? throw new ArgumentNullException(nameof(options));
}

public HttpMessageHandler? CreateHandler(string clientName, string handlerContext)
public HttpMessageHandler? CreateHandler(TypedClientSignature clientName, string handlerContext)
=> _options.CurrentValue.HandlerFactory.Invoke(clientName, handlerContext);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http;
using Microsoft.Extensions.Options;
using Ringleader.Shared;
using System;
using System.Net.Http;
Expand All @@ -14,21 +15,24 @@ public class DefaultContextualHttpClientFactory : IContextualHttpClientFactory
private readonly IServiceProvider _serviceProvider;
private readonly IHttpClientContextResolver _resolver;
private readonly IHttpClientFactory _httpClientFactory;
private readonly IOptionsMonitor<ContextualClientFactoryOptions> _options;

public DefaultContextualHttpClientFactory(IServiceProvider serviceProvider, IHttpClientContextResolver resolver, IHttpClientFactory httpClientFactory)
public DefaultContextualHttpClientFactory(IServiceProvider serviceProvider, IHttpClientContextResolver resolver, IHttpClientFactory httpClientFactory, IOptionsMonitor<ContextualClientFactoryOptions> options)
{
_options = options;
_serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
_resolver = resolver ?? throw new ArgumentNullException(nameof(resolver));
_httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
}

public TClient CreateClient<TClient>(string handlerContext)
public TClient CreateClient<TClient>(string handlerContext) where TClient : class
{
var client = CreateClient(typeof(TClient).Name, handlerContext);
return _serviceProvider.GetRequiredService<ITypedHttpClientFactory<TClient>>().CreateClient(client);
var client = CreateClient(TypedClientSignature.For<TClient>(), handlerContext);
return _options.CurrentValue.ResolveTypedClientImplementation<TClient>(_serviceProvider, client);
}

public HttpClient CreateClient(string name, string handlerContext)
=> _httpClientFactory.CreateClient(_resolver.CreateContextName(name, handlerContext));
=> _httpClientFactory.CreateClient(_resolver.CreateContextName(name, handlerContext));

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public static IContextualHttpClientBuilder AddContextualHttpClientFactory<TPrima
/// <param name="services"></param>
/// <param name="primaryHandlerResolver">Function for optionally resolving a custom primary <see cref="HttpMessageHandler"/> instance based on a client name and supplied handler context</param>
/// <returns></returns>
public static IContextualHttpClientBuilder AddContextualHttpClientFactory(this IServiceCollection services, Func<string, string, HttpMessageHandler?> primaryHandlerResolver)
public static IContextualHttpClientBuilder AddContextualHttpClientFactory(this IServiceCollection services, Func<TypedClientSignature, string, HttpMessageHandler?> primaryHandlerResolver)
{
services.Configure<DefaultActionedPrimaryHandlerFactoryOptions>(o => o.HandlerFactory = primaryHandlerResolver);
services.AddSingleton<IPrimaryHandlerFactory, DefaultActionedPrimaryHandlerFactory>();
Expand Down Expand Up @@ -91,5 +91,22 @@ public static IContextualHttpClientBuilder WithContextualFactory<TContextualClie
builder.Services.AddSingleton<IContextualHttpClientFactory, TContextualClientFactory>();
return builder;
}

/// <summary>
/// Register a typed client mapping between the <typeparamref name="TInterface"/> interface and <typeparamref name="TImplementation"/> implementation to enable contextual resolution
/// </summary>
/// <typeparam name="TInterface">Typed client interface</typeparam>
/// <typeparam name="TImplementation">Typed client implementation of <typeparamref name="TInterface"/></typeparam>
/// <param name="builder"></param>
/// <returns></returns>
public static IContextualHttpClientBuilder WithTypedClientImplementation<TInterface, TImplementation>(this IContextualHttpClientBuilder builder)
where TInterface : class
where TImplementation : class, TInterface
{
builder.Services
.AddOptions<ContextualClientFactoryOptions>()
.Configure(c => c.SetTypedClientImplementation<TInterface, TImplementation>());
return builder;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public interface IContextualHttpClientFactory
/// <typeparam name="TClient"></typeparam>
/// <param name="handlerContext">A string-based context for primary handler resolution and partitioning</param>
/// <returns></returns>
TClient CreateClient<TClient>(string handlerContext);
TClient CreateClient<TClient>(string handlerContext) where TClient : class;

/// <summary>
/// Create a named <see cref="HttpClient"/> for a specified string context
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using Ringleader.Shared;
using System;
using System.Net.Http;

namespace Ringleader.HttpClientFactory
Expand All @@ -11,6 +12,6 @@ public interface IPrimaryHandlerFactory
/// <param name="clientName"><see cref="HttpClient"/> name</param>
/// <param name="handlerContext">Handler context for the <see cref="HttpClient"/></param>
/// <returns>An <see cref="HttpMessageHandler"/> configured based on the context, or <see langword="null"/> to use the default handler</returns>
HttpMessageHandler? CreateHandler(string clientName, string handlerContext);
HttpMessageHandler? CreateHandler(TypedClientSignature clientName, string handlerContext);
}
}
Loading