diff --git a/.github/workflows/itests.yml b/.github/workflows/itests.yml index d06c12cd5..4dcdfb951 100644 --- a/.github/workflows/itests.yml +++ b/.github/workflows/itests.yml @@ -20,7 +20,7 @@ jobs: strategy: fail-fast: false matrix: - dotnet-version: ['6.0', '7.0', '8.0'] + dotnet-version: ['6.0', '7.0', '8.0', '9.0'] include: - dotnet-version: '6.0' display-name: '.NET 6.0' @@ -37,6 +37,11 @@ jobs: framework: 'net8' prefix: 'net8' install-version: '8.0.x' + - dotnet-version: '9.0' + display-name: '.NET 9.0' + framework: 'net9' + prefix: 'net9' + install-version: '9.0.x' env: NUPKG_OUTDIR: bin/Release/nugets GOVER: 1.20.3 @@ -103,14 +108,22 @@ jobs: - name: Parse release version run: python ./.github/scripts/get_release_version.py - name: Setup ${{ matrix.display-name }} - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v3 with: dotnet-version: ${{ matrix.install-version }} - - name: Setup .NET 8.0 # net8 is always required. - uses: actions/setup-dotnet@v1 + dotnet-quality: 'ga' # Prefer a GA release, but use the RC if not available + - name: Setup .NET 8 (required) + uses: actions/setup-dotnet@v3 if: ${{ matrix.install-version != '8.0.x' }} with: - dotnet-version: 8.0.x + dotnet-version: '8.0.x' + dotnet-quality: 'ga' + - name: Setup .NET 9 (required) + uses: actions/setup-dotnet@v3 + if: ${{ matrix.install-version != '9.0.x' }} + with: + dotnet-version: '9.0.x' + dotnet-quality: 'ga' - name: Build # disable deterministic builds, just for test run. Deterministic builds break coverage for some reason run: dotnet build --configuration release /p:GITHUB_ACTIONS=false diff --git a/.github/workflows/sdk_build.yml b/.github/workflows/sdk_build.yml index 5e6fd3532..b6e263530 100644 --- a/.github/workflows/sdk_build.yml +++ b/.github/workflows/sdk_build.yml @@ -24,9 +24,10 @@ jobs: - name: Parse release version run: python ./.github/scripts/get_release_version.py - name: Setup .NET Core - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v3 with: - dotnet-version: 8.0.x + dotnet-version: 9.0.x + dotnet-quality: 'ga' - name: Build run: dotnet build --configuration release - name: Generate Packages @@ -43,39 +44,49 @@ jobs: strategy: fail-fast: false matrix: - dotnet-version: ['6.0', '7.0', '8.0'] + dotnet-version: ['6.0', '7.0', '8.0', '9.0'] include: - dotnet-version: '6.0' - install-3: false display-name: '.NET 6.0' framework: 'net6' prefix: 'net6' install-version: '6.0.x' - dotnet-version: '7.0' - install-3: false display-name: '.NET 7.0' framework: 'net7' prefix: 'net7' install-version: '7.0.x' - dotnet-version: '8.0' - install-3: false display-name: '.NET 8.0' framework: 'net8' prefix: 'net8' install-version: '8.0.x' + - dotnet-version: '9.0' + display-name: '.NET 9.0' + framework: 'net9' + prefix: 'net9' + install-version: '9.0.x' steps: - uses: actions/checkout@v1 - name: Parse release version run: python ./.github/scripts/get_release_version.py - name: Setup ${{ matrix.display-name }} - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v3 with: dotnet-version: ${{ matrix.install-version }} - - name: Setup .NET 8.0 # net8 is always required. - uses: actions/setup-dotnet@v1 + dotnet-quality: 'ga' # Prefer a GA release, but use the RC if not available + - name: Setup .NET 8 (required) + uses: actions/setup-dotnet@v3 if: ${{ matrix.install-version != '8.0.x' }} with: - dotnet-version: 8.0.x + dotnet-version: '8.0.x' + dotnet-quality: 'ga' + - name: Setup .NET 9 (required) + uses: actions/setup-dotnet@v3 + if: ${{ matrix.install-version != '9.0.x' }} + with: + dotnet-version: '9.0.x' + dotnet-quality: 'ga' - name: Build # disable deterministic builds, just for test run. Deterministic builds break coverage for some reason run: dotnet build --configuration release /p:GITHUB_ACTIONS=false 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 c18aa650f..b0d3e3a65 100644 --- a/all.sln +++ b/all.sln @@ -1,5 +1,5 @@ Microsoft Visual Studio Solution File, Format Version 12.00 -# 17 +# Visual Studio Version 17 VisualStudioVersion = 17.3.32929.385 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.Actors", "src\Dapr.Actors\Dapr.Actors.csproj", "{C2DB4B64-B7C3-4FED-8753-C040F677C69A}" @@ -121,6 +121,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}" @@ -335,6 +343,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 @@ -370,7 +390,7 @@ Global {290D1278-F613-4DF3-9DF5-F37E38CDC363}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {290D1278-F613-4DF3-9DF5-F37E38CDC363}.Debug|Any CPU.Build.0 = Debug|Any CPU {290D1278-F613-4DF3-9DF5-F37E38CDC363}.Release|Any CPU.ActiveCfg = Release|Any CPU - {290D1278-F613-4DF3-9DF5-F37E38CDC363}.Release|Any CPU.Build.0 = Release|Any CP + {290D1278-F613-4DF3-9DF5-F37E38CDC363}.Release|Any CPU.Build.0 = Release|Any CPU {C8BB6A85-A7EA-40C0-893D-F36F317829B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C8BB6A85-A7EA-40C0-893D-F36F317829B3}.Debug|Any CPU.Build.0 = Debug|Any CPU {C8BB6A85-A7EA-40C0-893D-F36F317829B3}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -440,6 +460,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 60a4a1a61..ce80b3ea9 100644 --- a/daprdocs/content/en/dotnet-sdk-docs/_index.md +++ b/daprdocs/content/en/dotnet-sdk-docs/_index.md @@ -18,7 +18,15 @@ Dapr offers a variety of packages to help with the development of .NET applicati - [Dapr CLI]({{< ref install-dapr-cli.md >}}) installed - Initialized [Dapr environment]({{< ref install-dapr-selfhost.md >}}) -- [.NET 6](https://dotnet.microsoft.com/download) or [.NET 8+](https://dotnet.microsoft.com/download) installed +- [.NET 6](https://dotnet.microsoft.com/download), [.NET 8](https://dotnet.microsoft.com/download) or [.NET 9](https://dotnet.microsoft.com/download) installed + +{{% alert title="Note" color="primary" %}} + +Note that while .NET 6 is generally supported as the minimum .NET requirement across the Dapr .NET SDK packages +and .NET 7 is the minimally supported version of .NET by Dapr.Workflows in Dapr v1.15, only .NET 8 and .NET 9 will +continue to be supported by Dapr in v1.16 and later. + +{{% /alert %}} ## Installation @@ -76,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-actors/dotnet-actors-howto.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-actors/dotnet-actors-howto.md index eaa13625d..aba62bf07 100644 --- a/daprdocs/content/en/dotnet-sdk-docs/dotnet-actors/dotnet-actors-howto.md +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-actors/dotnet-actors-howto.md @@ -45,7 +45,15 @@ This project contains the implementation of the actor client which calls MyActor - [Dapr CLI]({{< ref install-dapr-cli.md >}}) installed. - Initialized [Dapr environment]({{< ref install-dapr-selfhost.md >}}). -- [.NET 6+](https://dotnet.microsoft.com/download) installed. Dapr .NET SDK uses [ASP.NET Core](https://docs.microsoft.com/aspnet/core/introduction-to-aspnet-core?view=aspnetcore-6.0). +- [.NET 6](https://dotnet.microsoft.com/download), [.NET 8](https://dotnet.microsoft.com/download) or [.NET 9](https://dotnet.microsoft.com/download) installed + +{{% alert title="Note" color="primary" %}} + +Note that while .NET 6 is generally supported as the minimum .NET requirement across the Dapr .NET SDK packages +and .NET 7 is the minimally supported version of .NET by Dapr.Workflows in Dapr v1.15, only .NET 8 and .NET 9 will +continue to be supported by Dapr in v1.16 and later. + +{{% /alert %}} ## Step 0: Prepare 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/daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/dotnet-jobs-howto.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/dotnet-jobs-howto.md index c8bc66175..8d98d1ca5 100644 --- a/daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/dotnet-jobs-howto.md +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/dotnet-jobs-howto.md @@ -16,10 +16,17 @@ In the .NET example project: - The main [`Program.cs`](https://github.com/dapr/dotnet-sdk/tree/master/examples/Jobs/JobsSample/Program.cs) file comprises the entirety of this demonstration. ## Prerequisites -- [.NET 6+](https://dotnet.microsoft.com/download) installed - [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/) - [Initialized Dapr environment](https://docs.dapr.io/getting-started/install-dapr-selfhost) -- [Dapr Jobs .NET SDK](https://github.com/dapr/dotnet-sdk) +- [.NET 6](https://dotnet.microsoft.com/download), [.NET 8](https://dotnet.microsoft.com/download) or [.NET 9](https://dotnet.microsoft.com/download) installed + +{{% alert title="Note" color="primary" %}} + +Note that while .NET 6 is generally supported as the minimum .NET requirement across the Dapr .NET SDK packages +and .NET 7 is the minimally supported version of .NET by Dapr.Workflows in Dapr v1.15, only .NET 8 and .NET 9 will +continue to be supported by Dapr in v1.16 and later. + +{{% /alert %}} ## Set up the environment Clone the [.NET SDK repo](https://github.com/dapr/dotnet-sdk). diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-workflow/dotnet-workflow-howto.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-workflow/dotnet-workflow-howto.md index f6d18bc58..9be910234 100644 --- a/daprdocs/content/en/dotnet-sdk-docs/dotnet-workflow/dotnet-workflow-howto.md +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-workflow/dotnet-workflow-howto.md @@ -18,11 +18,17 @@ In the .NET example project: ## Prerequisites -- [.NET 6+](https://dotnet.microsoft.com/download) installed - [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/) - [Initialized Dapr environment](https://docs.dapr.io/getting-started/install-dapr-selfhost/) -- [Dapr .NET SDK](https://github.com/dapr/dotnet-sdk/) +- [.NET 7](https://dotnet.microsoft.com/download), [.NET 8](https://dotnet.microsoft.com/download) or [.NET 9](https://dotnet.microsoft.com/download) installed +{{% alert title="Note" color="primary" %}} + +Note that while .NET 6 is generally supported as the minimum .NET requirement across the Dapr .NET SDK packages +and .NET 7 is the minimally supported version of .NET by Dapr.Workflows in Dapr v1.15, only .NET 8 and .NET 9 will +continue to be supported by Dapr in v1.16 and later. + +{{% /alert %}} ## Set up the environment @@ -83,7 +89,7 @@ Run the following command to start a workflow. {{% codetab %}} ```bash -curl -i -X POST http://localhost:3500/v1.0-beta1/workflows/dapr/OrderProcessingWorkflow/start?instanceID=12345678 \ +curl -i -X POST http://localhost:3500/v1.0/workflows/dapr/OrderProcessingWorkflow/start?instanceID=12345678 \ -H "Content-Type: application/json" \ -d '{"Name": "Paperclips", "TotalCost": 99.95, "Quantity": 1}' ``` @@ -93,7 +99,7 @@ curl -i -X POST http://localhost:3500/v1.0-beta1/workflows/dapr/OrderProcessingW {{% codetab %}} ```powershell -curl -i -X POST http://localhost:3500/v1.0-beta1/workflows/dapr/OrderProcessingWorkflow/start?instanceID=12345678 ` +curl -i -X POST http://localhost:3500/v1.0/workflows/dapr/OrderProcessingWorkflow/start?instanceID=12345678 ` -H "Content-Type: application/json" ` -d '{"Name": "Paperclips", "TotalCost": 99.95, "Quantity": 1}' ``` @@ -111,7 +117,7 @@ If successful, you should see a response like the following: Send an HTTP request to get the status of the workflow that was started: ```bash -curl -i -X GET http://localhost:3500/v1.0-beta1/workflows/dapr/12345678 +curl -i -X GET http://localhost:3500/v1.0/workflows/dapr/12345678 ``` The workflow is designed to take several seconds to complete. If the workflow hasn't completed when you issue the HTTP request, you'll see the following JSON response (formatted for readability) with workflow status as `RUNNING`: 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/examples/GeneratedActor/ActorClient/IGenericClientActor.cs b/examples/GeneratedActor/ActorClient/IGenericClientActor.cs new file mode 100644 index 000000000..166f4a9ef --- /dev/null +++ b/examples/GeneratedActor/ActorClient/IGenericClientActor.cs @@ -0,0 +1,27 @@ +// ------------------------------------------------------------------------ +// 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. +// ------------------------------------------------------------------------ + +using Dapr.Actors.Generators; + +namespace GeneratedActor +{ + [GenerateActorClient] + internal interface IGenericClientActor + { + [ActorMethod(Name = "GetState")] + Task GetStateAsync(CancellationToken cancellationToken = default); + + [ActorMethod(Name = "SetState")] + Task SetStateAsync(TGenericType2 state, CancellationToken cancellationToken = default); + } +} diff --git a/global.json b/global.json index fe53f92ae..139cca3e3 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,7 @@ { "_comment": "This policy allows the 8.0.100 SDK or patches in that family.", "sdk": { - "version": "8.0.100", - "rollForward": "minor" + "version": "9.0.100", + "rollForward": "latestFeature" } } \ No newline at end of file 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.Actors.Generators/ActorClientGenerator.cs b/src/Dapr.Actors.Generators/ActorClientGenerator.cs index 001604d53..0f064e801 100644 --- a/src/Dapr.Actors.Generators/ActorClientGenerator.cs +++ b/src/Dapr.Actors.Generators/ActorClientGenerator.cs @@ -161,12 +161,23 @@ private static void GenerateActorClientCode(SourceProductionContext context, Act .Append(SyntaxKind.SealedKeyword) .Select(sk => SyntaxFactory.Token(sk)); - var actorClientClassDeclaration = SyntaxFactory.ClassDeclaration(descriptor.ClientTypeName) - .WithModifiers(SyntaxFactory.TokenList(actorClientClassModifiers)) - .WithMembers(SyntaxFactory.List(actorMembers)) - .WithBaseList(SyntaxFactory.BaseList( - SyntaxFactory.Token(SyntaxKind.ColonToken), - SyntaxFactory.SeparatedList(new[] { actorClientBaseInterface }))); + var actorClientClassTypeParameters = descriptor.InterfaceType.TypeParameters + .Select(x => SyntaxFactory.TypeParameter(x.ToString())); + + var actorClientClassDeclaration = (actorClientClassTypeParameters.Count() == 0) + ? SyntaxFactory.ClassDeclaration(descriptor.ClientTypeName) + .WithModifiers(SyntaxFactory.TokenList(actorClientClassModifiers)) + .WithMembers(SyntaxFactory.List(actorMembers)) + .WithBaseList(SyntaxFactory.BaseList( + SyntaxFactory.Token(SyntaxKind.ColonToken), + SyntaxFactory.SeparatedList(new[] { actorClientBaseInterface }))) + : SyntaxFactory.ClassDeclaration(descriptor.ClientTypeName) + .WithModifiers(SyntaxFactory.TokenList(actorClientClassModifiers)) + .WithTypeParameterList(SyntaxFactory.TypeParameterList(SyntaxFactory.SeparatedList(actorClientClassTypeParameters))) + .WithMembers(SyntaxFactory.List(actorMembers)) + .WithBaseList(SyntaxFactory.BaseList( + SyntaxFactory.Token(SyntaxKind.ColonToken), + SyntaxFactory.SeparatedList(new[] { actorClientBaseInterface }))); var namespaceDeclaration = SyntaxFactory.NamespaceDeclaration(SyntaxFactory.ParseName(descriptor.NamespaceName)) .WithMembers(SyntaxFactory.List(new[] { actorClientClassDeclaration })) 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/Dapr.Common.csproj b/src/Dapr.Common/Dapr.Common.csproj index 31af3952c..d1e106b6d 100644 --- a/src/Dapr.Common/Dapr.Common.csproj +++ b/src/Dapr.Common/Dapr.Common.csproj @@ -1,7 +1,7 @@  - net6;net7;net8 + net6;net7;net8;net9 enable enable 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/Dapr.Jobs.csproj b/src/Dapr.Jobs/Dapr.Jobs.csproj index 74c9bec23..9f209b0bd 100644 --- a/src/Dapr.Jobs/Dapr.Jobs.csproj +++ b/src/Dapr.Jobs/Dapr.Jobs.csproj @@ -1,7 +1,6 @@  - net6;net8 enable enable Dapr.Jobs 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.Jobs/Models/DaprJobSchedule.cs b/src/Dapr.Jobs/Models/DaprJobSchedule.cs index c1b592e12..e00c77f49 100644 --- a/src/Dapr.Jobs/Models/DaprJobSchedule.cs +++ b/src/Dapr.Jobs/Models/DaprJobSchedule.cs @@ -67,7 +67,6 @@ public static DaprJobSchedule FromCronExpression(CronExpressionBuilder builder) /// public static DaprJobSchedule FromDateTime(DateTimeOffset scheduledTime) { - ArgumentNullException.ThrowIfNull(scheduledTime, nameof(scheduledTime)); return new DaprJobSchedule(scheduledTime.ToString("O")); } @@ -77,7 +76,9 @@ public static DaprJobSchedule FromDateTime(DateTimeOffset scheduledTime) /// The systemd Cron-like expression indicating when the job should be triggered. public static DaprJobSchedule FromExpression(string expression) { +#if NET6_0 ArgumentNullException.ThrowIfNull(expression, nameof(expression)); +#endif return new DaprJobSchedule(expression); } @@ -87,7 +88,6 @@ public static DaprJobSchedule FromExpression(string expression) /// The duration interval. public static DaprJobSchedule FromDuration(TimeSpan duration) { - ArgumentNullException.ThrowIfNull(duration, nameof(duration)); return new DaprJobSchedule(duration.ToDurationString()); } 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/Dapr.Protos.csproj b/src/Dapr.Protos/Dapr.Protos.csproj index 8a8804b22..5331f229c 100644 --- a/src/Dapr.Protos/Dapr.Protos.csproj +++ b/src/Dapr.Protos/Dapr.Protos.csproj @@ -1,6 +1,7 @@  + net6;net7;net8;net9 enable enable This package contains the reference protos used by develop services using Dapr. 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..0ab371e6d 100644 --- a/src/Dapr.Protos/Protos/dapr/proto/runtime/v1/dapr.proto +++ b/src/Dapr.Protos/Protos/dapr/proto/runtime/v1/dapr.proto @@ -151,25 +151,39 @@ service Dapr { rpc SubtleVerifyAlpha1(SubtleVerifyRequest) returns (SubtleVerifyResponse); // Starts a new instance of a workflow - rpc StartWorkflowAlpha1 (StartWorkflowRequest) returns (StartWorkflowResponse) {} + rpc StartWorkflowAlpha1 (StartWorkflowRequest) returns (StartWorkflowResponse) { + option deprecated = true; + } // Gets details about a started workflow instance - rpc GetWorkflowAlpha1 (GetWorkflowRequest) returns (GetWorkflowResponse) {} + rpc GetWorkflowAlpha1 (GetWorkflowRequest) returns (GetWorkflowResponse) { + option deprecated = true; + } // Purge Workflow - rpc PurgeWorkflowAlpha1 (PurgeWorkflowRequest) returns (google.protobuf.Empty) {} + rpc PurgeWorkflowAlpha1 (PurgeWorkflowRequest) returns (google.protobuf.Empty) { + option deprecated = true; + } // Terminates a running workflow instance - rpc TerminateWorkflowAlpha1 (TerminateWorkflowRequest) returns (google.protobuf.Empty) {} + rpc TerminateWorkflowAlpha1 (TerminateWorkflowRequest) returns (google.protobuf.Empty) { + option deprecated = true; + } // Pauses a running workflow instance - rpc PauseWorkflowAlpha1 (PauseWorkflowRequest) returns (google.protobuf.Empty) {} + rpc PauseWorkflowAlpha1 (PauseWorkflowRequest) returns (google.protobuf.Empty) { + option deprecated = true; + } // Resumes a paused workflow instance - rpc ResumeWorkflowAlpha1 (ResumeWorkflowRequest) returns (google.protobuf.Empty) {} + rpc ResumeWorkflowAlpha1 (ResumeWorkflowRequest) returns (google.protobuf.Empty) { + option deprecated = true; + } // Raise an event to a running workflow instance - rpc RaiseEventWorkflowAlpha1 (RaiseEventWorkflowRequest) returns (google.protobuf.Empty) {} + rpc RaiseEventWorkflowAlpha1 (RaiseEventWorkflowRequest) returns (google.protobuf.Empty) { + option deprecated = true; + } // Starts a new instance of a workflow rpc StartWorkflowBeta1 (StartWorkflowRequest) returns (StartWorkflowResponse) {} @@ -191,6 +205,7 @@ service Dapr { // Raise an event to a running workflow instance rpc RaiseEventWorkflowBeta1 (RaiseEventWorkflowRequest) returns (google.protobuf.Empty) {} + // Shutdown the sidecar rpc Shutdown (ShutdownRequest) returns (google.protobuf.Empty) {} @@ -202,6 +217,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 +1224,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 +1292,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/src/Dapr.Workflow/Dapr.Workflow.csproj b/src/Dapr.Workflow/Dapr.Workflow.csproj index 360d121ef..f24d41e40 100644 --- a/src/Dapr.Workflow/Dapr.Workflow.csproj +++ b/src/Dapr.Workflow/Dapr.Workflow.csproj @@ -3,6 +3,7 @@ + net6;net7;net8;net9 enable Dapr.Workflow Dapr Workflow Authoring SDK diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 35f0fbf7c..a74833a37 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -3,7 +3,7 @@ - net6;net8 + net6;net8;net9 $(RepoRoot)bin\$(Configuration)\prod\$(MSBuildProjectName)\ $(OutputPath)$(MSBuildProjectName).xml 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.Actors.Generators.Test/ActorClientGeneratorTests.cs b/test/Dapr.Actors.Generators.Test/ActorClientGeneratorTests.cs index 4c0ef194e..3515bc8b0 100644 --- a/test/Dapr.Actors.Generators.Test/ActorClientGeneratorTests.cs +++ b/test/Dapr.Actors.Generators.Test/ActorClientGeneratorTests.cs @@ -168,6 +168,92 @@ public System.Threading.Tasks.Task TestMethod() await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync(); } + [Fact] + public async Task TestSingleGenericInternalInterface() + { + var originalSource = @" +using Dapr.Actors.Generators; +using System.Threading.Tasks; + +namespace Test +{ + [GenerateActorClient] + internal interface ITestActor + { + Task TestMethod(); + } +}"; + + var generatedSource = @"// +#nullable enable +namespace Test +{ + internal sealed class TestActorClient : Test.ITestActor + { + private readonly Dapr.Actors.Client.ActorProxy actorProxy; + public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) + { + if (actorProxy is null) + { + throw new System.ArgumentNullException(nameof(actorProxy)); + } + + this.actorProxy = actorProxy; + } + + public System.Threading.Tasks.Task TestMethod() + { + return this.actorProxy.InvokeMethodAsync(""TestMethod""); + } + } +}"; + + await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync(); + } + + [Fact] + public async Task TestMultipleGenericsInternalInterface() + { + var originalSource = @" +using Dapr.Actors.Generators; +using System.Threading.Tasks; + +namespace Test +{ + [GenerateActorClient] + internal interface ITestActor + { + Task TestMethod(); + } +}"; + + var generatedSource = @"// +#nullable enable +namespace Test +{ + internal sealed class TestActorClient : Test.ITestActor + { + private readonly Dapr.Actors.Client.ActorProxy actorProxy; + public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) + { + if (actorProxy is null) + { + throw new System.ArgumentNullException(nameof(actorProxy)); + } + + this.actorProxy = actorProxy; + } + + public System.Threading.Tasks.Task TestMethod() + { + return this.actorProxy.InvokeMethodAsync(""TestMethod""); + } + } +}"; + + await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync(); + } + [Fact] public async Task TestRenamedClient() { @@ -211,6 +297,92 @@ public System.Threading.Tasks.Task TestMethod() await CreateTest(originalSource, "Test.MyTestActorClient.g.cs", generatedSource).RunAsync(); } + [Fact] + public async Task TestSingleGenericRenamedClient() + { + var originalSource = @" +using Dapr.Actors.Generators; +using System.Threading.Tasks; + +namespace Test +{ + [GenerateActorClient(Name = ""MyTestActorClient"")] + internal interface ITestActor + { + Task TestMethod(); + } +}"; + + var generatedSource = @"// +#nullable enable +namespace Test +{ + internal sealed class MyTestActorClient : Test.ITestActor + { + private readonly Dapr.Actors.Client.ActorProxy actorProxy; + public MyTestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) + { + if (actorProxy is null) + { + throw new System.ArgumentNullException(nameof(actorProxy)); + } + + this.actorProxy = actorProxy; + } + + public System.Threading.Tasks.Task TestMethod() + { + return this.actorProxy.InvokeMethodAsync(""TestMethod""); + } + } +}"; + + await CreateTest(originalSource, "Test.MyTestActorClient.g.cs", generatedSource).RunAsync(); + } + + [Fact] + public async Task TestMultipleGenericsRenamedClient() + { + var originalSource = @" +using Dapr.Actors.Generators; +using System.Threading.Tasks; + +namespace Test +{ + [GenerateActorClient(Name = ""MyTestActorClient"")] + internal interface ITestActor + { + Task TestMethod(); + } +}"; + + var generatedSource = @"// +#nullable enable +namespace Test +{ + internal sealed class MyTestActorClient : Test.ITestActor + { + private readonly Dapr.Actors.Client.ActorProxy actorProxy; + public MyTestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) + { + if (actorProxy is null) + { + throw new System.ArgumentNullException(nameof(actorProxy)); + } + + this.actorProxy = actorProxy; + } + + public System.Threading.Tasks.Task TestMethod() + { + return this.actorProxy.InvokeMethodAsync(""TestMethod""); + } + } +}"; + + await CreateTest(originalSource, "Test.MyTestActorClient.g.cs", generatedSource).RunAsync(); + } + [Fact] public async Task TestCustomNamespace() { diff --git a/test/Dapr.Actors.Generators.Test/CSharpSourceGeneratorVerifier.cs b/test/Dapr.Actors.Generators.Test/CSharpSourceGeneratorVerifier.cs index c64fd3427..ef1d7b9a0 100644 --- a/test/Dapr.Actors.Generators.Test/CSharpSourceGeneratorVerifier.cs +++ b/test/Dapr.Actors.Generators.Test/CSharpSourceGeneratorVerifier.cs @@ -34,6 +34,8 @@ public Test() 7; #elif NET8_0 8; +#elif NET9_0 + 9; #endif // diff --git a/test/Dapr.AspNetCore.IntegrationTest/CloudEventsIntegrationTest.cs b/test/Dapr.AspNetCore.IntegrationTest/CloudEventsIntegrationTest.cs index dd940a75d..9b0b5d3a3 100644 --- a/test/Dapr.AspNetCore.IntegrationTest/CloudEventsIntegrationTest.cs +++ b/test/Dapr.AspNetCore.IntegrationTest/CloudEventsIntegrationTest.cs @@ -146,7 +146,7 @@ public async Task CanSendBinaryCloudEvent_WithContentType() using (var factory = new AppWebApplicationFactory()) { var httpClient = factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions { HandleCookies = false }); - + var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/register-user") { Content = new StringContent( @@ -158,10 +158,10 @@ public async Task CanSendBinaryCloudEvent_WithContentType() Encoding.UTF8) }; request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); - + var response = await httpClient.SendAsync(request); response.EnsureSuccessStatusCode(); - + var userInfo = await JsonSerializer.DeserializeAsync(await response.Content.ReadAsStreamAsync(), this.options); userInfo.Name.Should().Be("jimmy"); } diff --git a/test/Dapr.AspNetCore.IntegrationTest/Dapr.AspNetCore.IntegrationTest.csproj b/test/Dapr.AspNetCore.IntegrationTest/Dapr.AspNetCore.IntegrationTest.csproj index d51dc70e8..d7dd6d52a 100644 --- a/test/Dapr.AspNetCore.IntegrationTest/Dapr.AspNetCore.IntegrationTest.csproj +++ b/test/Dapr.AspNetCore.IntegrationTest/Dapr.AspNetCore.IntegrationTest.csproj @@ -7,14 +7,36 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + - + + + + + + + + + + + + diff --git a/test/Dapr.AspNetCore.Test/CloudEventsMiddlewareTest.cs b/test/Dapr.AspNetCore.Test/CloudEventsMiddlewareTest.cs index 9c1f1e005..2f9fab936 100644 --- a/test/Dapr.AspNetCore.Test/CloudEventsMiddlewareTest.cs +++ b/test/Dapr.AspNetCore.Test/CloudEventsMiddlewareTest.cs @@ -11,6 +11,8 @@ // limitations under the License. // ------------------------------------------------------------------------ +using Microsoft.Extensions.DependencyInjection; + namespace Dapr.AspNetCore.Test { using System.IO; @@ -33,7 +35,10 @@ public class CloudEventsMiddlewareTest [InlineData("application/cloudevents-batch+json")] // we don't support batch public async Task InvokeAsync_IgnoresOtherContentTypes(string contentType) { - var app = new ApplicationBuilder(null); + var serviceCollection = new ServiceCollection(); + var provider = serviceCollection.BuildServiceProvider(); + + var app = new ApplicationBuilder(provider); app.UseCloudEvents(); // Do verification in the scope of the middleware @@ -46,9 +51,10 @@ public async Task InvokeAsync_IgnoresOtherContentTypes(string contentType) var pipeline = app.Build(); - var context = new DefaultHttpContext(); - context.Request.ContentType = contentType; - context.Request.Body = MakeBody("Hello, world!"); + var context = new DefaultHttpContext + { + Request = { ContentType = contentType, Body = MakeBody("Hello, world!") } + }; await pipeline.Invoke(context); } @@ -62,7 +68,10 @@ public async Task InvokeAsync_IgnoresOtherContentTypes(string contentType) public async Task InvokeAsync_ReplacesBodyJson(string dataContentType, string charSet) { var encoding = charSet == null ? null : Encoding.GetEncoding(charSet); - var app = new ApplicationBuilder(null); + var serviceCollection = new ServiceCollection(); + var provider = serviceCollection.BuildServiceProvider(); + + var app = new ApplicationBuilder(provider); app.UseCloudEvents(); // Do verification in the scope of the middleware @@ -75,11 +84,17 @@ public async Task InvokeAsync_ReplacesBodyJson(string dataContentType, string ch var pipeline = app.Build(); - var context = new DefaultHttpContext(); - context.Request.ContentType = charSet == null ? "application/cloudevents+json" : $"application/cloudevents+json;charset={charSet}"; - context.Request.Body = dataContentType == null ? - MakeBody("{ \"data\": { \"name\":\"jimmy\" } }", encoding) : - MakeBody($"{{ \"datacontenttype\": \"{dataContentType}\", \"data\": {{ \"name\":\"jimmy\" }} }}", encoding); + var context = new DefaultHttpContext { Request = + { + ContentType = + charSet == null + ? "application/cloudevents+json" + : $"application/cloudevents+json;charset={charSet}", + Body = dataContentType == null ? + MakeBody("{ \"data\": { \"name\":\"jimmy\" } }", encoding) : + MakeBody($"{{ \"datacontenttype\": \"{dataContentType}\", \"data\": {{ \"name\":\"jimmy\" }} }}", encoding) + } + }; await pipeline.Invoke(context); } @@ -93,7 +108,10 @@ public async Task InvokeAsync_ReplacesBodyJson(string dataContentType, string ch public async Task InvokeAsync_ReplacesPascalCasedBodyJson(string dataContentType, string charSet) { var encoding = charSet == null ? null : Encoding.GetEncoding(charSet); - var app = new ApplicationBuilder(null); + var serviceCollection = new ServiceCollection(); + var provider = serviceCollection.BuildServiceProvider(); + + var app = new ApplicationBuilder(provider); app.UseCloudEvents(); // Do verification in the scope of the middleware @@ -106,11 +124,17 @@ public async Task InvokeAsync_ReplacesPascalCasedBodyJson(string dataContentType var pipeline = app.Build(); - var context = new DefaultHttpContext(); - context.Request.ContentType = charSet == null ? "application/cloudevents+json" : $"application/cloudevents+json;charset={charSet}"; - context.Request.Body = dataContentType == null ? - MakeBody("{ \"Data\": { \"name\":\"jimmy\" } }", encoding) : - MakeBody($"{{ \"DataContentType\": \"{dataContentType}\", \"Data\": {{ \"name\":\"jimmy\" }} }}", encoding); + var context = new DefaultHttpContext { Request = + { + ContentType = + charSet == null + ? "application/cloudevents+json" + : $"application/cloudevents+json;charset={charSet}", + Body = dataContentType == null ? + MakeBody("{ \"Data\": { \"name\":\"jimmy\" } }", encoding) : + MakeBody($"{{ \"DataContentType\": \"{dataContentType}\", \"Data\": {{ \"name\":\"jimmy\" }} }}", encoding) + } + }; await pipeline.Invoke(context); } @@ -124,7 +148,10 @@ public async Task InvokeAsync_ReplacesPascalCasedBodyJson(string dataContentType public async Task InvokeAsync_ForwardsJsonPropertiesAsHeaders(string dataContentType, string charSet) { var encoding = charSet == null ? null : Encoding.GetEncoding(charSet); - var app = new ApplicationBuilder(null); + var serviceCollection = new ServiceCollection(); + var provider = serviceCollection.BuildServiceProvider(); + + var app = new ApplicationBuilder(provider); app.UseCloudEvents(new CloudEventsMiddlewareOptions { ForwardCloudEventPropertiesAsHeaders = true @@ -143,11 +170,17 @@ public async Task InvokeAsync_ForwardsJsonPropertiesAsHeaders(string dataContent var pipeline = app.Build(); - var context = new DefaultHttpContext(); - context.Request.ContentType = charSet == null ? "application/cloudevents+json" : $"application/cloudevents+json;charset={charSet}"; - context.Request.Body = dataContentType == null ? - MakeBody("{ \"type\": \"Test.Type\", \"subject\": \"Test.Subject\", \"data\": { \"name\":\"jimmy\" } }", encoding) : - MakeBody($"{{ \"datacontenttype\": \"{dataContentType}\", \"type\":\"Test.Type\", \"subject\": \"Test.Subject\", \"data\": {{ \"name\":\"jimmy\" }} }}", encoding); + var context = new DefaultHttpContext { Request = + { + ContentType = + charSet == null + ? "application/cloudevents+json" + : $"application/cloudevents+json;charset={charSet}", + Body = dataContentType == null ? + MakeBody("{ \"type\": \"Test.Type\", \"subject\": \"Test.Subject\", \"data\": { \"name\":\"jimmy\" } }", encoding) : + MakeBody($"{{ \"datacontenttype\": \"{dataContentType}\", \"type\":\"Test.Type\", \"subject\": \"Test.Subject\", \"data\": {{ \"name\":\"jimmy\" }} }}", encoding) + } + }; await pipeline.Invoke(context); } @@ -161,7 +194,10 @@ public async Task InvokeAsync_ForwardsJsonPropertiesAsHeaders(string dataContent public async Task InvokeAsync_ForwardsIncludedJsonPropertiesAsHeaders(string dataContentType, string charSet) { var encoding = charSet == null ? null : Encoding.GetEncoding(charSet); - var app = new ApplicationBuilder(null); + var serviceCollection = new ServiceCollection(); + var provider = serviceCollection.BuildServiceProvider(); + + var app = new ApplicationBuilder(provider); app.UseCloudEvents(new CloudEventsMiddlewareOptions { ForwardCloudEventPropertiesAsHeaders = true, @@ -181,11 +217,17 @@ public async Task InvokeAsync_ForwardsIncludedJsonPropertiesAsHeaders(string dat var pipeline = app.Build(); - var context = new DefaultHttpContext(); - context.Request.ContentType = charSet == null ? "application/cloudevents+json" : $"application/cloudevents+json;charset={charSet}"; - context.Request.Body = dataContentType == null ? - MakeBody("{ \"type\": \"Test.Type\", \"subject\": \"Test.Subject\", \"data\": { \"name\":\"jimmy\" } }", encoding) : - MakeBody($"{{ \"datacontenttype\": \"{dataContentType}\", \"type\":\"Test.Type\", \"subject\": \"Test.Subject\", \"data\": {{ \"name\":\"jimmy\" }} }}", encoding); + var context = new DefaultHttpContext { Request = + { + ContentType = + charSet == null + ? "application/cloudevents+json" + : $"application/cloudevents+json;charset={charSet}", + Body = dataContentType == null ? + MakeBody("{ \"type\": \"Test.Type\", \"subject\": \"Test.Subject\", \"data\": { \"name\":\"jimmy\" } }", encoding) : + MakeBody($"{{ \"datacontenttype\": \"{dataContentType}\", \"type\":\"Test.Type\", \"subject\": \"Test.Subject\", \"data\": {{ \"name\":\"jimmy\" }} }}", encoding) + } + }; await pipeline.Invoke(context); } @@ -199,7 +241,10 @@ public async Task InvokeAsync_ForwardsIncludedJsonPropertiesAsHeaders(string dat public async Task InvokeAsync_DoesNotForwardExcludedJsonPropertiesAsHeaders(string dataContentType, string charSet) { var encoding = charSet == null ? null : Encoding.GetEncoding(charSet); - var app = new ApplicationBuilder(null); + var serviceCollection = new ServiceCollection(); + var provider = serviceCollection.BuildServiceProvider(); + + var app = new ApplicationBuilder(provider); app.UseCloudEvents(new CloudEventsMiddlewareOptions { ForwardCloudEventPropertiesAsHeaders = true, @@ -219,11 +264,17 @@ public async Task InvokeAsync_DoesNotForwardExcludedJsonPropertiesAsHeaders(stri var pipeline = app.Build(); - var context = new DefaultHttpContext(); - context.Request.ContentType = charSet == null ? "application/cloudevents+json" : $"application/cloudevents+json;charset={charSet}"; - context.Request.Body = dataContentType == null ? - MakeBody("{ \"type\": \"Test.Type\", \"subject\": \"Test.Subject\", \"data\": { \"name\":\"jimmy\" } }", encoding) : - MakeBody($"{{ \"datacontenttype\": \"{dataContentType}\", \"type\":\"Test.Type\", \"subject\": \"Test.Subject\", \"data\": {{ \"name\":\"jimmy\" }} }}", encoding); + var context = new DefaultHttpContext { Request = + { + ContentType = + charSet == null + ? "application/cloudevents+json" + : $"application/cloudevents+json;charset={charSet}", + Body = dataContentType == null ? + MakeBody("{ \"type\": \"Test.Type\", \"subject\": \"Test.Subject\", \"data\": { \"name\":\"jimmy\" } }", encoding) : + MakeBody($"{{ \"datacontenttype\": \"{dataContentType}\", \"type\":\"Test.Type\", \"subject\": \"Test.Subject\", \"data\": {{ \"name\":\"jimmy\" }} }}", encoding) + } + }; await pipeline.Invoke(context); } @@ -234,10 +285,13 @@ public async Task InvokeAsync_ReplacesBodyNonJsonData() // Our logic is based on the content-type, not the content. // Since this is for text-plain content, we're going to encode it as a JSON string // and store it in the data attribute - the middleware should JSON-decode it. - var input = "{ \"message\": \"hello, world\"}"; + const string input = "{ \"message\": \"hello, world\"}"; var expected = input; - var app = new ApplicationBuilder(null); + var serviceCollection = new ServiceCollection(); + var provider = serviceCollection.BuildServiceProvider(); + + var app = new ApplicationBuilder(provider); app.UseCloudEvents(); // Do verification in the scope of the middleware @@ -251,9 +305,12 @@ public async Task InvokeAsync_ReplacesBodyNonJsonData() var pipeline = app.Build(); - var context = new DefaultHttpContext(); - context.Request.ContentType = "application/cloudevents+json"; - context.Request.Body = MakeBody($"{{ \"datacontenttype\": \"text/plain\", \"data\": {JsonSerializer.Serialize(input)} }}"); + var context = new DefaultHttpContext { Request = + { + ContentType = "application/cloudevents+json", + Body = MakeBody($"{{ \"datacontenttype\": \"text/plain\", \"data\": {JsonSerializer.Serialize(input)} }}") + } + }; await pipeline.Invoke(context); } @@ -262,10 +319,13 @@ public async Task InvokeAsync_ReplacesBodyNonJsonData() public async Task InvokeAsync_ReplacesBodyNonJsonData_ExceptWhenSuppressed() { // Our logic is based on the content-type, not the content. This test tests the old bad behavior. - var input = "{ \"message\": \"hello, world\"}"; + const string input = "{ \"message\": \"hello, world\"}"; var expected = JsonSerializer.Serialize(input); - var app = new ApplicationBuilder(null); + var serviceCollection = new ServiceCollection(); + var provider = serviceCollection.BuildServiceProvider(); + + var app = new ApplicationBuilder(provider); app.UseCloudEvents(new CloudEventsMiddlewareOptions() { SuppressJsonDecodingOfTextPayloads = true, }); // Do verification in the scope of the middleware @@ -279,9 +339,12 @@ public async Task InvokeAsync_ReplacesBodyNonJsonData_ExceptWhenSuppressed() var pipeline = app.Build(); - var context = new DefaultHttpContext(); - context.Request.ContentType = "application/cloudevents+json"; - context.Request.Body = MakeBody($"{{ \"datacontenttype\": \"text/plain\", \"data\": {JsonSerializer.Serialize(input)} }}"); + var context = new DefaultHttpContext { Request = + { + ContentType = "application/cloudevents+json", + Body = MakeBody($"{{ \"datacontenttype\": \"text/plain\", \"data\": {JsonSerializer.Serialize(input)} }}") + } + }; await pipeline.Invoke(context); } @@ -291,10 +354,13 @@ public async Task InvokeAsync_ReplacesBodyNonJsonData_ExceptWhenSuppressed() [Fact] public async Task InvokeAsync_ReplacesBodyJson_NormalizesPayloadCharset() { - var dataContentType = "application/person+json;charset=UTF-16"; - var charSet = "UTF-16"; + const string dataContentType = "application/person+json;charset=UTF-16"; + const string charSet = "UTF-16"; var encoding = Encoding.GetEncoding(charSet); - var app = new ApplicationBuilder(null); + var serviceCollection = new ServiceCollection(); + var provider = serviceCollection.BuildServiceProvider(); + + var app = new ApplicationBuilder(provider); app.UseCloudEvents(); // Do verification in the scope of the middleware @@ -307,10 +373,11 @@ public async Task InvokeAsync_ReplacesBodyJson_NormalizesPayloadCharset() var pipeline = app.Build(); - var context = new DefaultHttpContext(); - context.Request.ContentType = $"application/cloudevents+json;charset={charSet}"; - context.Request.Body = - MakeBody($"{{ \"datacontenttype\": \"{dataContentType}\", \"data\": {{ \"name\":\"jimmy\" }} }}", encoding); + var context = new DefaultHttpContext { Request = + { + ContentType = $"application/cloudevents+json;charset={charSet}", Body = MakeBody($"{{ \"datacontenttype\": \"{dataContentType}\", \"data\": {{ \"name\":\"jimmy\" }} }}", encoding) + } + }; await pipeline.Invoke(context); } @@ -318,8 +385,11 @@ public async Task InvokeAsync_ReplacesBodyJson_NormalizesPayloadCharset() [Fact] public async Task InvokeAsync_ReadsBinaryData() { - var dataContentType = "application/octet-stream"; - var app = new ApplicationBuilder(null); + const string dataContentType = "application/octet-stream"; + var serviceCollection = new ServiceCollection(); + var provider = serviceCollection.BuildServiceProvider(); + + var app = new ApplicationBuilder(provider); app.UseCloudEvents(); var data = new byte[] { 1, 2, 3 }; @@ -328,15 +398,18 @@ public async Task InvokeAsync_ReadsBinaryData() { httpContext.Request.ContentType.Should().Be(dataContentType); var bytes = new byte[httpContext.Request.Body.Length]; +#if NET9_0 + httpContext.Request.Body.ReadExactly(bytes, 0, bytes.Length); +#else httpContext.Request.Body.Read(bytes, 0, bytes.Length); +#endif bytes.Should().Equal(data); return Task.CompletedTask; }); var pipeline = app.Build(); - var context = new DefaultHttpContext(); - context.Request.ContentType = "application/cloudevents+json"; + var context = new DefaultHttpContext { Request = { ContentType = "application/cloudevents+json" } }; var base64Str = System.Convert.ToBase64String(data); context.Request.Body = @@ -348,10 +421,13 @@ public async Task InvokeAsync_ReadsBinaryData() [Fact] public async Task InvokeAsync_DataAndData64Set_ReturnsBadRequest() { - var dataContentType = "application/octet-stream"; - var app = new ApplicationBuilder(null); + const string dataContentType = "application/octet-stream"; + var serviceCollection = new ServiceCollection(); + var provider = serviceCollection.BuildServiceProvider(); + + var app = new ApplicationBuilder(provider); app.UseCloudEvents(); - var data = "{\"id\": \"1\"}"; + const string data = "{\"id\": \"1\"}"; // Do verification in the scope of the middleware app.Run(httpContext => @@ -364,8 +440,7 @@ public async Task InvokeAsync_DataAndData64Set_ReturnsBadRequest() var pipeline = app.Build(); - var context = new DefaultHttpContext(); - context.Request.ContentType = "application/cloudevents+json"; + var context = new DefaultHttpContext { Request = { ContentType = "application/cloudevents+json" } }; var bytes = Encoding.UTF8.GetBytes(data); var base64Str = System.Convert.ToBase64String(bytes); context.Request.Body = @@ -391,7 +466,11 @@ private static string ReadBody(Stream stream, Encoding encoding = null) encoding ??= Encoding.UTF8; var bytes = new byte[stream.Length]; +#if NET9_0 + stream.ReadExactly(bytes, 0, bytes.Length); +#else stream.Read(bytes, 0, bytes.Length); +#endif var str = encoding.GetString(bytes); return str; } diff --git a/test/Dapr.Client.Test/DaprClientTest.cs b/test/Dapr.Client.Test/DaprClientTest.cs index 01d22edcf..e280728c2 100644 --- a/test/Dapr.Client.Test/DaprClientTest.cs +++ b/test/Dapr.Client.Test/DaprClientTest.cs @@ -45,7 +45,7 @@ public void CreateInvokeHttpClient_WithoutAppId() var client = DaprClient.CreateInvokeHttpClient(daprEndpoint: "http://localhost:3500"); Assert.Null(client.BaseAddress); } - + [Fact] public void CreateInvokeHttpClient_InvalidDaprEndpoint_InvalidFormat() { 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.E2E.Test/DaprTestApp.cs b/test/Dapr.E2E.Test/DaprTestApp.cs index 152aeee98..2330785d8 100644 --- a/test/Dapr.E2E.Test/DaprTestApp.cs +++ b/test/Dapr.E2E.Test/DaprTestApp.cs @@ -139,6 +139,7 @@ private static string GetTargetFrameworkName() ".NETCoreApp,Version=v6.0" => "net6", ".NETCoreApp,Version=v7.0" => "net7", ".NETCoreApp,Version=v8.0" => "net8", + ".NETCoreApp,Version=v9.0" => "net9", _ => throw new InvalidOperationException($"Unsupported target framework: {targetFrameworkName}") }; } diff --git a/test/Dapr.Jobs.Test/Extensions/DaprJobsServiceCollectionExtensionsTests.cs b/test/Dapr.Jobs.Test/Extensions/DaprJobsServiceCollectionExtensionsTests.cs index 281477d4e..bd5e4acd0 100644 --- a/test/Dapr.Jobs.Test/Extensions/DaprJobsServiceCollectionExtensionsTests.cs +++ b/test/Dapr.Jobs.Test/Extensions/DaprJobsServiceCollectionExtensionsTests.cs @@ -12,31 +12,57 @@ // ------------------------------------------------------------------------ using System; +using System.Collections.Generic; +using System.Linq; using System.Net.Http; using System.Threading.Tasks; using Dapr.Jobs.Extensions; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace Dapr.Jobs.Test.Extensions; public class DaprJobsServiceCollectionExtensionsTest { + [Fact] + public void AddDaprJobsClient_FromIConfiguration() + { + const string apiToken = "abc123"; + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary { { "DAPR_API_TOKEN", apiToken } }) + .Build(); + + var services = new ServiceCollection(); + services.AddSingleton(configuration); + + services.AddDaprJobsClient(); + + var app = services.BuildServiceProvider(); + + var jobsClient = app.GetRequiredService() as DaprJobsGrpcClient; + + Assert.NotNull(jobsClient!.DaprApiToken); + Assert.Equal(apiToken, jobsClient.DaprApiToken); + } + [Fact] public void AddDaprJobsClient_RegistersDaprClientOnlyOnce() { var services = new ServiceCollection(); - var clientBuilder = new Action(builder => - builder.UseDaprApiToken("abc")); + var clientBuilder = new Action((sp, builder) => + { + builder.UseDaprApiToken("abc"); + }); services.AddDaprJobsClient(); //Sets a default API token value of an empty string services.AddDaprJobsClient(clientBuilder); //Sets the API token value var serviceProvider = services.BuildServiceProvider(); var daprJobClient = serviceProvider.GetService() as DaprJobsGrpcClient; - - Assert.Null(daprJobClient!.apiTokenHeader); - Assert.False(daprJobClient.httpClient.DefaultRequestHeaders.TryGetValues("dapr-api-token", out var _)); + + Assert.NotNull(daprJobClient!.HttpClient); + Assert.False(daprJobClient.HttpClient.DefaultRequestHeaders.TryGetValues("dapr-api-token", out var _)); } [Fact] @@ -63,8 +89,8 @@ public void AddDaprJobsClient_RegistersUsingDependencyFromIServiceProvider() services.AddDaprJobsClient((provider, builder) => { var configProvider = provider.GetRequiredService(); - var daprApiToken = configProvider.GetApiTokenValue(); - builder.UseDaprApiToken(daprApiToken); + var apiToken = TestSecretRetriever.GetApiTokenValue(); + builder.UseDaprApiToken(apiToken); }); var serviceProvider = services.BuildServiceProvider(); @@ -72,10 +98,15 @@ public void AddDaprJobsClient_RegistersUsingDependencyFromIServiceProvider() //Validate it's set on the GrpcClient - note that it doesn't get set on the HttpClient Assert.NotNull(client); - Assert.NotNull(client.apiTokenHeader); - Assert.True(client.apiTokenHeader.HasValue); - Assert.Equal("dapr-api-token", client.apiTokenHeader.Value.Key); - Assert.Equal("abcdef", client.apiTokenHeader.Value.Value); + Assert.NotNull(client.DaprApiToken); + Assert.Equal("abcdef", client.DaprApiToken); + Assert.NotNull(client.HttpClient); + + if (!client.HttpClient.DefaultRequestHeaders.TryGetValues("dapr-api-token", out var daprApiToken)) + { + Assert.Fail(); + } + Assert.Equal("abcdef", daprApiToken.FirstOrDefault()); } [Fact] @@ -83,7 +114,7 @@ public void RegisterJobsClient_ShouldRegisterSingleton_WhenLifetimeIsSingleton() { var services = new ServiceCollection(); - services.AddDaprJobsClient(options => { }, ServiceLifetime.Singleton); + services.AddDaprJobsClient((serviceProvider, options) => { }, ServiceLifetime.Singleton); var serviceProvider = services.BuildServiceProvider(); var daprJobsClient1 = serviceProvider.GetService(); @@ -100,7 +131,7 @@ public async Task RegisterJobsClient_ShouldRegisterScoped_WhenLifetimeIsScoped() { var services = new ServiceCollection(); - services.AddDaprJobsClient(options => { }, ServiceLifetime.Scoped); + services.AddDaprJobsClient((serviceProvider, options) => { }, ServiceLifetime.Scoped); var serviceProvider = services.BuildServiceProvider(); await using var scope1 = serviceProvider.CreateAsyncScope(); @@ -119,7 +150,7 @@ public void RegisterJobsClient_ShouldRegisterTransient_WhenLifetimeIsTransient() { var services = new ServiceCollection(); - services.AddDaprJobsClient(options => { }, ServiceLifetime.Transient); + services.AddDaprJobsClient((serviceProvider, options) => { }, ServiceLifetime.Transient); var serviceProvider = services.BuildServiceProvider(); var daprJobsClient1 = serviceProvider.GetService(); @@ -132,6 +163,6 @@ public void RegisterJobsClient_ShouldRegisterTransient_WhenLifetimeIsTransient() private class TestSecretRetriever { - public string GetApiTokenValue() => "abcdef"; + public static string GetApiTokenValue() => "abcdef"; } } diff --git a/test/Directory.Build.props b/test/Directory.Build.props index 50b029a12..e3a49b72f 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -2,7 +2,7 @@ - net6;net7;net8 + net6;net7;net8;net9 $(RepoRoot)bin\$(Configuration)\test\$(MSBuildProjectName)\