Skip to content

Commit 51cc2f5

Browse files
authored
Consolidate client registration implementation (#1503)
* Implemented generic Dapr client builder method to use across all Dapr packages to reduce code duplication throughout. * Implemented changes to Dapr.AI to use generic client builder extension. * Updating IDaprClient to implement IDisposable instead of putting it on each client type * Modified Conversation client to utilize the GrpcClient pattern of the other clients for consistency. * Updated Dapr.Jobs to use generic builder extensions * Refactored Dapr.Messaging for consistency with other Dapr clients (e.g. implements IDisposable, primary constructors, use of generic service registration extension) * Tweak to use same import reference as other clients for consistency * Renamed to AddDaprConversationClient to remain consistent with 1.14 naming - no need to introduce a breaking change here. Signed-off-by: Whit Waldo <[email protected]>
1 parent affa797 commit 51cc2f5

30 files changed

+707
-297
lines changed

src/Dapr.AI/Conversation/DaprConversationClient.cs

Lines changed: 19 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,26 @@
1111
// limitations under the License.
1212
// ------------------------------------------------------------------------
1313

14-
using Dapr.Common;
15-
using Dapr.Common.Extensions;
16-
using P = Dapr.Client.Autogen.Grpc.v1;
14+
using Autogenerated = Dapr.Client.Autogen.Grpc.v1.Dapr;
1715

1816
namespace Dapr.AI.Conversation;
1917

2018
/// <summary>
19+
/// <para>
2120
/// Used to interact with the Dapr conversation building block.
21+
/// Use <see cref="DaprConversationClientBuilder"/> to create a <see cref="DaprConversationClient"/> or register
22+
/// for use with dependency injection via
23+
/// <see><cref>DaprJobsServiceCollectionExtensions.AddDaprJobsClient</cref></see>.
24+
/// </para>
25+
/// <para>
26+
/// Implementations of <see cref="DaprConversationClient"/> implement <see cref="IDisposable"/> because the
27+
/// client accesses network resources. For best performance, create a single long-lived client instance
28+
/// and share it for the lifetime of the application. This is done for you if created via the DI extensions. Avoid
29+
/// creating a disposing a client instance for each operation that the application performs - this can lead to socket
30+
/// exhaustion and other problems.
31+
/// </para>
2232
/// </summary>
23-
public sealed class DaprConversationClient : DaprAIClient
33+
public abstract class DaprConversationClient : DaprAIClient
2434
{
2535
/// <summary>
2636
/// The HTTP client used by the client for calling the Dapr runtime.
@@ -42,15 +52,15 @@ public sealed class DaprConversationClient : DaprAIClient
4252
/// <remarks>
4353
/// Property exposed for testing purposes.
4454
/// </remarks>
45-
internal P.Dapr.DaprClient Client { get; }
55+
internal Autogenerated.DaprClient Client { get; }
4656

4757
/// <summary>
4858
/// Used to initialize a new instance of a <see cref="DaprConversationClient"/>.
4959
/// </summary>
5060
/// <param name="client">The Dapr client.</param>
5161
/// <param name="httpClient">The HTTP client used by the client for calling the Dapr runtime.</param>
5262
/// <param name="daprApiToken">An optional token required to send requests to the Dapr sidecar.</param>
53-
public DaprConversationClient(P.Dapr.DaprClient client,
63+
protected DaprConversationClient(Autogenerated.DaprClient client,
5464
HttpClient httpClient,
5565
string? daprApiToken = null)
5666
{
@@ -67,54 +77,7 @@ public DaprConversationClient(P.Dapr.DaprClient client,
6777
/// <param name="options">Optional options used to configure the conversation.</param>
6878
/// <param name="cancellationToken">Cancellation token.</param>
6979
/// <returns>The response(s) provided by the LLM provider.</returns>
70-
public override async Task<DaprConversationResponse> ConverseAsync(string daprConversationComponentName, IReadOnlyList<DaprConversationInput> inputs, ConversationOptions? options = null,
71-
CancellationToken cancellationToken = default)
72-
{
73-
var request = new P.ConversationRequest
74-
{
75-
Name = daprConversationComponentName
76-
};
77-
78-
if (options is not null)
79-
{
80-
if (options.ConversationId is not null)
81-
{
82-
request.ContextID = options.ConversationId;
83-
}
84-
85-
request.ScrubPII = options.ScrubPII;
86-
87-
foreach (var (key, value) in options.Metadata)
88-
{
89-
request.Metadata.Add(key, value);
90-
}
91-
92-
foreach (var (key, value) in options.Parameters)
93-
{
94-
request.Parameters.Add(key, value);
95-
}
96-
}
97-
98-
foreach (var input in inputs)
99-
{
100-
request.Inputs.Add(new P.ConversationInput
101-
{
102-
ScrubPII = input.ScrubPII,
103-
Content = input.Content,
104-
Role = input.Role.GetValueFromEnumMember()
105-
});
106-
}
107-
108-
var grpCCallOptions =
109-
DaprClientUtilities.ConfigureGrpcCallOptions(typeof(DaprConversationClient).Assembly, this.DaprApiToken,
110-
cancellationToken);
111-
112-
var result = await Client.ConverseAlpha1Async(request, grpCCallOptions).ConfigureAwait(false);
113-
var outputs = result.Outputs.Select(output => new DaprConversationResult(output.Result)
114-
{
115-
Parameters = output.Parameters.ToDictionary(kvp => kvp.Key, parameter => parameter.Value)
116-
}).ToList();
117-
118-
return new DaprConversationResponse(outputs);
119-
}
80+
public abstract Task<DaprConversationResponse> ConverseAsync(string daprConversationComponentName,
81+
IReadOnlyList<DaprConversationInput> inputs, ConversationOptions? options = null,
82+
CancellationToken cancellationToken = default);
12083
}

src/Dapr.AI/Conversation/DaprConversationClientBuilder.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,11 @@ public sealed class DaprConversationClientBuilder : DaprGenericClientBuilder<Dap
2525
/// <summary>
2626
/// Used to initialize a new instance of the <see cref="DaprConversationClient"/>.
2727
/// </summary>
28-
/// <param name="configuration"></param>
28+
/// <param name="configuration">An optional <see cref="IConfiguration"/> to configure the client with.</param>
2929
public DaprConversationClientBuilder(IConfiguration? configuration = null) : base(configuration)
3030
{
3131
}
32-
32+
3333
/// <summary>
3434
/// Builds the client instance from the properties of the builder.
3535
/// </summary>
@@ -41,6 +41,6 @@ public override DaprConversationClient Build()
4141
{
4242
var daprClientDependencies = BuildDaprClientDependencies(typeof(DaprConversationClient).Assembly);
4343
var client = new Autogenerated.DaprClient(daprClientDependencies.channel);
44-
return new DaprConversationClient(client, daprClientDependencies.httpClient, daprClientDependencies.daprApiToken);
44+
return new DaprConversationGrpcClient(client, daprClientDependencies.httpClient, daprClientDependencies.daprApiToken);
4545
}
4646
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// ------------------------------------------------------------------------
2+
// Copyright 2025 The Dapr Authors
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
// Unless required by applicable law or agreed to in writing, software
8+
// distributed under the License is distributed on an "AS IS" BASIS,
9+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
// See the License for the specific language governing permissions and
11+
// limitations under the License.
12+
// ------------------------------------------------------------------------
13+
14+
using Dapr.Common;
15+
using Dapr.Common.Extensions;
16+
using Autogenerated = Dapr.Client.Autogen.Grpc.v1;
17+
18+
namespace Dapr.AI.Conversation;
19+
20+
/// <summary>
21+
/// Used to initialize a new instance of a <see cref="DaprConversationClient"/>.
22+
/// </summary>
23+
/// <param name="client">The Dapr client.</param>
24+
/// <param name="httpClient">The HTTP client used by the client for calling the Dapr runtime.</param>
25+
/// <param name="daprApiToken">An optional token required to send requests to the Dapr sidecar.</param>
26+
internal sealed class DaprConversationGrpcClient(Autogenerated.Dapr.DaprClient client, HttpClient httpClient, string? daprApiToken = null) : DaprConversationClient(client, httpClient, daprApiToken: daprApiToken)
27+
{
28+
/// <summary>
29+
/// Sends various inputs to the large language model via the Conversational building block on the Dapr sidecar.
30+
/// </summary>
31+
/// <param name="daprConversationComponentName">The name of the Dapr conversation component.</param>
32+
/// <param name="inputs">The input values to send.</param>
33+
/// <param name="options">Optional options used to configure the conversation.</param>
34+
/// <param name="cancellationToken">Cancellation token.</param>
35+
/// <returns>The response(s) provided by the LLM provider.</returns>
36+
public override async Task<DaprConversationResponse> ConverseAsync(string daprConversationComponentName, IReadOnlyList<DaprConversationInput> inputs, ConversationOptions? options = null,
37+
CancellationToken cancellationToken = default)
38+
{
39+
var request = new Autogenerated.ConversationRequest
40+
{
41+
Name = daprConversationComponentName
42+
};
43+
44+
if (options is not null)
45+
{
46+
if (options.ConversationId is not null)
47+
{
48+
request.ContextID = options.ConversationId;
49+
}
50+
51+
request.ScrubPII = options.ScrubPII;
52+
53+
foreach (var (key, value) in options.Metadata)
54+
{
55+
request.Metadata.Add(key, value);
56+
}
57+
58+
foreach (var (key, value) in options.Parameters)
59+
{
60+
request.Parameters.Add(key, value);
61+
}
62+
}
63+
64+
foreach (var input in inputs)
65+
{
66+
request.Inputs.Add(new Autogenerated.ConversationInput
67+
{
68+
ScrubPII = input.ScrubPII,
69+
Content = input.Content,
70+
Role = input.Role.GetValueFromEnumMember()
71+
});
72+
}
73+
74+
var grpCCallOptions =
75+
DaprClientUtilities.ConfigureGrpcCallOptions(typeof(DaprConversationClient).Assembly, this.DaprApiToken,
76+
cancellationToken);
77+
78+
var result = await Client.ConverseAlpha1Async(request, grpCCallOptions).ConfigureAwait(false);
79+
var outputs = result.Outputs.Select(output => new DaprConversationResult(output.Result)
80+
{
81+
Parameters = output.Parameters.ToDictionary(kvp => kvp.Key, parameter => parameter.Value)
82+
}).ToList();
83+
84+
return new DaprConversationResponse(outputs);
85+
}
86+
87+
/// <inheritdoc />
88+
protected override void Dispose(bool disposing)
89+
{
90+
if (disposing)
91+
{
92+
this.HttpClient.Dispose();
93+
}
94+
}
95+
}

src/Dapr.AI/Conversation/Extensions/DaprAiConversationBuilder.cs

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,10 @@ namespace Dapr.AI.Conversation.Extensions;
1818
/// <summary>
1919
/// Used by the fluent registration builder to configure a Dapr AI conversational manager.
2020
/// </summary>
21-
public sealed class DaprAiConversationBuilder : IDaprAiConversationBuilder
21+
public sealed class DaprAiConversationBuilder(IServiceCollection services) : IDaprAiConversationBuilder
2222
{
2323
/// <summary>
2424
/// The registered services on the builder.
2525
/// </summary>
26-
public IServiceCollection Services { get; }
27-
28-
/// <summary>
29-
/// Used to initialize a new <see cref="DaprAiConversationBuilder"/>.
30-
/// </summary>
31-
public DaprAiConversationBuilder(IServiceCollection services)
32-
{
33-
Services = services;
34-
}
26+
public IServiceCollection Services { get; } = services;
3527
}

src/Dapr.AI/Conversation/Extensions/DaprAiConversationBuilderExtensions.cs

Lines changed: 7 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,8 @@
1111
// limitations under the License.
1212
// ------------------------------------------------------------------------
1313

14-
using Microsoft.Extensions.Configuration;
14+
using Dapr.Common.Extensions;
1515
using Microsoft.Extensions.DependencyInjection;
16-
using Microsoft.Extensions.DependencyInjection.Extensions;
1716

1817
namespace Dapr.AI.Conversation.Extensions;
1918

@@ -23,42 +22,11 @@ namespace Dapr.AI.Conversation.Extensions;
2322
public static class DaprAiConversationBuilderExtensions
2423
{
2524
/// <summary>
26-
/// Registers the necessary functionality for the Dapr AI conversation functionality.
25+
/// Registers the necessary functionality for the Dapr AI Conversation functionality.
2726
/// </summary>
28-
/// <returns></returns>
29-
public static IDaprAiConversationBuilder AddDaprConversationClient(this IServiceCollection services, Action<IServiceProvider, DaprConversationClientBuilder>? configure = null, ServiceLifetime lifetime = ServiceLifetime.Singleton)
30-
{
31-
ArgumentNullException.ThrowIfNull(services, nameof(services));
32-
33-
services.AddHttpClient();
34-
35-
var registration = new Func<IServiceProvider, DaprConversationClient>(provider =>
36-
{
37-
var configuration = provider.GetService<IConfiguration>();
38-
var builder = new DaprConversationClientBuilder(configuration);
39-
40-
var httpClientFactory = provider.GetRequiredService<IHttpClientFactory>();
41-
builder.UseHttpClientFactory(httpClientFactory);
42-
43-
configure?.Invoke(provider, builder);
44-
45-
return builder.Build();
46-
});
47-
48-
switch (lifetime)
49-
{
50-
case ServiceLifetime.Scoped:
51-
services.TryAddScoped(registration);
52-
break;
53-
case ServiceLifetime.Transient:
54-
services.TryAddTransient(registration);
55-
break;
56-
case ServiceLifetime.Singleton:
57-
default:
58-
services.TryAddSingleton(registration);
59-
break;
60-
}
61-
62-
return new DaprAiConversationBuilder(services);
63-
}
27+
public static IDaprAiConversationBuilder AddDaprConversationClient(
28+
this IServiceCollection services,
29+
Action<IServiceProvider, DaprConversationClientBuilder>? configure = null,
30+
ServiceLifetime lifetime = ServiceLifetime.Singleton) => services
31+
.AddDaprClient<DaprConversationClient, DaprConversationGrpcClient, DaprAiConversationBuilder, DaprConversationClientBuilder>(configure, lifetime);
6432
}

src/Dapr.AI/Conversation/Extensions/IDaprAiConversationBuilder.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,4 @@ namespace Dapr.AI.Conversation.Extensions;
1818
/// <summary>
1919
/// Provides a root builder for the Dapr AI conversational functionality facilitating a more fluent-style registration.
2020
/// </summary>
21-
public interface IDaprAiConversationBuilder : IDaprAiServiceBuilder
22-
{
23-
}
21+
public interface IDaprAiConversationBuilder : IDaprAiServiceBuilder;

src/Dapr.AI/DaprAIClient.cs

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,32 @@
1111
// limitations under the License.
1212
// ------------------------------------------------------------------------
1313

14-
using Dapr.AI.Conversation;
14+
using Dapr.Common;
1515

1616
namespace Dapr.AI;
1717

1818
/// <summary>
1919
/// The base implementation of a Dapr AI client.
2020
/// </summary>
21-
public abstract class DaprAIClient
21+
public abstract class DaprAIClient : IDaprClient
2222
{
23+
private bool disposed;
24+
25+
/// <inheritdoc />
26+
public void Dispose()
27+
{
28+
if (!this.disposed)
29+
{
30+
Dispose(disposing: true);
31+
this.disposed = true;
32+
}
33+
}
34+
2335
/// <summary>
24-
/// Sends various inputs to the large language model via the Conversational building block on the Dapr sidecar.
36+
/// Disposes the resources associated with the object.
2537
/// </summary>
26-
/// <param name="daprConversationComponentName">The name of the Dapr conversation component.</param>
27-
/// <param name="inputs">The input values to send.</param>
28-
/// <param name="options">Optional options used to configure the conversation.</param>
29-
/// <param name="cancellationToken">Cancellation token.</param>
30-
/// <returns>The response(s) provided by the LLM provider.</returns>
31-
public abstract Task<DaprConversationResponse> ConverseAsync(string daprConversationComponentName,
32-
IReadOnlyList<DaprConversationInput> inputs, ConversationOptions? options = null,
33-
CancellationToken cancellationToken = default);
38+
/// <param name="disposing"><c>true</c> if called by a call to the <c>Dispose</c> method; otherwise false.</param>
39+
protected virtual void Dispose(bool disposing)
40+
{
41+
}
3442
}

src/Dapr.AI/Extensions/IDaprAiServiceBuilder.cs

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,12 @@
1111
// limitations under the License.
1212
// ------------------------------------------------------------------------
1313

14+
using Dapr.Common;
1415
using Microsoft.Extensions.DependencyInjection;
1516

1617
namespace Dapr.AI.Extensions;
1718

1819
/// <summary>
1920
/// Responsible for registering Dapr AI service functionality.
2021
/// </summary>
21-
public interface IDaprAiServiceBuilder
22-
{
23-
/// <summary>
24-
/// The registered services on the builder.
25-
/// </summary>
26-
public IServiceCollection Services { get; }
27-
}
22+
public interface IDaprAiServiceBuilder : IDaprServiceBuilder;

src/Dapr.Common/Dapr.Common.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
</PropertyGroup>
77

88
<ItemGroup>
9+
<ProjectReference Include="..\Dapr.Protos\Dapr.Protos.csproj" />
910
<PackageReference Include="Google.Api.CommonProtos" />
1011
<PackageReference Include="Grpc.Net.Client" />
1112
<PackageReference Include="Microsoft.Extensions.Http" />

0 commit comments

Comments
 (0)