diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ChatClientBuilderServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ChatClientBuilderServiceCollectionExtensions.cs index c3d8ab88edb..68c7c6f2cea 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ChatClientBuilderServiceCollectionExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ChatClientBuilderServiceCollectionExtensions.cs @@ -13,27 +13,31 @@ public static class ChatClientBuilderServiceCollectionExtensions /// Registers a singleton in the . /// The to which the client should be added. /// The inner that represents the underlying backend. + /// The service lifetime for the client. Defaults to . /// A that can be used to build a pipeline around the inner client. /// The client is registered as a singleton service. public static ChatClientBuilder AddChatClient( this IServiceCollection serviceCollection, - IChatClient innerClient) - => AddChatClient(serviceCollection, _ => innerClient); + IChatClient innerClient, + ServiceLifetime lifetime = ServiceLifetime.Singleton) + => AddChatClient(serviceCollection, _ => innerClient, lifetime); /// Registers a singleton in the . /// The to which the client should be added. /// A callback that produces the inner that represents the underlying backend. + /// The service lifetime for the client. Defaults to . /// A that can be used to build a pipeline around the inner client. /// The client is registered as a singleton service. public static ChatClientBuilder AddChatClient( this IServiceCollection serviceCollection, - Func innerClientFactory) + Func innerClientFactory, + ServiceLifetime lifetime = ServiceLifetime.Singleton) { _ = Throw.IfNull(serviceCollection); _ = Throw.IfNull(innerClientFactory); var builder = new ChatClientBuilder(innerClientFactory); - _ = serviceCollection.AddSingleton(builder.Build); + serviceCollection.Add(new ServiceDescriptor(typeof(IChatClient), builder.Build, lifetime)); return builder; } @@ -41,31 +45,35 @@ public static ChatClientBuilder AddChatClient( /// The to which the client should be added. /// The key with which to associate the client. /// The inner that represents the underlying backend. + /// The service lifetime for the client. Defaults to . /// A that can be used to build a pipeline around the inner client. /// The client is registered as a scoped service. public static ChatClientBuilder AddKeyedChatClient( this IServiceCollection serviceCollection, object serviceKey, - IChatClient innerClient) - => AddKeyedChatClient(serviceCollection, serviceKey, _ => innerClient); + IChatClient innerClient, + ServiceLifetime lifetime = ServiceLifetime.Singleton) + => AddKeyedChatClient(serviceCollection, serviceKey, _ => innerClient, lifetime); /// Registers a keyed singleton in the . /// The to which the client should be added. /// The key with which to associate the client. /// A callback that produces the inner that represents the underlying backend. + /// The service lifetime for the client. Defaults to . /// A that can be used to build a pipeline around the inner client. /// The client is registered as a scoped service. public static ChatClientBuilder AddKeyedChatClient( this IServiceCollection serviceCollection, object serviceKey, - Func innerClientFactory) + Func innerClientFactory, + ServiceLifetime lifetime = ServiceLifetime.Singleton) { _ = Throw.IfNull(serviceCollection); _ = Throw.IfNull(serviceKey); _ = Throw.IfNull(innerClientFactory); var builder = new ChatClientBuilder(innerClientFactory); - _ = serviceCollection.AddKeyedSingleton(serviceKey, (services, _) => builder.Build(services)); + serviceCollection.Add(new ServiceDescriptor(typeof(IChatClient), serviceKey, factory: (services, serviceKey) => builder.Build(services), lifetime)); return builder; } } diff --git a/src/Libraries/Microsoft.Extensions.AI/Embeddings/EmbeddingGeneratorBuilderServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.AI/Embeddings/EmbeddingGeneratorBuilderServiceCollectionExtensions.cs index 1c57fb08215..b35fcd7f4ff 100644 --- a/src/Libraries/Microsoft.Extensions.AI/Embeddings/EmbeddingGeneratorBuilderServiceCollectionExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI/Embeddings/EmbeddingGeneratorBuilderServiceCollectionExtensions.cs @@ -15,31 +15,35 @@ public static class EmbeddingGeneratorBuilderServiceCollectionExtensions /// The type of embeddings to generate. /// The to which the generator should be added. /// The inner that represents the underlying backend. + /// The service lifetime for the client. Defaults to . /// An that can be used to build a pipeline around the inner generator. /// The generator is registered as a singleton service. public static EmbeddingGeneratorBuilder AddEmbeddingGenerator( this IServiceCollection serviceCollection, - IEmbeddingGenerator innerGenerator) + IEmbeddingGenerator innerGenerator, + ServiceLifetime lifetime = ServiceLifetime.Singleton) where TEmbedding : Embedding - => AddEmbeddingGenerator(serviceCollection, _ => innerGenerator); + => AddEmbeddingGenerator(serviceCollection, _ => innerGenerator, lifetime); /// Registers a singleton embedding generator in the . /// The type from which embeddings will be generated. /// The type of embeddings to generate. /// The to which the generator should be added. /// A callback that produces the inner that represents the underlying backend. + /// The service lifetime for the client. Defaults to . /// An that can be used to build a pipeline around the inner generator. /// The generator is registered as a singleton service. public static EmbeddingGeneratorBuilder AddEmbeddingGenerator( this IServiceCollection serviceCollection, - Func> innerGeneratorFactory) + Func> innerGeneratorFactory, + ServiceLifetime lifetime = ServiceLifetime.Singleton) where TEmbedding : Embedding { _ = Throw.IfNull(serviceCollection); _ = Throw.IfNull(innerGeneratorFactory); var builder = new EmbeddingGeneratorBuilder(innerGeneratorFactory); - _ = serviceCollection.AddSingleton(builder.Build); + serviceCollection.Add(new ServiceDescriptor(typeof(IEmbeddingGenerator), builder.Build, lifetime)); return builder; } @@ -49,14 +53,16 @@ public static EmbeddingGeneratorBuilder AddEmbeddingGenerato /// The to which the generator should be added. /// The key with which to associated the generator. /// The inner that represents the underlying backend. + /// The service lifetime for the client. Defaults to . /// An that can be used to build a pipeline around the inner generator. /// The generator is registered as a singleton service. public static EmbeddingGeneratorBuilder AddKeyedEmbeddingGenerator( this IServiceCollection serviceCollection, object serviceKey, - IEmbeddingGenerator innerGenerator) + IEmbeddingGenerator innerGenerator, + ServiceLifetime lifetime = ServiceLifetime.Singleton) where TEmbedding : Embedding - => AddKeyedEmbeddingGenerator(serviceCollection, serviceKey, _ => innerGenerator); + => AddKeyedEmbeddingGenerator(serviceCollection, serviceKey, _ => innerGenerator, lifetime); /// Registers a keyed singleton embedding generator in the . /// The type from which embeddings will be generated. @@ -64,12 +70,14 @@ public static EmbeddingGeneratorBuilder AddKeyedEmbeddingGen /// The to which the generator should be added. /// The key with which to associated the generator. /// A callback that produces the inner that represents the underlying backend. + /// The service lifetime for the client. Defaults to . /// An that can be used to build a pipeline around the inner generator. /// The generator is registered as a singleton service. public static EmbeddingGeneratorBuilder AddKeyedEmbeddingGenerator( this IServiceCollection serviceCollection, object serviceKey, - Func> innerGeneratorFactory) + Func> innerGeneratorFactory, + ServiceLifetime lifetime = ServiceLifetime.Singleton) where TEmbedding : Embedding { _ = Throw.IfNull(serviceCollection); @@ -77,7 +85,7 @@ public static EmbeddingGeneratorBuilder AddKeyedEmbeddingGen _ = Throw.IfNull(innerGeneratorFactory); var builder = new EmbeddingGeneratorBuilder(innerGeneratorFactory); - _ = serviceCollection.AddKeyedSingleton(serviceKey, (services, _) => builder.Build(services)); + serviceCollection.Add(new ServiceDescriptor(typeof(IEmbeddingGenerator), serviceKey, factory: (services, serviceKey) => builder.Build(services), lifetime)); return builder; } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/DependencyInjectionPatterns.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/DependencyInjectionPatterns.cs index c99d4511f75..c2f288165cb 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/DependencyInjectionPatterns.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/DependencyInjectionPatterns.cs @@ -109,6 +109,96 @@ public void CanRegisterKeyedSingletonUsingSharedInstance() Assert.IsType(instance.InnerClient); } + [Theory] + [InlineData(null)] + [InlineData(ServiceLifetime.Singleton)] + [InlineData(ServiceLifetime.Scoped)] + [InlineData(ServiceLifetime.Transient)] + public void AddChatClient_RegistersExpectedLifetime(ServiceLifetime? lifetime) + { + ServiceCollection sc = new(); + ServiceLifetime expectedLifetime = lifetime ?? ServiceLifetime.Singleton; + ChatClientBuilder builder = lifetime.HasValue + ? sc.AddChatClient(services => new TestChatClient(), lifetime.Value) + : sc.AddChatClient(services => new TestChatClient()); + + ServiceDescriptor sd = Assert.Single(sc); + Assert.Equal(typeof(IChatClient), sd.ServiceType); + Assert.False(sd.IsKeyedService); + Assert.Null(sd.ImplementationInstance); + Assert.NotNull(sd.ImplementationFactory); + Assert.IsType(sd.ImplementationFactory(null!)); + Assert.Equal(expectedLifetime, sd.Lifetime); + } + + [Theory] + [InlineData(null)] + [InlineData(ServiceLifetime.Singleton)] + [InlineData(ServiceLifetime.Scoped)] + [InlineData(ServiceLifetime.Transient)] + public void AddKeyedChatClient_RegistersExpectedLifetime(ServiceLifetime? lifetime) + { + ServiceCollection sc = new(); + ServiceLifetime expectedLifetime = lifetime ?? ServiceLifetime.Singleton; + ChatClientBuilder builder = lifetime.HasValue + ? sc.AddKeyedChatClient("key", services => new TestChatClient(), lifetime.Value) + : sc.AddKeyedChatClient("key", services => new TestChatClient()); + + ServiceDescriptor sd = Assert.Single(sc); + Assert.Equal(typeof(IChatClient), sd.ServiceType); + Assert.True(sd.IsKeyedService); + Assert.Equal("key", sd.ServiceKey); + Assert.Null(sd.KeyedImplementationInstance); + Assert.NotNull(sd.KeyedImplementationFactory); + Assert.IsType(sd.KeyedImplementationFactory(null!, null!)); + Assert.Equal(expectedLifetime, sd.Lifetime); + } + + [Theory] + [InlineData(null)] + [InlineData(ServiceLifetime.Singleton)] + [InlineData(ServiceLifetime.Scoped)] + [InlineData(ServiceLifetime.Transient)] + public void AddEmbeddingGenerator_RegistersExpectedLifetime(ServiceLifetime? lifetime) + { + ServiceCollection sc = new(); + ServiceLifetime expectedLifetime = lifetime ?? ServiceLifetime.Singleton; + var builder = lifetime.HasValue + ? sc.AddEmbeddingGenerator(services => new TestEmbeddingGenerator(), lifetime.Value) + : sc.AddEmbeddingGenerator(services => new TestEmbeddingGenerator()); + + ServiceDescriptor sd = Assert.Single(sc); + Assert.Equal(typeof(IEmbeddingGenerator>), sd.ServiceType); + Assert.False(sd.IsKeyedService); + Assert.Null(sd.ImplementationInstance); + Assert.NotNull(sd.ImplementationFactory); + Assert.IsType(sd.ImplementationFactory(null!)); + Assert.Equal(expectedLifetime, sd.Lifetime); + } + + [Theory] + [InlineData(null)] + [InlineData(ServiceLifetime.Singleton)] + [InlineData(ServiceLifetime.Scoped)] + [InlineData(ServiceLifetime.Transient)] + public void AddKeyedEmbeddingGenerator_RegistersExpectedLifetime(ServiceLifetime? lifetime) + { + ServiceCollection sc = new(); + ServiceLifetime expectedLifetime = lifetime ?? ServiceLifetime.Singleton; + var builder = lifetime.HasValue + ? sc.AddKeyedEmbeddingGenerator("key", services => new TestEmbeddingGenerator(), lifetime.Value) + : sc.AddKeyedEmbeddingGenerator("key", services => new TestEmbeddingGenerator()); + + ServiceDescriptor sd = Assert.Single(sc); + Assert.Equal(typeof(IEmbeddingGenerator>), sd.ServiceType); + Assert.True(sd.IsKeyedService); + Assert.Equal("key", sd.ServiceKey); + Assert.Null(sd.KeyedImplementationInstance); + Assert.NotNull(sd.KeyedImplementationFactory); + Assert.IsType(sd.KeyedImplementationFactory(null!, null!)); + Assert.Equal(expectedLifetime, sd.Lifetime); + } + public class SingletonMiddleware(IChatClient inner, IServiceProvider services) : DelegatingChatClient(inner) { public new IChatClient InnerClient => base.InnerClient;