Skip to content

Commit b67faa0

Browse files
Add service lifetime support to DI helpers. (#5880)
* Add service lifetime support to DI helpers. * Add missing optional parameters.
1 parent 57e4c29 commit b67faa0

File tree

3 files changed

+122
-16
lines changed

3 files changed

+122
-16
lines changed

src/Libraries/Microsoft.Extensions.AI/ChatCompletion/ChatClientBuilderServiceCollectionExtensions.cs

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,59 +13,67 @@ public static class ChatClientBuilderServiceCollectionExtensions
1313
/// <summary>Registers a singleton <see cref="IChatClient"/> in the <see cref="IServiceCollection"/>.</summary>
1414
/// <param name="serviceCollection">The <see cref="IServiceCollection"/> to which the client should be added.</param>
1515
/// <param name="innerClient">The inner <see cref="IChatClient"/> that represents the underlying backend.</param>
16+
/// <param name="lifetime">The service lifetime for the client. Defaults to <see cref="ServiceLifetime.Singleton"/>.</param>
1617
/// <returns>A <see cref="ChatClientBuilder"/> that can be used to build a pipeline around the inner client.</returns>
1718
/// <remarks>The client is registered as a singleton service.</remarks>
1819
public static ChatClientBuilder AddChatClient(
1920
this IServiceCollection serviceCollection,
20-
IChatClient innerClient)
21-
=> AddChatClient(serviceCollection, _ => innerClient);
21+
IChatClient innerClient,
22+
ServiceLifetime lifetime = ServiceLifetime.Singleton)
23+
=> AddChatClient(serviceCollection, _ => innerClient, lifetime);
2224

2325
/// <summary>Registers a singleton <see cref="IChatClient"/> in the <see cref="IServiceCollection"/>.</summary>
2426
/// <param name="serviceCollection">The <see cref="IServiceCollection"/> to which the client should be added.</param>
2527
/// <param name="innerClientFactory">A callback that produces the inner <see cref="IChatClient"/> that represents the underlying backend.</param>
28+
/// <param name="lifetime">The service lifetime for the client. Defaults to <see cref="ServiceLifetime.Singleton"/>.</param>
2629
/// <returns>A <see cref="ChatClientBuilder"/> that can be used to build a pipeline around the inner client.</returns>
2730
/// <remarks>The client is registered as a singleton service.</remarks>
2831
public static ChatClientBuilder AddChatClient(
2932
this IServiceCollection serviceCollection,
30-
Func<IServiceProvider, IChatClient> innerClientFactory)
33+
Func<IServiceProvider, IChatClient> innerClientFactory,
34+
ServiceLifetime lifetime = ServiceLifetime.Singleton)
3135
{
3236
_ = Throw.IfNull(serviceCollection);
3337
_ = Throw.IfNull(innerClientFactory);
3438

3539
var builder = new ChatClientBuilder(innerClientFactory);
36-
_ = serviceCollection.AddSingleton(builder.Build);
40+
serviceCollection.Add(new ServiceDescriptor(typeof(IChatClient), builder.Build, lifetime));
3741
return builder;
3842
}
3943

4044
/// <summary>Registers a keyed singleton <see cref="IChatClient"/> in the <see cref="IServiceCollection"/>.</summary>
4145
/// <param name="serviceCollection">The <see cref="IServiceCollection"/> to which the client should be added.</param>
4246
/// <param name="serviceKey">The key with which to associate the client.</param>
4347
/// <param name="innerClient">The inner <see cref="IChatClient"/> that represents the underlying backend.</param>
48+
/// <param name="lifetime">The service lifetime for the client. Defaults to <see cref="ServiceLifetime.Singleton"/>.</param>
4449
/// <returns>A <see cref="ChatClientBuilder"/> that can be used to build a pipeline around the inner client.</returns>
4550
/// <remarks>The client is registered as a scoped service.</remarks>
4651
public static ChatClientBuilder AddKeyedChatClient(
4752
this IServiceCollection serviceCollection,
4853
object serviceKey,
49-
IChatClient innerClient)
50-
=> AddKeyedChatClient(serviceCollection, serviceKey, _ => innerClient);
54+
IChatClient innerClient,
55+
ServiceLifetime lifetime = ServiceLifetime.Singleton)
56+
=> AddKeyedChatClient(serviceCollection, serviceKey, _ => innerClient, lifetime);
5157

5258
/// <summary>Registers a keyed singleton <see cref="IChatClient"/> in the <see cref="IServiceCollection"/>.</summary>
5359
/// <param name="serviceCollection">The <see cref="IServiceCollection"/> to which the client should be added.</param>
5460
/// <param name="serviceKey">The key with which to associate the client.</param>
5561
/// <param name="innerClientFactory">A callback that produces the inner <see cref="IChatClient"/> that represents the underlying backend.</param>
62+
/// <param name="lifetime">The service lifetime for the client. Defaults to <see cref="ServiceLifetime.Singleton"/>.</param>
5663
/// <returns>A <see cref="ChatClientBuilder"/> that can be used to build a pipeline around the inner client.</returns>
5764
/// <remarks>The client is registered as a scoped service.</remarks>
5865
public static ChatClientBuilder AddKeyedChatClient(
5966
this IServiceCollection serviceCollection,
6067
object serviceKey,
61-
Func<IServiceProvider, IChatClient> innerClientFactory)
68+
Func<IServiceProvider, IChatClient> innerClientFactory,
69+
ServiceLifetime lifetime = ServiceLifetime.Singleton)
6270
{
6371
_ = Throw.IfNull(serviceCollection);
6472
_ = Throw.IfNull(serviceKey);
6573
_ = Throw.IfNull(innerClientFactory);
6674

6775
var builder = new ChatClientBuilder(innerClientFactory);
68-
_ = serviceCollection.AddKeyedSingleton(serviceKey, (services, _) => builder.Build(services));
76+
serviceCollection.Add(new ServiceDescriptor(typeof(IChatClient), serviceKey, factory: (services, serviceKey) => builder.Build(services), lifetime));
6977
return builder;
7078
}
7179
}

src/Libraries/Microsoft.Extensions.AI/Embeddings/EmbeddingGeneratorBuilderServiceCollectionExtensions.cs

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,31 +15,35 @@ public static class EmbeddingGeneratorBuilderServiceCollectionExtensions
1515
/// <typeparam name="TEmbedding">The type of embeddings to generate.</typeparam>
1616
/// <param name="serviceCollection">The <see cref="IServiceCollection"/> to which the generator should be added.</param>
1717
/// <param name="innerGenerator">The inner <see cref="IEmbeddingGenerator{TInput, TEmbedding}"/> that represents the underlying backend.</param>
18+
/// <param name="lifetime">The service lifetime for the client. Defaults to <see cref="ServiceLifetime.Singleton"/>.</param>
1819
/// <returns>An <see cref="EmbeddingGeneratorBuilder{TInput, TEmbedding}"/> that can be used to build a pipeline around the inner generator.</returns>
1920
/// <remarks>The generator is registered as a singleton service.</remarks>
2021
public static EmbeddingGeneratorBuilder<TInput, TEmbedding> AddEmbeddingGenerator<TInput, TEmbedding>(
2122
this IServiceCollection serviceCollection,
22-
IEmbeddingGenerator<TInput, TEmbedding> innerGenerator)
23+
IEmbeddingGenerator<TInput, TEmbedding> innerGenerator,
24+
ServiceLifetime lifetime = ServiceLifetime.Singleton)
2325
where TEmbedding : Embedding
24-
=> AddEmbeddingGenerator(serviceCollection, _ => innerGenerator);
26+
=> AddEmbeddingGenerator(serviceCollection, _ => innerGenerator, lifetime);
2527

2628
/// <summary>Registers a singleton embedding generator in the <see cref="IServiceCollection"/>.</summary>
2729
/// <typeparam name="TInput">The type from which embeddings will be generated.</typeparam>
2830
/// <typeparam name="TEmbedding">The type of embeddings to generate.</typeparam>
2931
/// <param name="serviceCollection">The <see cref="IServiceCollection"/> to which the generator should be added.</param>
3032
/// <param name="innerGeneratorFactory">A callback that produces the inner <see cref="IEmbeddingGenerator{TInput, TEmbedding}"/> that represents the underlying backend.</param>
33+
/// <param name="lifetime">The service lifetime for the client. Defaults to <see cref="ServiceLifetime.Singleton"/>.</param>
3134
/// <returns>An <see cref="EmbeddingGeneratorBuilder{TInput, TEmbedding}"/> that can be used to build a pipeline around the inner generator.</returns>
3235
/// <remarks>The generator is registered as a singleton service.</remarks>
3336
public static EmbeddingGeneratorBuilder<TInput, TEmbedding> AddEmbeddingGenerator<TInput, TEmbedding>(
3437
this IServiceCollection serviceCollection,
35-
Func<IServiceProvider, IEmbeddingGenerator<TInput, TEmbedding>> innerGeneratorFactory)
38+
Func<IServiceProvider, IEmbeddingGenerator<TInput, TEmbedding>> innerGeneratorFactory,
39+
ServiceLifetime lifetime = ServiceLifetime.Singleton)
3640
where TEmbedding : Embedding
3741
{
3842
_ = Throw.IfNull(serviceCollection);
3943
_ = Throw.IfNull(innerGeneratorFactory);
4044

4145
var builder = new EmbeddingGeneratorBuilder<TInput, TEmbedding>(innerGeneratorFactory);
42-
_ = serviceCollection.AddSingleton(builder.Build);
46+
serviceCollection.Add(new ServiceDescriptor(typeof(IEmbeddingGenerator<TInput, TEmbedding>), builder.Build, lifetime));
4347
return builder;
4448
}
4549

@@ -49,35 +53,39 @@ public static EmbeddingGeneratorBuilder<TInput, TEmbedding> AddEmbeddingGenerato
4953
/// <param name="serviceCollection">The <see cref="IServiceCollection"/> to which the generator should be added.</param>
5054
/// <param name="serviceKey">The key with which to associated the generator.</param>
5155
/// <param name="innerGenerator">The inner <see cref="IEmbeddingGenerator{TInput, TEmbedding}"/> that represents the underlying backend.</param>
56+
/// <param name="lifetime">The service lifetime for the client. Defaults to <see cref="ServiceLifetime.Singleton"/>.</param>
5257
/// <returns>An <see cref="EmbeddingGeneratorBuilder{TInput, TEmbedding}"/> that can be used to build a pipeline around the inner generator.</returns>
5358
/// <remarks>The generator is registered as a singleton service.</remarks>
5459
public static EmbeddingGeneratorBuilder<TInput, TEmbedding> AddKeyedEmbeddingGenerator<TInput, TEmbedding>(
5560
this IServiceCollection serviceCollection,
5661
object serviceKey,
57-
IEmbeddingGenerator<TInput, TEmbedding> innerGenerator)
62+
IEmbeddingGenerator<TInput, TEmbedding> innerGenerator,
63+
ServiceLifetime lifetime = ServiceLifetime.Singleton)
5864
where TEmbedding : Embedding
59-
=> AddKeyedEmbeddingGenerator(serviceCollection, serviceKey, _ => innerGenerator);
65+
=> AddKeyedEmbeddingGenerator(serviceCollection, serviceKey, _ => innerGenerator, lifetime);
6066

6167
/// <summary>Registers a keyed singleton embedding generator in the <see cref="IServiceCollection"/>.</summary>
6268
/// <typeparam name="TInput">The type from which embeddings will be generated.</typeparam>
6369
/// <typeparam name="TEmbedding">The type of embeddings to generate.</typeparam>
6470
/// <param name="serviceCollection">The <see cref="IServiceCollection"/> to which the generator should be added.</param>
6571
/// <param name="serviceKey">The key with which to associated the generator.</param>
6672
/// <param name="innerGeneratorFactory">A callback that produces the inner <see cref="IEmbeddingGenerator{TInput, TEmbedding}"/> that represents the underlying backend.</param>
73+
/// <param name="lifetime">The service lifetime for the client. Defaults to <see cref="ServiceLifetime.Singleton"/>.</param>
6774
/// <returns>An <see cref="EmbeddingGeneratorBuilder{TInput, TEmbedding}"/> that can be used to build a pipeline around the inner generator.</returns>
6875
/// <remarks>The generator is registered as a singleton service.</remarks>
6976
public static EmbeddingGeneratorBuilder<TInput, TEmbedding> AddKeyedEmbeddingGenerator<TInput, TEmbedding>(
7077
this IServiceCollection serviceCollection,
7178
object serviceKey,
72-
Func<IServiceProvider, IEmbeddingGenerator<TInput, TEmbedding>> innerGeneratorFactory)
79+
Func<IServiceProvider, IEmbeddingGenerator<TInput, TEmbedding>> innerGeneratorFactory,
80+
ServiceLifetime lifetime = ServiceLifetime.Singleton)
7381
where TEmbedding : Embedding
7482
{
7583
_ = Throw.IfNull(serviceCollection);
7684
_ = Throw.IfNull(serviceKey);
7785
_ = Throw.IfNull(innerGeneratorFactory);
7886

7987
var builder = new EmbeddingGeneratorBuilder<TInput, TEmbedding>(innerGeneratorFactory);
80-
_ = serviceCollection.AddKeyedSingleton(serviceKey, (services, _) => builder.Build(services));
88+
serviceCollection.Add(new ServiceDescriptor(typeof(IEmbeddingGenerator<TInput, TEmbedding>), serviceKey, factory: (services, serviceKey) => builder.Build(services), lifetime));
8189
return builder;
8290
}
8391
}

test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/DependencyInjectionPatterns.cs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,96 @@ public void CanRegisterKeyedSingletonUsingSharedInstance()
109109
Assert.IsType<TestChatClient>(instance.InnerClient);
110110
}
111111

112+
[Theory]
113+
[InlineData(null)]
114+
[InlineData(ServiceLifetime.Singleton)]
115+
[InlineData(ServiceLifetime.Scoped)]
116+
[InlineData(ServiceLifetime.Transient)]
117+
public void AddChatClient_RegistersExpectedLifetime(ServiceLifetime? lifetime)
118+
{
119+
ServiceCollection sc = new();
120+
ServiceLifetime expectedLifetime = lifetime ?? ServiceLifetime.Singleton;
121+
ChatClientBuilder builder = lifetime.HasValue
122+
? sc.AddChatClient(services => new TestChatClient(), lifetime.Value)
123+
: sc.AddChatClient(services => new TestChatClient());
124+
125+
ServiceDescriptor sd = Assert.Single(sc);
126+
Assert.Equal(typeof(IChatClient), sd.ServiceType);
127+
Assert.False(sd.IsKeyedService);
128+
Assert.Null(sd.ImplementationInstance);
129+
Assert.NotNull(sd.ImplementationFactory);
130+
Assert.IsType<TestChatClient>(sd.ImplementationFactory(null!));
131+
Assert.Equal(expectedLifetime, sd.Lifetime);
132+
}
133+
134+
[Theory]
135+
[InlineData(null)]
136+
[InlineData(ServiceLifetime.Singleton)]
137+
[InlineData(ServiceLifetime.Scoped)]
138+
[InlineData(ServiceLifetime.Transient)]
139+
public void AddKeyedChatClient_RegistersExpectedLifetime(ServiceLifetime? lifetime)
140+
{
141+
ServiceCollection sc = new();
142+
ServiceLifetime expectedLifetime = lifetime ?? ServiceLifetime.Singleton;
143+
ChatClientBuilder builder = lifetime.HasValue
144+
? sc.AddKeyedChatClient("key", services => new TestChatClient(), lifetime.Value)
145+
: sc.AddKeyedChatClient("key", services => new TestChatClient());
146+
147+
ServiceDescriptor sd = Assert.Single(sc);
148+
Assert.Equal(typeof(IChatClient), sd.ServiceType);
149+
Assert.True(sd.IsKeyedService);
150+
Assert.Equal("key", sd.ServiceKey);
151+
Assert.Null(sd.KeyedImplementationInstance);
152+
Assert.NotNull(sd.KeyedImplementationFactory);
153+
Assert.IsType<TestChatClient>(sd.KeyedImplementationFactory(null!, null!));
154+
Assert.Equal(expectedLifetime, sd.Lifetime);
155+
}
156+
157+
[Theory]
158+
[InlineData(null)]
159+
[InlineData(ServiceLifetime.Singleton)]
160+
[InlineData(ServiceLifetime.Scoped)]
161+
[InlineData(ServiceLifetime.Transient)]
162+
public void AddEmbeddingGenerator_RegistersExpectedLifetime(ServiceLifetime? lifetime)
163+
{
164+
ServiceCollection sc = new();
165+
ServiceLifetime expectedLifetime = lifetime ?? ServiceLifetime.Singleton;
166+
var builder = lifetime.HasValue
167+
? sc.AddEmbeddingGenerator(services => new TestEmbeddingGenerator(), lifetime.Value)
168+
: sc.AddEmbeddingGenerator(services => new TestEmbeddingGenerator());
169+
170+
ServiceDescriptor sd = Assert.Single(sc);
171+
Assert.Equal(typeof(IEmbeddingGenerator<string, Embedding<float>>), sd.ServiceType);
172+
Assert.False(sd.IsKeyedService);
173+
Assert.Null(sd.ImplementationInstance);
174+
Assert.NotNull(sd.ImplementationFactory);
175+
Assert.IsType<TestEmbeddingGenerator>(sd.ImplementationFactory(null!));
176+
Assert.Equal(expectedLifetime, sd.Lifetime);
177+
}
178+
179+
[Theory]
180+
[InlineData(null)]
181+
[InlineData(ServiceLifetime.Singleton)]
182+
[InlineData(ServiceLifetime.Scoped)]
183+
[InlineData(ServiceLifetime.Transient)]
184+
public void AddKeyedEmbeddingGenerator_RegistersExpectedLifetime(ServiceLifetime? lifetime)
185+
{
186+
ServiceCollection sc = new();
187+
ServiceLifetime expectedLifetime = lifetime ?? ServiceLifetime.Singleton;
188+
var builder = lifetime.HasValue
189+
? sc.AddKeyedEmbeddingGenerator("key", services => new TestEmbeddingGenerator(), lifetime.Value)
190+
: sc.AddKeyedEmbeddingGenerator("key", services => new TestEmbeddingGenerator());
191+
192+
ServiceDescriptor sd = Assert.Single(sc);
193+
Assert.Equal(typeof(IEmbeddingGenerator<string, Embedding<float>>), sd.ServiceType);
194+
Assert.True(sd.IsKeyedService);
195+
Assert.Equal("key", sd.ServiceKey);
196+
Assert.Null(sd.KeyedImplementationInstance);
197+
Assert.NotNull(sd.KeyedImplementationFactory);
198+
Assert.IsType<TestEmbeddingGenerator>(sd.KeyedImplementationFactory(null!, null!));
199+
Assert.Equal(expectedLifetime, sd.Lifetime);
200+
}
201+
112202
public class SingletonMiddleware(IChatClient inner, IServiceProvider services) : DelegatingChatClient(inner)
113203
{
114204
public new IChatClient InnerClient => base.InnerClient;

0 commit comments

Comments
 (0)