Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 15 additions & 10 deletions src/Dapr.Common/DaprGenericClientBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,17 @@ protected DaprGenericClientBuilder(IConfiguration? configuration = null)
/// <summary>
/// Property exposed for testing purposes.
/// </summary>
internal string GrpcEndpoint { get; private set; }
protected internal string GrpcEndpoint { get; private set; }

/// <summary>
/// Property exposed for testing purposes.
/// </summary>
internal string HttpEndpoint { get; private set; }
protected internal string HttpEndpoint { get; private set; }

/// <summary>
/// Property exposed for testing purposes.
/// </summary>
internal Func<HttpClient>? HttpClientFactory { get; set; }
protected internal Func<HttpClient>? HttpClientFactory { get; set; }

/// <summary>
/// Property exposed for testing purposes.
Expand All @@ -65,7 +65,7 @@ protected DaprGenericClientBuilder(IConfiguration? configuration = null)
/// <summary>
/// Property exposed for testing purposes.
/// </summary>
internal GrpcChannelOptions GrpcChannelOptions { get; private set; }
protected internal GrpcChannelOptions GrpcChannelOptions { get; private set; }

/// <summary>
/// Property exposed for testing purposes.
Expand All @@ -75,7 +75,7 @@ protected DaprGenericClientBuilder(IConfiguration? configuration = null)
/// <summary>
/// Property exposed for testing purposes.
/// </summary>
internal TimeSpan Timeout { get; private set; }
protected internal TimeSpan Timeout { get; private set; }

/// <summary>
/// Overrides the HTTP endpoint used by the Dapr client for communicating with the Dapr runtime.
Expand Down Expand Up @@ -185,8 +185,10 @@ public DaprGenericClientBuilder<TClientBuilder> UseTimeout(TimeSpan timeout)
/// runtime gRPC client used by the consuming package.
/// </summary>
/// <param name="assembly">The assembly the dependencies are being built for.</param>
/// <param name="providedHttpClient"></param>
/// <exception cref="InvalidOperationException"></exception>
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")
Expand All @@ -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);
Expand Down
56 changes: 56 additions & 0 deletions src/Dapr.Common/DaprHttpClientOptions.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Provides for a collection of options used to configure the Dapr <see cref="HttpClient"/> instance(s).
/// </summary>
public class DaprHttpClientOptions
{
/// <summary>
/// Gets or sets the HTTP endpoint used by the Dapr client.
/// </summary>
public string? HttpEndpoint { get; set; }

/// <summary>
/// Gets or sets the gRPC endpoint used by the Dapr client.
/// </summary>
public string? GrpcEndpoint { get; set; }

/// <summary>
/// Gets or sets the JSON serialization options.
/// </summary>
public JsonSerializerOptions? JsonSerializerOptions { get; set; }

/// <summary>
/// Gets or sets the gRPC channel options.
/// </summary>
public GrpcChannelOptions? GrpcChannelOptions { get; set; } = new GrpcChannelOptions
{
ThrowOperationCanceledOnCancellation = true
};

/// <summary>
/// Gets or sets the API token used for Dapr authentication.
/// </summary>
public string? DaprApiToken { get; set; }

/// <summary>
/// Gets or sets the timeout for HTTP requests.
/// </summary>
public TimeSpan Timeout { get; set; } = TimeSpan.Zero;
}
69 changes: 60 additions & 9 deletions src/Dapr.Common/Extensions/DaprClientBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -59,20 +60,70 @@ internal static TServiceBuilder AddDaprClient<TClient, TConcreteClient, TService
throw new ArgumentException($"{typeof(TServiceBuilder).Name} must be a concrete class",
nameof(TServiceBuilder));
}

//Ensure options and the HttpClient are registered
services.AddDaprHttpClient();

services.AddHttpClient();

//Register the client with the proper factory usage
var registration = new Func<IServiceProvider, TClient>(provider =>
{
var configuration = provider.GetService<IConfiguration>();
var options = provider.GetRequiredService<IOptions<DaprHttpClientOptions>>().Value;
var httpClientFactory = provider.GetRequiredService<IHttpClientFactory>();

// 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));
Expand Down
94 changes: 94 additions & 0 deletions src/Dapr.Common/Extensions/DaprServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Provides extension method implementations against an <see cref="IServiceCollection"/>.
/// </summary>
public static class DaprServiceCollectionExtensions
{
/// <summary>
/// The name of the Dapr HTTP client.
/// </summary>
public const string DaprHttpClientName = "DaprClient";

/// <summary>
/// Adds and configures a Dapr HTTP client to the service collection.
/// </summary>
public static IServiceCollection AddDaprHttpClient(
this IServiceCollection services,
Action<DaprHttpClientOptions>? configureOptions = null)
{
//Register the options with defaults from DaprDefaults
services.AddOptions<DaprHttpClientOptions>()
.Configure<IConfiguration>((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<IOptions<DaprHttpClientOptions>>().Value;
var configuration = sp.GetRequiredService<IConfiguration>();

//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;
}

/// <summary>
/// Extension method to use a configured <see cref="HttpClient"/> from an <see cref="IHttpClientFactory"/>
/// in a <see cref="DaprGenericClientBuilder{TClientBuilder}"/>.
/// </summary>
public static TClientBuilder UseDaprHttpClientFactory<TClientBuilder>(
this TClientBuilder builder,
IHttpClientFactory httpClientFactory,
IOptions<DaprHttpClientOptions> options)
where TClientBuilder : DaprGenericClientBuilder<IDaprClient>
{
builder.UseHttpClientFactory(() => httpClientFactory.CreateClient(DaprHttpClientName));
return builder;
}
}
Loading