From 7e70b118b890c9975c137e0d6f852e7bff09a354 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 10 Dec 2024 09:06:44 -0600 Subject: [PATCH] Add .NET client for LLM Conversations support (#1382) * Updated prototype Signed-off-by: Whit Waldo * Added Dapr.AI project and unit test project to contain the conversational building block (and potentially future other projects) Signed-off-by: Whit Waldo * Changed default values Signed-off-by: Whit Waldo * Removed unnecessary method Signed-off-by: Whit Waldo * Added a few unit tests Signed-off-by: Whit Waldo * Added example project Signed-off-by: Whit Waldo * Added missing copyright headers Signed-off-by: Whit Waldo * Changed type name -> DaprLlmInput to DaprConversationInput Signed-off-by: Whit Waldo * Returning read only list Signed-off-by: Whit Waldo * Update to use IReadOnlyDictionary Signed-off-by: Whit Waldo * Added method to abstract class Signed-off-by: Whit Waldo * Striving for consistency in how properties are specified on the record Signed-off-by: Whit Waldo * Refactored enum extensions out to Dapr.Common since it will be used in AI project Signed-off-by: Whit Waldo * Added JSON converter for System.Text.Json to handle enum serialization based on the enum member attributes Signed-off-by: Whit Waldo * Added unit tests to prove out generic enum JSON converter using EnumMember attributes Signed-off-by: Whit Waldo * Added JSON converter to new enum for Dapr Conversation role Signed-off-by: Whit Waldo * Set up role to map to the string used in grpc call to sidecar Signed-off-by: Whit Waldo * No need for the JSON converter after all Signed-off-by: Whit Waldo * Added missing package version to fix build error Signed-off-by: Whit Waldo * Removed duplicate using statement breaking build Signed-off-by: Whit Waldo * Fixed missing [Fact] annotation Signed-off-by: Whit Waldo * Updated proto types to reflect type name changes in https://github.com/dapr/dapr/pull/8250 Signed-off-by: Whit Waldo * Added support for service lifetime Signed-off-by: Whit Waldo * Building out documentation for Dapr AI Signed-off-by: Whit Waldo * Simplified registration Signed-off-by: Whit Waldo * Tweaked package version Signed-off-by: Whit Waldo * Using IConfiguration to source DaprClient values if provided in service provider Signed-off-by: Whit Waldo * Removed Models.* directories, flattened into Conversation namespace Signed-off-by: Whit Waldo * Swapped out to use IReadOnlyDictionary Signed-off-by: Whit Waldo * Added suggested optimization Signed-off-by: Whit Waldo * Fixed bad using statement Signed-off-by: Whit Waldo * Updates to use uniform method for standing up new Dapr clients Signed-off-by: Whit Waldo * Removed duplicate project reference Signed-off-by: Whit Waldo * Fixed build error Signed-off-by: Whit Waldo * Fixing build errors Signed-off-by: Whit Waldo * Fixed bad references Signed-off-by: Whit Waldo * Fixed several build errors Signed-off-by: Whit Waldo * Fixing more build errors Signed-off-by: Whit Waldo * Updated to fix several build errors Signed-off-by: Whit Waldo * Fixed bad refernce Signed-off-by: Whit Waldo * Fixing more build errors Signed-off-by: Whit Waldo * Role is required when submitting conversation input Signed-off-by: Whit Waldo * Removed impossible path since the role cannot be nullable Signed-off-by: Whit Waldo * Removed impossible path from logic now that role cannot be null Signed-off-by: Whit Waldo --------- Signed-off-by: Whit Waldo Signed-off-by: jev --- Directory.Packages.props | 100 +- all.sln | 24 + daprdocs/content/en/dotnet-sdk-docs/_index.md | 7 + .../en/dotnet-sdk-docs/dotnet-ai/_index.md | 83 + .../dotnet-ai/dotnet-ai-usage.md | 7 + .../ConversationalAI/ConversationalAI.csproj | 13 + examples/AI/ConversationalAI/Program.cs | 23 + src/Dapr.AI/AssemblyInfo.cs | 4 + .../Conversation/ConversationOptions.cs | 40 + .../Conversation/DaprConversationClient.cs | 116 + .../DaprConversationClientBuilder.cs | 46 + .../Conversation/DaprConversationInput.cs | 22 + .../Conversation/DaprConversationResponse.cs | 21 + .../Conversation/DaprConversationResult.cs | 28 + .../Conversation/DaprConversationRole.cs | 42 + .../Extensions/DaprAiConversationBuilder.cs | 35 + .../DaprAiConversationBuilderExtensions.cs | 64 + .../Extensions/IDaprAiConversationBuilder.cs | 23 + src/Dapr.AI/Dapr.AI.csproj | 26 + src/Dapr.AI/DaprAIClient.cs | 34 + .../Extensions/IDaprAiServiceBuilder.cs | 27 + src/Dapr.Client/DaprClientGrpc.cs | 3709 +++++++++-------- src/Dapr.Client/Extensions/EnumExtensions.cs | 41 - src/Dapr.Common/AssemblyInfo.cs | 4 + src/Dapr.Common/DaprClientUtilities.cs | 65 + src/Dapr.Common/DaprGenericClientBuilder.cs | 51 +- src/Dapr.Common/Extensions/EnumExtensions.cs | 15 +- .../GenericEnumJsonConverter.cs | 70 + src/Dapr.Jobs/DaprJobsClientBuilder.cs | 16 +- src/Dapr.Jobs/DaprJobsGrpcClient.cs | 82 +- .../DaprJobsServiceCollectionExtensions.cs | 44 +- .../DaprPublishSubscribeClientBuilder.cs | 5 +- .../DaprPublishSubscribeGrpcClient.cs | 30 +- ...ishSubscribeServiceCollectionExtensions.cs | 6 +- .../Protos/dapr/proto/common/v1/common.proto | 2 +- .../dapr/proto/runtime/v1/appcallback.proto | 2 +- .../Protos/dapr/proto/runtime/v1/dapr.proto | 57 +- .../DaprConversationClientBuilderTest.cs | 33 + ...DaprAiConversationBuilderExtensionsTest.cs | 75 + test/Dapr.AI.Test/Dapr.AI.Test.csproj | 28 + .../Extensions/EnumExtensionTest.cs | 38 - .../Extensions/EnumExtensionsTest.cs | 6 +- .../GenericEnumJsonConverterTest.cs | 52 + ...aprJobsServiceCollectionExtensionsTests.cs | 61 +- 44 files changed, 3165 insertions(+), 2112 deletions(-) create mode 100644 daprdocs/content/en/dotnet-sdk-docs/dotnet-ai/_index.md create mode 100644 daprdocs/content/en/dotnet-sdk-docs/dotnet-ai/dotnet-ai-usage.md create mode 100644 examples/AI/ConversationalAI/ConversationalAI.csproj create mode 100644 examples/AI/ConversationalAI/Program.cs create mode 100644 src/Dapr.AI/AssemblyInfo.cs create mode 100644 src/Dapr.AI/Conversation/ConversationOptions.cs create mode 100644 src/Dapr.AI/Conversation/DaprConversationClient.cs create mode 100644 src/Dapr.AI/Conversation/DaprConversationClientBuilder.cs create mode 100644 src/Dapr.AI/Conversation/DaprConversationInput.cs create mode 100644 src/Dapr.AI/Conversation/DaprConversationResponse.cs create mode 100644 src/Dapr.AI/Conversation/DaprConversationResult.cs create mode 100644 src/Dapr.AI/Conversation/DaprConversationRole.cs create mode 100644 src/Dapr.AI/Conversation/Extensions/DaprAiConversationBuilder.cs create mode 100644 src/Dapr.AI/Conversation/Extensions/DaprAiConversationBuilderExtensions.cs create mode 100644 src/Dapr.AI/Conversation/Extensions/IDaprAiConversationBuilder.cs create mode 100644 src/Dapr.AI/Dapr.AI.csproj create mode 100644 src/Dapr.AI/DaprAIClient.cs create mode 100644 src/Dapr.AI/Extensions/IDaprAiServiceBuilder.cs delete mode 100644 src/Dapr.Client/Extensions/EnumExtensions.cs create mode 100644 src/Dapr.Common/DaprClientUtilities.cs create mode 100644 src/Dapr.Common/JsonConverters/GenericEnumJsonConverter.cs create mode 100644 test/Dapr.AI.Test/Conversation/DaprConversationClientBuilderTest.cs create mode 100644 test/Dapr.AI.Test/Conversation/Extensions/DaprAiConversationBuilderExtensionsTest.cs create mode 100644 test/Dapr.AI.Test/Dapr.AI.Test.csproj delete mode 100644 test/Dapr.Client.Test/Extensions/EnumExtensionTest.cs create mode 100644 test/Dapr.Common.Test/JsonConverters/GenericEnumJsonConverterTest.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index a98e9db58..efb48fcc4 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,52 +1,52 @@ - - true - true - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + true + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/all.sln b/all.sln index 1dab60475..9a163b1d9 100644 --- a/all.sln +++ b/all.sln @@ -119,6 +119,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Common", "src\Dapr.Com EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Common.Test", "test\Dapr.Common.Test\Dapr.Common.Test.csproj", "{CDB47863-BEBD-4841-A807-46D868962521}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.AI", "src\Dapr.AI\Dapr.AI.csproj", "{273F2527-1658-4CCF-8DC6-600E921188C5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.AI.Test", "test\Dapr.AI.Test\Dapr.AI.Test.csproj", "{2F3700EF-1CDA-4C15-AC88-360230000ECD}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AI", "AI", "{3046DBF4-C2FF-4F3A-9176-E1C01E0A90E5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConversationalAI", "examples\AI\ConversationalAI\ConversationalAI.csproj", "{11011FF8-77EA-4B25-96C0-29D4D486EF1C}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkflowExternalInteraction", "examples\Workflow\WorkflowExternalInteraction\WorkflowExternalInteraction.csproj", "{43CB06A9-7E88-4C5F-BFB8-947E072CBC9F}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkflowMonitor", "examples\Workflow\WorkflowMonitor\WorkflowMonitor.csproj", "{7F73A3D8-FFC2-4E31-AA3D-A4840316A8C6}" @@ -331,6 +339,18 @@ Global {CDB47863-BEBD-4841-A807-46D868962521}.Debug|Any CPU.Build.0 = Debug|Any CPU {CDB47863-BEBD-4841-A807-46D868962521}.Release|Any CPU.ActiveCfg = Release|Any CPU {CDB47863-BEBD-4841-A807-46D868962521}.Release|Any CPU.Build.0 = Release|Any CPU + {273F2527-1658-4CCF-8DC6-600E921188C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {273F2527-1658-4CCF-8DC6-600E921188C5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {273F2527-1658-4CCF-8DC6-600E921188C5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {273F2527-1658-4CCF-8DC6-600E921188C5}.Release|Any CPU.Build.0 = Release|Any CPU + {2F3700EF-1CDA-4C15-AC88-360230000ECD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2F3700EF-1CDA-4C15-AC88-360230000ECD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2F3700EF-1CDA-4C15-AC88-360230000ECD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2F3700EF-1CDA-4C15-AC88-360230000ECD}.Release|Any CPU.Build.0 = Release|Any CPU + {11011FF8-77EA-4B25-96C0-29D4D486EF1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {11011FF8-77EA-4B25-96C0-29D4D486EF1C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {11011FF8-77EA-4B25-96C0-29D4D486EF1C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {11011FF8-77EA-4B25-96C0-29D4D486EF1C}.Release|Any CPU.Build.0 = Release|Any CPU {43CB06A9-7E88-4C5F-BFB8-947E072CBC9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {43CB06A9-7E88-4C5F-BFB8-947E072CBC9F}.Debug|Any CPU.Build.0 = Debug|Any CPU {43CB06A9-7E88-4C5F-BFB8-947E072CBC9F}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -439,6 +459,10 @@ Global {DFBABB04-50E9-42F6-B470-310E1B545638} = {27C5D71D-0721-4221-9286-B94AB07B58CF} {B445B19C-A925-4873-8CB7-8317898B6970} = {27C5D71D-0721-4221-9286-B94AB07B58CF} {CDB47863-BEBD-4841-A807-46D868962521} = {DD020B34-460F-455F-8D17-CF4A949F100B} + {273F2527-1658-4CCF-8DC6-600E921188C5} = {27C5D71D-0721-4221-9286-B94AB07B58CF} + {2F3700EF-1CDA-4C15-AC88-360230000ECD} = {DD020B34-460F-455F-8D17-CF4A949F100B} + {3046DBF4-C2FF-4F3A-9176-E1C01E0A90E5} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78} + {11011FF8-77EA-4B25-96C0-29D4D486EF1C} = {3046DBF4-C2FF-4F3A-9176-E1C01E0A90E5} {43CB06A9-7E88-4C5F-BFB8-947E072CBC9F} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9} {7F73A3D8-FFC2-4E31-AA3D-A4840316A8C6} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9} {945DD3B7-94E5-435E-B3CB-796C20A652C7} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9} diff --git a/daprdocs/content/en/dotnet-sdk-docs/_index.md b/daprdocs/content/en/dotnet-sdk-docs/_index.md index 82d16016d..ce80b3ea9 100644 --- a/daprdocs/content/en/dotnet-sdk-docs/_index.md +++ b/daprdocs/content/en/dotnet-sdk-docs/_index.md @@ -84,6 +84,13 @@ Put the Dapr .NET SDK to the test. Walk through the .NET quickstarts and tutoria +
+
+
AI
+

Create and manage AI operations in .NET

+ +
+
## More information diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-ai/_index.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-ai/_index.md new file mode 100644 index 000000000..4374a8598 --- /dev/null +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-ai/_index.md @@ -0,0 +1,83 @@ +_index.md + +--- +type: docs +title: "Getting started with the Dapr AI .NET SDK client" +linkTitle: "AI" +weight: 10000 +description: How to get up and running with the Dapr AI .NET SDK +no_list: true +--- + +The Dapr AI client package allows you to interact with the AI capabilities provided by the Dapr sidecar. + +## Installation + +To get started with the Dapr AI .NET SDK client, install the following package from NuGet: +```sh +dotnet add package Dapr.AI +``` + +A `DaprConversationClient` holes access to networking resources in the form of TCP sockets used to communicate with the Dapr sidecar. + +### Dependency Injection + +The `AddDaprAiConversation()` method will register the Dapr client ASP.NET Core dependency injection and is the recommended approach +for using this package. This method accepts an optional options delegate for configuring the `DaprConversationClient` and a +`ServiceLifetime` argument, allowing you to specify a different lifetime for the registered services instead of the default `Singleton` +value. + +The following example assumes all default values are acceptable and is sufficient to register the `DaprConversationClient`: + +```csharp +services.AddDaprAiConversation(); +``` + +The optional configuration delegate is used to configure the `DaprConversationClient` by specifying options on the +`DaprConversationClientBuilder` as in the following example: +```csharp +services.AddSingleton(); +services.AddDaprAiConversation((serviceProvider, clientBuilder) => { + //Inject a service to source a value from + var optionsProvider = serviceProvider.GetRequiredService(); + var standardTimeout = optionsProvider.GetStandardTimeout(); + + //Configure the value on the client builder + clientBuilder.UseTimeout(standardTimeout); +}); +``` + +### Manual Instantiation +Rather than using dependency injection, a `DaprConversationClient` can also be built using the static client builder. + +For best performance, create a single long-lived instance of `DaprConversationClient` and provide access to that shared instance throughout +your application. `DaprConversationClient` instances are thread-safe and intended to be shared. + +Avoid creating a `DaprConversationClient` per-operation. + +A `DaprConversationClient` can be configured by invoking methods on the `DaprConversationClientBuilder` class before calling `.Build()` +to create the client. The settings for each `DaprConversationClient` are separate and cannot be changed after calling `.Build()`. + +```csharp +var daprConversationClient = new DaprConversationClientBuilder() + .UseJsonSerializerSettings( ... ) //Configure JSON serializer + .Build(); +``` + +See the .NET [documentation here]({{< ref dotnet-client >}}) for more information about the options available when configuring the Dapr client via the builder. + +## Try it out +Put the Dapr AI .NET SDK to the test. Walk through the samples to see Dapr in action: + +| SDK Samples | Description | +| ----------- | ----------- | +| [SDK samples](https://github.com/dapr/dotnet-sdk/tree/master/examples) | Clone the SDK repo to try out some examples and get started. | + +## Building Blocks + +This part of the .NET SDK allows you to interface with the Conversations API to send and receive messages from +large language models. + +### Send messages + + diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-ai/dotnet-ai-usage.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-ai/dotnet-ai-usage.md new file mode 100644 index 000000000..93700c383 --- /dev/null +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-ai/dotnet-ai-usage.md @@ -0,0 +1,7 @@ +--- +type: docs +title: "Best practices with the Dapr AI .NET SDK client" +linkTitle: "Best Practices" +weight: 100000 +description: How to get up and running with the Dapr .NET SDK +--- \ No newline at end of file diff --git a/examples/AI/ConversationalAI/ConversationalAI.csproj b/examples/AI/ConversationalAI/ConversationalAI.csproj new file mode 100644 index 000000000..976265a5c --- /dev/null +++ b/examples/AI/ConversationalAI/ConversationalAI.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/examples/AI/ConversationalAI/Program.cs b/examples/AI/ConversationalAI/Program.cs new file mode 100644 index 000000000..bd3dc906a --- /dev/null +++ b/examples/AI/ConversationalAI/Program.cs @@ -0,0 +1,23 @@ +using Dapr.AI.Conversation; +using Dapr.AI.Conversation.Extensions; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddDaprAiConversation(); + +var app = builder.Build(); + +var conversationClient = app.Services.GetRequiredService(); +var response = await conversationClient.ConverseAsync("conversation", + new List + { + new DaprConversationInput( + "Please write a witty haiku about the Dapr distributed programming framework at dapr.io", + DaprConversationRole.Generic) + }); + +Console.WriteLine("Received the following from the LLM:"); +foreach (var resp in response.Outputs) +{ + Console.WriteLine($"\t{resp.Result}"); +} diff --git a/src/Dapr.AI/AssemblyInfo.cs b/src/Dapr.AI/AssemblyInfo.cs new file mode 100644 index 000000000..8d96dcf56 --- /dev/null +++ b/src/Dapr.AI/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Dapr.AI.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] + diff --git a/src/Dapr.AI/Conversation/ConversationOptions.cs b/src/Dapr.AI/Conversation/ConversationOptions.cs new file mode 100644 index 000000000..87a49117a --- /dev/null +++ b/src/Dapr.AI/Conversation/ConversationOptions.cs @@ -0,0 +1,40 @@ +// ------------------------------------------------------------------------ +// 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 Google.Protobuf.WellKnownTypes; + +namespace Dapr.AI.Conversation; + +/// +/// Options used to configure the conversation operation. +/// +/// The identifier of the conversation this is a continuation of. +public sealed record ConversationOptions(string? ConversationId = null) +{ + /// + /// Temperature for the LLM to optimize for creativity or predictability. + /// + public double Temperature { get; init; } = default; + /// + /// Flag that indicates whether data that comes back from the LLM should be scrubbed of PII data. + /// + public bool ScrubPII { get; init; } = default; + /// + /// The metadata passing to the conversation components. + /// + public IReadOnlyDictionary Metadata { get; init; } = new Dictionary(); + /// + /// Parameters for all custom fields. + /// + public IReadOnlyDictionary Parameters { get; init; } = new Dictionary(); +} diff --git a/src/Dapr.AI/Conversation/DaprConversationClient.cs b/src/Dapr.AI/Conversation/DaprConversationClient.cs new file mode 100644 index 000000000..2335197bc --- /dev/null +++ b/src/Dapr.AI/Conversation/DaprConversationClient.cs @@ -0,0 +1,116 @@ +// ------------------------------------------------------------------------ +// 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.Common; +using Dapr.Common.Extensions; +using P = Dapr.Client.Autogen.Grpc.v1; + +namespace Dapr.AI.Conversation; + +/// +/// Used to interact with the Dapr conversation building block. +/// +public sealed class DaprConversationClient : DaprAIClient +{ + /// + /// 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.Dapr.DaprClient Client { get; } + + /// + /// Used to initialize a new instance of a . + /// + /// The Dapr client. + /// The HTTP client used by the client for calling the Dapr runtime. + /// An optional token required to send requests to the Dapr sidecar. + public DaprConversationClient(P.Dapr.DaprClient client, + HttpClient httpClient, + string? daprApiToken = null) + { + this.Client = client; + this.HttpClient = httpClient; + this.DaprApiToken = daprApiToken; + } + + /// + /// Sends various inputs to the large language model via the Conversational building block on the Dapr sidecar. + /// + /// The name of the Dapr conversation component. + /// The input values to send. + /// Optional options used to configure the conversation. + /// Cancellation token. + /// The response(s) provided by the LLM provider. + public override async Task ConverseAsync(string daprConversationComponentName, IReadOnlyList inputs, ConversationOptions? options = null, + CancellationToken cancellationToken = default) + { + var request = new P.ConversationRequest + { + Name = daprConversationComponentName + }; + + if (options is not null) + { + request.ContextID = options.ConversationId; + request.ScrubPII = options.ScrubPII; + + foreach (var (key, value) in options.Metadata) + { + request.Metadata.Add(key, value); + } + + foreach (var (key, value) in options.Parameters) + { + request.Parameters.Add(key, value); + } + } + + foreach (var input in inputs) + { + request.Inputs.Add(new P.ConversationInput + { + ScrubPII = input.ScrubPII, + Message = input.Message, + Role = input.Role.GetValueFromEnumMember() + }); + } + + var grpCCallOptions = + DaprClientUtilities.ConfigureGrpcCallOptions(typeof(DaprConversationClient).Assembly, this.DaprApiToken, + cancellationToken); + + var result = await Client.ConverseAlpha1Async(request, grpCCallOptions).ConfigureAwait(false); + var outputs = result.Outputs.Select(output => new DaprConversationResult(output.Result) + { + Parameters = output.Parameters.ToDictionary(kvp => kvp.Key, parameter => parameter.Value) + }).ToList(); + + return new DaprConversationResponse(outputs); + } +} diff --git a/src/Dapr.AI/Conversation/DaprConversationClientBuilder.cs b/src/Dapr.AI/Conversation/DaprConversationClientBuilder.cs new file mode 100644 index 000000000..5e0a0825d --- /dev/null +++ b/src/Dapr.AI/Conversation/DaprConversationClientBuilder.cs @@ -0,0 +1,46 @@ +// ------------------------------------------------------------------------ +// 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.Common; +using Microsoft.Extensions.Configuration; +using Autogenerated = Dapr.Client.Autogen.Grpc.v1.Dapr; + +namespace Dapr.AI.Conversation; + +/// +/// Used to create a new instance of a . +/// +public sealed class DaprConversationClientBuilder : DaprGenericClientBuilder +{ + /// + /// Used to initialize a new instance of the . + /// + /// + public DaprConversationClientBuilder(IConfiguration? configuration = null) : base(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 DaprConversationClient Build() + { + var daprClientDependencies = BuildDaprClientDependencies(typeof(DaprConversationClient).Assembly); + var client = new Autogenerated.DaprClient(daprClientDependencies.channel); + return new DaprConversationClient(client, daprClientDependencies.httpClient, daprClientDependencies.daprApiToken); + } +} diff --git a/src/Dapr.AI/Conversation/DaprConversationInput.cs b/src/Dapr.AI/Conversation/DaprConversationInput.cs new file mode 100644 index 000000000..3485849c8 --- /dev/null +++ b/src/Dapr.AI/Conversation/DaprConversationInput.cs @@ -0,0 +1,22 @@ +// ------------------------------------------------------------------------ +// 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. +// ------------------------------------------------------------------------ + +namespace Dapr.AI.Conversation; + +/// +/// Represents an input for the Dapr Conversational API. +/// +/// The message to send to the LLM. +/// The role indicating the entity providing the message. +/// If true, scrubs the data that goes into the LLM. +public sealed record DaprConversationInput(string Message, DaprConversationRole Role, bool ScrubPII = false); diff --git a/src/Dapr.AI/Conversation/DaprConversationResponse.cs b/src/Dapr.AI/Conversation/DaprConversationResponse.cs new file mode 100644 index 000000000..36de7fd6e --- /dev/null +++ b/src/Dapr.AI/Conversation/DaprConversationResponse.cs @@ -0,0 +1,21 @@ +// ------------------------------------------------------------------------ +// 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. +// ------------------------------------------------------------------------ + +namespace Dapr.AI.Conversation; + +/// +/// The response for a conversation. +/// +/// The collection of conversation results. +/// The identifier of an existing or newly created conversation. +public record DaprConversationResponse(IReadOnlyList Outputs, string? ConversationId = null); diff --git a/src/Dapr.AI/Conversation/DaprConversationResult.cs b/src/Dapr.AI/Conversation/DaprConversationResult.cs new file mode 100644 index 000000000..700cc8730 --- /dev/null +++ b/src/Dapr.AI/Conversation/DaprConversationResult.cs @@ -0,0 +1,28 @@ +// ------------------------------------------------------------------------ +// 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 Google.Protobuf.WellKnownTypes; + +namespace Dapr.AI.Conversation; + +/// +/// The result for a single conversational input. +/// +/// The result for one conversation input. +public record DaprConversationResult(string Result) +{ + /// + /// Parameters for all custom fields. + /// + public IReadOnlyDictionary Parameters { get; init; } = new Dictionary(); +} diff --git a/src/Dapr.AI/Conversation/DaprConversationRole.cs b/src/Dapr.AI/Conversation/DaprConversationRole.cs new file mode 100644 index 000000000..3e48a41c1 --- /dev/null +++ b/src/Dapr.AI/Conversation/DaprConversationRole.cs @@ -0,0 +1,42 @@ +using System.Runtime.Serialization; +using System.Text.Json.Serialization; +using Dapr.Common.JsonConverters; + +namespace Dapr.AI.Conversation; + +/// +/// Represents who +/// +public enum DaprConversationRole +{ + /// + /// Represents a message sent by an AI. + /// + [EnumMember(Value="ai")] + AI, + /// + /// Represents a message sent by a human. + /// + [EnumMember(Value="human")] + Human, + /// + /// Represents a message sent by the system. + /// + [EnumMember(Value="system")] + System, + /// + /// Represents a message sent by a generic user. + /// + [EnumMember(Value="generic")] + Generic, + /// + /// Represents a message sent by a function. + /// + [EnumMember(Value="function")] + Function, + /// + /// Represents a message sent by a tool. + /// + [EnumMember(Value="tool")] + Tool +} diff --git a/src/Dapr.AI/Conversation/Extensions/DaprAiConversationBuilder.cs b/src/Dapr.AI/Conversation/Extensions/DaprAiConversationBuilder.cs new file mode 100644 index 000000000..876d223b1 --- /dev/null +++ b/src/Dapr.AI/Conversation/Extensions/DaprAiConversationBuilder.cs @@ -0,0 +1,35 @@ +// ------------------------------------------------------------------------ +// 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.DependencyInjection; + +namespace Dapr.AI.Conversation.Extensions; + +/// +/// Used by the fluent registration builder to configure a Dapr AI conversational manager. +/// +public sealed class DaprAiConversationBuilder : IDaprAiConversationBuilder +{ + /// + /// The registered services on the builder. + /// + public IServiceCollection Services { get; } + + /// + /// Used to initialize a new . + /// + public DaprAiConversationBuilder(IServiceCollection services) + { + Services = services; + } +} diff --git a/src/Dapr.AI/Conversation/Extensions/DaprAiConversationBuilderExtensions.cs b/src/Dapr.AI/Conversation/Extensions/DaprAiConversationBuilderExtensions.cs new file mode 100644 index 000000000..902fd82a3 --- /dev/null +++ b/src/Dapr.AI/Conversation/Extensions/DaprAiConversationBuilderExtensions.cs @@ -0,0 +1,64 @@ +// ------------------------------------------------------------------------ +// 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.AI.Conversation.Extensions; + +/// +/// Contains the dependency injection registration extensions for the Dapr AI Conversation operations. +/// +public static class DaprAiConversationBuilderExtensions +{ + /// + /// Registers the necessary functionality for the Dapr AI conversation functionality. + /// + /// + public static IDaprAiConversationBuilder AddDaprAiConversation(this IServiceCollection services, Action? configure = null, ServiceLifetime lifetime = ServiceLifetime.Singleton) + { + ArgumentNullException.ThrowIfNull(services, nameof(services)); + + services.AddHttpClient(); + + var registration = new Func(provider => + { + var configuration = provider.GetService(); + var builder = new DaprConversationClientBuilder(configuration); + + var httpClientFactory = provider.GetRequiredService(); + builder.UseHttpClientFactory(httpClientFactory); + + configure?.Invoke(provider, builder); + + return builder.Build(); + }); + + switch (lifetime) + { + case ServiceLifetime.Scoped: + services.TryAddScoped(registration); + break; + case ServiceLifetime.Transient: + services.TryAddTransient(registration); + break; + case ServiceLifetime.Singleton: + default: + services.TryAddSingleton(registration); + break; + } + + return new DaprAiConversationBuilder(services); + } +} diff --git a/src/Dapr.AI/Conversation/Extensions/IDaprAiConversationBuilder.cs b/src/Dapr.AI/Conversation/Extensions/IDaprAiConversationBuilder.cs new file mode 100644 index 000000000..30d3822d4 --- /dev/null +++ b/src/Dapr.AI/Conversation/Extensions/IDaprAiConversationBuilder.cs @@ -0,0 +1,23 @@ +// ------------------------------------------------------------------------ +// 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.AI.Extensions; + +namespace Dapr.AI.Conversation.Extensions; + +/// +/// Provides a root builder for the Dapr AI conversational functionality facilitating a more fluent-style registration. +/// +public interface IDaprAiConversationBuilder : IDaprAiServiceBuilder +{ +} diff --git a/src/Dapr.AI/Dapr.AI.csproj b/src/Dapr.AI/Dapr.AI.csproj new file mode 100644 index 000000000..8220c5c4d --- /dev/null +++ b/src/Dapr.AI/Dapr.AI.csproj @@ -0,0 +1,26 @@ + + + + net6;net8 + enable + enable + Dapr.AI + Dapr AI SDK + Dapr AI SDK for performing operations associated with artificial intelligence. + alpha + + + + + + + + + + + + + + + + diff --git a/src/Dapr.AI/DaprAIClient.cs b/src/Dapr.AI/DaprAIClient.cs new file mode 100644 index 000000000..a2fd2255f --- /dev/null +++ b/src/Dapr.AI/DaprAIClient.cs @@ -0,0 +1,34 @@ +// ------------------------------------------------------------------------ +// 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.AI.Conversation; + +namespace Dapr.AI; + +/// +/// The base implementation of a Dapr AI client. +/// +public abstract class DaprAIClient +{ + /// + /// Sends various inputs to the large language model via the Conversational building block on the Dapr sidecar. + /// + /// The name of the Dapr conversation component. + /// The input values to send. + /// Optional options used to configure the conversation. + /// Cancellation token. + /// The response(s) provided by the LLM provider. + public abstract Task ConverseAsync(string daprConversationComponentName, + IReadOnlyList inputs, ConversationOptions? options = null, + CancellationToken cancellationToken = default); +} diff --git a/src/Dapr.AI/Extensions/IDaprAiServiceBuilder.cs b/src/Dapr.AI/Extensions/IDaprAiServiceBuilder.cs new file mode 100644 index 000000000..8a0a80c2c --- /dev/null +++ b/src/Dapr.AI/Extensions/IDaprAiServiceBuilder.cs @@ -0,0 +1,27 @@ +// ------------------------------------------------------------------------ +// 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.DependencyInjection; + +namespace Dapr.AI.Extensions; + +/// +/// Responsible for registering Dapr AI service functionality. +/// +public interface IDaprAiServiceBuilder +{ + /// + /// The registered services on the builder. + /// + public IServiceCollection Services { get; } +} diff --git a/src/Dapr.Client/DaprClientGrpc.cs b/src/Dapr.Client/DaprClientGrpc.cs index c70aef77b..bd0bd1d01 100644 --- a/src/Dapr.Client/DaprClientGrpc.cs +++ b/src/Dapr.Client/DaprClientGrpc.cs @@ -11,2257 +11,2258 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Client +using Dapr.Common.Extensions; + +namespace Dapr.Client; + +using System; +using System.Buffers; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Json; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Google.Protobuf; +using Google.Protobuf.WellKnownTypes; +using Grpc.Core; +using Grpc.Net.Client; +using Autogenerated = Dapr.Client.Autogen.Grpc.v1; + +/// +/// A client for interacting with the Dapr endpoints. +/// +internal class DaprClientGrpc : DaprClient { - using System; - using System.Buffers; - using System.Collections.Generic; - using System.IO; - using System.Linq; - using System.Net.Http; - using System.Net.Http.Json; - using System.Runtime.CompilerServices; - using System.Runtime.InteropServices; - using System.Text.Json; - using System.Threading; - using System.Threading.Tasks; - using Google.Protobuf; - using Google.Protobuf.WellKnownTypes; - using Grpc.Core; - using Grpc.Net.Client; - using Autogenerated = Dapr.Client.Autogen.Grpc.v1; + private const string AppIdKey = "appId"; + private const string MethodNameKey = "methodName"; - /// - /// A client for interacting with the Dapr endpoints. - /// - internal class DaprClientGrpc : DaprClient - { - private const string AppIdKey = "appId"; - private const string MethodNameKey = "methodName"; + private readonly Uri httpEndpoint; + private readonly HttpClient httpClient; - private readonly Uri httpEndpoint; - private readonly HttpClient httpClient; + private readonly JsonSerializerOptions jsonSerializerOptions; - private readonly JsonSerializerOptions jsonSerializerOptions; + private readonly GrpcChannel channel; + private readonly Autogenerated.Dapr.DaprClient client; + private readonly KeyValuePair? apiTokenHeader; - private readonly GrpcChannel channel; - private readonly Autogenerated.Dapr.DaprClient client; - private readonly KeyValuePair? apiTokenHeader; + // property exposed for testing purposes + internal Autogenerated.Dapr.DaprClient Client => client; - // property exposed for testing purposes - internal Autogenerated.Dapr.DaprClient Client => client; + public override JsonSerializerOptions JsonSerializerOptions => jsonSerializerOptions; - public override JsonSerializerOptions JsonSerializerOptions => jsonSerializerOptions; + internal DaprClientGrpc( + GrpcChannel channel, + Autogenerated.Dapr.DaprClient inner, + HttpClient httpClient, + Uri httpEndpoint, + JsonSerializerOptions jsonSerializerOptions, + KeyValuePair? apiTokenHeader) + { + this.channel = channel; + this.client = inner; + this.httpClient = httpClient; + this.httpEndpoint = httpEndpoint; + this.jsonSerializerOptions = jsonSerializerOptions; + this.apiTokenHeader = apiTokenHeader; + + this.httpClient.DefaultRequestHeaders.UserAgent.Add(UserAgent()); + } - internal DaprClientGrpc( - GrpcChannel channel, - Autogenerated.Dapr.DaprClient inner, - HttpClient httpClient, - Uri httpEndpoint, - JsonSerializerOptions jsonSerializerOptions, - KeyValuePair? apiTokenHeader) - { - this.channel = channel; - this.client = inner; - this.httpClient = httpClient; - this.httpEndpoint = httpEndpoint; - this.jsonSerializerOptions = jsonSerializerOptions; - this.apiTokenHeader = apiTokenHeader; + #region Publish Apis + /// + public override Task PublishEventAsync( + string pubsubName, + string topicName, + TData data, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(pubsubName, nameof(pubsubName)); + ArgumentVerifier.ThrowIfNullOrEmpty(topicName, nameof(topicName)); + ArgumentVerifier.ThrowIfNull(data, nameof(data)); - this.httpClient.DefaultRequestHeaders.UserAgent.Add(UserAgent()); - } + var content = TypeConverters.ToJsonByteString(data, this.JsonSerializerOptions); + return MakePublishRequest(pubsubName, topicName, content, null, data is CloudEvent ? Constants.ContentTypeCloudEvent : null, cancellationToken); + } - #region Publish Apis - /// - public override Task PublishEventAsync( - string pubsubName, - string topicName, - TData data, - CancellationToken cancellationToken = default) - { - ArgumentVerifier.ThrowIfNullOrEmpty(pubsubName, nameof(pubsubName)); - ArgumentVerifier.ThrowIfNullOrEmpty(topicName, nameof(topicName)); - ArgumentVerifier.ThrowIfNull(data, nameof(data)); + public override Task PublishEventAsync( + string pubsubName, + string topicName, + TData data, + Dictionary metadata, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(pubsubName, nameof(pubsubName)); + ArgumentVerifier.ThrowIfNullOrEmpty(topicName, nameof(topicName)); + ArgumentVerifier.ThrowIfNull(data, nameof(data)); + ArgumentVerifier.ThrowIfNull(metadata, nameof(metadata)); - var content = TypeConverters.ToJsonByteString(data, this.JsonSerializerOptions); - return MakePublishRequest(pubsubName, topicName, content, null, data is CloudEvent ? Constants.ContentTypeCloudEvent : null, cancellationToken); - } + var content = TypeConverters.ToJsonByteString(data, this.JsonSerializerOptions); + return MakePublishRequest(pubsubName, topicName, content, metadata, data is CloudEvent ? Constants.ContentTypeCloudEvent : null, cancellationToken); + } - public override Task PublishEventAsync( - string pubsubName, - string topicName, - TData data, - Dictionary metadata, - CancellationToken cancellationToken = default) - { - ArgumentVerifier.ThrowIfNullOrEmpty(pubsubName, nameof(pubsubName)); - ArgumentVerifier.ThrowIfNullOrEmpty(topicName, nameof(topicName)); - ArgumentVerifier.ThrowIfNull(data, nameof(data)); - ArgumentVerifier.ThrowIfNull(metadata, nameof(metadata)); + /// + public override Task PublishEventAsync( + string pubsubName, + string topicName, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(pubsubName, nameof(pubsubName)); + ArgumentVerifier.ThrowIfNullOrEmpty(topicName, nameof(topicName)); + return MakePublishRequest(pubsubName, topicName, null, null, null, cancellationToken); + } - var content = TypeConverters.ToJsonByteString(data, this.JsonSerializerOptions); - return MakePublishRequest(pubsubName, topicName, content, metadata, data is CloudEvent ? Constants.ContentTypeCloudEvent : null, cancellationToken); - } + public override Task PublishEventAsync( + string pubsubName, + string topicName, + Dictionary metadata, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(pubsubName, nameof(pubsubName)); + ArgumentVerifier.ThrowIfNullOrEmpty(topicName, nameof(topicName)); + ArgumentVerifier.ThrowIfNull(metadata, nameof(metadata)); + return MakePublishRequest(pubsubName, topicName, null, metadata, null, cancellationToken); + } - /// - public override Task PublishEventAsync( - string pubsubName, - string topicName, - CancellationToken cancellationToken = default) - { - ArgumentVerifier.ThrowIfNullOrEmpty(pubsubName, nameof(pubsubName)); - ArgumentVerifier.ThrowIfNullOrEmpty(topicName, nameof(topicName)); - return MakePublishRequest(pubsubName, topicName, null, null, null, cancellationToken); - } + public override Task PublishByteEventAsync( + string pubsubName, + string topicName, + ReadOnlyMemory data, + string dataContentType = Constants.ContentTypeApplicationJson, + Dictionary metadata = default, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(pubsubName, nameof(pubsubName)); + ArgumentVerifier.ThrowIfNullOrEmpty(topicName, nameof(topicName)); + return MakePublishRequest(pubsubName, topicName, ByteString.CopyFrom(data.Span), metadata, dataContentType, cancellationToken); + } - public override Task PublishEventAsync( - string pubsubName, - string topicName, - Dictionary metadata, - CancellationToken cancellationToken = default) + private async Task MakePublishRequest( + string pubsubName, + string topicName, + ByteString content, + Dictionary metadata, + string dataContentType, + CancellationToken cancellationToken) + { + var envelope = new Autogenerated.PublishEventRequest() { - ArgumentVerifier.ThrowIfNullOrEmpty(pubsubName, nameof(pubsubName)); - ArgumentVerifier.ThrowIfNullOrEmpty(topicName, nameof(topicName)); - ArgumentVerifier.ThrowIfNull(metadata, nameof(metadata)); - return MakePublishRequest(pubsubName, topicName, null, metadata, null, cancellationToken); - } + PubsubName = pubsubName, + Topic = topicName, + }; - public override Task PublishByteEventAsync( - string pubsubName, - string topicName, - ReadOnlyMemory data, - string dataContentType = Constants.ContentTypeApplicationJson, - Dictionary metadata = default, - CancellationToken cancellationToken = default) + if (content != null) { - ArgumentVerifier.ThrowIfNullOrEmpty(pubsubName, nameof(pubsubName)); - ArgumentVerifier.ThrowIfNullOrEmpty(topicName, nameof(topicName)); - return MakePublishRequest(pubsubName, topicName, ByteString.CopyFrom(data.Span), metadata, dataContentType, cancellationToken); + envelope.Data = content; + envelope.DataContentType = dataContentType ?? Constants.ContentTypeApplicationJson; } - private async Task MakePublishRequest( - string pubsubName, - string topicName, - ByteString content, - Dictionary metadata, - string dataContentType, - CancellationToken cancellationToken) + if (metadata != null) { - var envelope = new Autogenerated.PublishEventRequest() - { - PubsubName = pubsubName, - Topic = topicName, - }; - - if (content != null) - { - envelope.Data = content; - envelope.DataContentType = dataContentType ?? Constants.ContentTypeApplicationJson; - } - - if (metadata != null) + foreach (var kvp in metadata) { - foreach (var kvp in metadata) - { - envelope.Metadata.Add(kvp.Key, kvp.Value); - } + envelope.Metadata.Add(kvp.Key, kvp.Value); } + } - var options = CreateCallOptions(headers: null, cancellationToken); + var options = CreateCallOptions(headers: null, cancellationToken); - try - { - await client.PublishEventAsync(envelope, options); - } - catch (RpcException ex) - { - throw new DaprException("Publish operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); - } + try + { + await client.PublishEventAsync(envelope, options); } - - /// - public override Task> BulkPublishEventAsync( - string pubsubName, - string topicName, - IReadOnlyList events, - Dictionary metadata = default, - CancellationToken cancellationToken = default) + catch (RpcException ex) { - ArgumentVerifier.ThrowIfNullOrEmpty(pubsubName, nameof(pubsubName)); - ArgumentVerifier.ThrowIfNullOrEmpty(topicName, nameof(topicName)); - ArgumentVerifier.ThrowIfNull(events, nameof(events)); - return MakeBulkPublishRequest(pubsubName, topicName, events, metadata, cancellationToken); + throw new DaprException("Publish operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); } + } + + /// + public override Task> BulkPublishEventAsync( + string pubsubName, + string topicName, + IReadOnlyList events, + Dictionary metadata = default, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(pubsubName, nameof(pubsubName)); + ArgumentVerifier.ThrowIfNullOrEmpty(topicName, nameof(topicName)); + ArgumentVerifier.ThrowIfNull(events, nameof(events)); + return MakeBulkPublishRequest(pubsubName, topicName, events, metadata, cancellationToken); + } - private async Task> MakeBulkPublishRequest( - string pubsubName, - string topicName, - IReadOnlyList events, - Dictionary metadata, - CancellationToken cancellationToken) - { - var envelope = new Autogenerated.BulkPublishRequest() - { - PubsubName = pubsubName, - Topic = topicName, - }; + private async Task> MakeBulkPublishRequest( + string pubsubName, + string topicName, + IReadOnlyList events, + Dictionary metadata, + CancellationToken cancellationToken) + { + var envelope = new Autogenerated.BulkPublishRequest() + { + PubsubName = pubsubName, + Topic = topicName, + }; - Dictionary> entryMap = new Dictionary>(); + Dictionary> entryMap = new Dictionary>(); - for (int counter = 0; counter < events.Count; counter++) + for (int counter = 0; counter < events.Count; counter++) + { + var entry = new Autogenerated.BulkPublishRequestEntry() { - var entry = new Autogenerated.BulkPublishRequestEntry() - { - EntryId = counter.ToString(), - Event = TypeConverters.ToJsonByteString(events[counter], this.jsonSerializerOptions), - ContentType = events[counter] is CloudEvent ? Constants.ContentTypeCloudEvent : Constants.ContentTypeApplicationJson, - Metadata = {}, - }; - envelope.Entries.Add(entry); - entryMap.Add(counter.ToString(), new BulkPublishEntry( - entry.EntryId, events[counter], entry.ContentType, entry.Metadata)); - } + EntryId = counter.ToString(), + Event = TypeConverters.ToJsonByteString(events[counter], this.jsonSerializerOptions), + ContentType = events[counter] is CloudEvent ? Constants.ContentTypeCloudEvent : Constants.ContentTypeApplicationJson, + Metadata = {}, + }; + envelope.Entries.Add(entry); + entryMap.Add(counter.ToString(), new BulkPublishEntry( + entry.EntryId, events[counter], entry.ContentType, entry.Metadata)); + } - if (metadata != null) + if (metadata != null) + { + foreach (var kvp in metadata) { - foreach (var kvp in metadata) - { - envelope.Metadata.Add(kvp.Key, kvp.Value); - } + envelope.Metadata.Add(kvp.Key, kvp.Value); } + } - var options = CreateCallOptions(headers: null, cancellationToken); + var options = CreateCallOptions(headers: null, cancellationToken); - try - { - var response = await client.BulkPublishEventAlpha1Async(envelope, options); + try + { + var response = await client.BulkPublishEventAlpha1Async(envelope, options); - List> failedEntries = new List>(); + List> failedEntries = new List>(); - foreach (var entry in response.FailedEntries) - { - BulkPublishResponseFailedEntry domainEntry = new BulkPublishResponseFailedEntry( - entryMap[entry.EntryId], entry.Error); - failedEntries.Add(domainEntry); - } - - var bulkPublishResponse = new BulkPublishResponse(failedEntries); - - return bulkPublishResponse; - } - catch (RpcException ex) + foreach (var entry in response.FailedEntries) { - throw new DaprException("Bulk Publish operation failed: the Dapr endpoint indicated a " + - "failure. See InnerException for details.", ex); + BulkPublishResponseFailedEntry domainEntry = new BulkPublishResponseFailedEntry( + entryMap[entry.EntryId], entry.Error); + failedEntries.Add(domainEntry); } + + var bulkPublishResponse = new BulkPublishResponse(failedEntries); + + return bulkPublishResponse; + } + catch (RpcException ex) + { + throw new DaprException("Bulk Publish operation failed: the Dapr endpoint indicated a " + + "failure. See InnerException for details.", ex); } - #endregion + } + #endregion - #region InvokeBinding Apis + #region InvokeBinding Apis - /// - public override async Task InvokeBindingAsync( - string bindingName, - string operation, - TRequest data, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default) - { - ArgumentVerifier.ThrowIfNullOrEmpty(bindingName, nameof(bindingName)); - ArgumentVerifier.ThrowIfNullOrEmpty(operation, nameof(operation)); + /// + public override async Task InvokeBindingAsync( + string bindingName, + string operation, + TRequest data, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(bindingName, nameof(bindingName)); + ArgumentVerifier.ThrowIfNullOrEmpty(operation, nameof(operation)); + + var bytes = TypeConverters.ToJsonByteString(data, this.jsonSerializerOptions); + _ = await MakeInvokeBindingRequestAsync(bindingName, operation, bytes, metadata, cancellationToken); + } + + /// + public override async Task InvokeBindingAsync( + string bindingName, + string operation, + TRequest data, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(bindingName, nameof(bindingName)); + ArgumentVerifier.ThrowIfNullOrEmpty(operation, nameof(operation)); - var bytes = TypeConverters.ToJsonByteString(data, this.jsonSerializerOptions); - _ = await MakeInvokeBindingRequestAsync(bindingName, operation, bytes, metadata, cancellationToken); + var bytes = TypeConverters.ToJsonByteString(data, this.jsonSerializerOptions); + var response = await MakeInvokeBindingRequestAsync(bindingName, operation, bytes, metadata, cancellationToken); + + try + { + return TypeConverters.FromJsonByteString(response.Data, this.JsonSerializerOptions); } + catch (JsonException ex) + { + throw new DaprException("Binding operation failed: the response payload could not be deserialized. See InnerException for details.", ex); + } + } - /// - public override async Task InvokeBindingAsync( - string bindingName, - string operation, - TRequest data, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default) + public override async Task InvokeBindingAsync(BindingRequest request, CancellationToken cancellationToken = default) + { + var bytes = ByteString.CopyFrom(request.Data.Span); + var response = await this.MakeInvokeBindingRequestAsync(request.BindingName, request.Operation, bytes, request.Metadata, cancellationToken); + return new BindingResponse(request, response.Data.Memory, response.Metadata); + } + + private async Task MakeInvokeBindingRequestAsync( + string name, + string operation, + ByteString data, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + var envelope = new Autogenerated.InvokeBindingRequest() { - ArgumentVerifier.ThrowIfNullOrEmpty(bindingName, nameof(bindingName)); - ArgumentVerifier.ThrowIfNullOrEmpty(operation, nameof(operation)); + Name = name, + Operation = operation + }; - var bytes = TypeConverters.ToJsonByteString(data, this.jsonSerializerOptions); - var response = await MakeInvokeBindingRequestAsync(bindingName, operation, bytes, metadata, cancellationToken); + if (data != null) + { + envelope.Data = data; + } - try - { - return TypeConverters.FromJsonByteString(response.Data, this.JsonSerializerOptions); - } - catch (JsonException ex) + if (metadata != null) + { + foreach (var kvp in metadata) { - throw new DaprException("Binding operation failed: the response payload could not be deserialized. See InnerException for details.", ex); + envelope.Metadata.Add(kvp.Key, kvp.Value); } } - public override async Task InvokeBindingAsync(BindingRequest request, CancellationToken cancellationToken = default) + var options = CreateCallOptions(headers: null, cancellationToken); + try { - var bytes = ByteString.CopyFrom(request.Data.Span); - var response = await this.MakeInvokeBindingRequestAsync(request.BindingName, request.Operation, bytes, request.Metadata, cancellationToken); - return new BindingResponse(request, response.Data.Memory, response.Metadata); + return await client.InvokeBindingAsync(envelope, options); } - - private async Task MakeInvokeBindingRequestAsync( - string name, - string operation, - ByteString data, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default) + catch (RpcException ex) { - var envelope = new Autogenerated.InvokeBindingRequest() - { - Name = name, - Operation = operation - }; + throw new DaprException("Binding operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + } + } + #endregion - if (data != null) - { - envelope.Data = data; - } + #region InvokeMethod Apis - if (metadata != null) - { - foreach (var kvp in metadata) - { - envelope.Metadata.Add(kvp.Key, kvp.Value); - } - } + /// + /// Creates an that can be used to perform service invocation for the + /// application identified by and invokes the method specified by + /// with the HTTP method specified by . + /// + /// The to use for the invocation request. + /// The Dapr application id to invoke the method on. + /// The name of the method to invoke. + /// An for use with SendInvokeMethodRequestAsync. + public override HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMethod, string appId, string methodName) + { + return CreateInvokeMethodRequest(httpMethod, appId, methodName, new List>()); + } - var options = CreateCallOptions(headers: null, cancellationToken); - try - { - return await client.InvokeBindingAsync(envelope, options); - } - catch (RpcException ex) - { - throw new DaprException("Binding operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); - } - } - #endregion - - #region InvokeMethod Apis - - /// - /// Creates an that can be used to perform service invocation for the - /// application identified by and invokes the method specified by - /// with the HTTP method specified by . - /// - /// The to use for the invocation request. - /// The Dapr application id to invoke the method on. - /// The name of the method to invoke. - /// An for use with SendInvokeMethodRequestAsync. - public override HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMethod, string appId, string methodName) - { - return CreateInvokeMethodRequest(httpMethod, appId, methodName, new List>()); - } - - /// - /// Creates an that can be used to perform service invocation for the - /// application identified by and invokes the method specified by - /// with the HTTP method specified by . - /// - /// The to use for the invocation request. - /// The Dapr application id to invoke the method on. - /// The name of the method to invoke. - /// A collection of key/value pairs to populate the query string from. - /// An for use with SendInvokeMethodRequestAsync. - public override HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMethod, string appId, string methodName, - IReadOnlyCollection> queryStringParameters) - { - ArgumentVerifier.ThrowIfNull(httpMethod, nameof(httpMethod)); - ArgumentVerifier.ThrowIfNullOrEmpty(appId, nameof(appId)); - ArgumentVerifier.ThrowIfNull(methodName, nameof(methodName)); - - // Note about this, it's possible to construct invalid stuff using path navigation operators - // like `../..`. But the principle of garbage in -> garbage out holds. - // - // This approach avoids some common pitfalls that could lead to undesired encoding. - var path = $"/v1.0/invoke/{appId}/method/{methodName.TrimStart('/')}"; - var requestUri = new Uri(this.httpEndpoint, path).AddQueryParameters(queryStringParameters); - var request = new HttpRequestMessage(httpMethod, requestUri); + /// + /// Creates an that can be used to perform service invocation for the + /// application identified by and invokes the method specified by + /// with the HTTP method specified by . + /// + /// The to use for the invocation request. + /// The Dapr application id to invoke the method on. + /// The name of the method to invoke. + /// A collection of key/value pairs to populate the query string from. + /// An for use with SendInvokeMethodRequestAsync. + public override HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMethod, string appId, string methodName, + IReadOnlyCollection> queryStringParameters) + { + ArgumentVerifier.ThrowIfNull(httpMethod, nameof(httpMethod)); + ArgumentVerifier.ThrowIfNullOrEmpty(appId, nameof(appId)); + ArgumentVerifier.ThrowIfNull(methodName, nameof(methodName)); + + // Note about this, it's possible to construct invalid stuff using path navigation operators + // like `../..`. But the principle of garbage in -> garbage out holds. + // + // This approach avoids some common pitfalls that could lead to undesired encoding. + var path = $"/v1.0/invoke/{appId}/method/{methodName.TrimStart('/')}"; + var requestUri = new Uri(this.httpEndpoint, path).AddQueryParameters(queryStringParameters); + var request = new HttpRequestMessage(httpMethod, requestUri); - request.Options.Set(new HttpRequestOptionsKey(AppIdKey), appId); - request.Options.Set(new HttpRequestOptionsKey(MethodNameKey), methodName); - - if (this.apiTokenHeader is not null) - { - request.Headers.TryAddWithoutValidation(this.apiTokenHeader.Value.Key, this.apiTokenHeader.Value.Value); - } + request.Options.Set(new HttpRequestOptionsKey(AppIdKey), appId); + request.Options.Set(new HttpRequestOptionsKey(MethodNameKey), methodName); - return request; + if (this.apiTokenHeader is not null) + { + request.Headers.TryAddWithoutValidation(this.apiTokenHeader.Value.Key, this.apiTokenHeader.Value.Value); } - /// - /// Creates an that can be used to perform service invocation for the - /// application identified by and invokes the method specified by - /// with the HTTP method specified by and a JSON serialized request body specified by - /// . - /// - /// The type of the data that will be JSON serialized and provided as the request body. - /// The to use for the invocation request. - /// The Dapr application id to invoke the method on. - /// The name of the method to invoke. - /// The data that will be JSON serialized and provided as the request body. - /// A collection of key/value pairs to populate the query string from. - /// An for use with SendInvokeMethodRequestAsync. - public override HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMethod, string appId, string methodName, - IReadOnlyCollection> queryStringParameters, TRequest data) - { - ArgumentVerifier.ThrowIfNull(httpMethod, nameof(httpMethod)); - ArgumentVerifier.ThrowIfNullOrEmpty(appId, nameof(appId)); - ArgumentVerifier.ThrowIfNull(methodName, nameof(methodName)); + return request; + } - var request = CreateInvokeMethodRequest(httpMethod, appId, methodName, queryStringParameters); - request.Content = JsonContent.Create(data, options: this.JsonSerializerOptions); - return request; - } + /// + /// Creates an that can be used to perform service invocation for the + /// application identified by and invokes the method specified by + /// with the HTTP method specified by and a JSON serialized request body specified by + /// . + /// + /// The type of the data that will be JSON serialized and provided as the request body. + /// The to use for the invocation request. + /// The Dapr application id to invoke the method on. + /// The name of the method to invoke. + /// The data that will be JSON serialized and provided as the request body. + /// A collection of key/value pairs to populate the query string from. + /// An for use with SendInvokeMethodRequestAsync. + public override HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMethod, string appId, string methodName, + IReadOnlyCollection> queryStringParameters, TRequest data) + { + ArgumentVerifier.ThrowIfNull(httpMethod, nameof(httpMethod)); + ArgumentVerifier.ThrowIfNullOrEmpty(appId, nameof(appId)); + ArgumentVerifier.ThrowIfNull(methodName, nameof(methodName)); - public override async Task InvokeMethodWithResponseAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) + var request = CreateInvokeMethodRequest(httpMethod, appId, methodName, queryStringParameters); + request.Content = JsonContent.Create(data, options: this.JsonSerializerOptions); + return request; + } + + public override async Task InvokeMethodWithResponseAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNull(request, nameof(request)); + + if (!this.httpEndpoint.IsBaseOf(request.RequestUri)) { - ArgumentVerifier.ThrowIfNull(request, nameof(request)); + throw new InvalidOperationException("The provided request URI is not a Dapr service invocation URI."); + } - if (!this.httpEndpoint.IsBaseOf(request.RequestUri)) - { - throw new InvalidOperationException("The provided request URI is not a Dapr service invocation URI."); - } + // Note: we intentionally DO NOT validate the status code here. + // This method allows you to 'invoke' without exceptions on non-2xx. + try + { + return await this.httpClient.SendAsync(request, cancellationToken); + } + catch (HttpRequestException ex) + { + // Our code path for creating requests places these keys in the request properties. We don't want to fail + // if they are not present. + request.Options.TryGetValue(new HttpRequestOptionsKey(AppIdKey), out var appId); + request.Options.TryGetValue(new HttpRequestOptionsKey(MethodNameKey), out var methodName); - // Note: we intentionally DO NOT validate the status code here. - // This method allows you to 'invoke' without exceptions on non-2xx. - try - { - return await this.httpClient.SendAsync(request, cancellationToken); - } - catch (HttpRequestException ex) - { - // Our code path for creating requests places these keys in the request properties. We don't want to fail - // if they are not present. - request.Options.TryGetValue(new HttpRequestOptionsKey(AppIdKey), out var appId); - request.Options.TryGetValue(new HttpRequestOptionsKey(MethodNameKey), out var methodName); - - throw new InvocationException( - appId: appId as string, - methodName: methodName as string, - innerException: ex, - response: null); - } + throw new InvocationException( + appId: appId as string, + methodName: methodName as string, + innerException: ex, + response: null); } + } - /// - /// - /// Creates an that can be used to perform Dapr service invocation using - /// objects. - /// - /// - /// The client will read the property, and - /// interpret the hostname as the destination app-id. The - /// property will be replaced with a new URI with the authority section replaced by the instance's value - /// and the path portion of the URI rewritten to follow the format of a Dapr service invocation request. - /// - /// - /// - /// An optional app-id. If specified, the app-id will be configured as the value of - /// so that relative URIs can be used. It is mandatory to set this parameter if your app-id contains at least one upper letter. - /// If some requests use absolute URL with an app-id which contains at least one upper letter, it will not work, the workaround is to create one HttpClient for each app-id with the app-ip parameter set. - /// - /// An that can be used to perform service invocation requests. - /// - /// + /// + /// + /// Creates an that can be used to perform Dapr service invocation using + /// objects. + /// + /// + /// The client will read the property, and + /// interpret the hostname as the destination app-id. The + /// property will be replaced with a new URI with the authority section replaced by the instance's value + /// and the path portion of the URI rewritten to follow the format of a Dapr service invocation request. + /// + /// + /// + /// An optional app-id. If specified, the app-id will be configured as the value of + /// so that relative URIs can be used. It is mandatory to set this parameter if your app-id contains at least one upper letter. + /// If some requests use absolute URL with an app-id which contains at least one upper letter, it will not work, the workaround is to create one HttpClient for each app-id with the app-ip parameter set. + /// + /// An that can be used to perform service invocation requests. + /// + /// #nullable enable - public override HttpClient CreateInvokableHttpClient(string? appId = null) => - DaprClient.CreateInvokeHttpClient(appId, this.httpEndpoint?.AbsoluteUri, this.apiTokenHeader?.Value); - #nullable disable + public override HttpClient CreateInvokableHttpClient(string? appId = null) => + DaprClient.CreateInvokeHttpClient(appId, this.httpEndpoint?.AbsoluteUri, this.apiTokenHeader?.Value); +#nullable disable - public async override Task InvokeMethodAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) + public async override Task InvokeMethodAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNull(request, nameof(request)); + + var response = await InvokeMethodWithResponseAsync(request, cancellationToken); + try + { + response.EnsureSuccessStatusCode(); + } + catch (HttpRequestException ex) { - ArgumentVerifier.ThrowIfNull(request, nameof(request)); + // Our code path for creating requests places these keys in the request properties. We don't want to fail + // if they are not present. + request.Options.TryGetValue(new HttpRequestOptionsKey(AppIdKey), out var appId); + request.Options.TryGetValue(new HttpRequestOptionsKey(MethodNameKey), out var methodName); - var response = await InvokeMethodWithResponseAsync(request, cancellationToken); - try - { - response.EnsureSuccessStatusCode(); - } - catch (HttpRequestException ex) - { - // Our code path for creating requests places these keys in the request properties. We don't want to fail - // if they are not present. - request.Options.TryGetValue(new HttpRequestOptionsKey(AppIdKey), out var appId); - request.Options.TryGetValue(new HttpRequestOptionsKey(MethodNameKey), out var methodName); - - throw new InvocationException( - appId: appId as string, - methodName: methodName as string, - innerException: ex, - response: response); - } + throw new InvocationException( + appId: appId as string, + methodName: methodName as string, + innerException: ex, + response: response); } + } - public async override Task InvokeMethodAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) - { - ArgumentVerifier.ThrowIfNull(request, nameof(request)); + public async override Task InvokeMethodAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNull(request, nameof(request)); - var response = await InvokeMethodWithResponseAsync(request, cancellationToken); - try - { - response.EnsureSuccessStatusCode(); - } - catch (HttpRequestException ex) - { - // Our code path for creating requests places these keys in the request properties. We don't want to fail - // if they are not present. - request.Options.TryGetValue(new HttpRequestOptionsKey(AppIdKey), out var appId); - request.Options.TryGetValue(new HttpRequestOptionsKey(MethodNameKey), out var methodName); - - throw new InvocationException( - appId: appId as string, - methodName: methodName as string, - innerException: ex, - response: response); - } + var response = await InvokeMethodWithResponseAsync(request, cancellationToken); + try + { + response.EnsureSuccessStatusCode(); + } + catch (HttpRequestException ex) + { + // Our code path for creating requests places these keys in the request properties. We don't want to fail + // if they are not present. + request.Options.TryGetValue(new HttpRequestOptionsKey(AppIdKey), out var appId); + request.Options.TryGetValue(new HttpRequestOptionsKey(MethodNameKey), out var methodName); - try - { - return await response.Content.ReadFromJsonAsync(this.jsonSerializerOptions, cancellationToken); - } - catch (HttpRequestException ex) - { - // Our code path for creating requests places these keys in the request properties. We don't want to fail - // if they are not present. - request.Options.TryGetValue(new HttpRequestOptionsKey(AppIdKey), out var appId); - request.Options.TryGetValue(new HttpRequestOptionsKey(MethodNameKey), out var methodName); - - throw new InvocationException( - appId: appId as string, - methodName: methodName as string, - innerException: ex, - response: response); - } - catch (JsonException ex) - { - request.Options.TryGetValue(new HttpRequestOptionsKey(AppIdKey), out var appId); - request.Options.TryGetValue(new HttpRequestOptionsKey(MethodNameKey), out var methodName); - - throw new InvocationException( - appId: appId as string, - methodName: methodName as string, - innerException: ex, - response: response); - } + throw new InvocationException( + appId: appId as string, + methodName: methodName as string, + innerException: ex, + response: response); } - public override async Task InvokeMethodGrpcAsync(string appId, string methodName, CancellationToken cancellationToken = default) + try { - ArgumentVerifier.ThrowIfNullOrEmpty(appId, nameof(appId)); - ArgumentVerifier.ThrowIfNullOrEmpty(methodName, nameof(methodName)); - - var envelope = new Autogenerated.InvokeServiceRequest() - { - Id = appId, - Message = new Autogenerated.InvokeRequest() - { - Method = methodName, - }, - }; + return await response.Content.ReadFromJsonAsync(this.jsonSerializerOptions, cancellationToken); + } + catch (HttpRequestException ex) + { + // Our code path for creating requests places these keys in the request properties. We don't want to fail + // if they are not present. + request.Options.TryGetValue(new HttpRequestOptionsKey(AppIdKey), out var appId); + request.Options.TryGetValue(new HttpRequestOptionsKey(MethodNameKey), out var methodName); - var options = CreateCallOptions(headers: null, cancellationToken); + throw new InvocationException( + appId: appId as string, + methodName: methodName as string, + innerException: ex, + response: response); + } + catch (JsonException ex) + { + request.Options.TryGetValue(new HttpRequestOptionsKey(AppIdKey), out var appId); + request.Options.TryGetValue(new HttpRequestOptionsKey(MethodNameKey), out var methodName); - try - { - _ = await this.Client.InvokeServiceAsync(envelope, options); - } - catch (RpcException ex) - { - throw new InvocationException(appId, methodName, ex); - } + throw new InvocationException( + appId: appId as string, + methodName: methodName as string, + innerException: ex, + response: response); } + } - public override async Task InvokeMethodGrpcAsync(string appId, string methodName, TRequest data, CancellationToken cancellationToken = default) - { - ArgumentVerifier.ThrowIfNullOrEmpty(appId, nameof(appId)); - ArgumentVerifier.ThrowIfNullOrEmpty(methodName, nameof(methodName)); + public override async Task InvokeMethodGrpcAsync(string appId, string methodName, CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(appId, nameof(appId)); + ArgumentVerifier.ThrowIfNullOrEmpty(methodName, nameof(methodName)); - var envelope = new Autogenerated.InvokeServiceRequest() + var envelope = new Autogenerated.InvokeServiceRequest() + { + Id = appId, + Message = new Autogenerated.InvokeRequest() { - Id = appId, - Message = new Autogenerated.InvokeRequest() - { - Method = methodName, - ContentType = Constants.ContentTypeApplicationGrpc, - Data = Any.Pack(data), - }, - }; + Method = methodName, + }, + }; - var options = CreateCallOptions(headers: null, cancellationToken); + var options = CreateCallOptions(headers: null, cancellationToken); - try - { - _ = await this.Client.InvokeServiceAsync(envelope, options); - } - catch (RpcException ex) - { - throw new InvocationException(appId, methodName, ex); - } + try + { + _ = await this.Client.InvokeServiceAsync(envelope, options); } - - public override async Task InvokeMethodGrpcAsync(string appId, string methodName, CancellationToken cancellationToken = default) + catch (RpcException ex) { - ArgumentVerifier.ThrowIfNullOrEmpty(appId, nameof(appId)); - ArgumentVerifier.ThrowIfNullOrEmpty(methodName, nameof(methodName)); + throw new InvocationException(appId, methodName, ex); + } + } - var envelope = new Autogenerated.InvokeServiceRequest() + public override async Task InvokeMethodGrpcAsync(string appId, string methodName, TRequest data, CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(appId, nameof(appId)); + ArgumentVerifier.ThrowIfNullOrEmpty(methodName, nameof(methodName)); + + var envelope = new Autogenerated.InvokeServiceRequest() + { + Id = appId, + Message = new Autogenerated.InvokeRequest() { - Id = appId, - Message = new Autogenerated.InvokeRequest() - { - Method = methodName, - }, - }; + Method = methodName, + ContentType = Constants.ContentTypeApplicationGrpc, + Data = Any.Pack(data), + }, + }; - var options = CreateCallOptions(headers: null, cancellationToken); + var options = CreateCallOptions(headers: null, cancellationToken); - try - { - var response = await this.Client.InvokeServiceAsync(envelope, options); - return response.Data.Unpack(); - } - catch (RpcException ex) - { - throw new InvocationException(appId, methodName, ex); - } + try + { + _ = await this.Client.InvokeServiceAsync(envelope, options); } - - public override async Task InvokeMethodGrpcAsync(string appId, string methodName, TRequest data, CancellationToken cancellationToken = default) + catch (RpcException ex) { - ArgumentVerifier.ThrowIfNullOrEmpty(appId, nameof(appId)); - ArgumentVerifier.ThrowIfNullOrEmpty(methodName, nameof(methodName)); + throw new InvocationException(appId, methodName, ex); + } + } + + public override async Task InvokeMethodGrpcAsync(string appId, string methodName, CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(appId, nameof(appId)); + ArgumentVerifier.ThrowIfNullOrEmpty(methodName, nameof(methodName)); - var envelope = new Autogenerated.InvokeServiceRequest() + var envelope = new Autogenerated.InvokeServiceRequest() + { + Id = appId, + Message = new Autogenerated.InvokeRequest() { - Id = appId, - Message = new Autogenerated.InvokeRequest() - { - Method = methodName, - ContentType = Constants.ContentTypeApplicationGrpc, - Data = Any.Pack(data), - }, - }; + Method = methodName, + }, + }; - var options = CreateCallOptions(headers: null, cancellationToken); + var options = CreateCallOptions(headers: null, cancellationToken); - try - { - var response = await this.Client.InvokeServiceAsync(envelope, options); - return response.Data.Unpack(); - } - catch (RpcException ex) - { - throw new InvocationException(appId, methodName, ex); - } + try + { + var response = await this.Client.InvokeServiceAsync(envelope, options); + return response.Data.Unpack(); } + catch (RpcException ex) + { + throw new InvocationException(appId, methodName, ex); + } + } - #endregion - - #region State Apis + public override async Task InvokeMethodGrpcAsync(string appId, string methodName, TRequest data, CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(appId, nameof(appId)); + ArgumentVerifier.ThrowIfNullOrEmpty(methodName, nameof(methodName)); - /// - public override async Task> GetBulkStateAsync(string storeName, IReadOnlyList keys, int? parallelism, IReadOnlyDictionary metadata = default, CancellationToken cancellationToken = default) + var envelope = new Autogenerated.InvokeServiceRequest() { - var rawBulkState = await GetBulkStateRawAsync(storeName, keys, parallelism, metadata, cancellationToken); - - var bulkResponse = new List(); - foreach (var item in rawBulkState) + Id = appId, + Message = new Autogenerated.InvokeRequest() { - bulkResponse.Add(new BulkStateItem(item.Key, item.Value.ToStringUtf8(), item.Etag)); - } + Method = methodName, + ContentType = Constants.ContentTypeApplicationGrpc, + Data = Any.Pack(data), + }, + }; - return bulkResponse; - } - - /// - public override async Task>> GetBulkStateAsync( - string storeName, - IReadOnlyList keys, - int? parallelism, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default) - { - var rawBulkState = await GetBulkStateRawAsync(storeName, keys, parallelism, metadata, cancellationToken); - - var bulkResponse = new List>(); - foreach (var item in rawBulkState) - { - var deserializedValue = TypeConverters.FromJsonByteString(item.Value, this.JsonSerializerOptions); - bulkResponse.Add(new BulkStateItem(item.Key, deserializedValue, item.Etag)); - } + var options = CreateCallOptions(headers: null, cancellationToken); - return bulkResponse; + try + { + var response = await this.Client.InvokeServiceAsync(envelope, options); + return response.Data.Unpack(); } - - /// - /// Retrieves the bulk state data, but rather than deserializing the values, leaves the specific handling - /// to the public callers of this method to avoid duplicate deserialization. - /// - private async Task> GetBulkStateRawAsync( - string storeName, - IReadOnlyList keys, - int? parallelism, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default) + catch (RpcException ex) { - ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); - if (keys.Count == 0) - throw new ArgumentException("keys do not contain any elements"); - - var envelope = new Autogenerated.GetBulkStateRequest() - { - StoreName = storeName, - Parallelism = parallelism ?? default - }; + throw new InvocationException(appId, methodName, ex); + } + } - if (metadata != null) - { - foreach (var kvp in metadata) - { - envelope.Metadata.Add(kvp.Key, kvp.Value); - } - } + #endregion - envelope.Keys.AddRange(keys); + #region State Apis - var options = CreateCallOptions(headers: null, cancellationToken); - Autogenerated.GetBulkStateResponse response; + /// + public override async Task> GetBulkStateAsync(string storeName, IReadOnlyList keys, int? parallelism, IReadOnlyDictionary metadata = default, CancellationToken cancellationToken = default) + { + var rawBulkState = await GetBulkStateRawAsync(storeName, keys, parallelism, metadata, cancellationToken); - try - { - response = await client.GetBulkStateAsync(envelope, options); - } - catch (RpcException ex) - { - throw new DaprException( - "State operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", - ex); - } + var bulkResponse = new List(); + foreach (var item in rawBulkState) + { + bulkResponse.Add(new BulkStateItem(item.Key, item.Value.ToStringUtf8(), item.Etag)); + } - var bulkResponse = new List<(string Key, string Etag, ByteString Value)>(); - foreach (var item in response.Items) - { - bulkResponse.Add((item.Key, item.Etag, item.Data)); - } + return bulkResponse; + } + + /// + public override async Task>> GetBulkStateAsync( + string storeName, + IReadOnlyList keys, + int? parallelism, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + var rawBulkState = await GetBulkStateRawAsync(storeName, keys, parallelism, metadata, cancellationToken); - return bulkResponse; + var bulkResponse = new List>(); + foreach (var item in rawBulkState) + { + var deserializedValue = TypeConverters.FromJsonByteString(item.Value, this.JsonSerializerOptions); + bulkResponse.Add(new BulkStateItem(item.Key, deserializedValue, item.Etag)); } - - /// - public override async Task GetStateAsync( - string storeName, - string key, - ConsistencyMode? consistencyMode = default, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default) - { - ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); - ArgumentVerifier.ThrowIfNullOrEmpty(key, nameof(key)); - - var envelope = new Autogenerated.GetStateRequest() - { - StoreName = storeName, - Key = key, - }; - if (metadata != null) - { - foreach (var kvp in metadata) - { - envelope.Metadata.Add(kvp.Key, kvp.Value); - } - } + return bulkResponse; + } - if (consistencyMode != null) - { - envelope.Consistency = GetStateConsistencyForConsistencyMode(consistencyMode.Value); - } + /// + /// Retrieves the bulk state data, but rather than deserializing the values, leaves the specific handling + /// to the public callers of this method to avoid duplicate deserialization. + /// + private async Task> GetBulkStateRawAsync( + string storeName, + IReadOnlyList keys, + int? parallelism, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); + if (keys.Count == 0) + throw new ArgumentException("keys do not contain any elements"); - var options = CreateCallOptions(headers: null, cancellationToken); - Autogenerated.GetStateResponse response; + var envelope = new Autogenerated.GetBulkStateRequest() + { + StoreName = storeName, + Parallelism = parallelism ?? default + }; - try - { - response = await client.GetStateAsync(envelope, options); - } - catch (RpcException ex) + if (metadata != null) + { + foreach (var kvp in metadata) { - throw new DaprException("State operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + envelope.Metadata.Add(kvp.Key, kvp.Value); } + } - try - { - return TypeConverters.FromJsonByteString(response.Data, this.JsonSerializerOptions); - } - catch (JsonException ex) - { - throw new DaprException("State operation failed: the state payload could not be deserialized. See InnerException for details.", ex); - } + envelope.Keys.AddRange(keys); + + var options = CreateCallOptions(headers: null, cancellationToken); + Autogenerated.GetBulkStateResponse response; + + try + { + response = await client.GetBulkStateAsync(envelope, options); + } + catch (RpcException ex) + { + throw new DaprException( + "State operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", + ex); } - /// - public override async Task SaveBulkStateAsync(string storeName, IReadOnlyList> items, CancellationToken cancellationToken = default) + var bulkResponse = new List<(string Key, string Etag, ByteString Value)>(); + foreach (var item in response.Items) { - ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); + bulkResponse.Add((item.Key, item.Etag, item.Data)); + } - if (items.Count == 0) - { - throw new ArgumentException("items do not contain any elements"); - } + return bulkResponse; + } + + /// + public override async Task GetStateAsync( + string storeName, + string key, + ConsistencyMode? consistencyMode = default, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); + ArgumentVerifier.ThrowIfNullOrEmpty(key, nameof(key)); - var envelope = new Autogenerated.SaveStateRequest() - { - StoreName = storeName, - }; + var envelope = new Autogenerated.GetStateRequest() + { + StoreName = storeName, + Key = key, + }; - foreach (var item in items) + if (metadata != null) + { + foreach (var kvp in metadata) { - var stateItem = new Autogenerated.StateItem() - { - Key = item.Key, - }; - - if (item.ETag != null) - { - stateItem.Etag = new Autogenerated.Etag() { Value = item.ETag }; - } + envelope.Metadata.Add(kvp.Key, kvp.Value); + } + } - if (item.Metadata != null) - { - foreach (var kvp in item.Metadata) - { - stateItem.Metadata.Add(kvp.Key, kvp.Value); - } - } + if (consistencyMode != null) + { + envelope.Consistency = GetStateConsistencyForConsistencyMode(consistencyMode.Value); + } - if (item.StateOptions != null) - { - stateItem.Options = ToAutoGeneratedStateOptions(item.StateOptions); - } + var options = CreateCallOptions(headers: null, cancellationToken); + Autogenerated.GetStateResponse response; - if (item.Value != null) - { - stateItem.Value = TypeConverters.ToJsonByteString(item.Value, this.jsonSerializerOptions); - } + try + { + response = await client.GetStateAsync(envelope, options); + } + catch (RpcException ex) + { + throw new DaprException("State operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + } - envelope.States.Add(stateItem); - } + try + { + return TypeConverters.FromJsonByteString(response.Data, this.JsonSerializerOptions); + } + catch (JsonException ex) + { + throw new DaprException("State operation failed: the state payload could not be deserialized. See InnerException for details.", ex); + } + } - try - { - await this.Client.SaveStateAsync(envelope, cancellationToken: cancellationToken); - } - catch (RpcException ex) - { - throw new DaprException("State operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); - } + /// + public override async Task SaveBulkStateAsync(string storeName, IReadOnlyList> items, CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); + if (items.Count == 0) + { + throw new ArgumentException("items do not contain any elements"); } - /// - public override async Task DeleteBulkStateAsync(string storeName, IReadOnlyList items, CancellationToken cancellationToken = default) + var envelope = new Autogenerated.SaveStateRequest() + { + StoreName = storeName, + }; + + foreach (var item in items) { - var envelope = new Autogenerated.DeleteBulkStateRequest() + var stateItem = new Autogenerated.StateItem() { - StoreName = storeName, + Key = item.Key, }; - foreach (var item in items) + if (item.ETag != null) { - var stateItem = new Autogenerated.StateItem() - { - Key = item.Key, - }; - - if (item.ETag != null) - { - stateItem.Etag = new Autogenerated.Etag() { Value = item.ETag }; - } - - if (item.Metadata != null) - { - foreach (var kvp in item.Metadata) - { - stateItem.Metadata.Add(kvp.Key, kvp.Value); - } - } + stateItem.Etag = new Autogenerated.Etag() { Value = item.ETag }; + } - if (item.StateOptions != null) + if (item.Metadata != null) + { + foreach (var kvp in item.Metadata) { - stateItem.Options = ToAutoGeneratedStateOptions(item.StateOptions); + stateItem.Metadata.Add(kvp.Key, kvp.Value); } - - envelope.States.Add(stateItem); } - try + if (item.StateOptions != null) { - await this.Client.DeleteBulkStateAsync(envelope, cancellationToken: cancellationToken); + stateItem.Options = ToAutoGeneratedStateOptions(item.StateOptions); } - catch (RpcException ex) + + if (item.Value != null) { - throw new DaprException("State operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + stateItem.Value = TypeConverters.ToJsonByteString(item.Value, this.jsonSerializerOptions); } + envelope.States.Add(stateItem); + } + + try + { + await this.Client.SaveStateAsync(envelope, cancellationToken: cancellationToken); + } + catch (RpcException ex) + { + throw new DaprException("State operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); } - /// - public override async Task<(TValue value, string etag)> GetStateAndETagAsync( - string storeName, - string key, - ConsistencyMode? consistencyMode = default, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default) + } + + /// + public override async Task DeleteBulkStateAsync(string storeName, IReadOnlyList items, CancellationToken cancellationToken = default) + { + var envelope = new Autogenerated.DeleteBulkStateRequest() { - ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); - ArgumentVerifier.ThrowIfNullOrEmpty(key, nameof(key)); + StoreName = storeName, + }; - var envelope = new Autogenerated.GetStateRequest() + foreach (var item in items) + { + var stateItem = new Autogenerated.StateItem() { - StoreName = storeName, - Key = key + Key = item.Key, }; - if (metadata != null) + if (item.ETag != null) { - foreach (var kvp in metadata) - { - envelope.Metadata.Add(kvp.Key, kvp.Value); - } + stateItem.Etag = new Autogenerated.Etag() { Value = item.ETag }; } - if (consistencyMode != null) + if (item.Metadata != null) { - envelope.Consistency = GetStateConsistencyForConsistencyMode(consistencyMode.Value); + foreach (var kvp in item.Metadata) + { + stateItem.Metadata.Add(kvp.Key, kvp.Value); + } } - var options = CreateCallOptions(headers: null, cancellationToken); - Autogenerated.GetStateResponse response; - - try - { - response = await client.GetStateAsync(envelope, options); - } - catch (RpcException ex) + if (item.StateOptions != null) { - throw new DaprException("State operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + stateItem.Options = ToAutoGeneratedStateOptions(item.StateOptions); } - try - { - return (TypeConverters.FromJsonByteString(response.Data, this.JsonSerializerOptions), response.Etag); - } - catch (JsonException ex) - { - throw new DaprException("State operation failed: the state payload could not be deserialized. See InnerException for details.", ex); - } + envelope.States.Add(stateItem); } - /// - public override async Task SaveStateAsync( - string storeName, - string key, - TValue value, - StateOptions stateOptions = default, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default) - { - ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); - ArgumentVerifier.ThrowIfNullOrEmpty(key, nameof(key)); - - _ = await this.MakeSaveStateCallAsync( - storeName, - key, - value, - etag: null, - stateOptions, - metadata, - cancellationToken); + try + { + await this.Client.DeleteBulkStateAsync(envelope, cancellationToken: cancellationToken); + } + catch (RpcException ex) + { + throw new DaprException("State operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); } - /// - public override async Task TrySaveStateAsync( - string storeName, - string key, - TValue value, - string etag, - StateOptions stateOptions = default, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default) + } + + /// + public override async Task<(TValue value, string etag)> GetStateAndETagAsync( + string storeName, + string key, + ConsistencyMode? consistencyMode = default, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); + ArgumentVerifier.ThrowIfNullOrEmpty(key, nameof(key)); + + var envelope = new Autogenerated.GetStateRequest() { - ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); - ArgumentVerifier.ThrowIfNullOrEmpty(key, nameof(key)); - // Not all state stores treat empty etag as invalid. Therefore, we will not verify an empty etag and - // rely on bubbling up the error if any from Dapr runtime - ArgumentVerifier.ThrowIfNull(etag, nameof(etag)); + StoreName = storeName, + Key = key + }; - return await this.MakeSaveStateCallAsync(storeName, key, value, etag, stateOptions, metadata, cancellationToken); + if (metadata != null) + { + foreach (var kvp in metadata) + { + envelope.Metadata.Add(kvp.Key, kvp.Value); + } } - private async Task MakeSaveStateCallAsync( - string storeName, - string key, - TValue value, - string etag = default, - StateOptions stateOptions = default, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default) + if (consistencyMode != null) { - var envelope = new Autogenerated.SaveStateRequest() - { - StoreName = storeName, - }; + envelope.Consistency = GetStateConsistencyForConsistencyMode(consistencyMode.Value); + } + var options = CreateCallOptions(headers: null, cancellationToken); + Autogenerated.GetStateResponse response; - var stateItem = new Autogenerated.StateItem() - { - Key = key, - }; + try + { + response = await client.GetStateAsync(envelope, options); + } + catch (RpcException ex) + { + throw new DaprException("State operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + } - if (metadata != null) - { - foreach (var kvp in metadata) - { - stateItem.Metadata.Add(kvp.Key, kvp.Value); - } - } + try + { + return (TypeConverters.FromJsonByteString(response.Data, this.JsonSerializerOptions), response.Etag); + } + catch (JsonException ex) + { + throw new DaprException("State operation failed: the state payload could not be deserialized. See InnerException for details.", ex); + } + } - if (etag != null) - { - stateItem.Etag = new Autogenerated.Etag() { Value = etag }; - } + /// + public override async Task SaveStateAsync( + string storeName, + string key, + TValue value, + StateOptions stateOptions = default, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); + ArgumentVerifier.ThrowIfNullOrEmpty(key, nameof(key)); + + _ = await this.MakeSaveStateCallAsync( + storeName, + key, + value, + etag: null, + stateOptions, + metadata, + cancellationToken); + } - if (stateOptions != null) - { - stateItem.Options = ToAutoGeneratedStateOptions(stateOptions); - } + /// + public override async Task TrySaveStateAsync( + string storeName, + string key, + TValue value, + string etag, + StateOptions stateOptions = default, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); + ArgumentVerifier.ThrowIfNullOrEmpty(key, nameof(key)); + // Not all state stores treat empty etag as invalid. Therefore, we will not verify an empty etag and + // rely on bubbling up the error if any from Dapr runtime + ArgumentVerifier.ThrowIfNull(etag, nameof(etag)); - if (value != null) - { - stateItem.Value = TypeConverters.ToJsonByteString(value, this.jsonSerializerOptions); - } + return await this.MakeSaveStateCallAsync(storeName, key, value, etag, stateOptions, metadata, cancellationToken); + } - envelope.States.Add(stateItem); + private async Task MakeSaveStateCallAsync( + string storeName, + string key, + TValue value, + string etag = default, + StateOptions stateOptions = default, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + var envelope = new Autogenerated.SaveStateRequest() + { + StoreName = storeName, + }; - var options = CreateCallOptions(headers: null, cancellationToken); - try - { - await client.SaveStateAsync(envelope, options); - return true; - } - catch (RpcException rpc) when (etag != null && rpc.StatusCode == StatusCode.Aborted) - { - // This kind of failure indicates an ETag mismatch. Aborted doesn't seem like - // the right status code at first, but check the docs, it fits this use-case. - // - // When an ETag is used we surface this though the Try... pattern - return false; - } - catch (RpcException ex) - { - throw new DaprException("State operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); - } - } + var stateItem = new Autogenerated.StateItem() + { + Key = key, + }; - /// - public override async Task ExecuteStateTransactionAsync( - string storeName, - IReadOnlyList operations, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default) + if (metadata != null) { - ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); - ArgumentVerifier.ThrowIfNull(operations, nameof(operations)); - if (operations.Count == 0) + foreach (var kvp in metadata) { - throw new ArgumentException($"{nameof(operations)} does not contain any elements"); + stateItem.Metadata.Add(kvp.Key, kvp.Value); } + } - await this.MakeExecuteStateTransactionCallAsync( - storeName, - operations, - metadata, - cancellationToken); + if (etag != null) + { + stateItem.Etag = new Autogenerated.Etag() { Value = etag }; } - private async Task MakeExecuteStateTransactionCallAsync( - string storeName, - IReadOnlyList states, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default) + if (stateOptions != null) { - var envelope = new Autogenerated.ExecuteStateTransactionRequest() - { - StoreName = storeName, - }; + stateItem.Options = ToAutoGeneratedStateOptions(stateOptions); + } - foreach (var state in states) - { - var stateOperation = new Autogenerated.TransactionalStateOperation - { - OperationType = state.OperationType.ToString().ToLower(), - Request = ToAutogeneratedStateItem(state) - }; + if (value != null) + { + stateItem.Value = TypeConverters.ToJsonByteString(value, this.jsonSerializerOptions); + } - envelope.Operations.Add(stateOperation); + envelope.States.Add(stateItem); - } + var options = CreateCallOptions(headers: null, cancellationToken); + try + { + await client.SaveStateAsync(envelope, options); + return true; + } + catch (RpcException rpc) when (etag != null && rpc.StatusCode == StatusCode.Aborted) + { + // This kind of failure indicates an ETag mismatch. Aborted doesn't seem like + // the right status code at first, but check the docs, it fits this use-case. + // + // When an ETag is used we surface this though the Try... pattern + return false; + } + catch (RpcException ex) + { + throw new DaprException("State operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + } + } - // Add metadata that applies to all operations if specified - if (metadata != null) - { - foreach (var kvp in metadata) - { - envelope.Metadata.Add(kvp.Key, kvp.Value); - } - } - var options = CreateCallOptions(headers: null, cancellationToken); - try - { - await client.ExecuteStateTransactionAsync(envelope, options); - } - catch (RpcException ex) - { - throw new DaprException("State operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); - } + /// + public override async Task ExecuteStateTransactionAsync( + string storeName, + IReadOnlyList operations, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); + ArgumentVerifier.ThrowIfNull(operations, nameof(operations)); + if (operations.Count == 0) + { + throw new ArgumentException($"{nameof(operations)} does not contain any elements"); } - private Autogenerated.StateItem ToAutogeneratedStateItem(StateTransactionRequest state) + await this.MakeExecuteStateTransactionCallAsync( + storeName, + operations, + metadata, + cancellationToken); + } + + private async Task MakeExecuteStateTransactionCallAsync( + string storeName, + IReadOnlyList states, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + var envelope = new Autogenerated.ExecuteStateTransactionRequest() { - var stateOperation = new Autogenerated.StateItem - { - Key = state.Key - }; + StoreName = storeName, + }; - if (state.Value != null) + foreach (var state in states) + { + var stateOperation = new Autogenerated.TransactionalStateOperation { - stateOperation.Value = ByteString.CopyFrom(state.Value); - } + OperationType = state.OperationType.ToString().ToLower(), + Request = ToAutogeneratedStateItem(state) + }; - if (state.ETag != null) - { - stateOperation.Etag = new Autogenerated.Etag() { Value = state.ETag }; - } + envelope.Operations.Add(stateOperation); - if (state.Metadata != null) - { - foreach (var kvp in state.Metadata) - { - stateOperation.Metadata.Add(kvp.Key, kvp.Value); - } - } + } - if (state.Options != null) + // Add metadata that applies to all operations if specified + if (metadata != null) + { + foreach (var kvp in metadata) { - stateOperation.Options = ToAutoGeneratedStateOptions(state.Options); + envelope.Metadata.Add(kvp.Key, kvp.Value); } - - return stateOperation; } - - /// - public override async Task DeleteStateAsync( - string storeName, - string key, - StateOptions stateOptions = default, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default) + var options = CreateCallOptions(headers: null, cancellationToken); + try { - ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); - ArgumentVerifier.ThrowIfNullOrEmpty(key, nameof(key)); - - _ = await this.MakeDeleteStateCallAsync( - storeName, - key, - etag: null, - stateOptions, - metadata, - cancellationToken); + await client.ExecuteStateTransactionAsync(envelope, options); + } + catch (RpcException ex) + { + throw new DaprException("State operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); } + } - /// - public override async Task TryDeleteStateAsync( - string storeName, - string key, - string etag, - StateOptions stateOptions = default, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default) + private Autogenerated.StateItem ToAutogeneratedStateItem(StateTransactionRequest state) + { + var stateOperation = new Autogenerated.StateItem { - ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); - ArgumentVerifier.ThrowIfNullOrEmpty(key, nameof(key)); - // Not all state stores treat empty etag as invalid. Therefore, we will not verify an empty etag and - // rely on bubbling up the error if any from Dapr runtime - ArgumentVerifier.ThrowIfNull(etag, nameof(etag)); + Key = state.Key + }; - return await this.MakeDeleteStateCallAsync(storeName, key, etag, stateOptions, metadata, cancellationToken); + if (state.Value != null) + { + stateOperation.Value = ByteString.CopyFrom(state.Value); } - private async Task MakeDeleteStateCallAsync( - string storeName, - string key, - string etag = default, - StateOptions stateOptions = default, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default) + if (state.ETag != null) { - var deleteStateEnvelope = new Autogenerated.DeleteStateRequest() - { - StoreName = storeName, - Key = key, - }; + stateOperation.Etag = new Autogenerated.Etag() { Value = state.ETag }; + } - if (metadata != null) + if (state.Metadata != null) + { + foreach (var kvp in state.Metadata) { - foreach (var kvp in metadata) - { - deleteStateEnvelope.Metadata.Add(kvp.Key, kvp.Value); - } + stateOperation.Metadata.Add(kvp.Key, kvp.Value); } + } - if (etag != null) - { - deleteStateEnvelope.Etag = new Autogenerated.Etag() { Value = etag }; - } + if (state.Options != null) + { + stateOperation.Options = ToAutoGeneratedStateOptions(state.Options); + } - if (stateOptions != null) - { - deleteStateEnvelope.Options = ToAutoGeneratedStateOptions(stateOptions); - } + return stateOperation; + } - var options = CreateCallOptions(headers: null, cancellationToken); - try - { - await client.DeleteStateAsync(deleteStateEnvelope, options); - return true; - } - catch (RpcException rpc) when (etag != null && rpc.StatusCode == StatusCode.Aborted) - { - // This kind of failure indicates an ETag mismatch. Aborted doesn't seem like - // the right status code at first, but check the docs, it fits this use-case. - // - // When an ETag is used we surface this though the Try... pattern - return false; - } - catch (RpcException ex) - { - throw new DaprException("State operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); - } - } + /// + public override async Task DeleteStateAsync( + string storeName, + string key, + StateOptions stateOptions = default, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); + ArgumentVerifier.ThrowIfNullOrEmpty(key, nameof(key)); + + _ = await this.MakeDeleteStateCallAsync( + storeName, + key, + etag: null, + stateOptions, + metadata, + cancellationToken); + } + + /// + public override async Task TryDeleteStateAsync( + string storeName, + string key, + string etag, + StateOptions stateOptions = default, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); + ArgumentVerifier.ThrowIfNullOrEmpty(key, nameof(key)); + // Not all state stores treat empty etag as invalid. Therefore, we will not verify an empty etag and + // rely on bubbling up the error if any from Dapr runtime + ArgumentVerifier.ThrowIfNull(etag, nameof(etag)); + + return await this.MakeDeleteStateCallAsync(storeName, key, etag, stateOptions, metadata, cancellationToken); + } - /// - public async override Task> QueryStateAsync( - string storeName, - string jsonQuery, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default) + private async Task MakeDeleteStateCallAsync( + string storeName, + string key, + string etag = default, + StateOptions stateOptions = default, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + var deleteStateEnvelope = new Autogenerated.DeleteStateRequest() { - var queryRequest = new Autogenerated.QueryStateRequest() - { - StoreName = storeName, - Query = jsonQuery - }; + StoreName = storeName, + Key = key, + }; - if (metadata != null) + if (metadata != null) + { + foreach (var kvp in metadata) { - foreach (var kvp in metadata) - { - queryRequest.Metadata.Add(kvp.Key, kvp.Value); - } + deleteStateEnvelope.Metadata.Add(kvp.Key, kvp.Value); } + } - var options = CreateCallOptions(headers: null, cancellationToken); + if (etag != null) + { + deleteStateEnvelope.Etag = new Autogenerated.Etag() { Value = etag }; + } - try - { - var items = new List>(); - var failedKeys = new List(); - var queryResponse = await client.QueryStateAlpha1Async(queryRequest, options); - foreach (var item in queryResponse.Results) - { - if (!string.IsNullOrEmpty(item.Error)) - { - // When we encounter an error, we record the key and prepare to throw an exception at the end of the results. - failedKeys.Add(item.Key); - continue; - } - items.Add(new StateQueryItem(item.Key, TypeConverters.FromJsonByteString(item.Data, this.JsonSerializerOptions), item.Etag, item.Error)); - } + if (stateOptions != null) + { + deleteStateEnvelope.Options = ToAutoGeneratedStateOptions(stateOptions); + } - var results = new StateQueryResponse(items, queryResponse.Token, queryResponse.Metadata); - if (failedKeys.Count > 0) - { - // We encountered some bad keys so we throw instead of returning to alert the user. - throw new StateQueryException($"Encountered an error while processing state query results.", results, failedKeys); - } + var options = CreateCallOptions(headers: null, cancellationToken); - return results; - } - catch (RpcException ex) - { - throw new DaprException("Query state operation failed: the Dapr endpointed indicated a failure. See InnerException for details.", ex); - } - catch (JsonException ex) - { - throw new DaprException("State operation failed: the state payload could not be deserialized. See InnerException for details.", ex); - } + try + { + await client.DeleteStateAsync(deleteStateEnvelope, options); + return true; + } + catch (RpcException rpc) when (etag != null && rpc.StatusCode == StatusCode.Aborted) + { + // This kind of failure indicates an ETag mismatch. Aborted doesn't seem like + // the right status code at first, but check the docs, it fits this use-case. + // + // When an ETag is used we surface this though the Try... pattern + return false; + } + catch (RpcException ex) + { + throw new DaprException("State operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); } - #endregion + } - #region Secret Apis - /// - public async override Task> GetSecretAsync( - string storeName, - string key, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default) + /// + public async override Task> QueryStateAsync( + string storeName, + string jsonQuery, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + var queryRequest = new Autogenerated.QueryStateRequest() { - ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); - ArgumentVerifier.ThrowIfNullOrEmpty(key, nameof(key)); + StoreName = storeName, + Query = jsonQuery + }; - var envelope = new Autogenerated.GetSecretRequest() + if (metadata != null) + { + foreach (var kvp in metadata) { - StoreName = storeName, - Key = key - }; + queryRequest.Metadata.Add(kvp.Key, kvp.Value); + } + } - if (metadata != null) + var options = CreateCallOptions(headers: null, cancellationToken); + + try + { + var items = new List>(); + var failedKeys = new List(); + var queryResponse = await client.QueryStateAlpha1Async(queryRequest, options); + foreach (var item in queryResponse.Results) { - foreach (var kvp in metadata) + if (!string.IsNullOrEmpty(item.Error)) { - envelope.Metadata.Add(kvp.Key, kvp.Value); + // When we encounter an error, we record the key and prepare to throw an exception at the end of the results. + failedKeys.Add(item.Key); + continue; } + items.Add(new StateQueryItem(item.Key, TypeConverters.FromJsonByteString(item.Data, this.JsonSerializerOptions), item.Etag, item.Error)); } - var options = CreateCallOptions(headers: null, cancellationToken); - Autogenerated.GetSecretResponse response; - - try - { - response = await client.GetSecretAsync(envelope, options); - } - catch (RpcException ex) + var results = new StateQueryResponse(items, queryResponse.Token, queryResponse.Metadata); + if (failedKeys.Count > 0) { - throw new DaprException("Secret operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + // We encountered some bad keys so we throw instead of returning to alert the user. + throw new StateQueryException($"Encountered an error while processing state query results.", results, failedKeys); } - return response.Data.ToDictionary(kv => kv.Key, kv => kv.Value); + return results; + } + catch (RpcException ex) + { + throw new DaprException("Query state operation failed: the Dapr endpointed indicated a failure. See InnerException for details.", ex); + } + catch (JsonException ex) + { + throw new DaprException("State operation failed: the state payload could not be deserialized. See InnerException for details.", ex); } + } + #endregion + + #region Secret Apis + /// + public async override Task> GetSecretAsync( + string storeName, + string key, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); + ArgumentVerifier.ThrowIfNullOrEmpty(key, nameof(key)); - /// - public async override Task>> GetBulkSecretAsync( - string storeName, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default) + var envelope = new Autogenerated.GetSecretRequest() { - var envelope = new Autogenerated.GetBulkSecretRequest() - { - StoreName = storeName - }; + StoreName = storeName, + Key = key + }; - if (metadata != null) + if (metadata != null) + { + foreach (var kvp in metadata) { - foreach (var kvp in metadata) - { - envelope.Metadata.Add(kvp.Key, kvp.Value); - } + envelope.Metadata.Add(kvp.Key, kvp.Value); } + } - var options = CreateCallOptions(headers: null, cancellationToken); - Autogenerated.GetBulkSecretResponse response; + var options = CreateCallOptions(headers: null, cancellationToken); + Autogenerated.GetSecretResponse response; - try - { - response = await client.GetBulkSecretAsync(envelope, options); - } - catch (RpcException ex) + try + { + response = await client.GetSecretAsync(envelope, options); + } + catch (RpcException ex) + { + throw new DaprException("Secret operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + } + + return response.Data.ToDictionary(kv => kv.Key, kv => kv.Value); + } + + /// + public async override Task>> GetBulkSecretAsync( + string storeName, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + var envelope = new Autogenerated.GetBulkSecretRequest() + { + StoreName = storeName + }; + + if (metadata != null) + { + foreach (var kvp in metadata) { - throw new DaprException("Bulk secret operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + envelope.Metadata.Add(kvp.Key, kvp.Value); } + } + + var options = CreateCallOptions(headers: null, cancellationToken); + Autogenerated.GetBulkSecretResponse response; - return response.Data.ToDictionary(r => r.Key, r => r.Value.Secrets.ToDictionary(s => s.Key, s => s.Value)); + try + { + response = await client.GetBulkSecretAsync(envelope, options); + } + catch (RpcException ex) + { + throw new DaprException("Bulk secret operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); } - #endregion - #region Configuration API - /// - public async override Task GetConfiguration( - string storeName, - IReadOnlyList keys, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default) + return response.Data.ToDictionary(r => r.Key, r => r.Value.Secrets.ToDictionary(s => s.Key, s => s.Value)); + } + #endregion + + #region Configuration API + /// + public async override Task GetConfiguration( + string storeName, + IReadOnlyList keys, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); + + var request = new Autogenerated.GetConfigurationRequest() { - ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); + StoreName = storeName + }; - var request = new Autogenerated.GetConfigurationRequest() - { - StoreName = storeName - }; + if (keys != null && keys.Count > 0) + { + request.Keys.AddRange(keys); + } - if (keys != null && keys.Count > 0) + if (metadata != null) + { + foreach (var kvp in metadata) { - request.Keys.AddRange(keys); + request.Metadata.Add(kvp.Key, kvp.Value); } + } - if (metadata != null) - { - foreach (var kvp in metadata) - { - request.Metadata.Add(kvp.Key, kvp.Value); - } - } + var options = CreateCallOptions(headers: null, cancellationToken); + Autogenerated.GetConfigurationResponse response = new Autogenerated.GetConfigurationResponse(); + try + { + response = await client.GetConfigurationAsync(request, options); + } + catch (RpcException ex) + { + throw new DaprException("GetConfiguration operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + } - var options = CreateCallOptions(headers: null, cancellationToken); - Autogenerated.GetConfigurationResponse response = new Autogenerated.GetConfigurationResponse(); - try - { - response = await client.GetConfigurationAsync(request, options); - } - catch (RpcException ex) - { - throw new DaprException("GetConfiguration operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); - } + var responseItems = response.Items.ToDictionary(item => item.Key, item => new ConfigurationItem(item.Value.Value, item.Value.Version, item.Value.Metadata)); - var responseItems = response.Items.ToDictionary(item => item.Key, item => new ConfigurationItem(item.Value.Value, item.Value.Version, item.Value.Metadata)); + return new GetConfigurationResponse(responseItems); + } - return new GetConfigurationResponse(responseItems); - } + /// + public override Task SubscribeConfiguration( + string storeName, + IReadOnlyList keys, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); - /// - public override Task SubscribeConfiguration( - string storeName, - IReadOnlyList keys, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default) + Autogenerated.SubscribeConfigurationRequest request = new Autogenerated.SubscribeConfigurationRequest() { - ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); + StoreName = storeName + }; - Autogenerated.SubscribeConfigurationRequest request = new Autogenerated.SubscribeConfigurationRequest() - { - StoreName = storeName - }; + if (keys != null && keys.Count > 0) + { + request.Keys.AddRange(keys); + } - if (keys != null && keys.Count > 0) + if (metadata != null) + { + foreach (var kvp in metadata) { - request.Keys.AddRange(keys); + request.Metadata.Add(kvp.Key, kvp.Value); } + } - if (metadata != null) - { - foreach (var kvp in metadata) - { - request.Metadata.Add(kvp.Key, kvp.Value); - } - } + var options = CreateCallOptions(headers: null, cancellationToken: cancellationToken); + return Task.FromResult(new SubscribeConfigurationResponse(new DaprSubscribeConfigurationSource(client.SubscribeConfiguration(request, options)))); + } - var options = CreateCallOptions(headers: null, cancellationToken: cancellationToken); - return Task.FromResult(new SubscribeConfigurationResponse(new DaprSubscribeConfigurationSource(client.SubscribeConfiguration(request, options)))); - } + public override async Task UnsubscribeConfiguration( + string storeName, + string id, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); + ArgumentVerifier.ThrowIfNullOrEmpty(id, nameof(id)); - public override async Task UnsubscribeConfiguration( - string storeName, - string id, - CancellationToken cancellationToken = default) + Autogenerated.UnsubscribeConfigurationRequest request = new Autogenerated.UnsubscribeConfigurationRequest() { - ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); - ArgumentVerifier.ThrowIfNullOrEmpty(id, nameof(id)); - - Autogenerated.UnsubscribeConfigurationRequest request = new Autogenerated.UnsubscribeConfigurationRequest() - { - StoreName = storeName, - Id = id - }; + StoreName = storeName, + Id = id + }; - var options = CreateCallOptions(headers: null, cancellationToken); - var resp = await client.UnsubscribeConfigurationAsync(request, options); - return new UnsubscribeConfigurationResponse(resp.Ok, resp.Message); - } + var options = CreateCallOptions(headers: null, cancellationToken); + var resp = await client.UnsubscribeConfigurationAsync(request, options); + return new UnsubscribeConfigurationResponse(resp.Ok, resp.Message); + } - #endregion + #endregion - #region Cryptography + #region Cryptography - /// - [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public override async Task> EncryptAsync(string vaultResourceName, - ReadOnlyMemory plaintextBytes, string keyName, EncryptionOptions encryptionOptions, - CancellationToken cancellationToken = default) + /// + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public override async Task> EncryptAsync(string vaultResourceName, + ReadOnlyMemory plaintextBytes, string keyName, EncryptionOptions encryptionOptions, + CancellationToken cancellationToken = default) + { + if (MemoryMarshal.TryGetArray(plaintextBytes, out var plaintextSegment) && plaintextSegment.Array != null) { - if (MemoryMarshal.TryGetArray(plaintextBytes, out var plaintextSegment) && plaintextSegment.Array != null) - { - var encryptionResult = await EncryptAsync(vaultResourceName, new MemoryStream(plaintextSegment.Array), keyName, encryptionOptions, - cancellationToken); + var encryptionResult = await EncryptAsync(vaultResourceName, new MemoryStream(plaintextSegment.Array), keyName, encryptionOptions, + cancellationToken); - var bufferedResult = new ArrayBufferWriter(); + var bufferedResult = new ArrayBufferWriter(); - await foreach (var item in encryptionResult.WithCancellation(cancellationToken)) - { - bufferedResult.Write(item.Span); - } - - return bufferedResult.WrittenMemory; + await foreach (var item in encryptionResult.WithCancellation(cancellationToken)) + { + bufferedResult.Write(item.Span); } - - throw new ArgumentException("The input instance doesn't have a valid underlying data store.", nameof(plaintextBytes)); + + return bufferedResult.WrittenMemory; } - /// - [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public override async Task>> EncryptAsync(string vaultResourceName, Stream plaintextStream, - string keyName, EncryptionOptions encryptionOptions, CancellationToken cancellationToken = default) + throw new ArgumentException("The input instance doesn't have a valid underlying data store.", nameof(plaintextBytes)); + } + + /// + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public override async Task>> EncryptAsync(string vaultResourceName, Stream plaintextStream, + string keyName, EncryptionOptions encryptionOptions, CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); + ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); + ArgumentVerifier.ThrowIfNull(plaintextStream, nameof(plaintextStream)); + ArgumentVerifier.ThrowIfNull(encryptionOptions, nameof(encryptionOptions)); + + var shouldOmitDecryptionKeyName = string.IsNullOrWhiteSpace(encryptionOptions.DecryptionKeyName); //Whitespace isn't likely a valid key name either + + var encryptRequestOptions = new Autogenerated.EncryptRequestOptions + { + ComponentName = vaultResourceName, + DataEncryptionCipher = encryptionOptions.EncryptionCipher.GetValueFromEnumMember(), + KeyName = keyName, + KeyWrapAlgorithm = encryptionOptions.KeyWrapAlgorithm.GetValueFromEnumMember(), + OmitDecryptionKeyName = shouldOmitDecryptionKeyName + }; + + if (!shouldOmitDecryptionKeyName) { - ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); - ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); - ArgumentVerifier.ThrowIfNull(plaintextStream, nameof(plaintextStream)); - ArgumentVerifier.ThrowIfNull(encryptionOptions, nameof(encryptionOptions)); + ArgumentVerifier.ThrowIfNullOrEmpty(encryptionOptions.DecryptionKeyName, nameof(encryptionOptions.DecryptionKeyName)); + encryptRequestOptions.DecryptionKeyName = encryptRequestOptions.DecryptionKeyName; + } - var shouldOmitDecryptionKeyName = string.IsNullOrWhiteSpace(encryptionOptions.DecryptionKeyName); //Whitespace isn't likely a valid key name either + var options = CreateCallOptions(headers: null, cancellationToken); + var duplexStream = client.EncryptAlpha1(options); - var encryptRequestOptions = new Autogenerated.EncryptRequestOptions - { - ComponentName = vaultResourceName, - DataEncryptionCipher = encryptionOptions.EncryptionCipher.GetValueFromEnumMember(), - KeyName = keyName, - KeyWrapAlgorithm = encryptionOptions.KeyWrapAlgorithm.GetValueFromEnumMember(), - OmitDecryptionKeyName = shouldOmitDecryptionKeyName - }; + //Run both operations at the same time, but return the output of the streaming values coming from the operation + var receiveResult = Task.FromResult(RetrieveEncryptedStreamAsync(duplexStream, cancellationToken)); + return await Task.WhenAll( + //Stream the plaintext data to the sidecar in chunks + SendPlaintextStreamAsync(plaintextStream, encryptionOptions.StreamingBlockSizeInBytes, + duplexStream, encryptRequestOptions, cancellationToken), + //At the same time, retrieve the encrypted response from the sidecar + receiveResult).ContinueWith(_ => receiveResult.Result, cancellationToken); + } - if (!shouldOmitDecryptionKeyName) - { - ArgumentVerifier.ThrowIfNullOrEmpty(encryptionOptions.DecryptionKeyName, nameof(encryptionOptions.DecryptionKeyName)); - encryptRequestOptions.DecryptionKeyName = encryptRequestOptions.DecryptionKeyName; - } + /// + /// Sends the plaintext bytes in chunks to the sidecar to be encrypted. + /// + private async Task SendPlaintextStreamAsync(Stream plaintextStream, + int streamingBlockSizeInBytes, + AsyncDuplexStreamingCall duplexStream, + Autogenerated.EncryptRequestOptions encryptRequestOptions, + CancellationToken cancellationToken) + { + //Start with passing the metadata about the encryption request itself in the first message + await duplexStream.RequestStream.WriteAsync( + new Autogenerated.EncryptRequest {Options = encryptRequestOptions}, cancellationToken); - var options = CreateCallOptions(headers: null, cancellationToken); - var duplexStream = client.EncryptAlpha1(options); - - //Run both operations at the same time, but return the output of the streaming values coming from the operation - var receiveResult = Task.FromResult(RetrieveEncryptedStreamAsync(duplexStream, cancellationToken)); - return await Task.WhenAll( - //Stream the plaintext data to the sidecar in chunks - SendPlaintextStreamAsync(plaintextStream, encryptionOptions.StreamingBlockSizeInBytes, - duplexStream, encryptRequestOptions, cancellationToken), - //At the same time, retrieve the encrypted response from the sidecar - receiveResult).ContinueWith(_ => receiveResult.Result, cancellationToken); - } - - /// - /// Sends the plaintext bytes in chunks to the sidecar to be encrypted. - /// - private async Task SendPlaintextStreamAsync(Stream plaintextStream, - int streamingBlockSizeInBytes, - AsyncDuplexStreamingCall duplexStream, - Autogenerated.EncryptRequestOptions encryptRequestOptions, - CancellationToken cancellationToken) - { - //Start with passing the metadata about the encryption request itself in the first message - await duplexStream.RequestStream.WriteAsync( - new Autogenerated.EncryptRequest {Options = encryptRequestOptions}, cancellationToken); - - //Send the plaintext bytes in blocks in subsequent messages - await using (var bufferedStream = new BufferedStream(plaintextStream, streamingBlockSizeInBytes)) - { - var buffer = new byte[streamingBlockSizeInBytes]; - int bytesRead; - ulong sequenceNumber = 0; + //Send the plaintext bytes in blocks in subsequent messages + await using (var bufferedStream = new BufferedStream(plaintextStream, streamingBlockSizeInBytes)) + { + var buffer = new byte[streamingBlockSizeInBytes]; + int bytesRead; + ulong sequenceNumber = 0; - while ((bytesRead = - await bufferedStream.ReadAsync(buffer.AsMemory(0, streamingBlockSizeInBytes), cancellationToken)) != - 0) - { - await duplexStream.RequestStream.WriteAsync( - new Autogenerated.EncryptRequest + while ((bytesRead = + await bufferedStream.ReadAsync(buffer.AsMemory(0, streamingBlockSizeInBytes), cancellationToken)) != + 0) + { + await duplexStream.RequestStream.WriteAsync( + new Autogenerated.EncryptRequest + { + Payload = new Autogenerated.StreamPayload { - Payload = new Autogenerated.StreamPayload - { - Data = ByteString.CopyFrom(buffer, 0, bytesRead), Seq = sequenceNumber - } - }, cancellationToken); - - //Increment the sequence number - sequenceNumber++; - } - } + Data = ByteString.CopyFrom(buffer, 0, bytesRead), Seq = sequenceNumber + } + }, cancellationToken); - //Send the completion message - await duplexStream.RequestStream.CompleteAsync(); + //Increment the sequence number + sequenceNumber++; + } } - /// - /// Retrieves the encrypted bytes from the encryption operation on the sidecar and returns as an enumerable stream. - /// - private async IAsyncEnumerable> RetrieveEncryptedStreamAsync(AsyncDuplexStreamingCall duplexStream, [EnumeratorCancellation] CancellationToken cancellationToken) + //Send the completion message + await duplexStream.RequestStream.CompleteAsync(); + } + + /// + /// Retrieves the encrypted bytes from the encryption operation on the sidecar and returns as an enumerable stream. + /// + private async IAsyncEnumerable> RetrieveEncryptedStreamAsync(AsyncDuplexStreamingCall duplexStream, [EnumeratorCancellation] CancellationToken cancellationToken) + { + await foreach (var encryptResponse in duplexStream.ResponseStream.ReadAllAsync(cancellationToken) + .ConfigureAwait(false)) { - await foreach (var encryptResponse in duplexStream.ResponseStream.ReadAllAsync(cancellationToken) - .ConfigureAwait(false)) - { - yield return encryptResponse.Payload.Data.Memory; - } + yield return encryptResponse.Payload.Data.Memory; } + } - /// - [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public override async Task>> DecryptAsync(string vaultResourceName, Stream ciphertextStream, string keyName, - DecryptionOptions decryptionOptions, CancellationToken cancellationToken = default) - { - ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); - ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); - ArgumentVerifier.ThrowIfNull(ciphertextStream, nameof(ciphertextStream)); - ArgumentVerifier.ThrowIfNull(decryptionOptions, nameof(decryptionOptions)); + /// + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public override async Task>> DecryptAsync(string vaultResourceName, Stream ciphertextStream, string keyName, + DecryptionOptions decryptionOptions, CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); + ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); + ArgumentVerifier.ThrowIfNull(ciphertextStream, nameof(ciphertextStream)); + ArgumentVerifier.ThrowIfNull(decryptionOptions, nameof(decryptionOptions)); - var decryptRequestOptions = new Autogenerated.DecryptRequestOptions - { - ComponentName = vaultResourceName, - KeyName = keyName - }; + var decryptRequestOptions = new Autogenerated.DecryptRequestOptions + { + ComponentName = vaultResourceName, + KeyName = keyName + }; - var options = CreateCallOptions(headers: null, cancellationToken); - var duplexStream = client.DecryptAlpha1(options); + var options = CreateCallOptions(headers: null, cancellationToken); + var duplexStream = client.DecryptAlpha1(options); - //Run both operations at the same time, but return the output of the streaming values coming from the operation - var receiveResult = Task.FromResult(RetrieveDecryptedStreamAsync(duplexStream, cancellationToken)); - return await Task.WhenAll( + //Run both operations at the same time, but return the output of the streaming values coming from the operation + var receiveResult = Task.FromResult(RetrieveDecryptedStreamAsync(duplexStream, cancellationToken)); + return await Task.WhenAll( //Stream the ciphertext data to the sidecar in chunks SendCiphertextStreamAsync(ciphertextStream, decryptionOptions.StreamingBlockSizeInBytes, duplexStream, decryptRequestOptions, cancellationToken), //At the same time, retrieve the decrypted response from the sidecar receiveResult) - //Return only the result of the `RetrieveEncryptedStreamAsync` method + //Return only the result of the `RetrieveEncryptedStreamAsync` method .ContinueWith(t => receiveResult.Result, cancellationToken); - } + } - /// - [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public override Task>> DecryptAsync(string vaultResourceName, - Stream ciphertextStream, string keyName, CancellationToken cancellationToken = default) => - DecryptAsync(vaultResourceName, ciphertextStream, keyName, new DecryptionOptions(), - cancellationToken); + /// + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public override Task>> DecryptAsync(string vaultResourceName, + Stream ciphertextStream, string keyName, CancellationToken cancellationToken = default) => + DecryptAsync(vaultResourceName, ciphertextStream, keyName, new DecryptionOptions(), + cancellationToken); - /// - /// Sends the ciphertext bytes in chunks to the sidecar to be decrypted. - /// - private async Task SendCiphertextStreamAsync(Stream ciphertextStream, - int streamingBlockSizeInBytes, - AsyncDuplexStreamingCall duplexStream, - Autogenerated.DecryptRequestOptions decryptRequestOptions, - CancellationToken cancellationToken) - { - //Start with passing the metadata about the decryption request itself in the first message - await duplexStream.RequestStream.WriteAsync( - new Autogenerated.DecryptRequest { Options = decryptRequestOptions }, cancellationToken); + /// + /// Sends the ciphertext bytes in chunks to the sidecar to be decrypted. + /// + private async Task SendCiphertextStreamAsync(Stream ciphertextStream, + int streamingBlockSizeInBytes, + AsyncDuplexStreamingCall duplexStream, + Autogenerated.DecryptRequestOptions decryptRequestOptions, + CancellationToken cancellationToken) + { + //Start with passing the metadata about the decryption request itself in the first message + await duplexStream.RequestStream.WriteAsync( + new Autogenerated.DecryptRequest { Options = decryptRequestOptions }, cancellationToken); - //Send the ciphertext bytes in blocks in subsequent messages - await using (var bufferedStream = new BufferedStream(ciphertextStream, streamingBlockSizeInBytes)) - { - var buffer = new byte[streamingBlockSizeInBytes]; - int bytesRead; - ulong sequenceNumber = 0; + //Send the ciphertext bytes in blocks in subsequent messages + await using (var bufferedStream = new BufferedStream(ciphertextStream, streamingBlockSizeInBytes)) + { + var buffer = new byte[streamingBlockSizeInBytes]; + int bytesRead; + ulong sequenceNumber = 0; - while ((bytesRead = await bufferedStream.ReadAsync(buffer.AsMemory(0, streamingBlockSizeInBytes), cancellationToken)) != 0) + while ((bytesRead = await bufferedStream.ReadAsync(buffer.AsMemory(0, streamingBlockSizeInBytes), cancellationToken)) != 0) + { + await duplexStream.RequestStream.WriteAsync(new Autogenerated.DecryptRequest { - await duplexStream.RequestStream.WriteAsync(new Autogenerated.DecryptRequest + Payload = new Autogenerated.StreamPayload { - Payload = new Autogenerated.StreamPayload - { - Data = ByteString.CopyFrom(buffer, 0, bytesRead), - Seq = sequenceNumber - } - }, cancellationToken); + Data = ByteString.CopyFrom(buffer, 0, bytesRead), + Seq = sequenceNumber + } + }, cancellationToken); - //Increment the sequence number - sequenceNumber++; - } + //Increment the sequence number + sequenceNumber++; } - - //Send the completion message - await duplexStream.RequestStream.CompleteAsync(); } + + //Send the completion message + await duplexStream.RequestStream.CompleteAsync(); + } - /// - /// Retrieves the decrypted bytes from the decryption operation on the sidecar and returns as an enumerable stream. - /// - private async IAsyncEnumerable> RetrieveDecryptedStreamAsync( - AsyncDuplexStreamingCall duplexStream, - [EnumeratorCancellation] CancellationToken cancellationToken) + /// + /// Retrieves the decrypted bytes from the decryption operation on the sidecar and returns as an enumerable stream. + /// + private async IAsyncEnumerable> RetrieveDecryptedStreamAsync( + AsyncDuplexStreamingCall duplexStream, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + await foreach (var decryptResponse in duplexStream.ResponseStream.ReadAllAsync(cancellationToken) + .ConfigureAwait(false)) { - await foreach (var decryptResponse in duplexStream.ResponseStream.ReadAllAsync(cancellationToken) - .ConfigureAwait(false)) - { - yield return decryptResponse.Payload.Data.Memory; - } + yield return decryptResponse.Payload.Data.Memory; } + } - /// - [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public override async Task> DecryptAsync(string vaultResourceName, - ReadOnlyMemory ciphertextBytes, string keyName, DecryptionOptions decryptionOptions, - CancellationToken cancellationToken = default) + /// + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public override async Task> DecryptAsync(string vaultResourceName, + ReadOnlyMemory ciphertextBytes, string keyName, DecryptionOptions decryptionOptions, + CancellationToken cancellationToken = default) + { + if (MemoryMarshal.TryGetArray(ciphertextBytes, out var ciphertextSegment) && ciphertextSegment.Array != null) { - if (MemoryMarshal.TryGetArray(ciphertextBytes, out var ciphertextSegment) && ciphertextSegment.Array != null) - { - var decryptionResult = await DecryptAsync(vaultResourceName, new MemoryStream(ciphertextSegment.Array), - keyName, decryptionOptions, cancellationToken); + var decryptionResult = await DecryptAsync(vaultResourceName, new MemoryStream(ciphertextSegment.Array), + keyName, decryptionOptions, cancellationToken); - var bufferedResult = new ArrayBufferWriter(); - await foreach (var item in decryptionResult.WithCancellation(cancellationToken)) - { - bufferedResult.Write(item.Span); - } - - return bufferedResult.WrittenMemory; + var bufferedResult = new ArrayBufferWriter(); + await foreach (var item in decryptionResult.WithCancellation(cancellationToken)) + { + bufferedResult.Write(item.Span); } - throw new ArgumentException("The input instance doesn't have a valid underlying data store", nameof(ciphertextBytes)); + return bufferedResult.WrittenMemory; } - /// - [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public override async Task> DecryptAsync(string vaultResourceName, - ReadOnlyMemory ciphertextBytes, string keyName, CancellationToken cancellationToken = default) => - await DecryptAsync(vaultResourceName, ciphertextBytes, keyName, - new DecryptionOptions(), cancellationToken); - - #region Subtle Crypto Implementation - - ///// - //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - //public override async Task<(string Name, string PublicKey)> GetKeyAsync(string vaultResourceName, string keyName, Autogenerated.SubtleGetKeyRequest.Types.KeyFormat keyFormat, - // CancellationToken cancellationToken = default) - //{ - // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); - // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); - - // var envelope = new Autogenerated.SubtleGetKeyRequest() - // { - // ComponentName = vaultResourceName, Format = keyFormat, Name = keyName - // }; - - // var options = CreateCallOptions(headers: null, cancellationToken); - // Autogenerated.SubtleGetKeyResponse response; - - // try - // { - // response = await client.SubtleGetKeyAlpha1Async(envelope, options); - // } - // catch (RpcException ex) - // { - // throw new DaprException( - // "Cryptography operation failed: the Dapr endpoint indicated a failure. See InnerException for details", ex); - // } - - // return (response.Name, response.PublicKey); - //} - - ///// - //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - //public override async Task<(byte[] CipherTextBytes, byte[] AuthenticationTag)> EncryptAsync(string vaultResourceName, byte[] plainTextBytes, string algorithm, - // string keyName, byte[] nonce, byte[] associatedData, CancellationToken cancellationToken = default) - //{ - // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); - // ArgumentVerifier.ThrowIfNullOrEmpty(algorithm, nameof(algorithm)); - // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); - - // var envelope = new Autogenerated.SubtleEncryptRequest - // { - // ComponentName = vaultResourceName, - // Algorithm = algorithm, - // KeyName = keyName, - // Nonce = ByteString.CopyFrom(nonce), - // Plaintext = ByteString.CopyFrom(plainTextBytes), - // AssociatedData = ByteString.CopyFrom(associatedData) - // }; - - // var options = CreateCallOptions(headers: null, cancellationToken); - // Autogenerated.SubtleEncryptResponse response; - - // try - // { - // response = await client.SubtleEncryptAlpha1Async(envelope, options); - // } - // catch (RpcException ex) - // { - // throw new DaprException( - // "Cryptography operation failed: the Dapr endpoint indicated a failure. See InnerException for details", - // ex); - // } - - // return (response.Ciphertext.ToByteArray(), response.Tag.ToByteArray() ?? Array.Empty()); - //} - - ///// - //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - //public override async Task DecryptAsync(string vaultResourceName, byte[] cipherTextBytes, string algorithm, string keyName, byte[] nonce, byte[] tag, - // byte[] associatedData, CancellationToken cancellationToken = default) - //{ - // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); - // ArgumentVerifier.ThrowIfNullOrEmpty(algorithm, nameof(algorithm)); - // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); - - // var envelope = new Autogenerated.SubtleDecryptRequest - // { - // ComponentName = vaultResourceName, - // Algorithm = algorithm, - // KeyName = keyName, - // Nonce = ByteString.CopyFrom(nonce), - // Ciphertext = ByteString.CopyFrom(cipherTextBytes), - // AssociatedData = ByteString.CopyFrom(associatedData), - // Tag = ByteString.CopyFrom(tag) - // }; - - // var options = CreateCallOptions(headers: null, cancellationToken); - // Autogenerated.SubtleDecryptResponse response; - - // try - // { - // response = await client.SubtleDecryptAlpha1Async(envelope, options); - // } - // catch (RpcException ex) - // { - // throw new DaprException( - // "Cryptography operation failed: the Dapr endpoint included a failure. See InnerException for details", ex); - // } - - // return response.Plaintext.ToByteArray(); - //} - - ///// - //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - //public override async Task<(byte[] WrappedKey, byte[] AuthenticationTag)> WrapKeyAsync(string vaultResourceName, byte[] plainTextKey, string keyName, - // string algorithm, byte[] nonce, byte[] associatedData, CancellationToken cancellationToken = default) - //{ - // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); - // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); - // ArgumentVerifier.ThrowIfNullOrEmpty(algorithm, nameof(algorithm)); - - // var envelope = new Autogenerated.SubtleWrapKeyRequest - // { - // ComponentName = vaultResourceName, - // Algorithm = algorithm, - // KeyName = keyName, - // Nonce = ByteString.CopyFrom(nonce), - // PlaintextKey = ByteString.CopyFrom(plainTextKey), - // AssociatedData = ByteString.CopyFrom(associatedData) - // }; - - // var options = CreateCallOptions(headers: null, cancellationToken); - // Autogenerated.SubtleWrapKeyResponse response; - - // try - // { - // response = await client.SubtleWrapKeyAlpha1Async(envelope, options); - // } - // catch (RpcException ex) - // { - // throw new DaprException( - // "Cryptography operation failed: the Dapr endpoint included a failure. See InnerException for details", - // ex); - // } - - // return (response.WrappedKey.ToByteArray(), response.Tag.ToByteArray() ?? Array.Empty()); - //} - - ///// - //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - //public override async Task UnwrapKeyAsync(string vaultResourceName, byte[] wrappedKey, string algorithm, - // string keyName, byte[] nonce, byte[] tag, byte[] associatedData, CancellationToken cancellationToken = default) - //{ - // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); - // ArgumentVerifier.ThrowIfNullOrEmpty(algorithm, nameof(algorithm)); - // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); - - // var envelope = new Autogenerated.SubtleUnwrapKeyRequest - // { - // ComponentName = vaultResourceName, - // WrappedKey = ByteString.CopyFrom(wrappedKey), - // AssociatedData = ByteString.CopyFrom(associatedData), - // Algorithm = algorithm, - // KeyName = keyName, - // Nonce = ByteString.CopyFrom(nonce), - // Tag = ByteString.CopyFrom(tag) - // }; - - // var options = CreateCallOptions(headers: null, cancellationToken); - // Autogenerated.SubtleUnwrapKeyResponse response; - - // try - // { - // response = await client.SubtleUnwrapKeyAlpha1Async(envelope, options); - // } - // catch (RpcException ex) - // { - // throw new DaprException( - // "Cryptography operation failed: the Dapr endpoint included a failure. See InnerException for details", - // ex); - // } - - // return response.PlaintextKey.ToByteArray(); - //} - - ///// - //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - //public override async Task SignAsync(string vaultResourceName, byte[] digest, string algorithm, string keyName, CancellationToken cancellationToken = default) - //{ - // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); - // ArgumentVerifier.ThrowIfNullOrEmpty(algorithm, nameof(algorithm)); - // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); - - // var envelope = new Autogenerated.SubtleSignRequest - // { - // ComponentName = vaultResourceName, - // Digest = ByteString.CopyFrom(digest), - // Algorithm = algorithm, - // KeyName = keyName - // }; - - // var options = CreateCallOptions(headers: null, cancellationToken); - // Autogenerated.SubtleSignResponse response; - - // try - // { - // response = await client.SubtleSignAlpha1Async(envelope, options); - // } - // catch (RpcException ex) - // { - // throw new DaprException( - // "Cryptography operation failed: the Dapr endpoint included a failure. See InnerException for details", - // ex); - // } - - // return response.Signature.ToByteArray(); - //} - - ///// - //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - //public override async Task VerifyAsync(string vaultResourceName, byte[] digest, byte[] signature, - // string algorithm, string keyName, CancellationToken cancellationToken = default) - //{ - // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); - // ArgumentVerifier.ThrowIfNullOrEmpty(algorithm, nameof(algorithm)); - // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); - - // var envelope = new Autogenerated.SubtleVerifyRequest - // { - // ComponentName = vaultResourceName, - // Algorithm = algorithm, - // KeyName = keyName, - // Signature = ByteString.CopyFrom(signature), - // Digest = ByteString.CopyFrom(digest) - // }; - - // var options = CreateCallOptions(headers: null, cancellationToken); - // Autogenerated.SubtleVerifyResponse response; - - // try - // { - // response = await client.SubtleVerifyAlpha1Async(envelope, options); - // } - // catch (RpcException ex) - // { - // throw new DaprException( - // "Cryptography operation failed: the Dapr endpoint included a failure. See InnerException for details", - // ex); - // } - - // return response.Valid; - //} - - #endregion - - - #endregion - - #region Distributed Lock API - /// - [Obsolete] - public async override Task Lock( - string storeName, - string resourceId, - string lockOwner, - Int32 expiryInSeconds, - CancellationToken cancellationToken = default) - { - ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); - ArgumentVerifier.ThrowIfNullOrEmpty(resourceId, nameof(resourceId)); - ArgumentVerifier.ThrowIfNullOrEmpty(lockOwner, nameof(lockOwner)); - - if (expiryInSeconds == 0 || expiryInSeconds < 0) - { - throw new ArgumentException("The value cannot be zero or less than zero: " + expiryInSeconds); - } + throw new ArgumentException("The input instance doesn't have a valid underlying data store", nameof(ciphertextBytes)); + } - var request = new Autogenerated.TryLockRequest() - { - StoreName = storeName, - ResourceId = resourceId, - LockOwner = lockOwner, - ExpiryInSeconds = expiryInSeconds - }; + /// + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public override async Task> DecryptAsync(string vaultResourceName, + ReadOnlyMemory ciphertextBytes, string keyName, CancellationToken cancellationToken = default) => + await DecryptAsync(vaultResourceName, ciphertextBytes, keyName, + new DecryptionOptions(), cancellationToken); + + #region Subtle Crypto Implementation + + ///// + //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public override async Task<(string Name, string PublicKey)> GetKeyAsync(string vaultResourceName, string keyName, Autogenerated.SubtleGetKeyRequest.Types.KeyFormat keyFormat, + // CancellationToken cancellationToken = default) + //{ + // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); + // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); + + // var envelope = new Autogenerated.SubtleGetKeyRequest() + // { + // ComponentName = vaultResourceName, Format = keyFormat, Name = keyName + // }; + + // var options = CreateCallOptions(headers: null, cancellationToken); + // Autogenerated.SubtleGetKeyResponse response; + + // try + // { + // response = await client.SubtleGetKeyAlpha1Async(envelope, options); + // } + // catch (RpcException ex) + // { + // throw new DaprException( + // "Cryptography operation failed: the Dapr endpoint indicated a failure. See InnerException for details", ex); + // } + + // return (response.Name, response.PublicKey); + //} + + ///// + //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public override async Task<(byte[] CipherTextBytes, byte[] AuthenticationTag)> EncryptAsync(string vaultResourceName, byte[] plainTextBytes, string algorithm, + // string keyName, byte[] nonce, byte[] associatedData, CancellationToken cancellationToken = default) + //{ + // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); + // ArgumentVerifier.ThrowIfNullOrEmpty(algorithm, nameof(algorithm)); + // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); + + // var envelope = new Autogenerated.SubtleEncryptRequest + // { + // ComponentName = vaultResourceName, + // Algorithm = algorithm, + // KeyName = keyName, + // Nonce = ByteString.CopyFrom(nonce), + // Plaintext = ByteString.CopyFrom(plainTextBytes), + // AssociatedData = ByteString.CopyFrom(associatedData) + // }; + + // var options = CreateCallOptions(headers: null, cancellationToken); + // Autogenerated.SubtleEncryptResponse response; + + // try + // { + // response = await client.SubtleEncryptAlpha1Async(envelope, options); + // } + // catch (RpcException ex) + // { + // throw new DaprException( + // "Cryptography operation failed: the Dapr endpoint indicated a failure. See InnerException for details", + // ex); + // } + + // return (response.Ciphertext.ToByteArray(), response.Tag.ToByteArray() ?? Array.Empty()); + //} + + ///// + //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public override async Task DecryptAsync(string vaultResourceName, byte[] cipherTextBytes, string algorithm, string keyName, byte[] nonce, byte[] tag, + // byte[] associatedData, CancellationToken cancellationToken = default) + //{ + // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); + // ArgumentVerifier.ThrowIfNullOrEmpty(algorithm, nameof(algorithm)); + // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); + + // var envelope = new Autogenerated.SubtleDecryptRequest + // { + // ComponentName = vaultResourceName, + // Algorithm = algorithm, + // KeyName = keyName, + // Nonce = ByteString.CopyFrom(nonce), + // Ciphertext = ByteString.CopyFrom(cipherTextBytes), + // AssociatedData = ByteString.CopyFrom(associatedData), + // Tag = ByteString.CopyFrom(tag) + // }; + + // var options = CreateCallOptions(headers: null, cancellationToken); + // Autogenerated.SubtleDecryptResponse response; + + // try + // { + // response = await client.SubtleDecryptAlpha1Async(envelope, options); + // } + // catch (RpcException ex) + // { + // throw new DaprException( + // "Cryptography operation failed: the Dapr endpoint included a failure. See InnerException for details", ex); + // } + + // return response.Plaintext.ToByteArray(); + //} + + ///// + //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public override async Task<(byte[] WrappedKey, byte[] AuthenticationTag)> WrapKeyAsync(string vaultResourceName, byte[] plainTextKey, string keyName, + // string algorithm, byte[] nonce, byte[] associatedData, CancellationToken cancellationToken = default) + //{ + // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); + // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); + // ArgumentVerifier.ThrowIfNullOrEmpty(algorithm, nameof(algorithm)); + + // var envelope = new Autogenerated.SubtleWrapKeyRequest + // { + // ComponentName = vaultResourceName, + // Algorithm = algorithm, + // KeyName = keyName, + // Nonce = ByteString.CopyFrom(nonce), + // PlaintextKey = ByteString.CopyFrom(plainTextKey), + // AssociatedData = ByteString.CopyFrom(associatedData) + // }; + + // var options = CreateCallOptions(headers: null, cancellationToken); + // Autogenerated.SubtleWrapKeyResponse response; + + // try + // { + // response = await client.SubtleWrapKeyAlpha1Async(envelope, options); + // } + // catch (RpcException ex) + // { + // throw new DaprException( + // "Cryptography operation failed: the Dapr endpoint included a failure. See InnerException for details", + // ex); + // } + + // return (response.WrappedKey.ToByteArray(), response.Tag.ToByteArray() ?? Array.Empty()); + //} + + ///// + //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public override async Task UnwrapKeyAsync(string vaultResourceName, byte[] wrappedKey, string algorithm, + // string keyName, byte[] nonce, byte[] tag, byte[] associatedData, CancellationToken cancellationToken = default) + //{ + // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); + // ArgumentVerifier.ThrowIfNullOrEmpty(algorithm, nameof(algorithm)); + // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); + + // var envelope = new Autogenerated.SubtleUnwrapKeyRequest + // { + // ComponentName = vaultResourceName, + // WrappedKey = ByteString.CopyFrom(wrappedKey), + // AssociatedData = ByteString.CopyFrom(associatedData), + // Algorithm = algorithm, + // KeyName = keyName, + // Nonce = ByteString.CopyFrom(nonce), + // Tag = ByteString.CopyFrom(tag) + // }; + + // var options = CreateCallOptions(headers: null, cancellationToken); + // Autogenerated.SubtleUnwrapKeyResponse response; + + // try + // { + // response = await client.SubtleUnwrapKeyAlpha1Async(envelope, options); + // } + // catch (RpcException ex) + // { + // throw new DaprException( + // "Cryptography operation failed: the Dapr endpoint included a failure. See InnerException for details", + // ex); + // } + + // return response.PlaintextKey.ToByteArray(); + //} + + ///// + //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public override async Task SignAsync(string vaultResourceName, byte[] digest, string algorithm, string keyName, CancellationToken cancellationToken = default) + //{ + // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); + // ArgumentVerifier.ThrowIfNullOrEmpty(algorithm, nameof(algorithm)); + // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); + + // var envelope = new Autogenerated.SubtleSignRequest + // { + // ComponentName = vaultResourceName, + // Digest = ByteString.CopyFrom(digest), + // Algorithm = algorithm, + // KeyName = keyName + // }; + + // var options = CreateCallOptions(headers: null, cancellationToken); + // Autogenerated.SubtleSignResponse response; + + // try + // { + // response = await client.SubtleSignAlpha1Async(envelope, options); + // } + // catch (RpcException ex) + // { + // throw new DaprException( + // "Cryptography operation failed: the Dapr endpoint included a failure. See InnerException for details", + // ex); + // } + + // return response.Signature.ToByteArray(); + //} + + ///// + //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public override async Task VerifyAsync(string vaultResourceName, byte[] digest, byte[] signature, + // string algorithm, string keyName, CancellationToken cancellationToken = default) + //{ + // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); + // ArgumentVerifier.ThrowIfNullOrEmpty(algorithm, nameof(algorithm)); + // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); + + // var envelope = new Autogenerated.SubtleVerifyRequest + // { + // ComponentName = vaultResourceName, + // Algorithm = algorithm, + // KeyName = keyName, + // Signature = ByteString.CopyFrom(signature), + // Digest = ByteString.CopyFrom(digest) + // }; + + // var options = CreateCallOptions(headers: null, cancellationToken); + // Autogenerated.SubtleVerifyResponse response; + + // try + // { + // response = await client.SubtleVerifyAlpha1Async(envelope, options); + // } + // catch (RpcException ex) + // { + // throw new DaprException( + // "Cryptography operation failed: the Dapr endpoint included a failure. See InnerException for details", + // ex); + // } + + // return response.Valid; + //} + + #endregion + + + #endregion + + #region Distributed Lock API + /// + [Obsolete] + public async override Task Lock( + string storeName, + string resourceId, + string lockOwner, + Int32 expiryInSeconds, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); + ArgumentVerifier.ThrowIfNullOrEmpty(resourceId, nameof(resourceId)); + ArgumentVerifier.ThrowIfNullOrEmpty(lockOwner, nameof(lockOwner)); - try - { - var options = CreateCallOptions(headers: null, cancellationToken); - var response = await client.TryLockAlpha1Async(request, options); - return new TryLockResponse() - { - StoreName = storeName, - ResourceId = resourceId, - LockOwner = lockOwner, - Success = response.Success - }; - } - catch (RpcException ex) - { - throw new DaprException("Lock operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); - } + if (expiryInSeconds == 0 || expiryInSeconds < 0) + { + throw new ArgumentException("The value cannot be zero or less than zero: " + expiryInSeconds); } - /// - [Obsolete] - public async override Task Unlock( - string storeName, - string resourceId, - string lockOwner, - CancellationToken cancellationToken = default) + var request = new Autogenerated.TryLockRequest() { - ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); - ArgumentVerifier.ThrowIfNullOrEmpty(resourceId, nameof(resourceId)); - ArgumentVerifier.ThrowIfNullOrEmpty(lockOwner, nameof(lockOwner)); + StoreName = storeName, + ResourceId = resourceId, + LockOwner = lockOwner, + ExpiryInSeconds = expiryInSeconds + }; - var request = new Autogenerated.UnlockRequest() + try + { + var options = CreateCallOptions(headers: null, cancellationToken); + var response = await client.TryLockAlpha1Async(request, options); + return new TryLockResponse() { StoreName = storeName, ResourceId = resourceId, - LockOwner = lockOwner + LockOwner = lockOwner, + Success = response.Success }; + } + catch (RpcException ex) + { + throw new DaprException("Lock operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + } + } - var options = CreateCallOptions(headers: null, cancellationToken); - Autogenerated.UnlockResponse response = new Autogenerated.UnlockResponse(); - try - { - response = await client.UnlockAlpha1Async(request, options); - } - catch (RpcException ex) - { - throw new DaprException("Lock operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); - } + /// + [Obsolete] + public async override Task Unlock( + string storeName, + string resourceId, + string lockOwner, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); + ArgumentVerifier.ThrowIfNullOrEmpty(resourceId, nameof(resourceId)); + ArgumentVerifier.ThrowIfNullOrEmpty(lockOwner, nameof(lockOwner)); + + var request = new Autogenerated.UnlockRequest() + { + StoreName = storeName, + ResourceId = resourceId, + LockOwner = lockOwner + }; - return new UnlockResponse(GetUnLockStatus(response.Status)); + var options = CreateCallOptions(headers: null, cancellationToken); + Autogenerated.UnlockResponse response = new Autogenerated.UnlockResponse(); + try + { + response = await client.UnlockAlpha1Async(request, options); + } + catch (RpcException ex) + { + throw new DaprException("Lock operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); } - #endregion + return new UnlockResponse(GetUnLockStatus(response.Status)); + } - #region Dapr Sidecar Methods + #endregion - /// - public override async Task CheckHealthAsync(CancellationToken cancellationToken = default) - { - var path = "/v1.0/healthz"; - var request = new HttpRequestMessage(HttpMethod.Get, new Uri(this.httpEndpoint, path)); + #region Dapr Sidecar Methods - if (this.apiTokenHeader is not null) - { - request.Headers.Add(this.apiTokenHeader.Value.Key, this.apiTokenHeader.Value.Value); - } + /// + public override async Task CheckHealthAsync(CancellationToken cancellationToken = default) + { + var path = "/v1.0/healthz"; + var request = new HttpRequestMessage(HttpMethod.Get, new Uri(this.httpEndpoint, path)); - try - { - using var response = await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); - return response.IsSuccessStatusCode; - } - catch (HttpRequestException) - { - return false; - } + if (this.apiTokenHeader is not null) + { + request.Headers.Add(this.apiTokenHeader.Value.Key, this.apiTokenHeader.Value.Value); } - /// - public override async Task CheckOutboundHealthAsync(CancellationToken cancellationToken = default) + try + { + using var response = await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + return response.IsSuccessStatusCode; + } + catch (HttpRequestException) { - var path = "/v1.0/healthz/outbound"; - var request = new HttpRequestMessage(HttpMethod.Get, new Uri(this.httpEndpoint, path)); + return false; + } + } - if (this.apiTokenHeader is not null) - { - request.Headers.Add(this.apiTokenHeader.Value.Key, this.apiTokenHeader.Value.Value); - } + /// + public override async Task CheckOutboundHealthAsync(CancellationToken cancellationToken = default) + { + var path = "/v1.0/healthz/outbound"; + var request = new HttpRequestMessage(HttpMethod.Get, new Uri(this.httpEndpoint, path)); - try - { - using var response = await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); - return response.IsSuccessStatusCode; - } - catch (HttpRequestException) - { - return false; - } + if (this.apiTokenHeader is not null) + { + request.Headers.Add(this.apiTokenHeader.Value.Key, this.apiTokenHeader.Value.Value); } - /// - public override async Task WaitForSidecarAsync(CancellationToken cancellationToken = default) + try { - while (true) - { - var response = await CheckOutboundHealthAsync(cancellationToken); - if (response) - { - break; - } - await Task.Delay(TimeSpan.FromMilliseconds(500), cancellationToken); - } + using var response = await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + return response.IsSuccessStatusCode; } - - /// - public async override Task ShutdownSidecarAsync(CancellationToken cancellationToken = default) + catch (HttpRequestException) { - await client.ShutdownAsync(new Autogenerated.ShutdownRequest(), CreateCallOptions(null, cancellationToken)); + return false; } + } - /// - public override async Task GetMetadataAsync(CancellationToken cancellationToken = default) + /// + public override async Task WaitForSidecarAsync(CancellationToken cancellationToken = default) + { + while (true) { - var options = CreateCallOptions(headers: null, cancellationToken); - try - { - var response = await client.GetMetadataAsync(new Autogenerated.GetMetadataRequest(), options); - return new DaprMetadata(response.Id ?? "", - response.ActorRuntime?.ActiveActors?.Select(c => new DaprActorMetadata(c.Type, c.Count)).ToList() ?? - new List(), - response.ExtendedMetadata?.ToDictionary(c => c.Key, c => c.Value) ?? - new Dictionary(), - response.RegisteredComponents?.Select(c => - new DaprComponentsMetadata(c.Name, c.Type, c.Version, c.Capabilities.ToArray())).ToList() ?? - new List()); - } - catch (RpcException ex) + var response = await CheckOutboundHealthAsync(cancellationToken); + if (response) { - throw new DaprException("Get metadata operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + break; } + await Task.Delay(TimeSpan.FromMilliseconds(500), cancellationToken); } + } + + /// + public async override Task ShutdownSidecarAsync(CancellationToken cancellationToken = default) + { + await client.ShutdownAsync(new Autogenerated.ShutdownRequest(), CreateCallOptions(null, cancellationToken)); + } - /// - public override async Task SetMetadataAsync(string attributeName, string attributeValue, CancellationToken cancellationToken = default) + /// + public override async Task GetMetadataAsync(CancellationToken cancellationToken = default) + { + var options = CreateCallOptions(headers: null, cancellationToken); + try + { + var response = await client.GetMetadataAsync(new Autogenerated.GetMetadataRequest(), options); + return new DaprMetadata(response.Id ?? "", + response.ActorRuntime?.ActiveActors?.Select(c => new DaprActorMetadata(c.Type, c.Count)).ToList() ?? + new List(), + response.ExtendedMetadata?.ToDictionary(c => c.Key, c => c.Value) ?? + new Dictionary(), + response.RegisteredComponents?.Select(c => + new DaprComponentsMetadata(c.Name, c.Type, c.Version, c.Capabilities.ToArray())).ToList() ?? + new List()); + } + catch (RpcException ex) { - ArgumentVerifier.ThrowIfNullOrEmpty(attributeName, nameof(attributeName)); + throw new DaprException("Get metadata operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + } + } - var envelope = new Autogenerated.SetMetadataRequest() - { - Key = attributeName, - Value = attributeValue - }; + /// + public override async Task SetMetadataAsync(string attributeName, string attributeValue, CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(attributeName, nameof(attributeName)); - var options = CreateCallOptions(headers: null, cancellationToken); + var envelope = new Autogenerated.SetMetadataRequest() + { + Key = attributeName, + Value = attributeValue + }; - try - { - _ = await this.Client.SetMetadataAsync(envelope, options); - } - catch (RpcException ex) - { - throw new DaprException("Set metadata operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); - } - } - #endregion + var options = CreateCallOptions(headers: null, cancellationToken); - protected override void Dispose(bool disposing) + try { - if (disposing) - { - this.channel.Dispose(); - this.httpClient.Dispose(); - } + _ = await this.Client.SetMetadataAsync(envelope, options); } + catch (RpcException ex) + { + throw new DaprException("Set metadata operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + } + } + #endregion - #region Helper Methods - - private CallOptions CreateCallOptions(Metadata headers, CancellationToken cancellationToken) + protected override void Dispose(bool disposing) + { + if (disposing) { - var options = new CallOptions(headers: headers ?? new Metadata(), cancellationToken: cancellationToken); + this.channel.Dispose(); + this.httpClient.Dispose(); + } + } - options.Headers.Add("User-Agent", UserAgent().ToString()); + #region Helper Methods - // add token for dapr api token based authentication - if (this.apiTokenHeader is not null) - { - options.Headers.Add(this.apiTokenHeader.Value.Key, this.apiTokenHeader.Value.Value); - } + private CallOptions CreateCallOptions(Metadata headers, CancellationToken cancellationToken) + { + var options = new CallOptions(headers: headers ?? new Metadata(), cancellationToken: cancellationToken); - return options; - } + options.Headers.Add("User-Agent", UserAgent().ToString()); - /// - /// Makes Grpc call using the cancellationToken and handles Errors. - /// All common exception handling logic will reside here. - /// - /// - /// - /// - /// - private async Task MakeGrpcCallHandleError(Func> callFunc, CancellationToken cancellationToken = default) + // add token for dapr api token based authentication + if (this.apiTokenHeader is not null) { - var callOptions = CreateCallOptions(headers: null, cancellationToken); - return await callFunc.Invoke(callOptions); + options.Headers.Add(this.apiTokenHeader.Value.Key, this.apiTokenHeader.Value.Value); } - private Autogenerated.StateOptions ToAutoGeneratedStateOptions(StateOptions stateOptions) - { - var stateRequestOptions = new Autogenerated.StateOptions(); + return options; + } - if (stateOptions.Consistency != null) - { - stateRequestOptions.Consistency = GetStateConsistencyForConsistencyMode(stateOptions.Consistency.Value); - } + /// + /// Makes Grpc call using the cancellationToken and handles Errors. + /// All common exception handling logic will reside here. + /// + /// + /// + /// + /// + private async Task MakeGrpcCallHandleError(Func> callFunc, CancellationToken cancellationToken = default) + { + var callOptions = CreateCallOptions(headers: null, cancellationToken); + return await callFunc.Invoke(callOptions); + } - if (stateOptions.Concurrency != null) - { - stateRequestOptions.Concurrency = GetStateConcurrencyForConcurrencyMode(stateOptions.Concurrency.Value); - } + private Autogenerated.StateOptions ToAutoGeneratedStateOptions(StateOptions stateOptions) + { + var stateRequestOptions = new Autogenerated.StateOptions(); - return stateRequestOptions; + if (stateOptions.Consistency != null) + { + stateRequestOptions.Consistency = GetStateConsistencyForConsistencyMode(stateOptions.Consistency.Value); } - private static Autogenerated.StateOptions.Types.StateConsistency GetStateConsistencyForConsistencyMode(ConsistencyMode consistencyMode) + if (stateOptions.Concurrency != null) { - return consistencyMode switch - { - ConsistencyMode.Eventual => Autogenerated.StateOptions.Types.StateConsistency.ConsistencyEventual, - ConsistencyMode.Strong => Autogenerated.StateOptions.Types.StateConsistency.ConsistencyStrong, - _ => throw new ArgumentException($"{consistencyMode} Consistency Mode is not supported.") - }; + stateRequestOptions.Concurrency = GetStateConcurrencyForConcurrencyMode(stateOptions.Concurrency.Value); } - private static Autogenerated.StateOptions.Types.StateConcurrency GetStateConcurrencyForConcurrencyMode(ConcurrencyMode concurrencyMode) + return stateRequestOptions; + } + + private static Autogenerated.StateOptions.Types.StateConsistency GetStateConsistencyForConsistencyMode(ConsistencyMode consistencyMode) + { + return consistencyMode switch { - return concurrencyMode switch - { - ConcurrencyMode.FirstWrite => Autogenerated.StateOptions.Types.StateConcurrency.ConcurrencyFirstWrite, - ConcurrencyMode.LastWrite => Autogenerated.StateOptions.Types.StateConcurrency.ConcurrencyLastWrite, - _ => throw new ArgumentException($"{concurrencyMode} Concurrency Mode is not supported.") - }; - } + ConsistencyMode.Eventual => Autogenerated.StateOptions.Types.StateConsistency.ConsistencyEventual, + ConsistencyMode.Strong => Autogenerated.StateOptions.Types.StateConsistency.ConsistencyStrong, + _ => throw new ArgumentException($"{consistencyMode} Consistency Mode is not supported.") + }; + } - private static LockStatus GetUnLockStatus(Autogenerated.UnlockResponse.Types.Status status) + private static Autogenerated.StateOptions.Types.StateConcurrency GetStateConcurrencyForConcurrencyMode(ConcurrencyMode concurrencyMode) + { + return concurrencyMode switch { - return status switch - { - Autogenerated.UnlockResponse.Types.Status.Success => LockStatus.Success, - Autogenerated.UnlockResponse.Types.Status.LockDoesNotExist => LockStatus.LockDoesNotExist, - Autogenerated.UnlockResponse.Types.Status.LockBelongsToOthers => LockStatus.LockBelongsToOthers, - Autogenerated.UnlockResponse.Types.Status.InternalError => LockStatus.InternalError, - _ => throw new ArgumentException($"{status} Status is not supported.") - }; - } + ConcurrencyMode.FirstWrite => Autogenerated.StateOptions.Types.StateConcurrency.ConcurrencyFirstWrite, + ConcurrencyMode.LastWrite => Autogenerated.StateOptions.Types.StateConcurrency.ConcurrencyLastWrite, + _ => throw new ArgumentException($"{concurrencyMode} Concurrency Mode is not supported.") + }; + } - #endregion Helper Methods + private static LockStatus GetUnLockStatus(Autogenerated.UnlockResponse.Types.Status status) + { + return status switch + { + Autogenerated.UnlockResponse.Types.Status.Success => LockStatus.Success, + Autogenerated.UnlockResponse.Types.Status.LockDoesNotExist => LockStatus.LockDoesNotExist, + Autogenerated.UnlockResponse.Types.Status.LockBelongsToOthers => LockStatus.LockBelongsToOthers, + Autogenerated.UnlockResponse.Types.Status.InternalError => LockStatus.InternalError, + _ => throw new ArgumentException($"{status} Status is not supported.") + }; } + + #endregion Helper Methods } diff --git a/src/Dapr.Client/Extensions/EnumExtensions.cs b/src/Dapr.Client/Extensions/EnumExtensions.cs deleted file mode 100644 index df9c9ad33..000000000 --- a/src/Dapr.Client/Extensions/EnumExtensions.cs +++ /dev/null @@ -1,41 +0,0 @@ -// ------------------------------------------------------------------------ -// Copyright 2023 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. -// ------------------------------------------------------------------------ - -#nullable enable -using System; -using System.Reflection; -using System.Runtime.Serialization; - -namespace Dapr.Client -{ - internal static class EnumExtensions - { - /// - /// Reads the value of an enum out of the attached attribute. - /// - /// The enum. - /// The value of the enum to pull the value for. - /// - public static string GetValueFromEnumMember(this T value) where T : Enum - { - ArgumentNullException.ThrowIfNull(value, nameof(value)); - - var memberInfo = typeof(T).GetMember(value.ToString(), BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly); - if (memberInfo.Length <= 0) - return value.ToString(); - - var attributes = memberInfo[0].GetCustomAttributes(typeof(EnumMemberAttribute), false); - return (attributes.Length > 0 ? ((EnumMemberAttribute)attributes[0]).Value : value.ToString()) ?? value.ToString(); - } - } -} diff --git a/src/Dapr.Common/AssemblyInfo.cs b/src/Dapr.Common/AssemblyInfo.cs index 5044876a9..3037485a9 100644 --- a/src/Dapr.Common/AssemblyInfo.cs +++ b/src/Dapr.Common/AssemblyInfo.cs @@ -16,9 +16,11 @@ [assembly: InternalsVisibleTo("Dapr.Actors, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.Actors.Generators, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.Actors.AspNetCore, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.AI, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [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")] @@ -27,6 +29,7 @@ [assembly: InternalsVisibleTo("Dapr.Actors.AspNetCore.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.Actors.Generators.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.Actors.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.AI.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.AspNetCore.IntegrationTest, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.AspNetCore.IntegrationTest.App, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.AspNetCore.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] @@ -40,3 +43,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..1aa860bbe --- /dev/null +++ b/src/Dapr.Common/DaprClientUtilities.cs @@ -0,0 +1,65 @@ +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..60a9827a2 100644 --- a/src/Dapr.Common/DaprGenericClientBuilder.cs +++ b/src/Dapr.Common/DaprGenericClientBuilder.cs @@ -1,4 +1,18 @@ -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 Grpc.Net.Client; using Microsoft.Extensions.Configuration; @@ -170,8 +184,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 +199,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.Common/Extensions/EnumExtensions.cs b/src/Dapr.Common/Extensions/EnumExtensions.cs index ff9b43706..0216c9258 100644 --- a/src/Dapr.Common/Extensions/EnumExtensions.cs +++ b/src/Dapr.Common/Extensions/EnumExtensions.cs @@ -1,4 +1,17 @@ -using System.Reflection; +// ------------------------------------------------------------------------ +// 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.Runtime.Serialization; namespace Dapr.Common.Extensions; diff --git a/src/Dapr.Common/JsonConverters/GenericEnumJsonConverter.cs b/src/Dapr.Common/JsonConverters/GenericEnumJsonConverter.cs new file mode 100644 index 000000000..be79c101a --- /dev/null +++ b/src/Dapr.Common/JsonConverters/GenericEnumJsonConverter.cs @@ -0,0 +1,70 @@ +// ------------------------------------------------------------------------ +// 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.Text.Json; +using System.Text.Json.Serialization; +using Dapr.Common.Extensions; + +namespace Dapr.Common.JsonConverters; + +/// +/// A JsonConverter used to convert from an enum to a string and vice versa, but using the Enum extension written to pull +/// the value from the [EnumMember] attribute, if present. +/// +/// The enum type to convert. +internal sealed class GenericEnumJsonConverter : JsonConverter where T : struct, Enum +{ + private static readonly Dictionary enumMemberCache = new(); + + static GenericEnumJsonConverter() + { + foreach (var enumValue in Enum.GetValues()) + { + var enumMemberValue = enumValue.GetValueFromEnumMember(); + enumMemberCache[enumMemberValue] = enumValue; + } + } + + /// Reads and converts the JSON to type . + /// The reader. + /// The type to convert. + /// An object that specifies serialization options to use. + /// The converted value. + public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + //Get the string value from the JSON reader + var value = reader.GetString(); + + //Try pulling the value from the cache + if (value is not null && enumMemberCache.TryGetValue(value, out var enumValue)) + { + return enumValue; + } + + //If no match found, throw an exception + throw new JsonException($"Invalid valid for {typeToConvert.Name}: {value}"); + } + + /// Writes a specified value as JSON. + /// The writer to write to. + /// The value to convert to JSON. + /// An object that specifies serialization options to use. + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + //Get the value from the EnumMember attribute, if any + var enumMemberValue = value.GetValueFromEnumMember(); + + //Write the value to the JSON writer + writer.WriteStringValue(enumMemberValue); + } +} diff --git a/src/Dapr.Jobs/DaprJobsClientBuilder.cs b/src/Dapr.Jobs/DaprJobsClientBuilder.cs index 390d52236..509486a1e 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 . + /// + /// 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..1f035220e 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 Autogenerated.Dapr.DaprClient Client => client; + /// + /// Property exposed for testing purposes. + /// + internal readonly string? DaprApiToken; + /// + /// The autogenerated Dapr client. + /// + /// + /// Property exposed for testing purposes. + /// + internal Autogenerated.Dapr.DaprClient Client { get; } 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(); + 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); - } - - 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/PublishSubscribe/DaprPublishSubscribeClientBuilder.cs b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClientBuilder.cs index b94bc5cdf..829bab75d 100644 --- a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClientBuilder.cs +++ b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClientBuilder.cs @@ -39,9 +39,8 @@ public DaprPublishSubscribeClientBuilder(IConfiguration? configuration = null) : /// 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..39024cb35 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. + /// + private readonly P.DaprClient Client; /// /// Creates a new instance of a /// - public DaprPublishSubscribeGrpcClient(P.DaprClient client) + public DaprPublishSubscribeGrpcClient(P.DaprClient client, HttpClient httpClient, string? daprApiToken) { - daprClient = client; + 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, 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..3d9e3ee8d 100644 --- a/src/Dapr.Messaging/PublishSubscribe/Extensions/PublishSubscribeServiceCollectionExtensions.cs +++ b/src/Dapr.Messaging/PublishSubscribe/Extensions/PublishSubscribeServiceCollectionExtensions.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; namespace Dapr.Messaging.PublishSubscribe.Extensions; @@ -25,8 +26,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/src/Dapr.Protos/Protos/dapr/proto/common/v1/common.proto b/src/Dapr.Protos/Protos/dapr/proto/common/v1/common.proto index 0eb882b89..fc5e99835 100644 --- a/src/Dapr.Protos/Protos/dapr/proto/common/v1/common.proto +++ b/src/Dapr.Protos/Protos/dapr/proto/common/v1/common.proto @@ -157,4 +157,4 @@ message ConfigurationItem { // the metadata which will be passed to/from configuration store component. map metadata = 3; -} \ No newline at end of file +} diff --git a/src/Dapr.Protos/Protos/dapr/proto/runtime/v1/appcallback.proto b/src/Dapr.Protos/Protos/dapr/proto/runtime/v1/appcallback.proto index 51dee5539..144e8c87a 100644 --- a/src/Dapr.Protos/Protos/dapr/proto/runtime/v1/appcallback.proto +++ b/src/Dapr.Protos/Protos/dapr/proto/runtime/v1/appcallback.proto @@ -340,4 +340,4 @@ message ListInputBindingsResponse { // HealthCheckResponse is the message with the response to the health check. // This message is currently empty as used as placeholder. -message HealthCheckResponse {} \ No newline at end of file +message HealthCheckResponse {} diff --git a/src/Dapr.Protos/Protos/dapr/proto/runtime/v1/dapr.proto b/src/Dapr.Protos/Protos/dapr/proto/runtime/v1/dapr.proto index ecf0f76f7..470a0d009 100644 --- a/src/Dapr.Protos/Protos/dapr/proto/runtime/v1/dapr.proto +++ b/src/Dapr.Protos/Protos/dapr/proto/runtime/v1/dapr.proto @@ -202,6 +202,9 @@ service Dapr { // Delete a job rpc DeleteJobAlpha1(DeleteJobRequest) returns (DeleteJobResponse) {} + + // Converse with a LLM service + rpc ConverseAlpha1(ConversationRequest) returns (ConversationResponse) {} } // InvokeServiceRequest represents the request message for Service invocation. @@ -1206,7 +1209,7 @@ message Job { // // Systemd timer style cron accepts 6 fields: // seconds | minutes | hours | day of month | month | day of week - // 0-59 | 0-59 | 0-23 | 1-31 | 1-12/jan-dec | 0-7/sun-sat + // 0-59 | 0-59 | 0-23 | 1-31 | 1-12/jan-dec | 0-6/sun-sat // // "0 30 * * * *" - every hour on the half hour // "0 15 3 * * *" - every day at 03:15 @@ -1274,4 +1277,56 @@ message DeleteJobRequest { // DeleteJobResponse is the message response to delete the job by name. message DeleteJobResponse { // Empty +} + +// ConversationRequest is the request object for Conversation. +message ConversationRequest { + // The name of Conversation component + string name = 1; + + // The ID of an existing chat (like in ChatGPT) + optional string contextID = 2; + + // Inputs for the conversation, support multiple input in one time. + repeated ConversationInput inputs = 3; + + // Parameters for all custom fields. + map parameters = 4; + + // The metadata passing to conversation components. + map metadata = 5; + + // Scrub PII data that comes back from the LLM + optional bool scrubPII = 6; + + // Temperature for the LLM to optimize for creativity or predictability + optional double temperature = 7; +} + +message ConversationInput { + // The message to send to the llm + string message = 1; + + // The role to set for the message + optional string role = 2; + + // Scrub PII data that goes into the LLM + optional bool scrubPII = 3; +} + +// ConversationResult is the result for one input. +message ConversationResult { + // Result for the one conversation input. + string result = 1; + // Parameters for all custom fields. + map parameters = 2; +} + +// ConversationResponse is the response for Conversation. +message ConversationResponse { + // The ID of an existing chat (like in ChatGPT) + optional string contextID = 1; + + // An array of results. + repeated ConversationResult outputs = 2; } \ No newline at end of file diff --git a/test/Dapr.AI.Test/Conversation/DaprConversationClientBuilderTest.cs b/test/Dapr.AI.Test/Conversation/DaprConversationClientBuilderTest.cs new file mode 100644 index 000000000..901c4b656 --- /dev/null +++ b/test/Dapr.AI.Test/Conversation/DaprConversationClientBuilderTest.cs @@ -0,0 +1,33 @@ +// ------------------------------------------------------------------------ +// 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.AI.Conversation; + +namespace Dapr.AI.Test.Conversation; + +public class DaprConversationClientBuilderTest +{ + [Fact] + public void Build_WithDefaultConfiguration_ShouldReturnNewInstanceOfDaprConversationClient() + { + // Arrange + var conversationClientBuilder = new DaprConversationClientBuilder(); + + // Act + var client = conversationClientBuilder.Build(); + + // Assert + Assert.NotNull(client); + Assert.IsType(client); + } +} diff --git a/test/Dapr.AI.Test/Conversation/Extensions/DaprAiConversationBuilderExtensionsTest.cs b/test/Dapr.AI.Test/Conversation/Extensions/DaprAiConversationBuilderExtensionsTest.cs new file mode 100644 index 000000000..95a8e1e8c --- /dev/null +++ b/test/Dapr.AI.Test/Conversation/Extensions/DaprAiConversationBuilderExtensionsTest.cs @@ -0,0 +1,75 @@ +// ------------------------------------------------------------------------ +// 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; +using System.Collections.Generic; +using System.Net.Http; +using Dapr.AI.Conversation; +using Dapr.AI.Conversation.Extensions; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Dapr.AI.Test.Conversation.Extensions; + +public class DaprAiConversationBuilderExtensionsTest +{ + [Fact] + public void AddDaprConversationClient_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.AddDaprAiConversation(); + + var app = services.BuildServiceProvider(); + + var conversationClient = app.GetRequiredService() as DaprConversationClient; + + Assert.NotNull(conversationClient!.DaprApiToken); + Assert.Equal(apiToken, conversationClient.DaprApiToken); + } + + [Fact] + public void AddDaprAiConversation_WithoutConfigure_ShouldAddServices() + { + var services = new ServiceCollection(); + var builder = services.AddDaprAiConversation(); + Assert.NotNull(builder); + } + + [Fact] + public void AddDaprAiConversation_RegistersIHttpClientFactory() + { + var services = new ServiceCollection(); + services.AddDaprAiConversation(); + var serviceProvider = services.BuildServiceProvider(); + + var httpClientFactory = serviceProvider.GetService(); + Assert.NotNull(httpClientFactory); + + var daprConversationClient = serviceProvider.GetService(); + Assert.NotNull(daprConversationClient); + } + + [Fact] + public void AddDaprAiConversation_NullServices_ShouldThrowException() + { + IServiceCollection services = null; + Assert.Throws(() => services.AddDaprAiConversation()); + } +} diff --git a/test/Dapr.AI.Test/Dapr.AI.Test.csproj b/test/Dapr.AI.Test/Dapr.AI.Test.csproj new file mode 100644 index 000000000..f937f64e2 --- /dev/null +++ b/test/Dapr.AI.Test/Dapr.AI.Test.csproj @@ -0,0 +1,28 @@ + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + diff --git a/test/Dapr.Client.Test/Extensions/EnumExtensionTest.cs b/test/Dapr.Client.Test/Extensions/EnumExtensionTest.cs deleted file mode 100644 index 83c4354f9..000000000 --- a/test/Dapr.Client.Test/Extensions/EnumExtensionTest.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.Runtime.Serialization; -using Xunit; - -namespace Dapr.Client.Test.Extensions -{ - public class EnumExtensionTest - { - [Fact] - public void GetValueFromEnumMember_RedResolvesAsExpected() - { - var value = TestEnum.Red.GetValueFromEnumMember(); - Assert.Equal("red", value); - } - - [Fact] - public void GetValueFromEnumMember_YellowResolvesAsExpected() - { - var value = TestEnum.Yellow.GetValueFromEnumMember(); - Assert.Equal("YELLOW", value); - } - - [Fact] - public void GetValueFromEnumMember_BlueResolvesAsExpected() - { - var value = TestEnum.Blue.GetValueFromEnumMember(); - Assert.Equal("Blue", value); - } - } - - public enum TestEnum - { - [EnumMember(Value = "red")] - Red, - [EnumMember(Value = "YELLOW")] - Yellow, - Blue - } -} diff --git a/test/Dapr.Common.Test/Extensions/EnumExtensionsTest.cs b/test/Dapr.Common.Test/Extensions/EnumExtensionsTest.cs index 84e2998d6..e7b2d014b 100644 --- a/test/Dapr.Common.Test/Extensions/EnumExtensionsTest.cs +++ b/test/Dapr.Common.Test/Extensions/EnumExtensionsTest.cs @@ -12,14 +12,14 @@ public void GetValueFromEnumMember_RedResolvesAsExpected() var value = TestEnum.Red.GetValueFromEnumMember(); Assert.Equal("red", value); } - + [Fact] public void GetValueFromEnumMember_YellowResolvesAsExpected() { var value = TestEnum.Yellow.GetValueFromEnumMember(); Assert.Equal("YELLOW", value); } - + [Fact] public void GetValueFromEnumMember_BlueResolvesAsExpected() { @@ -27,6 +27,7 @@ public void GetValueFromEnumMember_BlueResolvesAsExpected() Assert.Equal("Blue", value); } } + public enum TestEnum { [EnumMember(Value = "red")] @@ -35,4 +36,3 @@ public enum TestEnum Yellow, Blue } - diff --git a/test/Dapr.Common.Test/JsonConverters/GenericEnumJsonConverterTest.cs b/test/Dapr.Common.Test/JsonConverters/GenericEnumJsonConverterTest.cs new file mode 100644 index 000000000..065a74220 --- /dev/null +++ b/test/Dapr.Common.Test/JsonConverters/GenericEnumJsonConverterTest.cs @@ -0,0 +1,52 @@ +using System.Runtime.Serialization; +using System.Text.Json; +using System.Text.Json.Serialization; +using Dapr.Common.JsonConverters; +using Xunit; + +namespace Dapr.Common.Test.JsonConverters; + +public class GenericEnumJsonConverterTest +{ + [Fact] + public void ShouldSerializeWithEnumMemberAttribute() + { + var testValue = new TestType("ColorTest", Color.Red); + var serializedValue = JsonSerializer.Serialize(testValue); + Assert.Equal("{\"Name\":\"ColorTest\",\"Color\":\"definitely-not-red\"}", serializedValue); + } + + [Fact] + public void ShouldSerializeWithoutEnumMemberAttribute() + { + var testValue = new TestType("ColorTest", Color.Green); + var serializedValue = JsonSerializer.Serialize(testValue); + Assert.Equal("{\"Name\":\"ColorTest\",\"Color\":\"Green\"}", serializedValue); + } + + [Fact] + public void ShouldDeserializeWithEnumMemberAttribute() + { + const string json = "{\"Name\":\"ColorTest\",\"Color\":\"definitely-not-red\"}"; + var deserializedValue = JsonSerializer.Deserialize(json); + Assert.Equal("ColorTest", deserializedValue.Name); + Assert.Equal(Color.Red, deserializedValue.Color); + } + + [Fact] + public void ShouldDeserializeWithoutEnumMemberAttribute() + { + const string json = "{\"Name\":\"ColorTest\",\"Color\":\"Green\"}"; + var deserializedValue = JsonSerializer.Deserialize(json); + Assert.Equal("ColorTest", deserializedValue.Name); + Assert.Equal(Color.Green, deserializedValue.Color); + } + + private record TestType(string Name, Color Color); + + [JsonConverter(typeof(GenericEnumJsonConverter))] + private enum Color { + [EnumMember(Value="definitely-not-red")] + Red, + Green }; +} 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"; } }