From c20c49d26412fe06c7fdb2e980e3472d4d60d5f0 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Thu, 21 Aug 2025 10:45:03 -0500 Subject: [PATCH] Tentative update to HttpClient registration Signed-off-by: Whit Waldo --- .../DaprConversationClientBuilder.cs | 5 +- .../DaprAiConversationBuilderExtensions.cs | 13 +++- src/Dapr.Common/DaprGenericClientBuilder.cs | 71 ++++--------------- .../Extensions/DaprClientBuilderExtensions.cs | 42 +++++++++-- .../Http/DefaultDaprHttpClientFactory.cs | 15 ++++ .../Http/IDaprHttpClientFactory.cs | 13 ++++ .../Encryption/DaprEncryptionClientBuilder.cs | 5 +- ...CryptographyServiceCollectionExtensions.cs | 26 ++++++- src/Dapr.Jobs/DaprJobsClientBuilder.cs | 5 +- .../DaprJobsServiceCollectionExtensions.cs | 17 ++++- .../DaprPublishSubscribeClientBuilder.cs | 5 +- ...ishSubscribeServiceCollectionExtensions.cs | 19 ++++- 12 files changed, 161 insertions(+), 75 deletions(-) create mode 100644 src/Dapr.Common/Http/DefaultDaprHttpClientFactory.cs create mode 100644 src/Dapr.Common/Http/IDaprHttpClientFactory.cs diff --git a/src/Dapr.AI/Conversation/DaprConversationClientBuilder.cs b/src/Dapr.AI/Conversation/DaprConversationClientBuilder.cs index a8a7983f1..f37105749 100644 --- a/src/Dapr.AI/Conversation/DaprConversationClientBuilder.cs +++ b/src/Dapr.AI/Conversation/DaprConversationClientBuilder.cs @@ -13,6 +13,7 @@ using System.Diagnostics.CodeAnalysis; using Dapr.Common; +using Dapr.Common.Http; using Microsoft.Extensions.Configuration; using Autogenerated = Dapr.Client.Autogen.Grpc.v1.Dapr; @@ -23,7 +24,7 @@ namespace Dapr.AI.Conversation; /// /// An optional to configure the client with. [Experimental("DAPR_CONVERSATION", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/conversation/conversation-overview/")] -public sealed class DaprConversationClientBuilder(IConfiguration? configuration = null) : DaprGenericClientBuilder(configuration) +public sealed class DaprConversationClientBuilder(IDaprHttpClientFactory daprHttpClientFactory, IConfiguration? configuration = null) : DaprGenericClientBuilder(daprHttpClientFactory, configuration) { /// /// Builds the client instance from the properties of the builder. @@ -35,7 +36,7 @@ public sealed class DaprConversationClientBuilder(IConfiguration? configuration [Experimental("DAPR_CONVERSATION", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/conversation/conversation-overview/")] public override DaprConversationClient Build() { - var daprClientDependencies = BuildDaprClientDependencies(typeof(DaprConversationClient).Assembly); + var daprClientDependencies = BuildDaprClientDependencies(); var client = new Autogenerated.DaprClient(daprClientDependencies.channel); return new DaprConversationGrpcClient(client, daprClientDependencies.httpClient, daprClientDependencies.daprApiToken); } diff --git a/src/Dapr.AI/Conversation/Extensions/DaprAiConversationBuilderExtensions.cs b/src/Dapr.AI/Conversation/Extensions/DaprAiConversationBuilderExtensions.cs index ba7c7b914..f3af22ffc 100644 --- a/src/Dapr.AI/Conversation/Extensions/DaprAiConversationBuilderExtensions.cs +++ b/src/Dapr.AI/Conversation/Extensions/DaprAiConversationBuilderExtensions.cs @@ -30,5 +30,16 @@ public static IDaprAiConversationBuilder AddDaprConversationClient( this IServiceCollection services, Action? configure = null, ServiceLifetime lifetime = ServiceLifetime.Singleton) => services - .AddDaprClient(configure, lifetime); + .AddDaprClient(configure, null, lifetime); + + /// + /// Registers the necessary functionality for the Dapr AI Conversation functionality. + /// + [Experimental("DAPR_CONVERSATION", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/conversation/conversation-overview/")] + public static IDaprAiConversationBuilder AddDaprConversationClient( + this IServiceCollection services, + Action? configure = null, + Action? configureHttpClient = null, + ServiceLifetime lifetime = ServiceLifetime.Singleton) => services + .AddDaprClient(configure, configureHttpClient, lifetime); } diff --git a/src/Dapr.Common/DaprGenericClientBuilder.cs b/src/Dapr.Common/DaprGenericClientBuilder.cs index 3e29a2eff..6674c81c6 100644 --- a/src/Dapr.Common/DaprGenericClientBuilder.cs +++ b/src/Dapr.Common/DaprGenericClientBuilder.cs @@ -11,8 +11,8 @@ // limitations under the License. // ------------------------------------------------------------------------ -using System.Reflection; using System.Text.Json; +using Dapr.Common.Http; using Grpc.Net.Client; using Microsoft.Extensions.Configuration; @@ -23,11 +23,16 @@ namespace Dapr.Common; /// public abstract class DaprGenericClientBuilder where TClientBuilder : class, IDaprClient { + private readonly IDaprHttpClientFactory daprHttpClientFactory; + /// /// Initializes a new instance of the class. /// - protected DaprGenericClientBuilder(IConfiguration? configuration = null) + protected DaprGenericClientBuilder(IDaprHttpClientFactory daprHttpClientFactory, IConfiguration? configuration = null) { + ArgumentNullException.ThrowIfNull(daprHttpClientFactory); + this.daprHttpClientFactory = daprHttpClientFactory; + this.GrpcEndpoint = DaprDefaults.GetDefaultGrpcEndpoint(); this.HttpEndpoint = DaprDefaults.GetDefaultHttpEndpoint(); @@ -52,11 +57,6 @@ protected DaprGenericClientBuilder(IConfiguration? configuration = null) /// internal string HttpEndpoint { get; private set; } - /// - /// Property exposed for testing purposes. - /// - internal Func? HttpClientFactory { get; set; } - /// /// Property exposed for testing purposes. /// @@ -94,27 +94,6 @@ public DaprGenericClientBuilder UseHttpEndpoint(string httpEndpo return this; } - /// - /// Exposed internally for testing purposes. - /// - internal DaprGenericClientBuilder UseHttpClientFactory(Func factory) - { - this.HttpClientFactory = factory; - return this; - } - - /// - /// Overrides the legacy mechanism for building an HttpClient and uses the new - /// introduced in .NET Core 2.1. - /// - /// The factory used to create instances. - /// - public DaprGenericClientBuilder UseHttpClientFactory(IHttpClientFactory httpClientFactory) - { - this.HttpClientFactory = httpClientFactory.CreateClient; - return this; - } - /// /// Overrides the gRPC endpoint used by the Dapr client for communicating with the Dapr runtime. /// @@ -184,9 +163,8 @@ public DaprGenericClientBuilder UseTimeout(TimeSpan timeout) /// Builds out the inner DaprClient that provides the core shape of the /// runtime gRPC client used by the consuming package. /// - /// The assembly the dependencies are being built for. /// - protected internal (GrpcChannel channel, HttpClient httpClient, Uri httpEndpoint, string daprApiToken) BuildDaprClientDependencies(Assembly assembly) + protected internal (GrpcChannel channel, HttpClient httpClient, Uri httpEndpoint, string daprApiToken) BuildDaprClientDependencies() { var grpcEndpoint = new Uri(this.GrpcEndpoint); if (grpcEndpoint.Scheme != "http" && grpcEndpoint.Scheme != "https") @@ -207,40 +185,17 @@ protected internal (GrpcChannel channel, HttpClient httpClient, Uri httpEndpoint } //Configure the HTTP client - var httpClient = ConfigureHttpClient(assembly); - this.GrpcChannelOptions.HttpClient = httpClient; + var httpClient = daprHttpClientFactory.CreateClient(); - var channel = GrpcChannel.ForAddress(this.GrpcEndpoint, this.GrpcChannelOptions); - return (channel, httpClient, httpEndpoint, this.DaprApiToken); - } - - /// - /// Configures the HTTP client. - /// - /// The assembly the user agent is built from. - /// The HTTP client to interact with the Dapr runtime with. - private HttpClient ConfigureHttpClient(Assembly assembly) - { - var httpClient = HttpClientFactory is not null ? HttpClientFactory() : new HttpClient(); - - //Set the timeout as necessary + //Update the timeout to use the one provided in this builder if (this.Timeout > TimeSpan.Zero) { httpClient.Timeout = this.Timeout; } + this.GrpcChannelOptions.HttpClient = httpClient; - //Set the user agent - var userAgent = DaprClientUtilities.GetUserAgent(assembly); - httpClient.DefaultRequestHeaders.Add("User-Agent", userAgent.ToString()); - - //Set the API token - var apiTokenHeader = DaprClientUtilities.GetDaprApiTokenHeader(this.DaprApiToken); - if (apiTokenHeader is not null) - { - httpClient.DefaultRequestHeaders.Add(apiTokenHeader.Value.Key, apiTokenHeader.Value.Value); - } - - return httpClient; + var channel = GrpcChannel.ForAddress(this.GrpcEndpoint, this.GrpcChannelOptions); + return (channel, httpClient, httpEndpoint, this.DaprApiToken); } /// diff --git a/src/Dapr.Common/Extensions/DaprClientBuilderExtensions.cs b/src/Dapr.Common/Extensions/DaprClientBuilderExtensions.cs index 1070133c2..e828e4f2b 100644 --- a/src/Dapr.Common/Extensions/DaprClientBuilderExtensions.cs +++ b/src/Dapr.Common/Extensions/DaprClientBuilderExtensions.cs @@ -12,8 +12,10 @@ // ------------------------------------------------------------------------ using System.Reflection; +using Dapr.Common.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http; using Autogenerated = Dapr.Client.Autogen.Grpc.v1.Dapr; namespace Dapr.Common.Extensions; @@ -32,12 +34,14 @@ internal static class DaprClientBuilderExtensions /// 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. + /// An optional method used to configure the HttpClient used by Dapr. /// 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, + Action? configureHttpClient = null, ServiceLifetime lifetime = ServiceLifetime.Singleton) where TClient : class, IDaprClient where TConcreteClient : TClient @@ -61,16 +65,46 @@ internal static TServiceBuilder AddDaprClient(serviceProvider => + { + var httpClientFactory = serviceProvider.GetRequiredService(); + var configuration = serviceProvider.GetService(); + + return new DefaultDaprHttpClientFactory(httpClientFactory, client => + { + // Configure the HTTP client + if (configuration is not null) + { + //Get the API token header + var daprApiToken = DaprDefaults.GetDefaultDaprApiToken(configuration); + var apiTokenHeader = DaprClientUtilities.GetDaprApiTokenHeader(daprApiToken); + if (apiTokenHeader is not null) + { + client.DefaultRequestHeaders.Add(apiTokenHeader.Value.Key, apiTokenHeader.Value.Value); + } + + // Set the user agent + var assembly = Assembly.GetExecutingAssembly(); + var userAgent = DaprClientUtilities.GetUserAgent(assembly); + client.DefaultRequestHeaders.Add("User-Agent", userAgent.ToString()); + } + + // Apply any configuration provided at registration by the developer, if available + configureHttpClient?.Invoke(serviceProvider, client); + }); + }); var registration = new Func(provider => { var configuration = provider.GetService(); - var builder = (TClientBuilder)Activator.CreateInstance(typeof(TClientBuilder), configuration)!; - - builder.UseDaprApiToken(DaprDefaults.GetDefaultDaprApiToken(configuration)); + var daprHttpClientFactory = provider.GetRequiredService(); + var builder = (TClientBuilder)Activator.CreateInstance(typeof(TClientBuilder), daprHttpClientFactory, configuration)!; + + builder.UseDaprApiToken(DaprDefaults.GetDefaultDaprApiToken(configuration)); configure?.Invoke(provider, builder); var (channel, httpClient, _, daprApiToken) = - builder.BuildDaprClientDependencies(Assembly.GetExecutingAssembly()); + builder.BuildDaprClientDependencies(); var daprClient = new Autogenerated.DaprClient(channel); return (TClient)Activator.CreateInstance(typeof(TConcreteClient), daprClient, httpClient, daprApiToken)!; }); diff --git a/src/Dapr.Common/Http/DefaultDaprHttpClientFactory.cs b/src/Dapr.Common/Http/DefaultDaprHttpClientFactory.cs new file mode 100644 index 000000000..516e1404f --- /dev/null +++ b/src/Dapr.Common/Http/DefaultDaprHttpClientFactory.cs @@ -0,0 +1,15 @@ +namespace Dapr.Common.Http; + +/// +/// Concrete implementation of a . +/// +public sealed class DefaultDaprHttpClientFactory(IHttpClientFactory httpClientFactory, Action configure) : IDaprHttpClientFactory +{ + /// + public HttpClient CreateClient() + { + var client = httpClientFactory.CreateClient(); + configure(client); + return client; + } +} diff --git a/src/Dapr.Common/Http/IDaprHttpClientFactory.cs b/src/Dapr.Common/Http/IDaprHttpClientFactory.cs new file mode 100644 index 000000000..fe85f6c5a --- /dev/null +++ b/src/Dapr.Common/Http/IDaprHttpClientFactory.cs @@ -0,0 +1,13 @@ +namespace Dapr.Common.Http; + +/// +/// Factory for creating Dapr-configured instances. +/// +public interface IDaprHttpClientFactory +{ + /// + /// Produces a Dapr-configured instance. + /// + /// + HttpClient CreateClient(); +} diff --git a/src/Dapr.Cryptography/Encryption/DaprEncryptionClientBuilder.cs b/src/Dapr.Cryptography/Encryption/DaprEncryptionClientBuilder.cs index 833a849f3..80854b0ab 100644 --- a/src/Dapr.Cryptography/Encryption/DaprEncryptionClientBuilder.cs +++ b/src/Dapr.Cryptography/Encryption/DaprEncryptionClientBuilder.cs @@ -13,6 +13,7 @@ using System.Diagnostics.CodeAnalysis; using Dapr.Common; +using Dapr.Common.Http; using Microsoft.Extensions.Configuration; using Autogenerated = Dapr.Client.Autogen.Grpc.v1; @@ -23,7 +24,7 @@ namespace Dapr.Cryptography.Encryption; /// /// An optional instance of . [Experimental("DAPR_CRYPTOGRAPHY", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/cryptography/cryptography-overview/")] -public sealed class DaprEncryptionClientBuilder(IConfiguration? configuration = null) : DaprGenericClientBuilder(configuration) +public sealed class DaprEncryptionClientBuilder(IDaprHttpClientFactory daprHttpClientFactory, IConfiguration? configuration = null) : DaprGenericClientBuilder(daprHttpClientFactory, configuration) { /// /// Builds the client instance from the properties of the builder. @@ -31,7 +32,7 @@ public sealed class DaprEncryptionClientBuilder(IConfiguration? configuration = /// The Dapr client instance. public override DaprEncryptionClient Build() { - var daprClientDependencies = this.BuildDaprClientDependencies(typeof(DaprEncryptionClient).Assembly); + var daprClientDependencies = this.BuildDaprClientDependencies(); var client = new Autogenerated.Dapr.DaprClient(daprClientDependencies.channel); return new DaprEncryptionGrpcClient(client, daprClientDependencies.httpClient, daprClientDependencies.daprApiToken); } diff --git a/src/Dapr.Cryptography/Encryption/Extensions/DaprCryptographyServiceCollectionExtensions.cs b/src/Dapr.Cryptography/Encryption/Extensions/DaprCryptographyServiceCollectionExtensions.cs index 825591104..55931a819 100644 --- a/src/Dapr.Cryptography/Encryption/Extensions/DaprCryptographyServiceCollectionExtensions.cs +++ b/src/Dapr.Cryptography/Encryption/Extensions/DaprCryptographyServiceCollectionExtensions.cs @@ -20,7 +20,8 @@ namespace Dapr.Cryptography.Encryption.Extensions; /// /// Contains extension methods for using Dapr cryptography with dependency injection. /// -[Experimental("DAPR_CRYPTOGRAPHY", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/cryptography/cryptography-overview/")] +[Experimental("DAPR_CRYPTOGRAPHY", + UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/cryptography/cryptography-overview/")] public static class DaprCryptographyServiceCollectionExtensions { /// @@ -39,6 +40,27 @@ public static IDaprCryptographyBuilder AddDaprEncryptionClient( services.AddTransient(); return services .AddDaprClient(configure, lifetime); + DaprEncryptionClientBuilder>(configure, null, lifetime); + } + + /// + /// Adds Dapr encryption/decryption support to the service collection. + /// + /// The . + /// Optionally allows greater configuration of the using injected services. + /// Optionally allows greater configuration of the used by the . + /// The lifetime of the registered services. + /// + public static IDaprCryptographyBuilder AddDaprEncryptionClient( + this IServiceCollection services, + Action? configure = null, + Action? configureHttpClient = null, + ServiceLifetime lifetime = ServiceLifetime.Singleton) + { + services.AddTransient(); + services.AddTransient(); + return services + .AddDaprClient(configure, configureHttpClient, lifetime); } } diff --git a/src/Dapr.Jobs/DaprJobsClientBuilder.cs b/src/Dapr.Jobs/DaprJobsClientBuilder.cs index 979f7fb8a..9e60cfffa 100644 --- a/src/Dapr.Jobs/DaprJobsClientBuilder.cs +++ b/src/Dapr.Jobs/DaprJobsClientBuilder.cs @@ -13,6 +13,7 @@ using System.Diagnostics.CodeAnalysis; using Dapr.Common; +using Dapr.Common.Http; using Microsoft.Extensions.Configuration; using Autogenerated = Dapr.Client.Autogen.Grpc.v1; @@ -23,7 +24,7 @@ namespace Dapr.Jobs; /// /// An optional instance of . [Experimental("DAPR_JOBS", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/jobs/jobs-overview/")] -public sealed class DaprJobsClientBuilder(IConfiguration? configuration = null) : DaprGenericClientBuilder(configuration) +public sealed class DaprJobsClientBuilder(IDaprHttpClientFactory daprHttpClientFactory, IConfiguration? configuration = null) : DaprGenericClientBuilder(daprHttpClientFactory, configuration) { /// /// Builds the client instance from the properties of the builder. @@ -31,7 +32,7 @@ public sealed class DaprJobsClientBuilder(IConfiguration? configuration = null) /// The Dapr client instance. public override DaprJobsClient Build() { - var daprClientDependencies = this.BuildDaprClientDependencies(typeof(DaprJobsClient).Assembly); + var daprClientDependencies = this.BuildDaprClientDependencies(); var client = new Autogenerated.Dapr.DaprClient(daprClientDependencies.channel); return new DaprJobsGrpcClient(client, daprClientDependencies.httpClient, daprClientDependencies.daprApiToken); } diff --git a/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs b/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs index 3103e072b..85b1d4c86 100644 --- a/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs +++ b/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs @@ -34,5 +34,20 @@ public static IDaprJobsBuilder AddDaprJobsClient( this IServiceCollection services, Action? configure = null, ServiceLifetime lifetime = ServiceLifetime.Singleton) => - services.AddDaprClient(configure, lifetime); + services.AddDaprClient(configure, null, lifetime); + + /// + /// Adds Dapr Jobs client support to the service collection. + /// + /// The . + /// Optionally allows greater configuration of the using injected services. + /// Optionally allows greater configuration of the used by the . + /// The lifetime of the registered services. + /// + public static IDaprJobsBuilder AddDaprJobsClient( + this IServiceCollection services, + Action? configure = null, + Action? configureHttpClient = null, + ServiceLifetime lifetime = ServiceLifetime.Singleton) => + services.AddDaprClient(configure, configureHttpClient, lifetime); } diff --git a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClientBuilder.cs b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClientBuilder.cs index 691ff9d38..9c0ed4bb5 100644 --- a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClientBuilder.cs +++ b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClientBuilder.cs @@ -12,6 +12,7 @@ // ------------------------------------------------------------------------ using Dapr.Common; +using Dapr.Common.Http; using Microsoft.Extensions.Configuration; using Autogenerated = Dapr.Client.Autogen.Grpc.v1; @@ -21,7 +22,7 @@ namespace Dapr.Messaging.PublishSubscribe; /// Builds a . /// /// An optional instance of . -public sealed class DaprPublishSubscribeClientBuilder(IConfiguration? configuration = null) : DaprGenericClientBuilder(configuration) +public sealed class DaprPublishSubscribeClientBuilder(IDaprHttpClientFactory daprHttpClientFactory, IConfiguration? configuration = null) : DaprGenericClientBuilder(daprHttpClientFactory,configuration) { /// /// Builds the client instance from the properties of the builder. @@ -32,7 +33,7 @@ public sealed class DaprPublishSubscribeClientBuilder(IConfiguration? configurat /// public override DaprPublishSubscribeClient Build() { - var daprClientDependencies = BuildDaprClientDependencies(typeof(DaprPublishSubscribeClient).Assembly); + var daprClientDependencies = BuildDaprClientDependencies(); var client = new Autogenerated.Dapr.DaprClient(daprClientDependencies.channel); return new DaprPublishSubscribeGrpcClient(client, daprClientDependencies.httpClient, daprClientDependencies.daprApiToken); } diff --git a/src/Dapr.Messaging/PublishSubscribe/Extensions/PublishSubscribeServiceCollectionExtensions.cs b/src/Dapr.Messaging/PublishSubscribe/Extensions/PublishSubscribeServiceCollectionExtensions.cs index 954940e53..141462106 100644 --- a/src/Dapr.Messaging/PublishSubscribe/Extensions/PublishSubscribeServiceCollectionExtensions.cs +++ b/src/Dapr.Messaging/PublishSubscribe/Extensions/PublishSubscribeServiceCollectionExtensions.cs @@ -1,4 +1,5 @@ using Dapr.Common.Extensions; +using Dapr.Common.Http; using Microsoft.Extensions.DependencyInjection; namespace Dapr.Messaging.PublishSubscribe.Extensions; @@ -8,17 +9,33 @@ namespace Dapr.Messaging.PublishSubscribe.Extensions; /// public static class PublishSubscribeServiceCollectionExtensions { + /// + /// Adds Dapr Publish/Subscribe support to the service collection. + /// + /// The . + /// Optionally allows greater configuration of the using injected services. + /// The lifetime of the registered services. + /// + public static IDaprPubSubBuilder AddDaprPubSubClient( + this IServiceCollection services, + Action? configure = null, + ServiceLifetime lifetime = ServiceLifetime.Singleton) => + services.AddDaprClient( + configure, null, lifetime); + /// /// Adds Dapr Publish/Subscribe support to the service collection. /// /// The . /// Optionally allows greater configuration of the using injected services. + /// Optionally allows greater configuration of the used by the . /// The lifetime of the registered services. /// public static IDaprPubSubBuilder AddDaprPubSubClient( this IServiceCollection services, Action? configure = null, + Action? configureHttpClient = null, ServiceLifetime lifetime = ServiceLifetime.Singleton) => services.AddDaprClient( - configure, lifetime); + configure, configureHttpClient, lifetime); }