From 094de84121486c58a4b2a39bf0b149008a2afc59 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sat, 21 Jun 2025 02:40:55 -0500 Subject: [PATCH] Simplified the common registration functionality to utilize an IOption with DaprHttpClientOptions, then values from IConfiguration, then environment variables, then constants for additional support. Signed-off-by: Whit Waldo --- src/Dapr.Common/DaprGenericClientBuilder.cs | 25 +++-- src/Dapr.Common/DaprHttpClientOptions.cs | 56 +++++++++++ .../Extensions/DaprClientBuilderExtensions.cs | 69 ++++++++++++-- .../DaprServiceCollectionExtensions.cs | 94 +++++++++++++++++++ 4 files changed, 225 insertions(+), 19 deletions(-) create mode 100644 src/Dapr.Common/DaprHttpClientOptions.cs create mode 100644 src/Dapr.Common/Extensions/DaprServiceCollectionExtensions.cs diff --git a/src/Dapr.Common/DaprGenericClientBuilder.cs b/src/Dapr.Common/DaprGenericClientBuilder.cs index 3e29a2eff..bbe247ac6 100644 --- a/src/Dapr.Common/DaprGenericClientBuilder.cs +++ b/src/Dapr.Common/DaprGenericClientBuilder.cs @@ -45,17 +45,17 @@ protected DaprGenericClientBuilder(IConfiguration? configuration = null) /// /// Property exposed for testing purposes. /// - internal string GrpcEndpoint { get; private set; } + protected internal string GrpcEndpoint { get; private set; } /// /// Property exposed for testing purposes. /// - internal string HttpEndpoint { get; private set; } + protected internal string HttpEndpoint { get; private set; } /// /// Property exposed for testing purposes. /// - internal Func? HttpClientFactory { get; set; } + protected internal Func? HttpClientFactory { get; set; } /// /// Property exposed for testing purposes. @@ -65,7 +65,7 @@ protected DaprGenericClientBuilder(IConfiguration? configuration = null) /// /// Property exposed for testing purposes. /// - internal GrpcChannelOptions GrpcChannelOptions { get; private set; } + protected internal GrpcChannelOptions GrpcChannelOptions { get; private set; } /// /// Property exposed for testing purposes. @@ -75,7 +75,7 @@ protected DaprGenericClientBuilder(IConfiguration? configuration = null) /// /// Property exposed for testing purposes. /// - internal TimeSpan Timeout { get; private set; } + protected internal TimeSpan Timeout { get; private set; } /// /// Overrides the HTTP endpoint used by the Dapr client for communicating with the Dapr runtime. @@ -185,8 +185,10 @@ public DaprGenericClientBuilder UseTimeout(TimeSpan timeout) /// 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(Assembly assembly, HttpClient? providedHttpClient = null) { var grpcEndpoint = new Uri(this.GrpcEndpoint); if (grpcEndpoint.Scheme != "http" && grpcEndpoint.Scheme != "https") @@ -205,10 +207,13 @@ protected internal (GrpcChannel channel, HttpClient httpClient, Uri httpEndpoint { throw new InvalidOperationException("The HTTP endpoint must use http or https."); } - - //Configure the HTTP client - var httpClient = ConfigureHttpClient(assembly); - this.GrpcChannelOptions.HttpClient = httpClient; + + // If provided with an HttpClient, use it directory - this supports the one registered in DI otherwise + var httpClient = providedHttpClient ?? ConfigureHttpClient(assembly); + + // Apply the current GprcChannelOptions + var options = this.GrpcChannelOptions; + options.HttpClient = httpClient; var channel = GrpcChannel.ForAddress(this.GrpcEndpoint, this.GrpcChannelOptions); return (channel, httpClient, httpEndpoint, this.DaprApiToken); diff --git a/src/Dapr.Common/DaprHttpClientOptions.cs b/src/Dapr.Common/DaprHttpClientOptions.cs new file mode 100644 index 000000000..92face4d4 --- /dev/null +++ b/src/Dapr.Common/DaprHttpClientOptions.cs @@ -0,0 +1,56 @@ +// ------------------------------------------------------------------------ +// 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.Text.Json; +using Grpc.Net.Client; + +namespace Dapr.Common; + +/// +/// Provides for a collection of options used to configure the Dapr instance(s). +/// +public class DaprHttpClientOptions +{ + /// + /// Gets or sets the HTTP endpoint used by the Dapr client. + /// + public string? HttpEndpoint { get; set; } + + /// + /// Gets or sets the gRPC endpoint used by the Dapr client. + /// + public string? GrpcEndpoint { get; set; } + + /// + /// Gets or sets the JSON serialization options. + /// + public JsonSerializerOptions? JsonSerializerOptions { get; set; } + + /// + /// Gets or sets the gRPC channel options. + /// + public GrpcChannelOptions? GrpcChannelOptions { get; set; } = new GrpcChannelOptions + { + ThrowOperationCanceledOnCancellation = true + }; + + /// + /// Gets or sets the API token used for Dapr authentication. + /// + public string? DaprApiToken { get; set; } + + /// + /// Gets or sets the timeout for HTTP requests. + /// + public TimeSpan Timeout { get; set; } = TimeSpan.Zero; +} diff --git a/src/Dapr.Common/Extensions/DaprClientBuilderExtensions.cs b/src/Dapr.Common/Extensions/DaprClientBuilderExtensions.cs index 1070133c2..7fb097d92 100644 --- a/src/Dapr.Common/Extensions/DaprClientBuilderExtensions.cs +++ b/src/Dapr.Common/Extensions/DaprClientBuilderExtensions.cs @@ -14,6 +14,7 @@ using System.Reflection; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using Autogenerated = Dapr.Client.Autogen.Grpc.v1.Dapr; namespace Dapr.Common.Extensions; @@ -59,20 +60,70 @@ internal static TServiceBuilder AddDaprClient(provider => { var configuration = provider.GetService(); + var options = provider.GetRequiredService>().Value; + var httpClientFactory = provider.GetRequiredService(); + + // Create the builder with the configuration to ensure it has access to all configuration values + // for defaults 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)!; + + // Apply options from DI, but only if they're explicitly set + // Otherwise, let the builder use its defaults from DaprDefaults + if (!string.IsNullOrEmpty(options.HttpEndpoint)) + { + builder.UseHttpEndpoint(options.HttpEndpoint); + } + + if (!string.IsNullOrEmpty(options.GrpcEndpoint)) + { + builder.UseGrpcEndpoint(options.GrpcEndpoint); + } + + if (options.JsonSerializerOptions != null) + { + builder.UseJsonSerializationOptions(options.JsonSerializerOptions); + } + + if (options.GrpcChannelOptions is not null) + { + builder.UseGrpcChannelOptions(options.GrpcChannelOptions); + } + + if (!string.IsNullOrEmpty(options.DaprApiToken)) + { + builder.UseDaprApiToken(options.DaprApiToken); + } + else + { + //Use DaprDefaults to get the token if not set in options + builder.UseDaprApiToken(DaprDefaults.GetDefaultDaprApiToken(configuration)); + } + + if (options.Timeout > TimeSpan.Zero) + { + builder.UseTimeout(options.Timeout); + } + + // Get an HttpClient from the factory + var httpClient = httpClientFactory.CreateClient(DaprServiceCollectionExtensions.DaprHttpClientName); + + // Allow additional configuration + configure?.Invoke(provider, builder); + + // Build dependencies using our pre-configured HttpClient + var (channel, _, _, _) = builder.BuildDaprClientDependencies(Assembly.GetExecutingAssembly(), httpClient); + + //Create the Dapr client + var daprClient = new Autogenerated.DaprClient(channel); + return (TClient)Activator.CreateInstance(typeof(TConcreteClient), daprClient, httpClient, builder.DaprApiToken)!; }); services.Add(new ServiceDescriptor(typeof(TClient), registration, lifetime)); diff --git a/src/Dapr.Common/Extensions/DaprServiceCollectionExtensions.cs b/src/Dapr.Common/Extensions/DaprServiceCollectionExtensions.cs new file mode 100644 index 000000000..9ef5ea716 --- /dev/null +++ b/src/Dapr.Common/Extensions/DaprServiceCollectionExtensions.cs @@ -0,0 +1,94 @@ +// ------------------------------------------------------------------------ +// 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 Microsoft.Extensions.Options; + +namespace Dapr.Common.Extensions; + +/// +/// Provides extension method implementations against an . +/// +public static class DaprServiceCollectionExtensions +{ + /// + /// The name of the Dapr HTTP client. + /// + public const string DaprHttpClientName = "DaprClient"; + + /// + /// Adds and configures a Dapr HTTP client to the service collection. + /// + public static IServiceCollection AddDaprHttpClient( + this IServiceCollection services, + Action? configureOptions = null) + { + //Register the options with defaults from DaprDefaults + services.AddOptions() + .Configure((options, configuration) => + { + //Only set these values from DaprDefaults if they haven't been explicitly configured already + options.HttpEndpoint ??= DaprDefaults.GetDefaultHttpEndpoint(); + options.GrpcEndpoint ??= DaprDefaults.GetDefaultGrpcEndpoint(); + options.DaprApiToken ??= DaprDefaults.GetDefaultDaprApiToken(configuration); + }); + + if (configureOptions != null) + { + services.Configure(configureOptions); + } + + // Add the HttpClient with configuration from the options and DaprDefaults + services.AddHttpClient(name: DaprHttpClientName, (sp, client) => + { + var options = sp.GetRequiredService>().Value; + var configuration = sp.GetRequiredService(); + + //Configure the timeout + if (options.Timeout > TimeSpan.Zero) + { + client.Timeout = options.Timeout; + } + + //Add user agent + var userAgent = DaprClientUtilities.GetUserAgent(Assembly.GetExecutingAssembly()); + client.DefaultRequestHeaders.Add("User-Agent", userAgent.ToString()); + + //Add API token if needed - use options first, then fall back to DaprDefaults + var apiToken = options.DaprApiToken ?? DaprDefaults.GetDefaultDaprApiToken(configuration); + var apiTokenHeader = DaprClientUtilities.GetDaprApiTokenHeader(apiToken); + if (apiTokenHeader is not null) + { + client.DefaultRequestHeaders.Add(apiTokenHeader.Value.Key, apiTokenHeader.Value.Value); + } + }); + + return services; + } + + /// + /// Extension method to use a configured from an + /// in a . + /// + public static TClientBuilder UseDaprHttpClientFactory( + this TClientBuilder builder, + IHttpClientFactory httpClientFactory, + IOptions options) + where TClientBuilder : DaprGenericClientBuilder + { + builder.UseHttpClientFactory(() => httpClientFactory.CreateClient(DaprHttpClientName)); + return builder; + } +}