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 _));