diff --git a/src/Dapr.AI/Conversation/DaprConversationClient.cs b/src/Dapr.AI/Conversation/DaprConversationClient.cs
index ed33e6774..b081427b0 100644
--- a/src/Dapr.AI/Conversation/DaprConversationClient.cs
+++ b/src/Dapr.AI/Conversation/DaprConversationClient.cs
@@ -11,16 +11,26 @@
// limitations under the License.
// ------------------------------------------------------------------------
-using Dapr.Common;
-using Dapr.Common.Extensions;
-using P = Dapr.Client.Autogen.Grpc.v1;
+using Autogenerated = Dapr.Client.Autogen.Grpc.v1.Dapr;
namespace Dapr.AI.Conversation;
///
+///
/// Used to interact with the Dapr conversation building block.
+/// Use to create a or register
+/// for use with dependency injection via
+/// DaprJobsServiceCollectionExtensions.AddDaprJobsClient.
+///
+///
+/// Implementations of implement because the
+/// client accesses network resources. For best performance, create a single long-lived client instance
+/// and share it for the lifetime of the application. This is done for you if created via the DI extensions. Avoid
+/// creating a disposing a client instance for each operation that the application performs - this can lead to socket
+/// exhaustion and other problems.
+///
///
-public sealed class DaprConversationClient : DaprAIClient
+public abstract class DaprConversationClient : DaprAIClient
{
///
/// The HTTP client used by the client for calling the Dapr runtime.
@@ -42,7 +52,7 @@ public sealed class DaprConversationClient : DaprAIClient
///
/// Property exposed for testing purposes.
///
- internal P.Dapr.DaprClient Client { get; }
+ internal Autogenerated.DaprClient Client { get; }
///
/// Used to initialize a new instance of a .
@@ -50,7 +60,7 @@ public sealed class DaprConversationClient : DaprAIClient
/// The Dapr client.
/// The HTTP client used by the client for calling the Dapr runtime.
/// An optional token required to send requests to the Dapr sidecar.
- public DaprConversationClient(P.Dapr.DaprClient client,
+ protected DaprConversationClient(Autogenerated.DaprClient client,
HttpClient httpClient,
string? daprApiToken = null)
{
@@ -67,54 +77,7 @@ public DaprConversationClient(P.Dapr.DaprClient client,
/// Optional options used to configure the conversation.
/// Cancellation token.
/// The response(s) provided by the LLM provider.
- public override async Task ConverseAsync(string daprConversationComponentName, IReadOnlyList inputs, ConversationOptions? options = null,
- CancellationToken cancellationToken = default)
- {
- var request = new P.ConversationRequest
- {
- Name = daprConversationComponentName
- };
-
- if (options is not null)
- {
- if (options.ConversationId is not null)
- {
- request.ContextID = options.ConversationId;
- }
-
- request.ScrubPII = options.ScrubPII;
-
- foreach (var (key, value) in options.Metadata)
- {
- request.Metadata.Add(key, value);
- }
-
- foreach (var (key, value) in options.Parameters)
- {
- request.Parameters.Add(key, value);
- }
- }
-
- foreach (var input in inputs)
- {
- request.Inputs.Add(new P.ConversationInput
- {
- ScrubPII = input.ScrubPII,
- Content = input.Content,
- Role = input.Role.GetValueFromEnumMember()
- });
- }
-
- var grpCCallOptions =
- DaprClientUtilities.ConfigureGrpcCallOptions(typeof(DaprConversationClient).Assembly, this.DaprApiToken,
- cancellationToken);
-
- var result = await Client.ConverseAlpha1Async(request, grpCCallOptions).ConfigureAwait(false);
- var outputs = result.Outputs.Select(output => new DaprConversationResult(output.Result)
- {
- Parameters = output.Parameters.ToDictionary(kvp => kvp.Key, parameter => parameter.Value)
- }).ToList();
-
- return new DaprConversationResponse(outputs);
- }
+ public abstract Task ConverseAsync(string daprConversationComponentName,
+ IReadOnlyList inputs, ConversationOptions? options = null,
+ CancellationToken cancellationToken = default);
}
diff --git a/src/Dapr.AI/Conversation/DaprConversationClientBuilder.cs b/src/Dapr.AI/Conversation/DaprConversationClientBuilder.cs
index 5e0a0825d..88cf3b2a9 100644
--- a/src/Dapr.AI/Conversation/DaprConversationClientBuilder.cs
+++ b/src/Dapr.AI/Conversation/DaprConversationClientBuilder.cs
@@ -25,11 +25,11 @@ public sealed class DaprConversationClientBuilder : DaprGenericClientBuilder
/// Used to initialize a new instance of the .
///
- ///
+ /// An optional to configure the client with.
public DaprConversationClientBuilder(IConfiguration? configuration = null) : base(configuration)
{
}
-
+
///
/// Builds the client instance from the properties of the builder.
///
@@ -41,6 +41,6 @@ public override DaprConversationClient Build()
{
var daprClientDependencies = BuildDaprClientDependencies(typeof(DaprConversationClient).Assembly);
var client = new Autogenerated.DaprClient(daprClientDependencies.channel);
- return new DaprConversationClient(client, daprClientDependencies.httpClient, daprClientDependencies.daprApiToken);
+ return new DaprConversationGrpcClient(client, daprClientDependencies.httpClient, daprClientDependencies.daprApiToken);
}
}
diff --git a/src/Dapr.AI/Conversation/DaprConversationGrpcClient.cs b/src/Dapr.AI/Conversation/DaprConversationGrpcClient.cs
new file mode 100644
index 000000000..6a1a5f438
--- /dev/null
+++ b/src/Dapr.AI/Conversation/DaprConversationGrpcClient.cs
@@ -0,0 +1,95 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+using Dapr.Common;
+using Dapr.Common.Extensions;
+using Autogenerated = Dapr.Client.Autogen.Grpc.v1;
+
+namespace Dapr.AI.Conversation;
+
+///
+/// Used to initialize a new instance of a .
+///
+/// The Dapr client.
+/// The HTTP client used by the client for calling the Dapr runtime.
+/// An optional token required to send requests to the Dapr sidecar.
+internal sealed class DaprConversationGrpcClient(Autogenerated.Dapr.DaprClient client, HttpClient httpClient, string? daprApiToken = null) : DaprConversationClient(client, httpClient, daprApiToken: daprApiToken)
+{
+ ///
+ /// Sends various inputs to the large language model via the Conversational building block on the Dapr sidecar.
+ ///
+ /// The name of the Dapr conversation component.
+ /// The input values to send.
+ /// Optional options used to configure the conversation.
+ /// Cancellation token.
+ /// The response(s) provided by the LLM provider.
+ public override async Task ConverseAsync(string daprConversationComponentName, IReadOnlyList inputs, ConversationOptions? options = null,
+ CancellationToken cancellationToken = default)
+ {
+ var request = new Autogenerated.ConversationRequest
+ {
+ Name = daprConversationComponentName
+ };
+
+ if (options is not null)
+ {
+ if (options.ConversationId is not null)
+ {
+ request.ContextID = options.ConversationId;
+ }
+
+ request.ScrubPII = options.ScrubPII;
+
+ foreach (var (key, value) in options.Metadata)
+ {
+ request.Metadata.Add(key, value);
+ }
+
+ foreach (var (key, value) in options.Parameters)
+ {
+ request.Parameters.Add(key, value);
+ }
+ }
+
+ foreach (var input in inputs)
+ {
+ request.Inputs.Add(new Autogenerated.ConversationInput
+ {
+ ScrubPII = input.ScrubPII,
+ Content = input.Content,
+ Role = input.Role.GetValueFromEnumMember()
+ });
+ }
+
+ var grpCCallOptions =
+ DaprClientUtilities.ConfigureGrpcCallOptions(typeof(DaprConversationClient).Assembly, this.DaprApiToken,
+ cancellationToken);
+
+ var result = await Client.ConverseAlpha1Async(request, grpCCallOptions).ConfigureAwait(false);
+ var outputs = result.Outputs.Select(output => new DaprConversationResult(output.Result)
+ {
+ Parameters = output.Parameters.ToDictionary(kvp => kvp.Key, parameter => parameter.Value)
+ }).ToList();
+
+ return new DaprConversationResponse(outputs);
+ }
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ this.HttpClient.Dispose();
+ }
+ }
+}
diff --git a/src/Dapr.AI/Conversation/Extensions/DaprAiConversationBuilder.cs b/src/Dapr.AI/Conversation/Extensions/DaprAiConversationBuilder.cs
index 876d223b1..4ba236259 100644
--- a/src/Dapr.AI/Conversation/Extensions/DaprAiConversationBuilder.cs
+++ b/src/Dapr.AI/Conversation/Extensions/DaprAiConversationBuilder.cs
@@ -18,18 +18,10 @@ namespace Dapr.AI.Conversation.Extensions;
///
/// Used by the fluent registration builder to configure a Dapr AI conversational manager.
///
-public sealed class DaprAiConversationBuilder : IDaprAiConversationBuilder
+public sealed class DaprAiConversationBuilder(IServiceCollection services) : IDaprAiConversationBuilder
{
///
/// The registered services on the builder.
///
- public IServiceCollection Services { get; }
-
- ///
- /// Used to initialize a new .
- ///
- public DaprAiConversationBuilder(IServiceCollection services)
- {
- Services = services;
- }
+ public IServiceCollection Services { get; } = services;
}
diff --git a/src/Dapr.AI/Conversation/Extensions/DaprAiConversationBuilderExtensions.cs b/src/Dapr.AI/Conversation/Extensions/DaprAiConversationBuilderExtensions.cs
index 2f049a906..42bd91804 100644
--- a/src/Dapr.AI/Conversation/Extensions/DaprAiConversationBuilderExtensions.cs
+++ b/src/Dapr.AI/Conversation/Extensions/DaprAiConversationBuilderExtensions.cs
@@ -11,9 +11,8 @@
// limitations under the License.
// ------------------------------------------------------------------------
-using Microsoft.Extensions.Configuration;
+using Dapr.Common.Extensions;
using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.DependencyInjection.Extensions;
namespace Dapr.AI.Conversation.Extensions;
@@ -23,42 +22,11 @@ namespace Dapr.AI.Conversation.Extensions;
public static class DaprAiConversationBuilderExtensions
{
///
- /// Registers the necessary functionality for the Dapr AI conversation functionality.
+ /// Registers the necessary functionality for the Dapr AI Conversation functionality.
///
- ///
- public static IDaprAiConversationBuilder AddDaprConversationClient(this IServiceCollection services, Action? configure = null, ServiceLifetime lifetime = ServiceLifetime.Singleton)
- {
- ArgumentNullException.ThrowIfNull(services, nameof(services));
-
- services.AddHttpClient();
-
- var registration = new Func(provider =>
- {
- var configuration = provider.GetService();
- var builder = new DaprConversationClientBuilder(configuration);
-
- var httpClientFactory = provider.GetRequiredService();
- builder.UseHttpClientFactory(httpClientFactory);
-
- configure?.Invoke(provider, builder);
-
- return builder.Build();
- });
-
- switch (lifetime)
- {
- case ServiceLifetime.Scoped:
- services.TryAddScoped(registration);
- break;
- case ServiceLifetime.Transient:
- services.TryAddTransient(registration);
- break;
- case ServiceLifetime.Singleton:
- default:
- services.TryAddSingleton(registration);
- break;
- }
-
- return new DaprAiConversationBuilder(services);
- }
+ public static IDaprAiConversationBuilder AddDaprConversationClient(
+ this IServiceCollection services,
+ Action? configure = null,
+ ServiceLifetime lifetime = ServiceLifetime.Singleton) => services
+ .AddDaprClient(configure, lifetime);
}
diff --git a/src/Dapr.AI/Conversation/Extensions/IDaprAiConversationBuilder.cs b/src/Dapr.AI/Conversation/Extensions/IDaprAiConversationBuilder.cs
index 30d3822d4..3afe63559 100644
--- a/src/Dapr.AI/Conversation/Extensions/IDaprAiConversationBuilder.cs
+++ b/src/Dapr.AI/Conversation/Extensions/IDaprAiConversationBuilder.cs
@@ -18,6 +18,4 @@ namespace Dapr.AI.Conversation.Extensions;
///
/// Provides a root builder for the Dapr AI conversational functionality facilitating a more fluent-style registration.
///
-public interface IDaprAiConversationBuilder : IDaprAiServiceBuilder
-{
-}
+public interface IDaprAiConversationBuilder : IDaprAiServiceBuilder;
diff --git a/src/Dapr.AI/DaprAIClient.cs b/src/Dapr.AI/DaprAIClient.cs
index a2fd2255f..ae029ece3 100644
--- a/src/Dapr.AI/DaprAIClient.cs
+++ b/src/Dapr.AI/DaprAIClient.cs
@@ -11,24 +11,32 @@
// limitations under the License.
// ------------------------------------------------------------------------
-using Dapr.AI.Conversation;
+using Dapr.Common;
namespace Dapr.AI;
///
/// The base implementation of a Dapr AI client.
///
-public abstract class DaprAIClient
+public abstract class DaprAIClient : IDaprClient
{
+ private bool disposed;
+
+ ///
+ public void Dispose()
+ {
+ if (!this.disposed)
+ {
+ Dispose(disposing: true);
+ this.disposed = true;
+ }
+ }
+
///
- /// Sends various inputs to the large language model via the Conversational building block on the Dapr sidecar.
+ /// Disposes the resources associated with the object.
///
- /// The name of the Dapr conversation component.
- /// The input values to send.
- /// Optional options used to configure the conversation.
- /// Cancellation token.
- /// The response(s) provided by the LLM provider.
- public abstract Task ConverseAsync(string daprConversationComponentName,
- IReadOnlyList inputs, ConversationOptions? options = null,
- CancellationToken cancellationToken = default);
+ /// true if called by a call to the Dispose method; otherwise false.
+ protected virtual void Dispose(bool disposing)
+ {
+ }
}
diff --git a/src/Dapr.AI/Extensions/IDaprAiServiceBuilder.cs b/src/Dapr.AI/Extensions/IDaprAiServiceBuilder.cs
index 8a0a80c2c..d7d84d1fb 100644
--- a/src/Dapr.AI/Extensions/IDaprAiServiceBuilder.cs
+++ b/src/Dapr.AI/Extensions/IDaprAiServiceBuilder.cs
@@ -11,6 +11,7 @@
// limitations under the License.
// ------------------------------------------------------------------------
+using Dapr.Common;
using Microsoft.Extensions.DependencyInjection;
namespace Dapr.AI.Extensions;
@@ -18,10 +19,4 @@ namespace Dapr.AI.Extensions;
///
/// Responsible for registering Dapr AI service functionality.
///
-public interface IDaprAiServiceBuilder
-{
- ///
- /// The registered services on the builder.
- ///
- public IServiceCollection Services { get; }
-}
+public interface IDaprAiServiceBuilder : IDaprServiceBuilder;
diff --git a/src/Dapr.Common/Dapr.Common.csproj b/src/Dapr.Common/Dapr.Common.csproj
index dac090d3e..03d23f9bd 100644
--- a/src/Dapr.Common/Dapr.Common.csproj
+++ b/src/Dapr.Common/Dapr.Common.csproj
@@ -6,6 +6,7 @@
+
diff --git a/src/Dapr.Common/DaprGenericClientBuilder.cs b/src/Dapr.Common/DaprGenericClientBuilder.cs
index 7a7abf025..8ff59490f 100644
--- a/src/Dapr.Common/DaprGenericClientBuilder.cs
+++ b/src/Dapr.Common/DaprGenericClientBuilder.cs
@@ -21,7 +21,7 @@ namespace Dapr.Common;
///
/// Builder for building a generic Dapr client.
///
-public abstract class DaprGenericClientBuilder where TClientBuilder : class
+public abstract class DaprGenericClientBuilder where TClientBuilder : class, IDaprClient
{
///
/// Initializes a new instance of the class.
@@ -186,7 +186,7 @@ public DaprGenericClientBuilder UseTimeout(TimeSpan timeout)
///
/// The assembly the dependencies are being built for.
///
- protected (GrpcChannel channel, HttpClient httpClient, Uri httpEndpoint, string daprApiToken) BuildDaprClientDependencies(Assembly assembly)
+ protected internal (GrpcChannel channel, HttpClient httpClient, Uri httpEndpoint, string daprApiToken) BuildDaprClientDependencies(Assembly assembly)
{
var grpcEndpoint = new Uri(this.GrpcEndpoint);
if (grpcEndpoint.Scheme != "http" && grpcEndpoint.Scheme != "https")
diff --git a/src/Dapr.Common/Extensions/DaprClientBuilderExtensions.cs b/src/Dapr.Common/Extensions/DaprClientBuilderExtensions.cs
new file mode 100644
index 000000000..1070133c2
--- /dev/null
+++ b/src/Dapr.Common/Extensions/DaprClientBuilderExtensions.cs
@@ -0,0 +1,82 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+using System.Reflection;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Autogenerated = Dapr.Client.Autogen.Grpc.v1.Dapr;
+
+namespace Dapr.Common.Extensions;
+
+///
+/// Generic extension used to build out type-specific Dapr clients.
+///
+internal static class DaprClientBuilderExtensions
+{
+ ///
+ /// Registers the necessary base functionality for a Dapr client.
+ ///
+ /// The type of the client builder interface.
+ /// The abstract Dapr client type being created.
+ /// The concrete Dapr client type being created.
+ /// The type of the static builder used to build the Dapr ot client.
+ /// The collection of services to which the Dapr client and associated services are being registered.
+ /// An optional method used to provide additional configurations to the client builder.
+ /// The registered lifetime of the Dapr client.
+ /// The collection of DI-registered services.
+ //internal static TBuilderInterface AddDaprClient(
+ internal static TServiceBuilder AddDaprClient(
+ this IServiceCollection services,
+ Action? configure = null,
+ ServiceLifetime lifetime = ServiceLifetime.Singleton)
+ where TClient : class, IDaprClient
+ where TConcreteClient : TClient
+ where TServiceBuilder : class, IDaprServiceBuilder
+ where TClientBuilder : DaprGenericClientBuilder
+ {
+ ArgumentNullException.ThrowIfNull(services, nameof(services));
+
+ //Ensure that TConcreteClient is a concrete class
+ if (typeof(TConcreteClient).IsInterface || typeof(TConcreteClient).IsAbstract)
+ {
+ throw new ArgumentException($"{typeof(TConcreteClient).Name} must be a concrete class",
+ nameof(TConcreteClient));
+ }
+
+ //Ensure that TServiceBuilder is a concrete class
+ if (typeof(TServiceBuilder).IsInterface || typeof(TServiceBuilder).IsAbstract)
+ {
+ throw new ArgumentException($"{typeof(TServiceBuilder).Name} must be a concrete class",
+ nameof(TServiceBuilder));
+ }
+
+ services.AddHttpClient();
+
+ var registration = new Func(provider =>
+ {
+ var configuration = provider.GetService();
+ var builder = (TClientBuilder)Activator.CreateInstance(typeof(TClientBuilder), configuration)!;
+
+ builder.UseDaprApiToken(DaprDefaults.GetDefaultDaprApiToken(configuration));
+ configure?.Invoke(provider, builder);
+ var (channel, httpClient, _, daprApiToken) =
+ builder.BuildDaprClientDependencies(Assembly.GetExecutingAssembly());
+ var daprClient = new Autogenerated.DaprClient(channel);
+ return (TClient)Activator.CreateInstance(typeof(TConcreteClient), daprClient, httpClient, daprApiToken)!;
+ });
+
+ services.Add(new ServiceDescriptor(typeof(TClient), registration, lifetime));
+
+ return (TServiceBuilder)Activator.CreateInstance(typeof(TServiceBuilder), services)!;
+ }
+}
diff --git a/src/Dapr.Common/IDaprClient.cs b/src/Dapr.Common/IDaprClient.cs
new file mode 100644
index 000000000..001cc17c7
--- /dev/null
+++ b/src/Dapr.Common/IDaprClient.cs
@@ -0,0 +1,19 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+namespace Dapr.Common;
+
+///
+/// Base interface for any of the specific Dapr clients.
+///
+public interface IDaprClient : IDisposable;
diff --git a/src/Dapr.Common/IDaprServiceBuilder.cs b/src/Dapr.Common/IDaprServiceBuilder.cs
new file mode 100644
index 000000000..18379d910
--- /dev/null
+++ b/src/Dapr.Common/IDaprServiceBuilder.cs
@@ -0,0 +1,27 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Dapr.Common;
+
+///
+/// Responsible for registering Dapr services with dependency injection.
+///
+public interface IDaprServiceBuilder
+{
+ ///
+ /// The registered services on the builder.
+ ///
+ public IServiceCollection Services { get; }
+}
diff --git a/src/Dapr.Jobs/DaprJobsClient.cs b/src/Dapr.Jobs/DaprJobsClient.cs
index 4dd4abd70..2aecf816a 100644
--- a/src/Dapr.Jobs/DaprJobsClient.cs
+++ b/src/Dapr.Jobs/DaprJobsClient.cs
@@ -11,8 +11,10 @@
// limitations under the License.
// ------------------------------------------------------------------------
+using Dapr.Common;
using Dapr.Jobs.Models;
using Dapr.Jobs.Models.Responses;
+using Autogenerated = Dapr.Client.Autogen.Grpc.v1.Dapr;
namespace Dapr.Jobs;
@@ -31,10 +33,34 @@ namespace Dapr.Jobs;
/// exhaustion and other problems.
///
///
-public abstract class DaprJobsClient : IDisposable
+public abstract class DaprJobsClient(Autogenerated.DaprClient client, HttpClient httpClient, string? daprApiToken = null) : IDaprClient
{
private bool disposed;
+ ///
+ /// The HTTP client used by the client for calling the Dapr runtime.
+ ///
+ ///
+ /// Property exposed for testing purposes.
+ ///
+ internal readonly HttpClient HttpClient = httpClient;
+
+ ///
+ /// The Dapr API token value.
+ ///
+ ///
+ /// Property exposed for testing purposes.
+ ///
+ internal readonly string? DaprApiToken = daprApiToken;
+
+ ///
+ /// The autogenerated Dapr client.
+ ///
+ ///
+ /// Property exposed for testing purposes.
+ ///
+ internal Autogenerated.DaprClient Client { get; } = client;
+
///
/// Schedules a job with Dapr.
///
diff --git a/src/Dapr.Jobs/DaprJobsGrpcClient.cs b/src/Dapr.Jobs/DaprJobsGrpcClient.cs
index 8743aa350..182db48cf 100644
--- a/src/Dapr.Jobs/DaprJobsGrpcClient.cs
+++ b/src/Dapr.Jobs/DaprJobsGrpcClient.cs
@@ -24,40 +24,8 @@ namespace Dapr.Jobs;
///
/// A client for interacting with the Dapr endpoints.
///
-internal sealed class DaprJobsGrpcClient : DaprJobsClient
+internal sealed class DaprJobsGrpcClient(Autogenerated.Dapr.DaprClient client, HttpClient httpClient, string? daprApiToken = null) : DaprJobsClient(client, httpClient, daprApiToken: daprApiToken)
{
- ///
- /// The HTTP client used by the client for calling the Dapr runtime.
- ///
- ///
- /// Property exposed for testing purposes.
- ///
- internal readonly HttpClient HttpClient;
- ///
- /// The Dapr API token value.
- ///
- ///
- /// Property exposed for testing purposes.
- ///
- internal readonly string? DaprApiToken;
- ///
- /// The autogenerated Dapr client.
- ///
- ///
- /// Property exposed for testing purposes.
- ///
- internal Autogenerated.Dapr.DaprClient Client { get; }
-
- internal DaprJobsGrpcClient(
- Autogenerated.Dapr.DaprClient innerClient,
- HttpClient httpClient,
- string? daprApiToken)
- {
- this.Client = innerClient;
- this.HttpClient = httpClient;
- this.DaprApiToken = daprApiToken;
- }
-
///
/// Schedules a job with Dapr.
///
diff --git a/src/Dapr.Jobs/Extensions/DaprJobsBuilder.cs b/src/Dapr.Jobs/Extensions/DaprJobsBuilder.cs
new file mode 100644
index 000000000..51bdb8985
--- /dev/null
+++ b/src/Dapr.Jobs/Extensions/DaprJobsBuilder.cs
@@ -0,0 +1,28 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Dapr.Jobs.Extensions;
+
+///
+/// Used by the fluent registration builder to configure a Dapr Jobs client.
+///
+///
+public sealed class DaprJobsBuilder(IServiceCollection services) : IDaprJobsBuilder
+{
+ ///
+ /// The registered services on the builder.
+ ///
+ public IServiceCollection Services { get; } = services;
+}
diff --git a/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs b/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs
index 03540aae1..7eed80abc 100644
--- a/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs
+++ b/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs
@@ -11,9 +11,8 @@
// limitations under the License.
// ------------------------------------------------------------------------
-using Microsoft.Extensions.Configuration;
+using Dapr.Common.Extensions;
using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.DependencyInjection.Extensions;
namespace Dapr.Jobs.Extensions;
@@ -25,45 +24,13 @@ public static class DaprJobsServiceCollectionExtensions
///
/// Adds Dapr Jobs client support to the service collection.
///
- /// The .
+ /// The .
/// Optionally allows greater configuration of the using injected services.
/// The lifetime of the registered services.
///
- public static IServiceCollection AddDaprJobsClient(this IServiceCollection serviceCollection, Action? configure = null, ServiceLifetime lifetime = ServiceLifetime.Singleton)
-
- {
- ArgumentNullException.ThrowIfNull(serviceCollection, nameof(serviceCollection));
-
- //Register the IHttpClientFactory implementation
- serviceCollection.AddHttpClient();
-
- var registration = new Func(serviceProvider =>
- {
- var httpClientFactory = serviceProvider.GetRequiredService();
- var configuration = serviceProvider.GetService();
-
- var builder = new DaprJobsClientBuilder(configuration);
- builder.UseHttpClientFactory(httpClientFactory);
-
- configure?.Invoke(serviceProvider, builder);
-
- return builder.Build();
- });
-
- switch (lifetime)
- {
- case ServiceLifetime.Scoped:
- serviceCollection.TryAddScoped(registration);
- break;
- case ServiceLifetime.Transient:
- serviceCollection.TryAddTransient(registration);
- break;
- case ServiceLifetime.Singleton:
- default:
- serviceCollection.TryAddSingleton(registration);
- break;
- }
-
- return serviceCollection;
- }
+ public static IDaprJobsBuilder AddDaprJobsClient(
+ this IServiceCollection services,
+ Action? configure = null,
+ ServiceLifetime lifetime = ServiceLifetime.Singleton) =>
+ services.AddDaprClient(configure, lifetime);
}
diff --git a/src/Dapr.Jobs/IDaprJobsBuilder.cs b/src/Dapr.Jobs/IDaprJobsBuilder.cs
new file mode 100644
index 000000000..d5211c5e5
--- /dev/null
+++ b/src/Dapr.Jobs/IDaprJobsBuilder.cs
@@ -0,0 +1,22 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+using Dapr.Common;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Dapr.Jobs;
+
+///
+/// Responsible for registering Dapr Jobs service functionality.
+///
+public interface IDaprJobsBuilder : IDaprServiceBuilder;
diff --git a/src/Dapr.Messaging/IDaprMessagingBuilder.cs b/src/Dapr.Messaging/IDaprMessagingBuilder.cs
new file mode 100644
index 000000000..79e5bf3f5
--- /dev/null
+++ b/src/Dapr.Messaging/IDaprMessagingBuilder.cs
@@ -0,0 +1,21 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+using Dapr.Common;
+
+namespace Dapr.Messaging;
+
+///
+/// Provides a root builder for the Dapr Messaging operations facilitating a more fluent-style registration.
+///
+public interface IDaprMessagingBuilder : IDaprServiceBuilder;
diff --git a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClient.cs b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClient.cs
index 8fbec2dfe..cbf25ea49 100644
--- a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClient.cs
+++ b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClient.cs
@@ -11,13 +11,42 @@
// limitations under the License.
// ------------------------------------------------------------------------
+using Dapr.Common;
+using P = Dapr.Client.Autogen.Grpc.v1;
+
namespace Dapr.Messaging.PublishSubscribe;
///
/// The base implementation of a Dapr pub/sub client.
///
-public abstract class DaprPublishSubscribeClient
+public abstract class DaprPublishSubscribeClient(P.Dapr.DaprClient client, HttpClient httpClient, string? daprApiToken = null) : IDaprClient
{
+ private bool disposed;
+
+ ///
+ /// The HTTP client used by the client for calling the Dapr runtime.
+ ///
+ ///
+ /// Property exposed for testing purposes.
+ ///
+ internal protected readonly HttpClient HttpClient = httpClient;
+
+ ///
+ /// The Dapr API token value.
+ ///
+ ///
+ /// Property exposed for testing purposes.
+ ///
+ internal protected readonly string? DaprApiToken = daprApiToken;
+
+ ///
+ /// The autogenerated Dapr client.
+ ///
+ ///
+ /// Property exposed for testing purposes.
+ ///
+ internal protected readonly P.Dapr.DaprClient Client = client;
+
///
/// Dynamically subscribes to a Publish/Subscribe component and topic.
///
@@ -28,4 +57,22 @@ public abstract class DaprPublishSubscribeClient
/// Cancellation token.
///
public abstract Task SubscribeAsync(string pubSubName, string topicName, DaprSubscriptionOptions options, TopicMessageHandler messageHandler, CancellationToken cancellationToken = default);
+
+ ///
+ public void Dispose()
+ {
+ if (!this.disposed)
+ {
+ Dispose(disposing: true);
+ this.disposed = true;
+ }
+ }
+
+ ///
+ /// Disposes the resources associated with the object.
+ ///
+ /// true if called by a call to the Dispose method; otherwise false.
+ protected virtual void Dispose(bool disposing)
+ {
+ }
}
diff --git a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs
index 33ef05494..ace670df3 100644
--- a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs
+++ b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs
@@ -18,40 +18,11 @@ namespace Dapr.Messaging.PublishSubscribe;
///
/// A client for interacting with the Dapr endpoints.
///
-internal sealed class DaprPublishSubscribeGrpcClient : DaprPublishSubscribeClient
+internal sealed class DaprPublishSubscribeGrpcClient(
+ P.DaprClient client,
+ HttpClient httpClient,
+ string? daprApiToken = null) : DaprPublishSubscribeClient(client, httpClient, daprApiToken)
{
- ///
- /// The HTTP client used by the client for calling the Dapr runtime.
- ///
- ///
- /// Property exposed for testing purposes.
- ///
- internal readonly HttpClient HttpClient;
- ///
- /// The Dapr API token value.
- ///
- ///
- /// Property exposed for testing purposes.
- ///
- internal readonly string? DaprApiToken;
- ///
- /// The autogenerated Dapr client.
- ///
- ///
- /// Property exposed for testing purposes.
- ///
- private readonly P.DaprClient Client;
-
- ///
- /// Creates a new instance of a
- ///
- public DaprPublishSubscribeGrpcClient(P.DaprClient client, HttpClient httpClient, string? daprApiToken)
- {
- this.Client = client;
- this.HttpClient = httpClient;
- this.DaprApiToken = daprApiToken;
- }
-
///
/// Dynamically subscribes to a Publish/Subscribe component and topic.
///
@@ -61,11 +32,25 @@ public DaprPublishSubscribeGrpcClient(P.DaprClient client, HttpClient httpClient
/// The delegate reflecting the action to take upon messages received by the subscription.
/// Cancellation token.
///
- public override async Task SubscribeAsync(string pubSubName, string topicName, DaprSubscriptionOptions options, TopicMessageHandler messageHandler, CancellationToken cancellationToken = default)
+ public override async Task SubscribeAsync(
+ string pubSubName,
+ string topicName,
+ DaprSubscriptionOptions options,
+ TopicMessageHandler messageHandler,
+ CancellationToken cancellationToken = default)
{
- var receiver = new PublishSubscribeReceiver(pubSubName, topicName, options, messageHandler, this.Client);
+ var receiver = new PublishSubscribeReceiver(pubSubName, topicName, options, messageHandler, Client);
await receiver.SubscribeAsync(cancellationToken);
return receiver;
}
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ this.HttpClient.Dispose();
+ }
+ }
}
diff --git a/src/Dapr.Messaging/PublishSubscribe/Extensions/DaprPubSubBuilder.cs b/src/Dapr.Messaging/PublishSubscribe/Extensions/DaprPubSubBuilder.cs
new file mode 100644
index 000000000..2772df2fd
--- /dev/null
+++ b/src/Dapr.Messaging/PublishSubscribe/Extensions/DaprPubSubBuilder.cs
@@ -0,0 +1,28 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Dapr.Messaging.PublishSubscribe.Extensions;
+
+///
+/// Used by the fluent registration builder to configure a Dapr Publish/Subscribe client.
+///
+///
+public sealed class DaprPubSubBuilder(IServiceCollection services) : IDaprPubSubBuilder
+{
+ ///
+ /// The registered services on the builder.
+ ///
+ public IServiceCollection Services { get; } = services;
+}
diff --git a/src/Dapr.Messaging/PublishSubscribe/Extensions/PublishSubscribeServiceCollectionExtensions.cs b/src/Dapr.Messaging/PublishSubscribe/Extensions/PublishSubscribeServiceCollectionExtensions.cs
index 3d9e3ee8d..954940e53 100644
--- a/src/Dapr.Messaging/PublishSubscribe/Extensions/PublishSubscribeServiceCollectionExtensions.cs
+++ b/src/Dapr.Messaging/PublishSubscribe/Extensions/PublishSubscribeServiceCollectionExtensions.cs
@@ -1,6 +1,5 @@
-using Microsoft.Extensions.Configuration;
+using Dapr.Common.Extensions;
using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.DependencyInjection.Extensions;
namespace Dapr.Messaging.PublishSubscribe.Extensions;
@@ -16,40 +15,10 @@ public static class PublishSubscribeServiceCollectionExtensions
/// Optionally allows greater configuration of the using injected services.
/// The lifetime of the registered services.
///
- public static IServiceCollection AddDaprPubSubClient(this IServiceCollection services, Action? configure = null, ServiceLifetime lifetime = ServiceLifetime.Singleton)
- {
- ArgumentNullException.ThrowIfNull(services, nameof(services));
-
- //Register the IHttpClientFactory implementation
- services.AddHttpClient();
-
- var registration = new Func(serviceProvider =>
- {
- var httpClientFactory = serviceProvider.GetRequiredService();
- var configuration = serviceProvider.GetService();
-
- var builder = new DaprPublishSubscribeClientBuilder(configuration);
- builder.UseHttpClientFactory(httpClientFactory);
-
- configure?.Invoke(serviceProvider, builder);
-
- return builder.Build();
- });
-
- switch (lifetime)
- {
- case ServiceLifetime.Scoped:
- services.TryAddScoped(registration);
- break;
- case ServiceLifetime.Transient:
- services.TryAddTransient(registration);
- break;
- default:
- case ServiceLifetime.Singleton:
- services.TryAddSingleton(registration);
- break;
- }
-
- return services;
- }
+ public static IDaprPubSubBuilder AddDaprPubSubClient(
+ this IServiceCollection services,
+ Action? configure = null,
+ ServiceLifetime lifetime = ServiceLifetime.Singleton) =>
+ services.AddDaprClient(
+ configure, lifetime);
}
diff --git a/src/Dapr.Messaging/PublishSubscribe/IDaprPubSubBuilder.cs b/src/Dapr.Messaging/PublishSubscribe/IDaprPubSubBuilder.cs
new file mode 100644
index 000000000..23e6df1f6
--- /dev/null
+++ b/src/Dapr.Messaging/PublishSubscribe/IDaprPubSubBuilder.cs
@@ -0,0 +1,19 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+namespace Dapr.Messaging.PublishSubscribe;
+
+///
+/// Provides a Dapr client builder specific for Publish/Subscribe operations.
+///
+public interface IDaprPubSubBuilder : IDaprMessagingBuilder;
diff --git a/test/Dapr.AI.Test/Conversation/DaprConversationClientBuilderTest.cs b/test/Dapr.AI.Test/Conversation/DaprConversationClientBuilderTest.cs
index 901c4b656..6546a0802 100644
--- a/test/Dapr.AI.Test/Conversation/DaprConversationClientBuilderTest.cs
+++ b/test/Dapr.AI.Test/Conversation/DaprConversationClientBuilderTest.cs
@@ -28,6 +28,6 @@ public void Build_WithDefaultConfiguration_ShouldReturnNewInstanceOfDaprConversa
// Assert
Assert.NotNull(client);
- Assert.IsType(client);
+ Assert.IsAssignableFrom(client);
}
}
diff --git a/test/Dapr.AI.Test/Conversation/Extensions/DaprAiConversationBuilderExtensionsTest.cs b/test/Dapr.AI.Test/Conversation/Extensions/DaprAiConversationBuilderExtensionsTest.cs
index 2ee321895..20f550f52 100644
--- a/test/Dapr.AI.Test/Conversation/Extensions/DaprAiConversationBuilderExtensionsTest.cs
+++ b/test/Dapr.AI.Test/Conversation/Extensions/DaprAiConversationBuilderExtensionsTest.cs
@@ -47,18 +47,16 @@ public void AddDaprConversationClient_FromIConfiguration()
}
[Fact]
- public void AddDaprConversationClient_RegistersDaprClientOnlyOnce()
+ public void AddDaprConversationClient_RegistersDaprClient_UsesMostRecentRegistration()
{
var services = new ServiceCollection();
- var clientBuilder = new Action((sp, builder) =>
+ services.AddDaprConversationClient((_, builder) =>
{
- builder.UseDaprApiToken("abc");
- });
-
+ builder.UseDaprApiToken("abc123");
+ }); //Sets the API token value
services.AddDaprConversationClient(); //Sets a default API token value of an empty string
- services.AddDaprConversationClient(clientBuilder); //Sets the API token value
-
+
var serviceProvider = services.BuildServiceProvider();
var daprConversationClient = serviceProvider.GetService();
diff --git a/test/Dapr.Common.Test/Dapr.Common.Test.csproj b/test/Dapr.Common.Test/Dapr.Common.Test.csproj
index 22120719c..730371053 100644
--- a/test/Dapr.Common.Test/Dapr.Common.Test.csproj
+++ b/test/Dapr.Common.Test/Dapr.Common.Test.csproj
@@ -7,6 +7,7 @@
+
all
diff --git a/test/Dapr.Common.Test/DaprGenericClientBuilderTest.cs b/test/Dapr.Common.Test/DaprGenericClientBuilderTest.cs
index d28b40058..a783a127b 100644
--- a/test/Dapr.Common.Test/DaprGenericClientBuilderTest.cs
+++ b/test/Dapr.Common.Test/DaprGenericClientBuilderTest.cs
@@ -83,12 +83,19 @@ public void DaprGenericClientBuilder_ShouldUpdateTimeout()
Assert.Equal(timeout, builder.Timeout);
}
- private class SampleDaprGenericClientBuilder : DaprGenericClientBuilder
+ private sealed class SampleDaprGenericClientBuilder : DaprGenericClientBuilder, IDaprClient
{
public override SampleDaprGenericClientBuilder Build()
{
// Implementation
throw new NotImplementedException();
}
+
+ ///
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ ///
+ public void Dispose()
+ {
+ }
}
}
diff --git a/test/Dapr.Common.Test/Extensions/DaprClientBuilderExtensionsTests.cs b/test/Dapr.Common.Test/Extensions/DaprClientBuilderExtensionsTests.cs
new file mode 100644
index 000000000..91659df77
--- /dev/null
+++ b/test/Dapr.Common.Test/Extensions/DaprClientBuilderExtensionsTests.cs
@@ -0,0 +1,178 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+#nullable enable
+using System;
+using System.Net.Http;
+using Dapr.Common.Extensions;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Moq;
+using Xunit;
+using Autogenerated = Dapr.Client.Autogen.Grpc.v1;
+
+namespace Dapr.Common.Test.Extensions;
+
+public sealed class DaprClientBuilderExtensionsTests
+{
+ [Fact]
+ public void AddDaprClient_CallsConfigureAction()
+ {
+ var services = new ServiceCollection();
+ var configurationMock = new Mock();
+ var serviceProviderMock = new Mock();
+ serviceProviderMock.Setup(sp => sp.GetService(typeof(IConfiguration))).Returns(configurationMock.Object);
+
+ var configureCalled = false;
+
+ services.AddDaprClient((
+ _,
+ _) =>
+ {
+ configureCalled = true;
+ });
+
+ var serviceProvider = services.BuildServiceProvider();
+ var client = serviceProvider.GetRequiredService();
+
+ Assert.NotNull(client);
+ Assert.True(configureCalled);
+ }
+
+ [Fact]
+ public void AddDaprClient_ThrowsIfServicesIsNull()
+ {
+#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type.
+ IServiceCollection services = null;
+#pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type.
+ Assert.Throws(() =>
+ // ReSharper disable once AssignNullToNotNullAttribute
+#pragma warning disable CS8604 // Possible null reference argument.
+ services.AddDaprClient(
+#pragma warning restore CS8604 // Possible null reference argument.
+ (_, _) => { }, ServiceLifetime.Singleton));
+ }
+
+ [Fact]
+ public void AddDaprClient_RegistersClientWithServiceCollection_Singleton()
+ {
+ var services = new ServiceCollection();
+ var configurationMock = new Mock();
+ var serviceProviderMock = new Mock();
+ serviceProviderMock.Setup(sp => sp.GetService(typeof(IConfiguration))).Returns(configurationMock.Object);
+
+ services.AddDaprClient((
+ _,
+ _) =>
+ {
+ }, ServiceLifetime.Singleton);
+
+ var serviceProvider = services.BuildServiceProvider();
+ var client1 = serviceProvider.GetRequiredService();
+ var client2 = serviceProvider.GetRequiredService();
+
+ Assert.NotNull(client1);
+ Assert.NotNull(client2);
+ Assert.Same(client1, client2); //Singletons should return the same instance
+ }
+
+ [Fact]
+ public void AddDaprClient_RegistersClientWithServiceCollection_Scoped()
+ {
+ var services = new ServiceCollection();
+ var configurationMock = new Mock();
+ var serviceProviderMock = new Mock();
+ serviceProviderMock.Setup(sp => sp.GetService(typeof(IConfiguration))).Returns(configurationMock.Object);
+
+ services.AddDaprClient((
+ _,
+ _) =>
+ {
+ }, ServiceLifetime.Transient);
+
+ var serviceProvider = services.BuildServiceProvider();
+ var client1 = serviceProvider.GetRequiredService();
+ var client2 = serviceProvider.GetRequiredService();
+
+ Assert.NotNull(client1);
+ Assert.NotNull(client2);
+ Assert.NotSame(client1, client2); //Transient should return different instances
+ }
+
+ [Fact]
+ public void AddDaprClient_RegistersClientWithServiceCollection_Transient()
+ {
+ var services = new ServiceCollection();
+ var configurationMock = new Mock();
+ var serviceProviderMock = new Mock();
+ serviceProviderMock.Setup(sp => sp.GetService(typeof(IConfiguration))).Returns(configurationMock.Object);
+
+ services.AddDaprClient((
+ _,
+ _) =>
+ {
+ }, ServiceLifetime.Scoped);
+
+ var serviceProvider = services.BuildServiceProvider();
+
+ using var scope1 = serviceProvider.CreateScope();
+ var client1 = scope1.ServiceProvider.GetRequiredService();
+ var client2 = scope1.ServiceProvider.GetRequiredService();
+
+ Assert.NotNull(client1);
+ Assert.NotNull(client2);
+ Assert.Same(client1, client2); //Transient should return the same instance within the same scope
+
+ using (var scope2 = serviceProvider.CreateScope())
+ {
+ var client3 = scope2.ServiceProvider.GetRequiredService();
+ Assert.NotNull(client3);
+ Assert.NotSame(client1, client3); //Scoped should return different instances across different scopes
+ }
+ }
+
+ private interface IDaprTestServiceBuilder : IDaprServiceBuilder;
+
+ private sealed class DaprTestBuilder(IServiceCollection services) : IDaprTestServiceBuilder
+ {
+ ///
+ /// The registered services on the builder.
+ ///
+ public IServiceCollection Services { get; } = services;
+ }
+
+ private sealed class DaprTestClientBuilder(IConfiguration? configuration = null) : DaprGenericClientBuilder(configuration)
+ {
+ public override DaprTestClient Build()
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ private abstract class DaprTestClient(Autogenerated.Dapr.DaprClient client, HttpClient httpClient, string? daprApiToken = null) : IDaprClient
+ {
+ internal readonly HttpClient HttpClient = httpClient;
+ internal readonly string? DaprApiToken = daprApiToken;
+ internal Autogenerated.Dapr.DaprClient Client { get; } = client;
+
+ public void Dispose()
+ {
+ // TODO release managed resources here
+ }
+ }
+
+ private sealed class DaprTestGrpcClient(
+ Autogenerated.Dapr.DaprClient client,
+ HttpClient httpClient,
+ string? daprApiToken = null) : DaprTestClient(client, httpClient, daprApiToken);
+}
diff --git a/test/Dapr.Jobs.Test/Extensions/DaprJobsServiceCollectionExtensionsTests.cs b/test/Dapr.Jobs.Test/Extensions/DaprJobsServiceCollectionExtensionsTests.cs
index 28a8a0681..814e52794 100644
--- a/test/Dapr.Jobs.Test/Extensions/DaprJobsServiceCollectionExtensionsTests.cs
+++ b/test/Dapr.Jobs.Test/Extensions/DaprJobsServiceCollectionExtensionsTests.cs
@@ -47,21 +47,19 @@ public void AddDaprJobsClient_FromIConfiguration()
}
[Fact]
- public void AddDaprJobsClient_RegistersDaprClientOnlyOnce()
+ public void AddDaprJobsClient_DaprClientRegistration_UseMostRecentVersion()
{
var services = new ServiceCollection();
-
- var clientBuilder = new Action((sp, builder) =>
+ services.AddDaprJobsClient((_, builder) =>
{
- builder.UseDaprApiToken("abc");
- });
-
+ //Sets the API token value
+ builder.UseDaprApiToken("abcd1234");
+ });
services.AddDaprJobsClient(); //Sets a default API token value of an empty string
- services.AddDaprJobsClient(clientBuilder); //Sets the API token value
-
+
var serviceProvider = services.BuildServiceProvider();
- var daprJobClient = serviceProvider.GetService() as DaprJobsGrpcClient;
+ var daprJobClient = serviceProvider.GetRequiredService() as DaprJobsGrpcClient;
Assert.NotNull(daprJobClient!.HttpClient);
Assert.False(daprJobClient.HttpClient.DefaultRequestHeaders.TryGetValues("dapr-api-token", out var _));