From 0d30c7b522144cc2ea634c13ac46f6f0de694c6a Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Fri, 11 Apr 2025 01:06:50 -0500 Subject: [PATCH 1/2] Initial pass to add Dapr.Bindings project and some tests Signed-off-by: Whit Waldo --- all.sln | 14 ++ src/Dapr.Bindings/Dapr.Bindings.csproj | 38 +++++ src/Dapr.Bindings/DaprBindingsClient.cs | 146 ++++++++++++++++++ .../DaprBindingsClientBuilder.cs | 39 +++++ src/Dapr.Bindings/DaprBindingsGrpcClient.cs | 144 +++++++++++++++++ .../Extensions/DaprBindingsBuilder.cs | 27 ++++ ...DaprBindingsServiceCollectionExtensions.cs | 38 +++++ src/Dapr.Bindings/IDaprBindingsBuilder.cs | 21 +++ .../Models/DaprBindingRequest.cs | 33 ++++ .../Models/DaprBindingResponse.cs | 25 +++ src/Dapr.Common/AssemblyInfo.cs | 2 + src/Dapr.Common/DaprGenericClientBuilder.cs | 2 +- src/Dapr.Jobs/Extensions/DaprJobsBuilder.cs | 1 - .../Dapr.Bindings.Test.csproj | 25 +++ 14 files changed, 553 insertions(+), 2 deletions(-) create mode 100644 src/Dapr.Bindings/Dapr.Bindings.csproj create mode 100644 src/Dapr.Bindings/DaprBindingsClient.cs create mode 100644 src/Dapr.Bindings/DaprBindingsClientBuilder.cs create mode 100644 src/Dapr.Bindings/DaprBindingsGrpcClient.cs create mode 100644 src/Dapr.Bindings/Extensions/DaprBindingsBuilder.cs create mode 100644 src/Dapr.Bindings/Extensions/DaprBindingsServiceCollectionExtensions.cs create mode 100644 src/Dapr.Bindings/IDaprBindingsBuilder.cs create mode 100644 src/Dapr.Bindings/Models/DaprBindingRequest.cs create mode 100644 src/Dapr.Bindings/Models/DaprBindingResponse.cs create mode 100644 test/Dapr.Bindings.Test/Dapr.Bindings.Test.csproj diff --git a/all.sln b/all.sln index ae51012e0..4464ddb03 100644 --- a/all.sln +++ b/all.sln @@ -169,6 +169,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Actors.Analyzers.Test" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Analyzers.Common", "test\Dapr.Analyzers.Common\Dapr.Analyzers.Common.csproj", "{7E23E229-6823-4D84-AF3A-AE14CEAEF52A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Bindings", "src\Dapr.Bindings\Dapr.Bindings.csproj", "{ACE88DA3-AFDC-478D-B3D9-B06460C64D2F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Bindings.Test", "test\Dapr.Bindings.Test\Dapr.Bindings.Test.csproj", "{A8A37DBD-8EB1-4346-9A0D-235403564355}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -445,6 +449,14 @@ Global {7E23E229-6823-4D84-AF3A-AE14CEAEF52A}.Debug|Any CPU.Build.0 = Debug|Any CPU {7E23E229-6823-4D84-AF3A-AE14CEAEF52A}.Release|Any CPU.ActiveCfg = Release|Any CPU {7E23E229-6823-4D84-AF3A-AE14CEAEF52A}.Release|Any CPU.Build.0 = Release|Any CPU + {ACE88DA3-AFDC-478D-B3D9-B06460C64D2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ACE88DA3-AFDC-478D-B3D9-B06460C64D2F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ACE88DA3-AFDC-478D-B3D9-B06460C64D2F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ACE88DA3-AFDC-478D-B3D9-B06460C64D2F}.Release|Any CPU.Build.0 = Release|Any CPU + {A8A37DBD-8EB1-4346-9A0D-235403564355}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A8A37DBD-8EB1-4346-9A0D-235403564355}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A8A37DBD-8EB1-4346-9A0D-235403564355}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A8A37DBD-8EB1-4346-9A0D-235403564355}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -525,6 +537,8 @@ Global {E49C822C-E921-48DF-897B-3E603CA596D2} = {27C5D71D-0721-4221-9286-B94AB07B58CF} {A2C0F203-11FF-4B7F-A94F-B9FD873573FE} = {DD020B34-460F-455F-8D17-CF4A949F100B} {7E23E229-6823-4D84-AF3A-AE14CEAEF52A} = {DD020B34-460F-455F-8D17-CF4A949F100B} + {ACE88DA3-AFDC-478D-B3D9-B06460C64D2F} = {27C5D71D-0721-4221-9286-B94AB07B58CF} + {A8A37DBD-8EB1-4346-9A0D-235403564355} = {DD020B34-460F-455F-8D17-CF4A949F100B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40} diff --git a/src/Dapr.Bindings/Dapr.Bindings.csproj b/src/Dapr.Bindings/Dapr.Bindings.csproj new file mode 100644 index 000000000..f4d55867c --- /dev/null +++ b/src/Dapr.Bindings/Dapr.Bindings.csproj @@ -0,0 +1,38 @@ + + + + enable + enable + Dapr.Bindings + Dapr Bindings SDK + Used to engage with the Dapr bindings building block. + alpha + + + + + + + + + + + + + + + + + + + + + + C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App\8.0.14\Microsoft.Extensions.Configuration.Abstractions.dll + + + C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App\8.0.14\Microsoft.Extensions.DependencyInjection.Abstractions.dll + + + + diff --git a/src/Dapr.Bindings/DaprBindingsClient.cs b/src/Dapr.Bindings/DaprBindingsClient.cs new file mode 100644 index 000000000..1ab2896ad --- /dev/null +++ b/src/Dapr.Bindings/DaprBindingsClient.cs @@ -0,0 +1,146 @@ +// ------------------------------------------------------------------------ +// 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 Dapr.Bindings.Models; +using Dapr.Common; +using Autogenerated = Dapr.Client.Autogen.Grpc.v1.Dapr; + +namespace Dapr.Bindings; + +/// +/// +/// Defines client operations for managing Dapr jobs. +/// Register the for use via dependency injection with +/// 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 abstract class DaprBindingsClient( + Autogenerated.DaprClient client, + HttpClient httpClient, + JsonSerializerOptions jsonSerializerOptions, + 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; + + /// + /// The JSON serializer options. + /// + /// + /// Property exposed for testing purposes. + /// + internal JsonSerializerOptions JsonSerializerOptions { get; } = jsonSerializerOptions; + + /// + /// Invokes an output binding. + /// + /// The type of the data that will be JSON serialized and provided as the binding payload. + /// The name of the binding to sent the event to. + /// The type of operation to perform on the binding. + /// The data that will be JSON serialized and provided as the binding payload. + /// A collection of metadata key-value pairs that will be provided to the binding. The valid metadata keys and values are determined by the type of binding used. + /// A that can be used to cancel the operation. + /// A that will complete when the operation has completed. + public abstract Task InvokeBindingAsync( + string bindingName, + string operation, + TRequest data, + IReadOnlyDictionary? metadata = null, + CancellationToken cancellationToken = default); + + /// + /// Invokes an output binding. + /// + /// The type of the data that will be JSON serialized and provided as the binding payload. + /// The type of the data that will be JSON deserialized from the binding response. + /// The name of the binding to sent the event to. + /// The type of operation to perform on the binding. + /// The data that will be JSON serialized and provided as the binding payload. + /// A collection of metadata key-value pairs that will be provided to the binding. The valid metadata keys and values are determined by the type of binding used. + /// A that can be used to cancel the operation. + /// A that will complete when the operation has completed. + public abstract Task InvokeBindingAsync( + string bindingName, + string operation, + TRequest data, + IReadOnlyDictionary? metadata = null, + CancellationToken cancellationToken = default); + + /// + /// Invokes a binding with the provided . This method allows for control of the binding + /// input and output using raw bytes. + /// + /// The to send. + /// A that can be used to cancel the operation. + /// A that will complete when the operation has completed. + public abstract Task InvokeBindingAsync(DaprBindingRequest request, CancellationToken cancellationToken = default); + + internal static KeyValuePair? GetDaprApiTokenHeader(string apiToken) + { + if (string.IsNullOrWhiteSpace(apiToken)) + { + return null; + } + + return new KeyValuePair("dapr-api-token", apiToken); + } + + /// + 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.Bindings/DaprBindingsClientBuilder.cs b/src/Dapr.Bindings/DaprBindingsClientBuilder.cs new file mode 100644 index 000000000..23248529a --- /dev/null +++ b/src/Dapr.Bindings/DaprBindingsClientBuilder.cs @@ -0,0 +1,39 @@ +// ------------------------------------------------------------------------ +// 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.Configuration; +using Autogenerated = Dapr.Client.Autogen.Grpc.v1; + +namespace Dapr.Bindings; + +/// +/// Builds a . +/// +/// An optional instance of . +public sealed class DaprBindingsClientBuilder(IConfiguration? configuration = null) : DaprGenericClientBuilder(configuration) +{ + /// + /// Builds the client instance from the properties of the builder. + /// + /// The Dapr client instance. + /// + /// Builds the client instance from the properties of the builder. + /// + public override DaprBindingsClient Build() + { + var daprClientDependencies = this.BuildDaprClientDependencies(typeof(DaprBindingsClient).Assembly); + var client = new Autogenerated.Dapr.DaprClient(daprClientDependencies.channel); + return new DaprBindingsGrpcClient(client, daprClientDependencies.httpClient, this.JsonSerializerOptions, daprClientDependencies.daprApiToken); + } +} diff --git a/src/Dapr.Bindings/DaprBindingsGrpcClient.cs b/src/Dapr.Bindings/DaprBindingsGrpcClient.cs new file mode 100644 index 000000000..ade4dfd69 --- /dev/null +++ b/src/Dapr.Bindings/DaprBindingsGrpcClient.cs @@ -0,0 +1,144 @@ +// ------------------------------------------------------------------------ +// 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 Dapr.Bindings.Models; +using Dapr.Common; +using Google.Protobuf; +using Grpc.Core; +using Autogenerated = Dapr.Client.Autogen.Grpc.v1; + +namespace Dapr.Bindings; + +/// +/// A client for interacting with the Dapr Bindings endpoints. +/// +internal sealed class DaprBindingsGrpcClient( + Autogenerated.Dapr.DaprClient client, + HttpClient httpClient, + JsonSerializerOptions jsonSerializerOptions, + string? daprApiToken = null) : DaprBindingsClient(client, httpClient, jsonSerializerOptions, daprApiToken) +{ + /// + /// Invokes an output binding. + /// + /// The type of the data that will be JSON serialized and provided as the binding payload. + /// The name of the binding to sent the event to. + /// The type of operation to perform on the binding. + /// The data that will be JSON serialized and provided as the binding payload. + /// A collection of metadata key-value pairs that will be provided to the binding. The valid metadata keys and values are determined by the type of binding used. + /// A that can be used to cancel the operation. + /// A that will complete when the operation has completed. + public override async Task InvokeBindingAsync( + string bindingName, + string operation, + TRequest data, + IReadOnlyDictionary? metadata = null, + CancellationToken cancellationToken = default) + { + ArgumentException.ThrowIfNullOrEmpty(bindingName, nameof(bindingName)); + ArgumentException.ThrowIfNullOrEmpty(operation, nameof(operation)); + + var serializedBytes = JsonSerializer.SerializeToUtf8Bytes(data, JsonSerializerOptions); + var bytes = ByteString.CopyFrom(serializedBytes); + await MakeInvokeBindingRequestAsync(bindingName, operation, bytes, metadata, cancellationToken); + } + + /// + /// Invokes an output binding. + /// + /// The type of the data that will be JSON serialized and provided as the binding payload. + /// The type of the data that will be JSON deserialized from the binding response. + /// The name of the binding to sent the event to. + /// The type of operation to perform on the binding. + /// The data that will be JSON serialized and provided as the binding payload. + /// A collection of metadata key-value pairs that will be provided to the binding. The valid metadata keys and values are determined by the type of binding used. + /// A that can be used to cancel the operation. + /// A that will complete when the operation has completed. + public override async Task InvokeBindingAsync( + string bindingName, + string operation, + TRequest data, + IReadOnlyDictionary? metadata = null, + CancellationToken cancellationToken = default) where TResponse : default + { + ArgumentException.ThrowIfNullOrEmpty(bindingName, nameof(bindingName)); + ArgumentException.ThrowIfNullOrEmpty(operation, nameof(operation)); + + var serializedBytes = JsonSerializer.SerializeToUtf8Bytes(data, JsonSerializerOptions); + var bytes = ByteString.CopyFrom(serializedBytes); + var response = await MakeInvokeBindingRequestAsync(bindingName, operation, bytes, metadata, cancellationToken); + + try + { + return response.Data.Length == 0 + ? default + : JsonSerializer.Deserialize(bytes.Span, JsonSerializerOptions);} + catch (JsonException ex) + { + throw new DaprException( + "Binding operation failed: the response payload could not be deserialized. See InnerException for details.", + ex); + } + } + + /// + /// Invokes a binding with the provided . This method allows for control of the binding + /// input and output using raw bytes. + /// + /// The to send. + /// A that can be used to cancel the operation. + /// A that will complete when the operation has completed. + public override async Task InvokeBindingAsync(DaprBindingRequest request, CancellationToken cancellationToken = default) + { + var bytes = ByteString.CopyFrom(request.Data.Span); + var response = await MakeInvokeBindingRequestAsync(request.BindingName, request.Operation, bytes, + request.Metadata, cancellationToken); + return new DaprBindingResponse(request, response.Data.Memory, response.Metadata); + } + + private async Task MakeInvokeBindingRequestAsync( + string name, + string operation, + ByteString? data = null, + IReadOnlyDictionary? metadata = null, + CancellationToken cancellationToken = default) + { + var envelope = new Autogenerated.InvokeBindingRequest { Name = name, Operation = operation }; + + if (data is not null) + { + envelope.Data = data; + } + + if (metadata is not null) + { + foreach (var kvp in metadata) + { + envelope.Metadata.Add(kvp.Key, kvp.Value); + } + } + + var grpcCallOptions = DaprClientUtilities.ConfigureGrpcCallOptions(typeof(DaprBindingsClient).Assembly, + this.DaprApiToken, cancellationToken); + try + { + return await Client.InvokeBindingAsync(envelope, grpcCallOptions); + } + catch (RpcException ex) + { + throw new DaprException( + "Binding operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + } + } +} diff --git a/src/Dapr.Bindings/Extensions/DaprBindingsBuilder.cs b/src/Dapr.Bindings/Extensions/DaprBindingsBuilder.cs new file mode 100644 index 000000000..c04d8232a --- /dev/null +++ b/src/Dapr.Bindings/Extensions/DaprBindingsBuilder.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.Bindings.Extensions; + +/// +/// Used by the fluent registration builder to configure a Dapr Bindings client. +/// +public sealed class DaprBindingsBuilder(IServiceCollection services) : IDaprBindingsBuilder +{ + /// + /// The registered services on the builder. + /// + public IServiceCollection Services { get; } = services; +} diff --git a/src/Dapr.Bindings/Extensions/DaprBindingsServiceCollectionExtensions.cs b/src/Dapr.Bindings/Extensions/DaprBindingsServiceCollectionExtensions.cs new file mode 100644 index 000000000..9161f593f --- /dev/null +++ b/src/Dapr.Bindings/Extensions/DaprBindingsServiceCollectionExtensions.cs @@ -0,0 +1,38 @@ +// ------------------------------------------------------------------------ +// 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.Extensions; +using Microsoft.Extensions.DependencyInjection; + +namespace Dapr.Bindings.Extensions; + +/// +/// Contains extension methods for using Dapr Bindings with dependency injection. +/// +public static class DaprBindingsServiceCollectionExtensions +{ + /// + /// Adds Dapr Bindings client support to the service collection. + /// + /// The . + /// Optionally allows greater configuration of the using injected services. + /// The lifetime of the registered services. + /// + public static IDaprBindingsBuilder AddDaprBindingsClient( + this IServiceCollection services, + Action? configure = null, + ServiceLifetime lifetime = ServiceLifetime.Singleton) => + services + .AddDaprClient( + configure, lifetime); +} diff --git a/src/Dapr.Bindings/IDaprBindingsBuilder.cs b/src/Dapr.Bindings/IDaprBindingsBuilder.cs new file mode 100644 index 000000000..3d257c4d8 --- /dev/null +++ b/src/Dapr.Bindings/IDaprBindingsBuilder.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.Bindings; + +/// +/// Responsible for registering Dapr Bindings service functionality. +/// +public interface IDaprBindingsBuilder : IDaprServiceBuilder; diff --git a/src/Dapr.Bindings/Models/DaprBindingRequest.cs b/src/Dapr.Bindings/Models/DaprBindingRequest.cs new file mode 100644 index 000000000..edf7bd64a --- /dev/null +++ b/src/Dapr.Bindings/Models/DaprBindingRequest.cs @@ -0,0 +1,33 @@ +// ------------------------------------------------------------------------ +// 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.Bindings.Models; + +/// +/// Represents the request used to invoke a binding. +/// +/// The name of the binding. +/// The type of the operation to perform on the binding. +public sealed record DaprBindingRequest(string BindingName, string Operation ) +{ + /// + /// The binding request payload. + /// + public ReadOnlyMemory Data { get; init; } = default; + + /// + /// The collection of metadata key/value pairs that will be provided to the binding. + /// The valid metadata keys and values are determined by the type of binding used. + /// + public Dictionary Metadata { get; init; } = []; +} diff --git a/src/Dapr.Bindings/Models/DaprBindingResponse.cs b/src/Dapr.Bindings/Models/DaprBindingResponse.cs new file mode 100644 index 000000000..6fb71ec8b --- /dev/null +++ b/src/Dapr.Bindings/Models/DaprBindingResponse.cs @@ -0,0 +1,25 @@ +// ------------------------------------------------------------------------ +// 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.Bindings.Models; + +/// +/// Represents the response from invoking a binding. +/// +/// The associated with this response. +/// The response payload. +/// The response metadata. +public sealed record DaprBindingResponse( + DaprBindingRequest Request, + ReadOnlyMemory Data, + IReadOnlyDictionary Metadata); diff --git a/src/Dapr.Common/AssemblyInfo.cs b/src/Dapr.Common/AssemblyInfo.cs index 3037485a9..90fb59cd0 100644 --- a/src/Dapr.Common/AssemblyInfo.cs +++ b/src/Dapr.Common/AssemblyInfo.cs @@ -18,6 +18,7 @@ [assembly: InternalsVisibleTo("Dapr.Actors.AspNetCore, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.AI, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.AspNetCore, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.Bindings, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.Client, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.Jobs, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.Messaging, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] @@ -33,6 +34,7 @@ [assembly: InternalsVisibleTo("Dapr.AspNetCore.IntegrationTest, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.AspNetCore.IntegrationTest.App, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.AspNetCore.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.Bindings.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.Client.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.Common.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.E2E.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] diff --git a/src/Dapr.Common/DaprGenericClientBuilder.cs b/src/Dapr.Common/DaprGenericClientBuilder.cs index 8ff59490f..3e29a2eff 100644 --- a/src/Dapr.Common/DaprGenericClientBuilder.cs +++ b/src/Dapr.Common/DaprGenericClientBuilder.cs @@ -200,7 +200,7 @@ protected internal (GrpcChannel channel, HttpClient httpClient, Uri httpEndpoint AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); } - var httpEndpoint = new Uri(this.HttpEndpoint); + var httpEndpoint = new Uri(this.HttpEndpoint); if (httpEndpoint.Scheme != "http" && httpEndpoint.Scheme != "https") { throw new InvalidOperationException("The HTTP endpoint must use http or https."); diff --git a/src/Dapr.Jobs/Extensions/DaprJobsBuilder.cs b/src/Dapr.Jobs/Extensions/DaprJobsBuilder.cs index 51bdb8985..22b2d7d58 100644 --- a/src/Dapr.Jobs/Extensions/DaprJobsBuilder.cs +++ b/src/Dapr.Jobs/Extensions/DaprJobsBuilder.cs @@ -18,7 +18,6 @@ namespace Dapr.Jobs.Extensions; /// /// Used by the fluent registration builder to configure a Dapr Jobs client. /// -/// public sealed class DaprJobsBuilder(IServiceCollection services) : IDaprJobsBuilder { /// diff --git a/test/Dapr.Bindings.Test/Dapr.Bindings.Test.csproj b/test/Dapr.Bindings.Test/Dapr.Bindings.Test.csproj new file mode 100644 index 000000000..7cb037c26 --- /dev/null +++ b/test/Dapr.Bindings.Test/Dapr.Bindings.Test.csproj @@ -0,0 +1,25 @@ + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + From e3db7863d454abb280cde1f1d8ebb378f58e289f Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Fri, 11 Apr 2025 01:07:30 -0500 Subject: [PATCH 2/2] Neglected to add Dapr.Bindings.Test project to last commit Signed-off-by: Whit Waldo --- .../DaprBindingsClientBuilderTests.cs | 121 +++++++++++++ ...BindingsServiceCollectionExtensionsTest.cs | 167 ++++++++++++++++++ ...aprJobsServiceCollectionExtensionsTests.cs | 1 - 3 files changed, 288 insertions(+), 1 deletion(-) create mode 100644 test/Dapr.Bindings.Test/DaprBindingsClientBuilderTests.cs create mode 100644 test/Dapr.Bindings.Test/Extensions/DaprBindingsServiceCollectionExtensionsTest.cs diff --git a/test/Dapr.Bindings.Test/DaprBindingsClientBuilderTests.cs b/test/Dapr.Bindings.Test/DaprBindingsClientBuilderTests.cs new file mode 100644 index 000000000..d4b14dfea --- /dev/null +++ b/test/Dapr.Bindings.Test/DaprBindingsClientBuilderTests.cs @@ -0,0 +1,121 @@ +// ------------------------------------------------------------------------ +// 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; +using System.Text.Json; +using Grpc.Net.Client; + +namespace Dapr.Bindings.Test; + +public class DaprBindingsClientBuilderTests +{ + [Fact] + public void DaprClientBuilder_UsesPropertyNameCaseHandlingInsensitiveByDefault() + { + DaprBindingsClientBuilder builder = new DaprBindingsClientBuilder(); + Assert.True(builder.JsonSerializerOptions.PropertyNameCaseInsensitive); + } + + [Fact] + public void DaprBindingsClientBuilder_UsesPropertyNameCaseHandlingAsSpecified() + { + var builder = new DaprBindingsClientBuilder(); + builder.UseJsonSerializationOptions(new JsonSerializerOptions + { + PropertyNameCaseInsensitive = false + }); + Assert.False(builder.JsonSerializerOptions.PropertyNameCaseInsensitive); + } + + [Fact] + public void DaprBindingsClientBuilder_UsesThrowOperationCanceledOnCancellation_ByDefault() + { + var builder = new DaprBindingsClientBuilder(); + var daprClient = builder.Build(); + Assert.True(builder.GrpcChannelOptions.ThrowOperationCanceledOnCancellation); + } + + [Fact] + public void DaprBindingsClientBuilder_DoesNotOverrideUserGrpcChannelOptions() + { + var builder = new DaprBindingsClientBuilder(); + var daprClient = builder.UseGrpcChannelOptions(new GrpcChannelOptions()).Build(); + Assert.False(builder.GrpcChannelOptions.ThrowOperationCanceledOnCancellation); + } + + [Fact] + public void DaprBindingsClientBuilder_ValidatesGrpcEndpointScheme() + { + var builder = new DaprBindingsClientBuilder(); + builder.UseGrpcEndpoint("ftp://example.com"); + + var ex = Assert.Throws(() => builder.Build()); + Assert.Equal("The gRPC endpoint must use http or https.", ex.Message); + } + + [Fact] + public void DaprBindingsClientBuilder_ValidatesHttpEndpointScheme() + { + var builder = new DaprBindingsClientBuilder(); + builder.UseHttpEndpoint("ftp://example.com"); + + var ex = Assert.Throws(() => builder.Build()); + Assert.Equal("The HTTP endpoint must use http or https.", ex.Message); + } + + [Fact] + public void DaprBindingsClientBuilder_SetsApiToken() + { + var builder = new DaprBindingsClientBuilder(); + builder.UseDaprApiToken("test_token"); + builder.Build(); + Assert.Equal("test_token", builder.DaprApiToken); + } + + [Fact] + public void DaprBindingsClientBuilder_SetsNullApiToken() + { + var builder = new DaprBindingsClientBuilder(); + builder.UseDaprApiToken(null); + builder.Build(); + Assert.Null(builder.DaprApiToken); + } + + [Fact] + public void DaprBindingsClientBuilder_ApiTokenSet_SetsApiTokenHeader() + { + var builder = new DaprBindingsClientBuilder(); + builder.UseDaprApiToken("test_token"); + + var entry = DaprBindingsClient.GetDaprApiTokenHeader(builder.DaprApiToken); + Assert.NotNull(entry); + Assert.Equal("test_token", entry.Value.Value); + } + + [Fact] + public void DaprBindingsClientBuilder_ApiTokenNotSet_EmptyApiTokenHeader() + { + var builder = new DaprBindingsClientBuilder(); + var entry = DaprBindingsClient.GetDaprApiTokenHeader(builder.DaprApiToken); + Assert.Equal(default, entry); + } + + [Fact] + public void DaprBindingsClientBuilder_SetsTimeout() + { + var builder = new DaprBindingsClientBuilder(); + builder.UseTimeout(TimeSpan.FromSeconds(2)); + builder.Build(); + Assert.Equal(2, builder.Timeout.Seconds); + } +} diff --git a/test/Dapr.Bindings.Test/Extensions/DaprBindingsServiceCollectionExtensionsTest.cs b/test/Dapr.Bindings.Test/Extensions/DaprBindingsServiceCollectionExtensionsTest.cs new file mode 100644 index 000000000..89f634658 --- /dev/null +++ b/test/Dapr.Bindings.Test/Extensions/DaprBindingsServiceCollectionExtensionsTest.cs @@ -0,0 +1,167 @@ +// ------------------------------------------------------------------------ +// 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.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using Dapr.Bindings.Extensions; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Dapr.Bindings.Test.Extensions; + +public class DaprBindingsServiceCollectionExtensionsTest +{ + [Fact] + + public void AddDaprBindingsClient_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.AddDaprBindingsClient(); + + var app = services.BuildServiceProvider(); + + var jobsClient = app.GetRequiredService() as DaprBindingsGrpcClient; + + Assert.NotNull(jobsClient!.DaprApiToken); + Assert.Equal(apiToken, jobsClient.DaprApiToken); + } + + [Fact] + public void AddDaprBindingsClient_DaprClientRegistration_UseMostRecentVersion() + { + var services = new ServiceCollection(); + + services.AddDaprBindingsClient((_, builder) => + { + //Sets the API token value + builder.UseDaprApiToken("abcd1234"); + }); + services.AddDaprBindingsClient(); //Sets a default API token value of an empty string + + var serviceProvider = services.BuildServiceProvider(); + var daprJobClient = serviceProvider.GetRequiredService() as DaprBindingsGrpcClient; + + Assert.NotNull(daprJobClient!.HttpClient); + Assert.False(daprJobClient.HttpClient.DefaultRequestHeaders.TryGetValues("dapr-api-token", out var _)); + } + + [Fact] + public void AddDaprBindingsClient_RegistersIHttpClientFactory() + { + var services = new ServiceCollection(); + + services.AddDaprBindingsClient(); + + var serviceProvider = services.BuildServiceProvider(); + + var httpClientFactory = serviceProvider.GetService(); + Assert.NotNull(httpClientFactory); + + var daprBindingsClient = serviceProvider.GetService(); + Assert.NotNull(daprBindingsClient); + } + + [Fact] + public void AddDaprBindingsClient_RegistersUsingDependencyFromIServiceProvider() + { + var services = new ServiceCollection(); + services.AddSingleton(); + services.AddDaprBindingsClient((provider, builder) => + { + var configProvider = provider.GetRequiredService(); + var apiToken = configProvider.GetApiTokenValue(); + builder.UseDaprApiToken(apiToken); + }); + + var serviceProvider = services.BuildServiceProvider(); + var client = serviceProvider.GetRequiredService() as DaprBindingsGrpcClient; + + //Validate it's set on the GrpcClient - note that it doesn't get set on the HttpClient + Assert.NotNull(client); + 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] + public void RegisterJobsClient_ShouldRegisterSingleton_WhenLifetimeIsSingleton() + { + var services = new ServiceCollection(); + + services.AddDaprBindingsClient((_, _) => { }, ServiceLifetime.Singleton); + var serviceProvider = services.BuildServiceProvider(); + + var DaprBindingsClient1 = serviceProvider.GetService(); + var DaprBindingsClient2 = serviceProvider.GetService(); + + Assert.NotNull(DaprBindingsClient1); + Assert.NotNull(DaprBindingsClient2); + + Assert.Same(DaprBindingsClient1, DaprBindingsClient2); + } + + [Fact] + public async Task RegisterJobsClient_ShouldRegisterScoped_WhenLifetimeIsScoped() + { + var services = new ServiceCollection(); + + services.AddDaprBindingsClient((_, _) => { }, ServiceLifetime.Scoped); + var serviceProvider = services.BuildServiceProvider(); + + await using var scope1 = serviceProvider.CreateAsyncScope(); + var DaprBindingsClient1 = scope1.ServiceProvider.GetService(); + + await using var scope2 = serviceProvider.CreateAsyncScope(); + var DaprBindingsClient2 = scope2.ServiceProvider.GetService(); + + Assert.NotNull(DaprBindingsClient1); + Assert.NotNull(DaprBindingsClient2); + Assert.NotSame(DaprBindingsClient1, DaprBindingsClient2); + } + + [Fact] + public void RegisterJobsClient_ShouldRegisterTransient_WhenLifetimeIsTransient() + { + var services = new ServiceCollection(); + + services.AddDaprBindingsClient((_, _) => { }, ServiceLifetime.Transient); + var serviceProvider = services.BuildServiceProvider(); + + var DaprBindingsClient1 = serviceProvider.GetService(); + var DaprBindingsClient2 = serviceProvider.GetService(); + + Assert.NotNull(DaprBindingsClient1); + Assert.NotNull(DaprBindingsClient2); + Assert.NotSame(DaprBindingsClient1, DaprBindingsClient2); + } + + private class TestSecretRetriever + { + public string GetApiTokenValue() => "abcdef"; + } +} diff --git a/test/Dapr.Jobs.Test/Extensions/DaprJobsServiceCollectionExtensionsTests.cs b/test/Dapr.Jobs.Test/Extensions/DaprJobsServiceCollectionExtensionsTests.cs index 814e52794..dd40287f9 100644 --- a/test/Dapr.Jobs.Test/Extensions/DaprJobsServiceCollectionExtensionsTests.cs +++ b/test/Dapr.Jobs.Test/Extensions/DaprJobsServiceCollectionExtensionsTests.cs @@ -11,7 +11,6 @@ // limitations under the License. // ------------------------------------------------------------------------ -using System; using System.Collections.Generic; using System.Linq; using System.Net.Http;