diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/dotnet-jobs-howto.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/dotnet-jobs-howto.md index 8d98d1ca5..f3bd61420 100644 --- a/daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/dotnet-jobs-howto.md +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/dotnet-jobs-howto.md @@ -102,6 +102,79 @@ builder.Services.AddDaprJobsClient((serviceProvider, daprJobsClientBuilder) => var app = builder.Build(); ``` +## Use the Dapr Jobs client using IConfiguration +It's possible to configure the Dapr Jobs client using the values in your registered `IConfiguration` as well without +explicitly specifying each of the value overrides using the `DaprJobsClientBuilder` as demonstrated in the previous +section. Rather, by populating an `IConfiguration` made available through dependency injection the `AddDaprJobsClient()` +registration will automatically use these values over their respective defaults. + +Start by populating the values in your configuration. This can be done in several different ways as demonstrated below. + +### Configuration via `ConfigurationBuilder` +Application settings can be configured without using a configuration source and by instead populating the value in-memory +using a `ConfigurationBuilder` instance: + +```csharp +var builder = WebApplication.CreateBuilder(); + +//Create the configuration +var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary { + { "DAPR_HTTP_ENDPOINT", "http://localhost:54321" }, + { "DAPR_API_TOKEN", "abc123" } + }) + .Build(); + +builder.Configuration.AddConfiguration(configuration); +builder.Services.AddDaprJobsClient(); //This will automatically populate the HTTP endpoint and API token values from the IConfiguration +``` + +### Configuration via Environment Variables +Application settings can be accessed from environment variables available to your application. + +The following environment variables will be used to populate both the HTTP endpoint and API token used to register the +Dapr Jobs client. + +| Key | Value | +| --- | --- | +| DAPR_HTTP_ENDPOINT | http://localhost:54321 | +| DAPR_API_TOKEN | abc123 | + +```csharp +var builder = WebApplication.CreateBuilder(); + +builder.Configuration.AddEnvironmentVariables(); +builder.Services.AddDaprJobsClient(); +``` + +The Dapr Jobs client will be configured to use both the HTTP endpoint `http://localhost:54321` and populate all outbound +requests with the API token header `abc123`. + +### Configuration via prefixed Environment Variables + +However, in shared-host scenarios where there are multiple applications all running on the same machine without using +containers or in development environments, it's not uncommon to prefix environment variables. The following example +assumes that both the HTTP endpoint and the API token will be pulled from environment variables prefixed with the +value "myapp_". The two environment variables used in this scenario are as follows: + +| Key | Value | +| --- | --- | +| myapp_DAPR_HTTP_ENDPOINT | http://localhost:54321 | +| myapp_DAPR_API_TOKEN | abc123 | + +These environment variables will be loaded into the registered configuration in the following example and made available +without the prefix attached. + +```csharp +var builder = WebApplication.CreateBuilder(); + +builder.Configuration.AddEnvironmentVariables(prefix: "myapp_"); +builder.Services.AddDaprJobsClient(); +``` + +The Dapr Jobs client will be configured to use both the HTTP endpoint `http://localhost:54321` and populate all outbound +requests with the API token header `abc123`. + ## Use the Dapr Jobs client without relying on dependency injection While the use of dependency injection simplifies the use of complex types in .NET and makes it easier to deal with complicated configurations, you're not required to register the `DaprJobsClient` in this way. Rather, you can also elect to create an instance of it from a `DaprJobsClientBuilder` instance as demonstrated below: diff --git a/src/Dapr.Common/AssemblyInfo.cs b/src/Dapr.Common/AssemblyInfo.cs index 5044876a9..a4a1cf5be 100644 --- a/src/Dapr.Common/AssemblyInfo.cs +++ b/src/Dapr.Common/AssemblyInfo.cs @@ -19,6 +19,7 @@ [assembly: InternalsVisibleTo("Dapr.AspNetCore, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.Client, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.Jobs, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.Messaging, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.Extensions.Configuration, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.Workflow, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] @@ -40,3 +41,4 @@ [assembly: InternalsVisibleTo("Dapr.E2E.Test.App.ReentrantActors, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.Extensions.Configuration.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.Jobs.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.Messaging.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] diff --git a/src/Dapr.Common/DaprClientUtilities.cs b/src/Dapr.Common/DaprClientUtilities.cs new file mode 100644 index 000000000..ba4591e46 --- /dev/null +++ b/src/Dapr.Common/DaprClientUtilities.cs @@ -0,0 +1,77 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 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.Net.Http.Headers; +using System.Reflection; +using Grpc.Core; + +namespace Dapr.Common; + +internal static class DaprClientUtilities +{ + /// + /// Provisions the gRPC call options used to provision the various Dapr clients. + /// + /// The Dapr API token, if any. + /// The assembly the user agent is built from. + /// Cancellation token. + /// The gRPC call options. + internal static CallOptions ConfigureGrpcCallOptions(Assembly assembly, string? daprApiToken, CancellationToken cancellationToken = default) + { + var callOptions = new CallOptions(headers: new Metadata(), cancellationToken: cancellationToken); + + //Add the user-agent header to the gRPC call options + var assemblyVersion = assembly + .GetCustomAttributes() + .FirstOrDefault()? + .InformationalVersion; + var userAgent = new ProductInfoHeaderValue("dapr-sdk-dotnet", $"v{assemblyVersion}").ToString(); + callOptions.Headers!.Add("User-Agent", userAgent); + + //Add the API token to the headers as well if it's populated + if (daprApiToken is not null) + { + var apiTokenHeader = GetDaprApiTokenHeader(daprApiToken); + if (apiTokenHeader is not null) + { + callOptions.Headers.Add(apiTokenHeader.Value.Key, apiTokenHeader.Value.Value); + } + } + + return callOptions; + } + + /// + /// Used to create the user-agent from the assembly attributes. + /// + /// The assembly the client is being built for. + /// The header value containing the user agent information. + public static ProductInfoHeaderValue GetUserAgent(Assembly assembly) + { + var assemblyVersion = assembly + .GetCustomAttributes() + .FirstOrDefault()? + .InformationalVersion; + return new ProductInfoHeaderValue("dapr-sdk-dotnet", $"v{assemblyVersion}"); + } + + /// + /// Used to provision the header used for the Dapr API token on the HTTP or gRPC connection. + /// + /// The value of the Dapr API token. + /// If a Dapr API token exists, the key/value pair to use for the header; otherwise null. + public static KeyValuePair? GetDaprApiTokenHeader(string? daprApiToken) => + string.IsNullOrWhiteSpace(daprApiToken) + ? null + : new KeyValuePair("dapr-api-token", daprApiToken); +} diff --git a/src/Dapr.Common/DaprGenericClientBuilder.cs b/src/Dapr.Common/DaprGenericClientBuilder.cs index 254953241..05deb21cc 100644 --- a/src/Dapr.Common/DaprGenericClientBuilder.cs +++ b/src/Dapr.Common/DaprGenericClientBuilder.cs @@ -1,4 +1,19 @@ -using System.Text.Json; +// ------------------------------------------------------------------------ +// Copyright 2024 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 System.Text.Json; +using System.Threading.Channels; using Grpc.Net.Client; using Microsoft.Extensions.Configuration; @@ -170,8 +185,9 @@ 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 (GrpcChannel channel, HttpClient httpClient, Uri httpEndpoint) BuildDaprClientDependencies() + protected (GrpcChannel channel, HttpClient httpClient, Uri httpEndpoint, string daprApiToken) BuildDaprClientDependencies(Assembly assembly) { var grpcEndpoint = new Uri(this.GrpcEndpoint); if (grpcEndpoint.Scheme != "http" && grpcEndpoint.Scheme != "https") @@ -184,22 +200,48 @@ public DaprGenericClientBuilder UseTimeout(TimeSpan timeout) // Set correct switch to make secure gRPC service calls. This switch must be set before creating the GrpcChannel. AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); } - + var httpEndpoint = new Uri(this.HttpEndpoint); if (httpEndpoint.Scheme != "http" && httpEndpoint.Scheme != "https") { throw new InvalidOperationException("The HTTP endpoint must use http or https."); } - var channel = GrpcChannel.ForAddress(this.GrpcEndpoint, this.GrpcChannelOptions); + //Configure the HTTP client + var httpClient = ConfigureHttpClient(assembly); + this.GrpcChannelOptions.HttpClient = httpClient; + + 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 if (this.Timeout > TimeSpan.Zero) { httpClient.Timeout = this.Timeout; } + + //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 (channel, httpClient, httpEndpoint); + return httpClient; } /// diff --git a/src/Dapr.Jobs/DaprJobsClientBuilder.cs b/src/Dapr.Jobs/DaprJobsClientBuilder.cs index 390d52236..9438e6240 100644 --- a/src/Dapr.Jobs/DaprJobsClientBuilder.cs +++ b/src/Dapr.Jobs/DaprJobsClientBuilder.cs @@ -12,6 +12,7 @@ // ------------------------------------------------------------------------ using Dapr.Common; +using Microsoft.Extensions.Configuration; using Autogenerated = Dapr.Client.Autogen.Grpc.v1; namespace Dapr.Jobs; @@ -21,17 +22,22 @@ namespace Dapr.Jobs; /// public sealed class DaprJobsClientBuilder : DaprGenericClientBuilder { + /// + /// Used to initialize a new instance of the . + /// + /// An optional instance of . + public DaprJobsClientBuilder(IConfiguration? configuration = null) : base(configuration) + { + } + /// /// Builds the client instance from the properties of the builder. /// /// The Dapr client instance. public override DaprJobsClient Build() { - var daprClientDependencies = this.BuildDaprClientDependencies(); - + var daprClientDependencies = this.BuildDaprClientDependencies(typeof(DaprJobsClient).Assembly); var client = new Autogenerated.Dapr.DaprClient(daprClientDependencies.channel); - var apiTokenHeader = this.DaprApiToken is not null ? DaprJobsClient.GetDaprApiTokenHeader(this.DaprApiToken) : null; - - return new DaprJobsGrpcClient(client, daprClientDependencies.httpClient, apiTokenHeader); + return new DaprJobsGrpcClient(client, daprClientDependencies.httpClient, daprClientDependencies.daprApiToken); } } diff --git a/src/Dapr.Jobs/DaprJobsGrpcClient.cs b/src/Dapr.Jobs/DaprJobsGrpcClient.cs index f23ef67fd..8f192190a 100644 --- a/src/Dapr.Jobs/DaprJobsGrpcClient.cs +++ b/src/Dapr.Jobs/DaprJobsGrpcClient.cs @@ -11,8 +11,7 @@ // limitations under the License. // ------------------------------------------------------------------------ -using System.Net.Http.Headers; -using System.Reflection; +using Dapr.Common; using Dapr.Jobs.Models; using Dapr.Jobs.Models.Responses; using Google.Protobuf; @@ -28,29 +27,35 @@ namespace Dapr.Jobs; internal sealed class DaprJobsGrpcClient : DaprJobsClient { /// - /// Present only for testing purposes. + /// The HTTP client used by the client for calling the Dapr runtime. /// - internal readonly HttpClient httpClient; - + /// + /// Property exposed for testing purposes. + /// + internal readonly HttpClient HttpClient; /// - /// Used to populate options headers with API token value. + /// The Dapr API token value. /// - internal readonly KeyValuePair? apiTokenHeader; - - private readonly Autogenerated.Dapr.DaprClient client; - private readonly string userAgent = UserAgent().ToString(); + /// + /// Property exposed for testing purposes. + /// + internal readonly string? DaprApiToken; + /// + /// The autogenerated Dapr client. + /// + /// + /// Property exposed for testing purposes. + /// + internal Autogenerated.Dapr.DaprClient Client { get; } - // property exposed for testing purposes - internal Autogenerated.Dapr.DaprClient Client => client; - internal DaprJobsGrpcClient( Autogenerated.Dapr.DaprClient innerClient, HttpClient httpClient, - KeyValuePair? apiTokenHeader) + string? daprApiToken) { - this.client = innerClient; - this.httpClient = httpClient; - this.apiTokenHeader = apiTokenHeader; + this.Client = innerClient; + this.HttpClient = httpClient; + this.DaprApiToken = daprApiToken; } /// @@ -107,11 +112,11 @@ public override async Task ScheduleJobAsync(string jobName, DaprJobSchedule sche var envelope = new Autogenerated.ScheduleJobRequest { Job = job }; - var callOptions = CreateCallOptions(headers: null, cancellationToken); - + var grpcCallOptions = DaprClientUtilities.ConfigureGrpcCallOptions(typeof(DaprJobsClient).Assembly, this.DaprApiToken, cancellationToken); + try { - await client.ScheduleJobAlpha1Async(envelope, callOptions); + await Client.ScheduleJobAlpha1Async(envelope, grpcCallOptions).ConfigureAwait(false); } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { @@ -146,8 +151,8 @@ public override async Task GetJobAsync(string jobName, Cancellat try { var envelope = new Autogenerated.GetJobRequest { Name = jobName }; - var callOptions = CreateCallOptions(headers: null, cancellationToken); - var response = await client.GetJobAlpha1Async(envelope, callOptions); + var grpcCallOptions = DaprClientUtilities.ConfigureGrpcCallOptions(typeof(DaprJobsClient).Assembly, this.DaprApiToken, cancellationToken); + var response = await Client.GetJobAlpha1Async(envelope, grpcCallOptions); return new DaprJobDetails(new DaprJobSchedule(response.Job.Schedule)) { DueTime = response.Job.DueTime is not null ? DateTime.Parse(response.Job.DueTime) : null, @@ -190,8 +195,8 @@ public override async Task DeleteJobAsync(string jobName, CancellationToken canc try { var envelope = new Autogenerated.DeleteJobRequest { Name = jobName }; - var callOptions = CreateCallOptions(headers: null, cancellationToken); - await client.DeleteJobAlpha1Async(envelope, callOptions); + var grpcCallOptions = DaprClientUtilities.ConfigureGrpcCallOptions(typeof(DaprJobsClient).Assembly, this.DaprApiToken, cancellationToken); + await Client.DeleteJobAlpha1Async(envelope, grpcCallOptions); } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { @@ -213,36 +218,7 @@ protected override void Dispose(bool disposing) { if (disposing) { - this.httpClient.Dispose(); - } - } - - private CallOptions CreateCallOptions(Metadata? headers, CancellationToken cancellationToken) - { - var callOptions = new CallOptions(headers: headers ?? new Metadata(), cancellationToken: cancellationToken); - - callOptions.Headers!.Add("User-Agent", this.userAgent); - - if (apiTokenHeader is not null) - { - callOptions.Headers.Add(apiTokenHeader.Value.Key, apiTokenHeader.Value.Value); + this.HttpClient.Dispose(); } - - return callOptions; - } - - /// - /// Returns the value for the User-Agent. - /// - /// A containing the value to use for the User-Agent. - private static ProductInfoHeaderValue UserAgent() - { - var assembly = typeof(DaprJobsClient).Assembly; - var assemblyVersion = assembly - .GetCustomAttributes() - .FirstOrDefault()? - .InformationalVersion; - - return new ProductInfoHeaderValue("dapr-sdk-dotnet", $"v{assemblyVersion}"); } } diff --git a/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs b/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs index 93265837b..e3680fd83 100644 --- a/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs +++ b/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs @@ -11,6 +11,7 @@ // limitations under the License. // ------------------------------------------------------------------------ +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -25,27 +26,29 @@ public static class DaprJobsServiceCollectionExtensions /// Adds Dapr Jobs client support to the service collection. /// /// The . - /// Optionally allows greater configuration of 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) + /// + 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(); + var builder = new DaprJobsClientBuilder(configuration); builder.UseHttpClientFactory(httpClientFactory); - configure?.Invoke(builder); + configure?.Invoke(serviceProvider, builder); return builder.Build(); }); - + switch (lifetime) { case ServiceLifetime.Scoped: @@ -59,35 +62,6 @@ public static IServiceCollection AddDaprJobsClient(this IServiceCollection servi serviceCollection.TryAddSingleton(registration); break; } - - return serviceCollection; - } - - /// - /// Adds Dapr Jobs client support to the service collection. - /// - /// 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, ServiceLifetime lifetime = ServiceLifetime.Singleton) - { - ArgumentNullException.ThrowIfNull(serviceCollection, nameof(serviceCollection)); - - //Register the IHttpClientFactory implementation - serviceCollection.AddHttpClient(); - - serviceCollection.TryAddSingleton(serviceProvider => - { - var httpClientFactory = serviceProvider.GetRequiredService(); - - var builder = new DaprJobsClientBuilder(); - builder.UseHttpClientFactory(httpClientFactory); - - configure?.Invoke(serviceProvider, builder); - - return builder.Build(); - }); return serviceCollection; } diff --git a/src/Dapr.Messaging/AssemblyInfo.cs b/src/Dapr.Messaging/AssemblyInfo.cs new file mode 100644 index 000000000..21a4f2d42 --- /dev/null +++ b/src/Dapr.Messaging/AssemblyInfo.cs @@ -0,0 +1,16 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 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.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Dapr.Messaging.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2" )] diff --git a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClientBuilder.cs b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClientBuilder.cs index b94bc5cdf..64a7de396 100644 --- a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClientBuilder.cs +++ b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClientBuilder.cs @@ -38,10 +38,9 @@ public DaprPublishSubscribeClientBuilder(IConfiguration? configuration = null) : /// Builds the client instance from the properties of the builder. /// public override DaprPublishSubscribeClient Build() - { - var daprClientDependencies = BuildDaprClientDependencies(); + { + var daprClientDependencies = BuildDaprClientDependencies(typeof(DaprPublishSubscribeClient).Assembly); var client = new Autogenerated.Dapr.DaprClient(daprClientDependencies.channel); - - return new DaprPublishSubscribeGrpcClient(client); + return new DaprPublishSubscribeGrpcClient(client, daprClientDependencies.httpClient, daprClientDependencies.daprApiToken); } } diff --git a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs index df6ccdcfe..cb2c42bf5 100644 --- a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs +++ b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs @@ -20,14 +20,36 @@ namespace Dapr.Messaging.PublishSubscribe; /// internal sealed class DaprPublishSubscribeGrpcClient : DaprPublishSubscribeClient { - private readonly P.DaprClient daprClient; - + /// + /// 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 P.DaprClient Client { get; } + /// /// Creates a new instance of a /// - public DaprPublishSubscribeGrpcClient(P.DaprClient client) + public DaprPublishSubscribeGrpcClient(P.DaprClient client, HttpClient httpClient, string? daprApiToken) { - daprClient = client; + this.Client = client; + this.HttpClient = httpClient; + this.DaprApiToken = daprApiToken; } /// @@ -41,7 +63,7 @@ public DaprPublishSubscribeGrpcClient(P.DaprClient client) /// public override async Task SubscribeAsync(string pubSubName, string topicName, DaprSubscriptionOptions options, TopicMessageHandler messageHandler, CancellationToken cancellationToken = default) { - var receiver = new PublishSubscribeReceiver(pubSubName, topicName, options, messageHandler, daprClient); + var receiver = new PublishSubscribeReceiver(pubSubName, topicName, options, messageHandler, this.Client); await receiver.SubscribeAsync(cancellationToken); return receiver; } diff --git a/src/Dapr.Messaging/PublishSubscribe/Extensions/PublishSubscribeServiceCollectionExtensions.cs b/src/Dapr.Messaging/PublishSubscribe/Extensions/PublishSubscribeServiceCollectionExtensions.cs index fe9b7c417..882f7c087 100644 --- a/src/Dapr.Messaging/PublishSubscribe/Extensions/PublishSubscribeServiceCollectionExtensions.cs +++ b/src/Dapr.Messaging/PublishSubscribe/Extensions/PublishSubscribeServiceCollectionExtensions.cs @@ -1,4 +1,18 @@ -using Microsoft.Extensions.DependencyInjection; +// ------------------------------------------------------------------------ +// Copyright 2024 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.Configuration; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; namespace Dapr.Messaging.PublishSubscribe.Extensions; @@ -25,8 +39,9 @@ public static IServiceCollection AddDaprPubSubClient(this IServiceCollection ser var registration = new Func(serviceProvider => { var httpClientFactory = serviceProvider.GetRequiredService(); + var configuration = serviceProvider.GetService(); - var builder = new DaprPublishSubscribeClientBuilder(); + var builder = new DaprPublishSubscribeClientBuilder(configuration); builder.UseHttpClientFactory(httpClientFactory); configure?.Invoke(serviceProvider, builder); diff --git a/test/Dapr.Jobs.Test/Extensions/DaprJobsServiceCollectionExtensionsTests.cs b/test/Dapr.Jobs.Test/Extensions/DaprJobsServiceCollectionExtensionsTests.cs index 281477d4e..bd5e4acd0 100644 --- a/test/Dapr.Jobs.Test/Extensions/DaprJobsServiceCollectionExtensionsTests.cs +++ b/test/Dapr.Jobs.Test/Extensions/DaprJobsServiceCollectionExtensionsTests.cs @@ -12,31 +12,57 @@ // ------------------------------------------------------------------------ using System; +using System.Collections.Generic; +using System.Linq; using System.Net.Http; using System.Threading.Tasks; using Dapr.Jobs.Extensions; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace Dapr.Jobs.Test.Extensions; public class DaprJobsServiceCollectionExtensionsTest { + [Fact] + public void AddDaprJobsClient_FromIConfiguration() + { + const string apiToken = "abc123"; + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary { { "DAPR_API_TOKEN", apiToken } }) + .Build(); + + var services = new ServiceCollection(); + services.AddSingleton(configuration); + + services.AddDaprJobsClient(); + + var app = services.BuildServiceProvider(); + + var jobsClient = app.GetRequiredService() as DaprJobsGrpcClient; + + Assert.NotNull(jobsClient!.DaprApiToken); + Assert.Equal(apiToken, jobsClient.DaprApiToken); + } + [Fact] public void AddDaprJobsClient_RegistersDaprClientOnlyOnce() { var services = new ServiceCollection(); - var clientBuilder = new Action(builder => - builder.UseDaprApiToken("abc")); + var clientBuilder = new Action((sp, builder) => + { + builder.UseDaprApiToken("abc"); + }); 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; - - Assert.Null(daprJobClient!.apiTokenHeader); - Assert.False(daprJobClient.httpClient.DefaultRequestHeaders.TryGetValues("dapr-api-token", out var _)); + + Assert.NotNull(daprJobClient!.HttpClient); + Assert.False(daprJobClient.HttpClient.DefaultRequestHeaders.TryGetValues("dapr-api-token", out var _)); } [Fact] @@ -63,8 +89,8 @@ public void AddDaprJobsClient_RegistersUsingDependencyFromIServiceProvider() services.AddDaprJobsClient((provider, builder) => { var configProvider = provider.GetRequiredService(); - var daprApiToken = configProvider.GetApiTokenValue(); - builder.UseDaprApiToken(daprApiToken); + var apiToken = TestSecretRetriever.GetApiTokenValue(); + builder.UseDaprApiToken(apiToken); }); var serviceProvider = services.BuildServiceProvider(); @@ -72,10 +98,15 @@ public void AddDaprJobsClient_RegistersUsingDependencyFromIServiceProvider() //Validate it's set on the GrpcClient - note that it doesn't get set on the HttpClient Assert.NotNull(client); - Assert.NotNull(client.apiTokenHeader); - Assert.True(client.apiTokenHeader.HasValue); - Assert.Equal("dapr-api-token", client.apiTokenHeader.Value.Key); - Assert.Equal("abcdef", client.apiTokenHeader.Value.Value); + Assert.NotNull(client.DaprApiToken); + Assert.Equal("abcdef", client.DaprApiToken); + Assert.NotNull(client.HttpClient); + + if (!client.HttpClient.DefaultRequestHeaders.TryGetValues("dapr-api-token", out var daprApiToken)) + { + Assert.Fail(); + } + Assert.Equal("abcdef", daprApiToken.FirstOrDefault()); } [Fact] @@ -83,7 +114,7 @@ public void RegisterJobsClient_ShouldRegisterSingleton_WhenLifetimeIsSingleton() { var services = new ServiceCollection(); - services.AddDaprJobsClient(options => { }, ServiceLifetime.Singleton); + services.AddDaprJobsClient((serviceProvider, options) => { }, ServiceLifetime.Singleton); var serviceProvider = services.BuildServiceProvider(); var daprJobsClient1 = serviceProvider.GetService(); @@ -100,7 +131,7 @@ public async Task RegisterJobsClient_ShouldRegisterScoped_WhenLifetimeIsScoped() { var services = new ServiceCollection(); - services.AddDaprJobsClient(options => { }, ServiceLifetime.Scoped); + services.AddDaprJobsClient((serviceProvider, options) => { }, ServiceLifetime.Scoped); var serviceProvider = services.BuildServiceProvider(); await using var scope1 = serviceProvider.CreateAsyncScope(); @@ -119,7 +150,7 @@ public void RegisterJobsClient_ShouldRegisterTransient_WhenLifetimeIsTransient() { var services = new ServiceCollection(); - services.AddDaprJobsClient(options => { }, ServiceLifetime.Transient); + services.AddDaprJobsClient((serviceProvider, options) => { }, ServiceLifetime.Transient); var serviceProvider = services.BuildServiceProvider(); var daprJobsClient1 = serviceProvider.GetService(); @@ -132,6 +163,6 @@ public void RegisterJobsClient_ShouldRegisterTransient_WhenLifetimeIsTransient() private class TestSecretRetriever { - public string GetApiTokenValue() => "abcdef"; + public static string GetApiTokenValue() => "abcdef"; } } diff --git a/test/Dapr.Messaging.Test/Extensions/PublishSubscribeServiceCollectionExtensionsTests.cs b/test/Dapr.Messaging.Test/Extensions/PublishSubscribeServiceCollectionExtensionsTests.cs index d239fb86d..b5a717df3 100644 --- a/test/Dapr.Messaging.Test/Extensions/PublishSubscribeServiceCollectionExtensionsTests.cs +++ b/test/Dapr.Messaging.Test/Extensions/PublishSubscribeServiceCollectionExtensionsTests.cs @@ -1,11 +1,50 @@ -using Dapr.Messaging.PublishSubscribe; +// ------------------------------------------------------------------------ +// Copyright 2024 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.Messaging.PublishSubscribe; using Dapr.Messaging.PublishSubscribe.Extensions; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace Dapr.Messaging.Test.Extensions; public sealed class PublishSubscribeServiceCollectionExtensionsTests { + [Fact] + public void AddDaprMessagingClient_FromIConfiguration() + { + const string apiToken = "abc123"; + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + {"DAPR_API_TOKEN", apiToken } + }) + .Build(); + + var services = new ServiceCollection(); + + services.AddSingleton(configuration); + + services.AddDaprPubSubClient(); + + var app = services.BuildServiceProvider(); + + var pubSubClient = app.GetRequiredService() as DaprPublishSubscribeGrpcClient; + + Assert.NotNull(pubSubClient!); + Assert.Equal(apiToken, pubSubClient.DaprApiToken); + } + [Fact] public void AddDaprPubSubClient_RegistersIHttpClientFactory() {