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

Change ChatClientBuilder to register singletons and support lambda-less chaining #5642

Merged
merged 5 commits into from
Nov 14, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Add generic keyed version
  • Loading branch information
SteveSandersonMS committed Nov 14, 2024
commit c2f96f51976b0743bb66ddf3cca4ac32bb13e059
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,18 @@ public static ChatClientBuilder AddKeyedChatClient(
IChatClient innerClient)
=> AddKeyedChatClient(serviceCollection, serviceKey, _ => innerClient);

/// <summary>Adds a chat client to the <see cref="IServiceCollection"/>.</summary>
/// <typeparam name="T">The type of the inner <see cref="IChatClient"/> that represents the underlying backend. This will be resolved from the service provider.</typeparam>
/// <param name="serviceCollection">The <see cref="IServiceCollection"/> to which the client should be added.</param>
/// <param name="serviceKey">The key with which to associate the client.</param>
/// <returns>A <see cref="ChatClientBuilder"/> that can be used to build a pipeline around the inner client.</returns>
/// <remarks>The client is registered as a scoped service.</remarks>
public static ChatClientBuilder AddKeyedChatClient<T>(
this IServiceCollection serviceCollection,
object serviceKey)
where T : IChatClient
=> AddKeyedChatClient(serviceCollection, serviceKey, services => services.GetRequiredService<T>());

/// <summary>Adds a chat client to the <see cref="IServiceCollection"/>.</summary>
/// <param name="serviceCollection">The <see cref="IServiceCollection"/> to which the client should be added.</param>
/// <param name="serviceKey">The key with which to associate the client.</param>
Expand All @@ -75,7 +87,7 @@ public static ChatClientBuilder AddKeyedChatClient(
_ = Throw.IfNull(innerClientFactory);

var builder = new ChatClientBuilder(innerClientFactory);
_ = serviceCollection.AddKeyedSingleton(serviceKey, builder.Build);
_ = serviceCollection.AddKeyedSingleton(serviceKey, (services, _) => builder.Build(services));
return builder;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,83 @@ public void CanRegisterSingletonUsingSharedInstance()
Assert.IsType<TestChatClient>(instance.InnerClient);
}

[Fact]
public void CanRegisterKeyedSingletonUsingGenericType()
{
// Arrange/Act
ServiceCollection.AddSingleton(services => new TestChatClient { Services = services });
ServiceCollection.AddKeyedChatClient<TestChatClient>("mykey")
.UseSingletonMiddleware();

// Assert
var services = ServiceCollection.BuildServiceProvider();
using var scope1 = services.CreateScope();
using var scope2 = services.CreateScope();

Assert.Null(services.GetService<IChatClient>());

var instance1 = scope1.ServiceProvider.GetRequiredKeyedService<IChatClient>("mykey");
var instance1Copy = scope1.ServiceProvider.GetRequiredKeyedService<IChatClient>("mykey");
var instance2 = scope2.ServiceProvider.GetRequiredKeyedService<IChatClient>("mykey");

// Each scope gets the same instance, because it's singleton
var instance = Assert.IsType<SingletonMiddleware>(instance1);
Assert.Same(instance, instance1Copy);
Assert.Same(instance, instance2);
Assert.IsType<TestChatClient>(instance.InnerClient);
}

[Fact]
public void CanRegisterKeyedSingletonUsingFactory()
{
// Arrange/Act
ServiceCollection.AddKeyedChatClient("mykey", services => new TestChatClient { Services = services })
.UseSingletonMiddleware();

// Assert
var services = ServiceCollection.BuildServiceProvider();
using var scope1 = services.CreateScope();
using var scope2 = services.CreateScope();

Assert.Null(services.GetService<IChatClient>());

var instance1 = scope1.ServiceProvider.GetRequiredKeyedService<IChatClient>("mykey");
var instance1Copy = scope1.ServiceProvider.GetRequiredKeyedService<IChatClient>("mykey");
var instance2 = scope2.ServiceProvider.GetRequiredKeyedService<IChatClient>("mykey");

// Each scope gets the same instance, because it's singleton
var instance = Assert.IsType<SingletonMiddleware>(instance1);
Assert.Same(instance, instance1Copy);
Assert.Same(instance, instance2);
Assert.IsType<TestChatClient>(instance.InnerClient);
}

[Fact]
public void CanRegisterKeyedSingletonUsingSharedInstance()
{
// Arrange/Act
using var singleton = new TestChatClient();
ServiceCollection.AddKeyedChatClient("mykey", singleton)
.UseSingletonMiddleware();

// Assert
var services = ServiceCollection.BuildServiceProvider();
using var scope1 = services.CreateScope();
using var scope2 = services.CreateScope();

Assert.Null(services.GetService<IChatClient>());

var instance1 = scope1.ServiceProvider.GetRequiredKeyedService<IChatClient>("mykey");
var instance1Copy = scope1.ServiceProvider.GetRequiredKeyedService<IChatClient>("mykey");
var instance2 = scope2.ServiceProvider.GetRequiredKeyedService<IChatClient>("mykey");

// Each scope gets the same instance, because it's singleton
var instance = Assert.IsType<SingletonMiddleware>(instance1);
Assert.Same(instance, instance1Copy);
Assert.Same(instance, instance2);
Assert.IsType<TestChatClient>(instance.InnerClient);
}

public class SingletonMiddleware(IServiceProvider services, IChatClient inner) : DelegatingChatClient(inner)
{
public new IChatClient InnerClient => base.InnerClient;
Expand Down