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