From 884c3986853a078ea9a5298d00b5ac7810069544 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Tue, 11 Feb 2025 18:46:29 +0000 Subject: [PATCH 1/3] Add service lifetime support to DI helpers. --- ...lientBuilderServiceCollectionExtensions.cs | 24 +++-- ...ratorBuilderServiceCollectionExtensions.cs | 24 +++-- .../DependencyInjectionPatterns.cs | 90 +++++++++++++++++++ 3 files changed, 122 insertions(+), 16 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ChatClientBuilderServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ChatClientBuilderServiceCollectionExtensions.cs index c3d8ab88edb..60553f2ae4f 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. /// 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) + => 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. /// 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. /// 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) + => 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. /// 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..adaafaceac3 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. /// 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) 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. /// 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. /// 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) 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. /// 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; From f30e23c1ebbcca5e8de51c62d99fb6c936b8c651 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Tue, 11 Feb 2025 19:52:23 +0000 Subject: [PATCH 2/3] Add missing optional parameters. --- .../ChatClientBuilderServiceCollectionExtensions.cs | 4 ++-- .../EmbeddingGeneratorBuilderServiceCollectionExtensions.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ChatClientBuilderServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ChatClientBuilderServiceCollectionExtensions.cs index 60553f2ae4f..4d4a96b5f5d 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ChatClientBuilderServiceCollectionExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ChatClientBuilderServiceCollectionExtensions.cs @@ -19,7 +19,7 @@ public static class ChatClientBuilderServiceCollectionExtensions public static ChatClientBuilder AddChatClient( this IServiceCollection serviceCollection, IChatClient innerClient, - ServiceLifetime lifetime) + ServiceLifetime lifetime = ServiceLifetime.Singleton) => AddChatClient(serviceCollection, _ => innerClient, lifetime); /// Registers a singleton in the . @@ -52,7 +52,7 @@ public static ChatClientBuilder AddKeyedChatClient( this IServiceCollection serviceCollection, object serviceKey, IChatClient innerClient, - ServiceLifetime lifetime) + ServiceLifetime lifetime = ServiceLifetime.Singleton) => AddKeyedChatClient(serviceCollection, serviceKey, _ => innerClient, lifetime); /// Registers a keyed singleton in the . diff --git a/src/Libraries/Microsoft.Extensions.AI/Embeddings/EmbeddingGeneratorBuilderServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.AI/Embeddings/EmbeddingGeneratorBuilderServiceCollectionExtensions.cs index adaafaceac3..4bc52f21b6e 100644 --- a/src/Libraries/Microsoft.Extensions.AI/Embeddings/EmbeddingGeneratorBuilderServiceCollectionExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI/Embeddings/EmbeddingGeneratorBuilderServiceCollectionExtensions.cs @@ -21,7 +21,7 @@ public static class EmbeddingGeneratorBuilderServiceCollectionExtensions public static EmbeddingGeneratorBuilder AddEmbeddingGenerator( this IServiceCollection serviceCollection, IEmbeddingGenerator innerGenerator, - ServiceLifetime lifetime) + ServiceLifetime lifetime = ServiceLifetime.Singleton) where TEmbedding : Embedding => AddEmbeddingGenerator(serviceCollection, _ => innerGenerator, lifetime); @@ -60,7 +60,7 @@ public static EmbeddingGeneratorBuilder AddKeyedEmbeddingGen this IServiceCollection serviceCollection, object serviceKey, IEmbeddingGenerator innerGenerator, - ServiceLifetime lifetime) + ServiceLifetime lifetime = ServiceLifetime.Singleton) where TEmbedding : Embedding => AddKeyedEmbeddingGenerator(serviceCollection, serviceKey, _ => innerGenerator, lifetime); From 89bd622f38536b664dbeff6adc08b4ad8bbdaccd Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Wed, 12 Feb 2025 18:12:26 +0000 Subject: [PATCH 3/3] Address feedback. --- .../ChatClientBuilderServiceCollectionExtensions.cs | 8 ++++---- ...mbeddingGeneratorBuilderServiceCollectionExtensions.cs | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ChatClientBuilderServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ChatClientBuilderServiceCollectionExtensions.cs index 4d4a96b5f5d..68c7c6f2cea 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ChatClientBuilderServiceCollectionExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ChatClientBuilderServiceCollectionExtensions.cs @@ -13,7 +13,7 @@ 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. + /// 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( @@ -25,7 +25,7 @@ public static ChatClientBuilder AddChatClient( /// 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. + /// 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( @@ -45,7 +45,7 @@ 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. + /// 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( @@ -59,7 +59,7 @@ public static ChatClientBuilder AddKeyedChatClient( /// 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. + /// 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( diff --git a/src/Libraries/Microsoft.Extensions.AI/Embeddings/EmbeddingGeneratorBuilderServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.AI/Embeddings/EmbeddingGeneratorBuilderServiceCollectionExtensions.cs index 4bc52f21b6e..b35fcd7f4ff 100644 --- a/src/Libraries/Microsoft.Extensions.AI/Embeddings/EmbeddingGeneratorBuilderServiceCollectionExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI/Embeddings/EmbeddingGeneratorBuilderServiceCollectionExtensions.cs @@ -15,7 +15,7 @@ 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. + /// 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( @@ -30,7 +30,7 @@ public static EmbeddingGeneratorBuilder AddEmbeddingGenerato /// 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. + /// 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( @@ -53,7 +53,7 @@ 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. + /// 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( @@ -70,7 +70,7 @@ 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. + /// 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(