diff --git a/Directory.Packages.props b/Directory.Packages.props
index a98e9db58..efb48fcc4 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -1,52 +1,52 @@
-
- true
- true
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/all.sln b/all.sln
index 1dab60475..9a163b1d9 100644
--- a/all.sln
+++ b/all.sln
@@ -119,6 +119,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Common", "src\Dapr.Com
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Common.Test", "test\Dapr.Common.Test\Dapr.Common.Test.csproj", "{CDB47863-BEBD-4841-A807-46D868962521}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.AI", "src\Dapr.AI\Dapr.AI.csproj", "{273F2527-1658-4CCF-8DC6-600E921188C5}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.AI.Test", "test\Dapr.AI.Test\Dapr.AI.Test.csproj", "{2F3700EF-1CDA-4C15-AC88-360230000ECD}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AI", "AI", "{3046DBF4-C2FF-4F3A-9176-E1C01E0A90E5}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConversationalAI", "examples\AI\ConversationalAI\ConversationalAI.csproj", "{11011FF8-77EA-4B25-96C0-29D4D486EF1C}"
+EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkflowExternalInteraction", "examples\Workflow\WorkflowExternalInteraction\WorkflowExternalInteraction.csproj", "{43CB06A9-7E88-4C5F-BFB8-947E072CBC9F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkflowMonitor", "examples\Workflow\WorkflowMonitor\WorkflowMonitor.csproj", "{7F73A3D8-FFC2-4E31-AA3D-A4840316A8C6}"
@@ -331,6 +339,18 @@ Global
{CDB47863-BEBD-4841-A807-46D868962521}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CDB47863-BEBD-4841-A807-46D868962521}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CDB47863-BEBD-4841-A807-46D868962521}.Release|Any CPU.Build.0 = Release|Any CPU
+ {273F2527-1658-4CCF-8DC6-600E921188C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {273F2527-1658-4CCF-8DC6-600E921188C5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {273F2527-1658-4CCF-8DC6-600E921188C5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {273F2527-1658-4CCF-8DC6-600E921188C5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2F3700EF-1CDA-4C15-AC88-360230000ECD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2F3700EF-1CDA-4C15-AC88-360230000ECD}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2F3700EF-1CDA-4C15-AC88-360230000ECD}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2F3700EF-1CDA-4C15-AC88-360230000ECD}.Release|Any CPU.Build.0 = Release|Any CPU
+ {11011FF8-77EA-4B25-96C0-29D4D486EF1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {11011FF8-77EA-4B25-96C0-29D4D486EF1C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {11011FF8-77EA-4B25-96C0-29D4D486EF1C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {11011FF8-77EA-4B25-96C0-29D4D486EF1C}.Release|Any CPU.Build.0 = Release|Any CPU
{43CB06A9-7E88-4C5F-BFB8-947E072CBC9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{43CB06A9-7E88-4C5F-BFB8-947E072CBC9F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{43CB06A9-7E88-4C5F-BFB8-947E072CBC9F}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -439,6 +459,10 @@ Global
{DFBABB04-50E9-42F6-B470-310E1B545638} = {27C5D71D-0721-4221-9286-B94AB07B58CF}
{B445B19C-A925-4873-8CB7-8317898B6970} = {27C5D71D-0721-4221-9286-B94AB07B58CF}
{CDB47863-BEBD-4841-A807-46D868962521} = {DD020B34-460F-455F-8D17-CF4A949F100B}
+ {273F2527-1658-4CCF-8DC6-600E921188C5} = {27C5D71D-0721-4221-9286-B94AB07B58CF}
+ {2F3700EF-1CDA-4C15-AC88-360230000ECD} = {DD020B34-460F-455F-8D17-CF4A949F100B}
+ {3046DBF4-C2FF-4F3A-9176-E1C01E0A90E5} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78}
+ {11011FF8-77EA-4B25-96C0-29D4D486EF1C} = {3046DBF4-C2FF-4F3A-9176-E1C01E0A90E5}
{43CB06A9-7E88-4C5F-BFB8-947E072CBC9F} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9}
{7F73A3D8-FFC2-4E31-AA3D-A4840316A8C6} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9}
{945DD3B7-94E5-435E-B3CB-796C20A652C7} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9}
diff --git a/daprdocs/content/en/dotnet-sdk-contributing/dotnet-contributing.md b/daprdocs/content/en/dotnet-sdk-contributing/dotnet-contributing.md
index 6664191d6..f0378b430 100644
--- a/daprdocs/content/en/dotnet-sdk-contributing/dotnet-contributing.md
+++ b/daprdocs/content/en/dotnet-sdk-contributing/dotnet-contributing.md
@@ -6,22 +6,112 @@ weight: 3000
description: Guidelines for contributing to the Dapr .NET SDK
---
-When contributing to the [.NET SDK](https://github.com/dapr/dotnet-sdk) the following rules and best-practices should be followed.
+# Welcome!
+If you're reading this, you're likely interested in contributing to Dapr and/or the Dapr .NET SDK. Welcome to the project
+and thank you for your interest in contributing!
+
+Please review the documentation, familiarize yourself with what Dapr is and what it's seeking to accomplish and reach
+out on [Discord](https://bit.ly/dapr-discord). Let us know how you'd like to contribute and we'd be happy to chime in
+with ideas and suggestions.
+
+There are many ways to contribute to Dapr:
+- Submit bug reports for the [Dapr runtime](https://github.com/dapr/dapr/issues/new/choose) or the [Dapr .NET SDK](https://github.com/dapr/dotnet-sdk/issues/new/choose)
+- Propose new [runtime capabilities](https://github.com/dapr/proposals/issues/new/choose) or [SDK functionality](https://github.com/dapr/dotnet-sdk/issues/new/choose)
+- Improve the documentation in either the [larger Dapr project](https://github.com/dapr/docs) or the [Dapr .NET SDK specifically](https://github.com/dapr/dotnet-sdk/tree/master/daprdocs)
+- Add new or improve existing [components](https://github.com/dapr/components-contrib/) that implement the various building blocks
+- Augment the [.NET pluggable component SDK capabilities](https://github.com/dapr-sandbox/components-dotnet-sdk)
+- Improve the Dapr .NET SDK code base and/or fix a bug (detailed below)
+
+If you're new to the code base, please feel encouraged to ask in the #dotnet-sdk channel in Discord about how
+to implement changes or generally ask questions. You are not required to seek permission to work on anything, but do
+note that if an issue is assigned to someone, it's an indication that someone might have already started work on it.
+Especially if it's been a while since the last activity on that issue, please feel free to reach out and see if it's
+still something they're interested in pursuing or whether you can take over, and open a pull request with your
+implementation.
+
+If you'd like to assign yourself to an issue, respond to the conversation with "/assign" and the bot will assign you
+to it.
+
+We have labeled some issues as `good-first-issue` or `help wanted` indicating that these are likely to be small,
+self-contained changes.
+
+If you're not certain about your implementation, please create it as a draft pull request and solicit feedback
+from the [.NET maintainers](https://github.com/orgs/dapr/teams/maintainers-dotnet-sdk) by tagging
+`@dapr/maintainers-dotnet-sdk` and providing some context about what you need assistance with.
+
+# Contribution Rules and Best Practices
+
+When contributing to the [.NET SDK](https://github.com/dapr/dotnet-sdk) the following rules and best-practices should
+be followed.
+
+## Pull Requests
+Pull requests that contain only formatting changes are generally discouraged. Pull requests should instead seek to
+fix a bug, add new functionality, or improve on existing capabilities.
+
+Do aim to minimize the contents of your pull request to span only a single issue. Broad PRs that touch on a lot of files
+are not likely to be reviewed or accepted in a short timeframe. Accommodating many different issues in a single PR makes
+it hard to determine whether your code fully addresses the underlying issue(s) or not and complicates the code review.
+
+## Tests
+All pull requests should include unit and/or integration tests that reflect the nature of what was added or changed
+so it's clear that the functionality works as intended. Avoid using auto-generated tests that duplicate testing the
+same functionality several times. Rather, seek to improve code coverage by validating each possible path of your
+changes so future contributors can more easily navigate the contours of your logic and more readily identify limitations.
## Examples
-The `examples` directory contains code samples for users to run to try out specific functionality of the various .NET SDK packages and extensions. When writing new and updated samples keep in mind:
+The `examples` directory contains code samples for users to run to try out specific functionality of the various
+Dapr .NET SDK packages and extensions. When writing new and updated samples keep in mind:
-- All examples should be runnable on Windows, Linux, and MacOS. While .NET Core code is consistent among operating systems, any pre/post example commands should provide options through [codetabs]({{< ref "contributing-docs.md#tabbed-content" >}})
-- Contain steps to download/install any required pre-requisites. Someone coming in with a fresh OS install should be able to start on the example and complete it without an error. Links to external download pages are fine.
+- All examples should be runnable on Windows, Linux, and MacOS. While .NET Core code is consistent among operating
+systems, any pre/post example commands should provide options through
+[codetabs]({{< ref "contributing-docs.md#tabbed-content" >}})
+- Contain steps to download/install any required pre-requisites. Someone coming in with a fresh OS install should be
+able to start on the example and complete it without an error. Links to external download pages are fine.
-## Docs
+## Documentation
-The `daprdocs` directory contains the markdown files that are rendered into the [Dapr Docs](https://docs.dapr.io) website. When the documentation website is built this repo is cloned and configured so that its contents are rendered with the docs content. When writing docs keep in mind:
+The `daprdocs` directory contains the markdown files that are rendered into the [Dapr Docs](https://docs.dapr.io) website. When the
+documentation website is built this repo is cloned and configured so that its contents are rendered with the docs
+content. When writing docs keep in mind:
- All rules in the [docs guide]({{< ref contributing-docs.md >}}) should be followed in addition to these.
- - All files and directories should be prefixed with `dotnet-` to ensure all file/directory names are globally unique across all Dapr documentation.
+ - All files and directories should be prefixed with `dotnet-` to ensure all file/directory names are globally
+ - unique across all Dapr documentation.
+
+All pull requests should strive to include both XML documentation in the code clearly indicating what functionality
+does and why it's there as well as changes to the published documentation to clarify for other developers how your change
+improves the Dapr framework.
## GitHub Dapr Bot Commands
-Checkout the [daprbot documentation](https://docs.dapr.io/contributing/daprbot/) for Github commands you can run in this repo for common tasks. For example, you can comment `/assign` on an issue to assign it to yourself.
+Checkout the [daprbot documentation](https://docs.dapr.io/contributing/daprbot/) for Github commands you can run in this repo for common tasks. For example,
+you can comment `/assign` on an issue to assign it to yourself.
+
+## Commit Sign-offs
+All code submitted to the Dapr .NET SDK must be signed off by the developer authoring it. This means that every
+commit must end with the following:
+> Signed-off-by: First Last
+
+The name and email address must match the registered GitHub name and email address of the user committing the changes.
+We use a bot to detect this in pull requests and we will be unable to merge the PR if this check fails to validate.
+
+If you notice that a PR has failed to validate because of a failed DCO check early on in the PR history, please consider
+squashing the PR locally and resubmitting to ensure that the sign-off statement is included in the commit history.
+
+# Languages, Tools and Processes
+All source code in the Dapr .NET SDK is written in C# and targets the latest language version available to the earliest
+supported .NET SDK. As of v1.15, this means that because .NET 6 is still supported, the latest language version available
+is [C# version 10](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-version-history#c-version-10).
+
+As of v1.15, the following versions of .NET are supported:
+
+| Version | Notes |
+| --- |-----------------------------------------------------------------|
+| .NET 6 | Will be discontinued in v1.16 |
+| .NET 7 | Only supported in Dapr.Workflows, will be discontinued in v1.16 |
+| .NET 8 | Will continue to be supported in v1.16 |
+| .NET 9 | Will continue to be supported in v1.16 |
+
+Contributors are welcome to use whatever IDE they're most comfortable developing in, but please do not submit
+IDE-specific preference files along with your contributions as these will be rejected.
\ No newline at end of file
diff --git a/daprdocs/content/en/dotnet-sdk-docs/_index.md b/daprdocs/content/en/dotnet-sdk-docs/_index.md
index 82d16016d..ce80b3ea9 100644
--- a/daprdocs/content/en/dotnet-sdk-docs/_index.md
+++ b/daprdocs/content/en/dotnet-sdk-docs/_index.md
@@ -84,6 +84,13 @@ Put the Dapr .NET SDK to the test. Walk through the .NET quickstarts and tutoria
+
+
+
AI
+
Create and manage AI operations in .NET
+
+
+
## More information
diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-ai/_index.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-ai/_index.md
new file mode 100644
index 000000000..dac06e0bc
--- /dev/null
+++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-ai/_index.md
@@ -0,0 +1,12 @@
+---
+type: docs
+title: "Dapr AI .NET SDK"
+linkTitle: "AI"
+weight: 50000
+description: Get up and running with the Dapr AI .NET SDK
+---
+
+With the Dapr AI package, you can interact with the Dapr AI workloads from a .NET application.
+
+Today, Dapr provides the Conversational API to engage with large language models. To get started with this workload,
+walk through the [Dapr Conversational AI]({{< ref dotnet-ai-conversation-howto.md >}}) how-to guide.
\ No newline at end of file
diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-ai/dotnet-ai-conversation-howto.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-ai/dotnet-ai-conversation-howto.md
new file mode 100644
index 000000000..9d8d869d8
--- /dev/null
+++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-ai/dotnet-ai-conversation-howto.md
@@ -0,0 +1,90 @@
+---
+type: docs
+title: "How to: Create and use Dapr AI Conversations in the .NET SDK"
+linkTitle: "How to: Use the AI Conversations client"
+weight: 500100
+description: Learn how to create and use the Dapr Conversational AI client using the .NET SDK
+---
+
+## Prerequisites
+- [.NET 6](https://dotnet.microsoft.com/download/dotnet/6.0), [.NET 8](https://dotnet.microsoft.com/download/dotnet/8.0), or [.NET 9](https://dotnet.microsoft.com/download/dotnet/9.0) installed
+- [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/)
+- [Initialized Dapr environment](https://docs.dapr.io/getting-started/install-dapr-selfhost)
+
+{{% alert title="Note" color="primary" %}}
+
+.NET 6 is supported as the minimum required for the Dapr .NET SDK packages in this release. Only .NET 8 and .NET 9
+will be supported in Dapr v1.16 and later releases.
+
+{{% /alert %}}
+
+## Installation
+
+To get started with the Dapr AI .NET SDK client, install the [Dapr.AI package](https://www.nuget.org/packages/Dapr.AI) from NuGet:
+```sh
+dotnet add package Dapr.AI
+```
+
+A `DaprConversationClient` maintains 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-conversation-usage.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-ai/dotnet-ai-conversation-usage.md
new file mode 100644
index 000000000..b4917e02e
--- /dev/null
+++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-ai/dotnet-ai-conversation-usage.md
@@ -0,0 +1,135 @@
+---
+type: docs
+title: "Dapr AI Client"
+linkTitle: "AI client"
+weight: 50005
+description: Learn how to create Dapr AI clients
+---
+
+The Dapr AI client package allows you to interact with the AI capabilities provided by the Dapr sidecar.
+
+## Lifetime management
+A `DaprConversationClient` is a version of the Dapr client that is dedicated to interacting with the Dapr Conversation
+API. It can be registered alongside a `DaprClient` and other Dapr clients without issue.
+
+It maintains access to networking resources in the form of TCP sockets used to communicate with the Dapr sidecar.
+
+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.
+
+This can be aided by utilizing the dependency injection functionality. The registration method supports registration using
+as a singleton, a scoped instance or as transient (meaning it's recreated every time it's injected), but also enables
+registration to utilize values from an `IConfiguration` or other injected service in a way that's impractical when
+creating the client from scratch in each of your classes.
+
+Avoid creating a `DaprConversationClient` for each operation.
+
+## Configuring DaprConversationClient via DaprConversationClientBuilder
+
+A `DaprConversationClient` can be configured by invoking methods on the `DaprConversationClientBuilder` class before
+calling `.Build()` to create the client itself. The settings for each `DaprConversationClient` are separate
+and cannot be changed after calling `.Build()`.
+
+```cs
+var daprConversationClient = new DaprConversationClientBuilder()
+ .UseDaprApiToken("abc123") // Specify the API token used to authenticate to other Dapr sidecars
+ .Build();
+```
+
+The `DaprConversationClientBuilder` contains settings for:
+
+- The HTTP endpoint of the Dapr sidecar
+- The gRPC endpoint of the Dapr sidecar
+- The `JsonSerializerOptions` object used to configure JSON serialization
+- The `GrpcChannelOptions` object used to configure gRPC
+- The API token used to authenticate requests to the sidecar
+- The factory method used to create the `HttpClient` instance used by the SDK
+- The timeout used for the `HttpClient` instance when making requests to the sidecar
+
+The SDK will read the following environment variables to configure the default values:
+
+- `DAPR_HTTP_ENDPOINT`: used to find the HTTP endpoint of the Dapr sidecar, example: `https://dapr-api.mycompany.com`
+- `DAPR_GRPC_ENDPOINT`: used to find the gRPC endpoint of the Dapr sidecar, example: `https://dapr-grpc-api.mycompany.com`
+- `DAPR_HTTP_PORT`: if `DAPR_HTTP_ENDPOINT` is not set, this is used to find the HTTP local endpoint of the Dapr sidecar
+- `DAPR_GRPC_PORT`: if `DAPR_GRPC_ENDPOINT` is not set, this is used to find the gRPC local endpoint of the Dapr sidecar
+- `DAPR_API_TOKEN`: used to set the API token
+
+### Configuring gRPC channel options
+
+Dapr's use of `CancellationToken` for cancellation relies on the configuration of the gRPC channel options. If you need
+to configure these options yourself, make sure to enable the [ThrowOperationCanceledOnCancellation setting](https://grpc.github.io/grpc/csharp-dotnet/api/Grpc.Net.Client.GrpcChannelOptions.html#Grpc_Net_Client_GrpcChannelOptions_ThrowOperationCanceledOnCancellation).
+
+```cs
+var daprConversationClient = new DaprConversationClientBuilder()
+ .UseGrpcChannelOptions(new GrpcChannelOptions { ... ThrowOperationCanceledOnCancellation = true })
+ .Build();
+```
+
+## Using cancellation with `DaprConversationClient`
+
+The APIs on `DaprConversationClient` perform asynchronous operations and accept an optional `CancellationToken` parameter. This
+follows a standard .NET practice for cancellable operations. Note that when cancellation occurs, there is no guarantee that
+the remote endpoint stops processing the request, only that the client has stopped waiting for completion.
+
+When an operation is cancelled, it will throw an `OperationCancelledException`.
+
+## Configuring `DaprConversationClient` via dependency injection
+
+Using the built-in extension methods for registering the `DaprConversationClient` in a dependency injection container can
+provide the benefit of registering the long-lived service a single time, centralize complex configuration and improve
+performance by ensuring similarly long-lived resources are re-purposed when possible (e.g. `HttpClient` instances).
+
+There are three overloads available to give the developer the greatest flexibility in configuring the client for their
+scenario. Each of these will register the `IHttpClientFactory` on your behalf if not already registered, and configure
+the `DaprConversationClientBuilder` to use it when creating the `HttpClient` instance in order to re-use the same instance as
+much as possible and avoid socket exhaustion and other issues.
+
+In the first approach, there's no configuration done by the developer and the `DaprConversationClient` is configured with the
+default settings.
+
+```cs
+var builder = WebApplication.CreateBuilder(args);
+
+builder.Services.AddDaprConversationClient(); //Registers the `DaprConversationClient` to be injected as needed
+var app = builder.Build();
+```
+
+Sometimes the developer will need to configure the created client using the various configuration options detailed
+above. This is done through an overload that passes in the `DaprConversationClientBuiler` and exposes methods for configuring
+the necessary options.
+
+```cs
+var builder = WebApplication.CreateBuilder(args);
+
+builder.Services.AddDaprConversationClient((_, daprConversationClientBuilder) => {
+ //Set the API token
+ daprConversationClientBuilder.UseDaprApiToken("abc123");
+ //Specify a non-standard HTTP endpoint
+ daprConversationClientBuilder.UseHttpEndpoint("http://dapr.my-company.com");
+});
+
+var app = builder.Build();
+```
+
+Finally, it's possible that the developer may need to retrieve information from another service in order to populate
+these configuration values. That value may be provided from a `DaprClient` instance, a vendor-specific SDK or some
+local service, but as long as it's also registered in DI, it can be injected into this configuration operation via the
+last overload:
+
+```cs
+var builder = WebApplication.CreateBuilder(args);
+
+//Register a fictional service that retrieves secrets from somewhere
+builder.Services.AddSingleton();
+
+builder.Services.AddDaprConversationClient((serviceProvider, daprConversationClientBuilder) => {
+ //Retrieve an instance of the `SecretService` from the service provider
+ var secretService = serviceProvider.GetRequiredService();
+ var daprApiToken = secretService.GetSecret("DaprApiToken").Value;
+
+ //Configure the `DaprConversationClientBuilder`
+ daprConversationClientBuilder.UseDaprApiToken(daprApiToken);
+});
+
+var app = builder.Build();
+```
\ No newline at end of file
diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-client/dotnet-daprclient-usage.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-client/dotnet-daprclient-usage.md
index 26328050c..08c67ab95 100644
--- a/daprdocs/content/en/dotnet-sdk-docs/dotnet-client/dotnet-daprclient-usage.md
+++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-client/dotnet-daprclient-usage.md
@@ -10,6 +10,48 @@ description: Essential tips and advice for using DaprClient
A `DaprClient` holds access to networking resources in the form of TCP sockets used to communicate with the Dapr sidecar. `DaprClient` implements `IDisposable` to support eager cleanup of resources.
+### Dependency Injection
+
+The `AddDaprClient()` method will register the Dapr client with ASP.NET Core dependency injection. This method accepts an optional
+options delegate for configuring the `DaprClient` and an `ServiceLifetime` argument, allowing you to specify a different lifetime
+for the registered resources instead of the default `Singleton` value.
+
+The following example assumes all default values are acceptable and is sufficient to register the `DaprClient`.
+
+```csharp
+services.AddDaprClient();
+```
+
+The optional configuration delegates are used to configure `DaprClient` by specifying options on the provided `DaprClientBuilder`
+as in the following example:
+
+```csharp
+services.AddDaprClient(daprBuilder => {
+ daprBuilder.UseJsonSerializerOptions(new JsonSerializerOptions {
+ WriteIndented = true,
+ MaxDepth = 8
+ });
+ daprBuilder.UseTimeout(TimeSpan.FromSeconds(30));
+});
+```
+
+The another optional configuration delegate overload provides access to both the `DaprClientBuilder` as well as an `IServiceProvider`
+allowing for more advanced configurations that may require injecting services from the dependency injection container.
+
+```csharp
+services.AddSingleton();
+services.AddDaprClient((serviceProvider, daprBuilder) => {
+ var sampleService = serviceProvider.GetRequiredService();
+ var timeoutValue = sampleService.TimeoutOptions;
+
+ daprBuilder.UseTimeout(timeoutValue);
+});
+```
+
+### Manual Instantiation
+
+Rather than using dependency injection, a `DaprClient` can also be built using the static client builder.
+
For best performance, create a single long-lived instance of `DaprClient` and provide access to that shared instance throughout your application. `DaprClient` instances are thread-safe and intended to be shared.
Avoid creating a `DaprClient` per-operation and disposing it when the operation is complete.
@@ -24,13 +66,41 @@ var daprClient = new DaprClientBuilder()
.Build();
```
-The `DaprClientBuilder` contains settings for:
+By default, the `DaprClientBuilder` will prioritize the following locations, in the following order, to source the configuration
+values:
+
+- The value provided to a method on the `DaprClientBuilder` (e.g. `UseTimeout(TimeSpan.FromSeconds(30))`)
+- The value pulled from an optionally injected `IConfiguration` matching the name expected in the associated environment variable
+- The value pulled from the associated environment variable
+- Default values
+
+### Configuring on `DaprClientBuilder`
+
+The `DaprClientBuilder` contains the following methods to set configuration options:
+
+- `UseHttpEndpoint(string)`: The HTTP endpoint of the Dapr sidecar
+- `UseGrpcEndpoint(string)`: Sets the gRPC endpoint of the Dapr sidecar
+- `UseGrpcChannelOptions(GrpcChannelOptions)`: Sets the gRPC channel options used to connect to the Dapr sidecar
+- `UseHttpClientFactory(IHttpClientFactory)`: Configures the DaprClient to use a registered `IHttpClientFactory` when building `HttpClient` instances
+- `UseJsonSerializationOptions(JsonSerializerOptions)`: Used to configure JSON serialization
+- `UseDaprApiToken(string)`: Adds the provided token to every request to authenticate to the Dapr sidecar
+- `UseTimeout(TimeSpan)`: Specifies a timeout value used by the `HttpClient` when communicating with the Dapr sidecar
-- The HTTP endpoint of the Dapr sidecar
-- The gRPC endpoint of the Dapr sidecar
-- The `JsonSerializerOptions` object used to configure JSON serialization
-- The `GrpcChannelOptions` object used to configure gRPC
-- The API Token used to authenticate requests to the sidecar
+### Configuring From `IConfiguration`
+Rather than rely on sourcing configuration values directly from environment variables or because the values are sourced
+from dependency injected services, another options is to make these values available on `IConfiguration`.
+
+For example, you might be registering your application in a multi-tenant environment and need to prefix the environment
+variables used. The following example shows how these values can be sourced from the environment variables to your
+`IConfiguration` when their keys are prefixed with `test_`;
+
+```csharp
+var builder = WebApplication.CreateBuilder(args);
+builder.Configuration.AddEnvironmentVariables("test_"); //Retrieves all environment variables that start with "test_" and removes the prefix when sourced from IConfiguration
+builder.Services.AddDaprClient();
+```
+
+### Configuring From Environment Variables
The SDK will read the following environment variables to configure the default values:
@@ -40,9 +110,14 @@ The SDK will read the following environment variables to configure the default v
- `DAPR_GRPC_PORT`: if `DAPR_GRPC_ENDPOINT` is not set, this is used to find the gRPC local endpoint of the Dapr sidecar
- `DAPR_API_TOKEN`: used to set the API Token
+{{% alert title="Note" color="primary" %}}
+If both `DAPR_HTTP_ENDPOINT` and `DAPR_HTTP_PORT` are specified, the port value from `DAPR_HTTP_PORT` will be ignored in favor of the port
+implicitly or explicitly defined on `DAPR_HTTP_ENDPOINT`. The same is true of both `DAPR_GRPC_ENDPOINT` and `DAPR_GRPC_PORT`.
+{{% /alert %}}
+
### Configuring gRPC channel options
-Dapr's use of `CancellationToken` for cancellation relies on the configuration of the gRPC channel options. If you need to configure these options yourself, make sure to enable the [ThrowOperationCanceledOnCancellation setting](https://grpc.github.io/grpc/csharp-dotnet/api/Grpc.Net.Client.GrpcChannelOptions.html#Grpc_Net_Client_GrpcChannelOptions_ThrowOperationCanceledOnCancellation).
+Dapr's use of `CancellationToken` for cancellation relies on the configuration of the gRPC channel options and this is enabled by default. If you need to configure these options yourself, make sure to enable the [ThrowOperationCanceledOnCancellation setting](https://grpc.github.io/grpc/csharp-dotnet/api/Grpc.Net.Client.GrpcChannelOptions.html#Grpc_Net_Client_GrpcChannelOptions_ThrowOperationCanceledOnCancellation).
```C#
var daprClient = new DaprClientBuilder()
diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-development/_index.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-development/_index.md
index 2cd86303f..26327efa5 100644
--- a/daprdocs/content/en/dotnet-sdk-docs/dotnet-development/_index.md
+++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-development/_index.md
@@ -2,7 +2,7 @@
type: docs
title: "Developing applications with the Dapr .NET SDK"
linkTitle: "Dev integrations"
-weight: 50000
+weight: 100000
description: Learn about local development integration options for .NET Dapr applications
---
diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-development/dotnet-development-dapr-aspire.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-development/dotnet-development-dapr-aspire.md
new file mode 100644
index 000000000..238a6d5a8
--- /dev/null
+++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-development/dotnet-development-dapr-aspire.md
@@ -0,0 +1,138 @@
+---
+type: docs
+title: "Dapr .NET SDK Development with .NET Aspire"
+linkTitle: ".NET Aspire"
+weight: 40000
+description: Learn about local development with .NET Aspire
+---
+
+# .NET Aspire
+
+[.NET Aspire](https://learn.microsoft.com/en-us/dotnet/aspire/get-started/aspire-overview) is a development tool
+designed to make it easier to include external software into .NET applications by providing a framework that allows
+third-party services to be readily integrated, observed and provisioned alongside your own software.
+
+Aspire simplifies local development by providing rich integration with popular IDEs including
+[Microsoft Visual Studio](https://visualstudio.microsoft.com/vs/),
+[Visual Studio Code](https://code.visualstudio.com/),
+[JetBrains Rider](https://blog.jetbrains.com/dotnet/2024/02/19/jetbrains-rider-and-the-net-aspire-plugin/) and others
+to launch your application with the debugger while automatically launching and provisioning access to other
+integrations as well, including Dapr.
+
+While Aspire also assists with deployment of your application to various cloud hosts like Microsoft Azure and
+Amazon AWS, deployment is currently outside the scope of this guide. More information can be found in Aspire's
+documentation [here](https://learn.microsoft.com/en-us/dotnet/aspire/deployment/overview).
+
+## Prerequisites
+- While the Dapr .NET SDK is compatible with [.NET 6](https://dotnet.microsoft.com/download/dotnet/6.0),
+[.NET 8](https://dotnet.microsoft.com/download/dotnet/8.0) or [.NET 9](https://dotnet.microsoft.com/download/dotnet/9.0),
+.NET Aspire is only compatible with [.NET 8](https://dotnet.microsoft.com/download/dotnet/8.0) or
+[.NET 9](https://dotnet.microsoft.com/download/dotnet/9.0).
+- An OCI compliant container runtime such as [Docker Desktop](https://www.docker.com/products/docker-desktop) or
+[Podman](https://podman.io/)
+- Install and initialize Dapr v1.13 or later
+
+## Using .NET Aspire via CLI
+
+We'll start by creating a brand new .NET application. Open your preferred CLI and navigate to the directory you wish
+to create your new .NET solution within. Start by using the following command to install a template that will create
+an empty Aspire application:
+
+```sh
+dotnet new install Aspire.ProjectTemplates
+```
+
+Once that's installed, proceed to create an empty .NET Aspire application in your current directory. The `-n` argument
+allows you to specify the name of the output solution. If it's excluded, the .NET CLI will instead use the name
+of the output directory, e.g. `C:\source\aspiredemo` will result in the solution being named `aspiredemo`. The rest
+of this tutorial will assume a solution named `aspiredemo`.
+
+```sh
+dotnet new aspire -n aspiredemo
+```
+
+This will create two Aspire-specific directories and one file in your directory:
+- `aspiredemo.AppHost/` contains the Aspire orchestration project that is used to configure each of the integrations
+used in your application(s).
+- `aspiredemo.ServiceDefaults/` contains a collection of extensions meant to be shared across your solution to aid in
+resilience, service discovery and telemetry capabilities offered by Aspire (these are distinct from the capabilities
+offered in Dapr itself).
+- `aspiredemo.sln` is the file that maintains the layout of your current solution
+
+We'll next create a project that'll serve as our Dapr application. From the same directory, use the following
+to create an empty ASP.NET Core project called `MyApp`. This will be created relative to your current directory in
+`MyApp\MyApp.csproj`.
+
+```sh
+dotnet new web MyApp
+```
+
+Next we'll configure the AppHost project to add the necessary package to support local Dapr development. Navigate
+into the AppHost directory with the following and install the `Aspire.Hosting.Dapr` package from NuGet into the project.
+We'll also add a reference to our `MyApp` project so we can reference it during the registration process.
+
+```sh
+cd aspiredemo.AppHost
+dotnet add package Aspire.Hosting.Dapr
+dotnet add reference ../MyApp/
+```
+
+Next, we need to configure Dapr as a resource to be loaded alongside your project. Open the `Program.cs` file in that
+project within your preferred IDE. It should look similar to the following:
+
+```csharp
+var builder = DistributedApplication.CreateBuilder(args);
+
+builder.Build().Run();
+```
+
+If you're familiar with the dependency injection approach used in ASP.NET Core projects or others utilizing the
+`Microsoft.Extensions.DependencyInjection` functionality, you'll find that this will be a familiar experience.
+
+Because we've already added a project reference to `MyApp`, we need to start by adding a reference in this configuration
+as well. Add the following before the `builder.Build().Run()` line:
+
+```csharp
+var myApp = builder
+ .AddProject("myapp")
+ .WithDaprSidecar();
+```
+
+Because the project reference has been added to this solution, your project shows up as a type within the `Projects.`
+namespace for our purposes here. The name of the variable you assign the project to doesn't much matter in this tutorial
+but would be used if you wanted to create a reference between this project and another using Aspire's service discovery
+functionality.
+
+Adding `.WithDaprSidecar()` configures Dapr as a .NET Aspire resource so that when the project runs, the sidecar will be
+deployed alongside your application. This accepts a number of different options and could optionally be configured as in
+the following example:
+
+```csharp
+DaprSidecarOptions sidecarOptions = new()
+{
+ AppId = "my-other-app",
+ AppPort = 8080, //Note that this argument is required if you intend to configure pubsub, actors or workflows as of Aspire v9.0
+ DaprGrpcPort = 50001,
+ DaprHttpPort = 3500,
+ MetricsPort = 9090
+};
+
+builder
+ .AddProject("myotherapp")
+ .WithReference(myApp)
+ .WithDaprSidecar(sidecarOptions);
+```
+
+{{% alert color="primary" %}}
+
+As indicated in the example above, as of .NET Aspire 9.0, if you intend to use any functionality in which Dapr needs to
+call into your application such as pubsub, actors or workflows, you will need to specify your AppPort as
+a configured option as Aspire will not automatically pass it to Dapr at runtime. It's expected that this behavior will
+change in a future release as a fix has been merged and can be tracked [here](https://github.com/dotnet/aspire/pull/6362).
+
+{{% /alert %}}
+
+When you open the solution in your IDE, ensure that the `aspiredemo.AppHost` is configured as your startup project, but
+when you launch it in a debug configuration, you'll note that your integrated console should reflect your expected Dapr
+logs and it will be available to your application.
+
diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-development/dotnet-development-docker-compose.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-development/dotnet-development-docker-compose.md
index 603be0b74..8b832ac26 100644
--- a/daprdocs/content/en/dotnet-sdk-docs/dotnet-development/dotnet-development-docker-compose.md
+++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-development/dotnet-development-docker-compose.md
@@ -2,7 +2,7 @@
type: docs
title: "Dapr .NET SDK Development with Docker-Compose"
linkTitle: "Docker Compose"
-weight: 50000
+weight: 60000
description: Learn about local development with Docker-Compose
---
diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-development/dotnet-development-tye.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-development/dotnet-development-tye.md
index 71ea568f1..0077bd564 100644
--- a/daprdocs/content/en/dotnet-sdk-docs/dotnet-development/dotnet-development-tye.md
+++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-development/dotnet-development-tye.md
@@ -2,7 +2,7 @@
type: docs
title: "Dapr .NET SDK Development with Project Tye"
linkTitle: "Project Tye"
-weight: 40000
+weight: 50000
description: Learn about local development with Project Tye
---
diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/_index.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/_index.md
index 049994221..60f756aa4 100644
--- a/daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/_index.md
+++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/_index.md
@@ -6,3 +6,8 @@ weight: 50000
description: Get up and running with Dapr Jobs and the Dapr .NET SDK
---
+With the Dapr Job package, you can interact with the Dapr Job APIs from a .NET application to trigger future operations
+to run according to a predefined schedule with an optional payload.
+
+To get started, walk through the [Dapr Jobs]({{< ref dotnet-jobs-howto.md >}}) how-to guide and refer to
+[best practices documentation]({{< ref dotnet-jobs-usage.md >}}) for additional guidance.
\ 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 8d98d1ca5..974b2f5ec 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
@@ -2,7 +2,7 @@
type: docs
title: "How to: Author and manage Dapr Jobs in the .NET SDK"
linkTitle: "How to: Author & manage jobs"
-weight: 10000
+weight: 51000
description: Learn how to author and manage Dapr Jobs using the .NET SDK
---
@@ -10,7 +10,7 @@ Let's create an endpoint that will be invoked by Dapr Jobs when it triggers, the
you will:
- Deploy a .NET Web API application ([JobsSample](https://github.com/dapr/dotnet-sdk/tree/master/examples/Jobs/JobsSample))
-- Utilize the .NET Jobs SDK to schedule a job invocation and set up the endpoint to be triggered
+- Utilize the Dapr .NET Jobs SDK to schedule a job invocation and set up the endpoint to be triggered
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.
@@ -18,13 +18,12 @@ In the .NET example project:
## Prerequisites
- [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/)
- [Initialized Dapr environment](https://docs.dapr.io/getting-started/install-dapr-selfhost)
-- [.NET 6](https://dotnet.microsoft.com/download), [.NET 8](https://dotnet.microsoft.com/download) or [.NET 9](https://dotnet.microsoft.com/download) installed
+- [.NET 6](https://dotnet.microsoft.com/download/dotnet/6.0), [.NET 8](https://dotnet.microsoft.com/download/dotnet/8.0) or [.NET 9](https://dotnet.microsoft.com/download/dotnet/9.0) installed
+- [Dapr.Jobs](https://www.nuget.org/packages/Dapr.Jobs) NuGet package installed to your project
{{% 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.
+Note that while .NET 6 is the minimum support version of .NET in Dapr v1.15, only .NET 8 and .NET 9 will continue to be supported by Dapr in v1.16 and later.
{{% /alert %}}
@@ -54,27 +53,33 @@ We'll run a command that starts both the Dapr sidecar and the .NET program at th
```sh
dapr run --app-id jobsapp --dapr-grpc-port 4001 --dapr-http-port 3500 -- dotnet run
```
+
> Dapr listens for HTTP requests at `http://localhost:3500` and internal Jobs gRPC requests at `http://localhost:4001`.
+
## Register the Dapr Jobs client with dependency injection
-The Dapr Jobs SDK provides an extension method to simplify the registration of the Dapr Jobs client. Before completing the dependency injection registration in `Program.cs`, add the following line:
+The Dapr Jobs SDK provides an extension method to simplify the registration of the Dapr Jobs client. Before completing
+the dependency injection registration in `Program.cs`, add the following line:
```cs
var builder = WebApplication.CreateBuilder(args);
//Add anywhere between these two
builder.Services.AddDaprJobsClient(); //That's it
+
var app = builder.Build();
```
> Note that in today's implementation of the Jobs API, the app that schedules the job will also be the app that receives the trigger notification. In other words, you cannot schedule a trigger to run in another application. As a result, while you don't explicitly need the Dapr Jobs client to be registered in your application to schedule a trigger invocation endpoint, your endpoint will never be invoked without the same app also scheduling the job somehow (whether via this Dapr Jobs .NET SDK or an HTTP call to the sidecar).
+
It's possible that you may want to provide some configuration options to the Dapr Jobs client that
-should be present with each call to the sidecar such as a Dapr API token or you want to use a non-standard
-HTTP or gRPC endpoint. This is possible through an overload of the register method that allows configuration of a `DaprJobsClientBuilder` instance:
+should be present with each call to the sidecar such as a Dapr API token, or you want to use a non-standard
+HTTP or gRPC endpoint. This is possible through use of an overload of the registration method that allows configuration of a
+`DaprJobsClientBuilder` instance:
```cs
var builder = WebApplication.CreateBuilder(args);
-builder.Services.AddDaprJobsClient(daprJobsClientBuilder =>
+builder.Services.AddDaprJobsClient((_, daprJobsClientBuilder) =>
{
daprJobsClientBuilder.UseDaprApiToken("abc123");
daprJobsClientBuilder.UseHttpEndpoint("http://localhost:8512"); //Non-standard sidecar HTTP endpoint
@@ -102,6 +107,79 @@ builder.Services.AddDaprJobsClient((serviceProvider, daprJobsClientBuilder) =>
var app = builder.Build();
```
+## Use the Dapr Jobs client using IConfiguration
+It's possible to configure the Dapr Jobs client using the values in your registered `IConfiguration` as well without
+explicitly specifying each of the value overrides using the `DaprJobsClientBuilder` as demonstrated in the previous
+section. Rather, by populating an `IConfiguration` made available through dependency injection the `AddDaprJobsClient()`
+registration will automatically use these values over their respective defaults.
+
+Start by populating the values in your configuration. This can be done in several different ways as demonstrated below.
+
+### Configuration via `ConfigurationBuilder`
+Application settings can be configured without using a configuration source and by instead populating the value in-memory
+using a `ConfigurationBuilder` instance:
+
+```csharp
+var builder = WebApplication.CreateBuilder();
+
+//Create the configuration
+var configuration = new ConfigurationBuilder()
+ .AddInMemoryCollection(new Dictionary {
+ { "DAPR_HTTP_ENDPOINT", "http://localhost:54321" },
+ { "DAPR_API_TOKEN", "abc123" }
+ })
+ .Build();
+
+builder.Configuration.AddConfiguration(configuration);
+builder.Services.AddDaprJobsClient(); //This will automatically populate the HTTP endpoint and API token values from the IConfiguration
+```
+
+### Configuration via Environment Variables
+Application settings can be accessed from environment variables available to your application.
+
+The following environment variables will be used to populate both the HTTP endpoint and API token used to register the
+Dapr Jobs client.
+
+| Key | Value |
+| --- | --- |
+| DAPR_HTTP_ENDPOINT | http://localhost:54321 |
+| DAPR_API_TOKEN | abc123 |
+
+```csharp
+var builder = WebApplication.CreateBuilder();
+
+builder.Configuration.AddEnvironmentVariables();
+builder.Services.AddDaprJobsClient();
+```
+
+The Dapr Jobs client will be configured to use both the HTTP endpoint `http://localhost:54321` and populate all outbound
+requests with the API token header `abc123`.
+
+### Configuration via prefixed Environment Variables
+
+However, in shared-host scenarios where there are multiple applications all running on the same machine without using
+containers or in development environments, it's not uncommon to prefix environment variables. The following example
+assumes that both the HTTP endpoint and the API token will be pulled from environment variables prefixed with the
+value "myapp_". The two environment variables used in this scenario are as follows:
+
+| Key | Value |
+| --- | --- |
+| myapp_DAPR_HTTP_ENDPOINT | http://localhost:54321 |
+| myapp_DAPR_API_TOKEN | abc123 |
+
+These environment variables will be loaded into the registered configuration in the following example and made available
+without the prefix attached.
+
+```csharp
+var builder = WebApplication.CreateBuilder();
+
+builder.Configuration.AddEnvironmentVariables(prefix: "myapp_");
+builder.Services.AddDaprJobsClient();
+```
+
+The Dapr Jobs client will be configured to use both the HTTP endpoint `http://localhost:54321` and populate all outbound
+requests with the API token header `abc123`.
+
## Use the Dapr Jobs client without relying on dependency injection
While the use of dependency injection simplifies the use of complex types in .NET and makes it easier to
deal with complicated configurations, you're not required to register the `DaprJobsClient` in this way. Rather, you can also elect to create an instance of it from a `DaprJobsClientBuilder` instance as demonstrated below:
@@ -118,7 +196,6 @@ public class MySampleClass
//Do something with the `daprJobsClient`
}
}
-
```
## Set up a endpoint to be invoked when the job is triggered
diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/dotnet-jobsclient-usage.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/dotnet-jobsclient-usage.md
index 4c28e6595..bbdbbdbe0 100644
--- a/daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/dotnet-jobsclient-usage.md
+++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/dotnet-jobsclient-usage.md
@@ -2,23 +2,32 @@
type: docs
title: "DaprJobsClient usage"
linkTitle: "DaprJobsClient usage"
-weight: 5000
+weight: 59000
description: Essential tips and advice for using DaprJobsClient
---
## Lifetime management
-A `DaprJobsClient` is a version of the Dapr client that is dedicated to interacting with the Dapr Jobs API. It can be registered alongside a `DaprClient` without issue.
+A `DaprJobsClient` is a version of the Dapr client that is dedicated to interacting with the Dapr Jobs API. It can be
+registered alongside a `DaprClient` and other Dapr clients without issue.
-It maintains access to networking resources in the form of TCP sockets used to communicate with the Dapr sidecar and implements `IDisposable` to support the eager cleanup of resources.
+It maintains access to networking resources in the form of TCP sockets used to communicate with the Dapr sidecar and
+implements `IDisposable` to support the eager cleanup of resources.
-For best performance, create a single long-lived instance of `DaprJobsClient` and provide access to that shared instance throughout your application. `DaprJobsClient` instances are thread-safe and intended to be shared.
+For best performance, create a single long-lived instance of `DaprJobsClient` and provide access to that shared instance
+throughout your application. `DaprJobsClient` instances are thread-safe and intended to be shared.
+
+This can be aided by utilizing the dependency injection functionality. The registration method supports registration using
+as a singleton, a scoped instance or as transient (meaning it's recreated every time it's injected), but also enables
+registration to utilize values from an `IConfiguration` or other injected service in a way that's impractical when
+creating the client from scratch in each of your classes.
Avoid creating a `DaprJobsClient` for each operation and disposing it when the operation is complete.
## Configuring DaprJobsClient via the DaprJobsClientBuilder
-A `DaprJobsClient` can be configured by invoking methods on the `DaprJobsClientBuilder` class before calling `.Build()` to create the client itself. The settings for each `DaprJobsClient` are separate
+A `DaprJobsClient` can be configured by invoking methods on the `DaprJobsClientBuilder` class before calling `.Build()`
+to create the client itself. The settings for each `DaprJobsClient` are separate
and cannot be changed after calling `.Build()`.
```cs
@@ -47,7 +56,8 @@ The SDK will read the following environment variables to configure the default v
### Configuring gRPC channel options
-Dapr's use of `CancellationToken` for cancellation relies on the configuration of the gRPC channel options. If you need to configure these options yourself, make sure to enable the [ThrowOperationCanceledOnCancellation setting](https://grpc.github.io/grpc/csharp-dotnet/api/Grpc.Net.Client.GrpcChannelOptions.html#Grpc_Net_Client_GrpcChannelOptions_ThrowOperationCanceledOnCancellation).
+Dapr's use of `CancellationToken` for cancellation relies on the configuration of the gRPC channel options. If you need
+to configure these options yourself, make sure to enable the [ThrowOperationCanceledOnCancellation setting](https://grpc.github.io/grpc/csharp-dotnet/api/Grpc.Net.Client.GrpcChannelOptions.html#Grpc_Net_Client_GrpcChannelOptions_ThrowOperationCanceledOnCancellation).
```cs
var daprJobsClient = new DaprJobsClientBuilder()
@@ -55,19 +65,27 @@ var daprJobsClient = new DaprJobsClientBuilder()
.Build();
```
-## Using cancellation with DaprJobsClient
+## Using cancellation with `DaprJobsClient`
-The APIs on DaprJobsClient perform asynchronous operations and accept an optional `CancellationToken` parameter. This follows a standard .NET idiom for cancellable operations. Note that when cancellation occurs, there is no guarantee that the remote endpoint stops processing the request, only that the client has stopped waiting for completion.
+The APIs on `DaprJobsClient` perform asynchronous operations and accept an optional `CancellationToken` parameter. This
+follows a standard .NET practice for cancellable operations. Note that when cancellation occurs, there is no guarantee that
+the remote endpoint stops processing the request, only that the client has stopped waiting for completion.
When an operation is cancelled, it will throw an `OperationCancelledException`.
-## Configuring DaprJobsClient via dependency injection
+## Configuring `DaprJobsClient` via dependency injection
-Using the built-in extension methods for registering the `DaprJobsClient` in a dependency injection container can provide the benefit of registering the long-lived service a single time, centralize complex configuration and improve performance by ensuring similarly long-lived resources are re-purposed when possible (e.g. `HttpClient` instances).
+Using the built-in extension methods for registering the `DaprJobsClient` in a dependency injection container can
+provide the benefit of registering the long-lived service a single time, centralize complex configuration and improve
+performance by ensuring similarly long-lived resources are re-purposed when possible (e.g. `HttpClient` instances).
-There are three overloads available to give the developer the greatest flexibility in configuring the client for their scenario. Each of these will register the `IHttpClientFactory` on your behalf if not already registered, and configure the `DaprJobsClientBuilder` to use it when creating the `HttpClient` instance in order to re-use the same instance as much as possible and avoid socket exhaution and other issues.
+There are three overloads available to give the developer the greatest flexibility in configuring the client for their
+scenario. Each of these will register the `IHttpClientFactory` on your behalf if not already registered, and configure
+the `DaprJobsClientBuilder` to use it when creating the `HttpClient` instance in order to re-use the same instance as
+much as possible and avoid socket exhaustion and other issues.
-In the first approach, there's no configuration done by the developer and the `DaprJobsClient` is configured with the default settings.
+In the first approach, there's no configuration done by the developer and the `DaprJobsClient` is configured with the
+default settings.
```cs
var builder = WebApplication.CreateBuilder(args);
@@ -76,12 +94,14 @@ builder.Services.AddDaprJobsClient(); //Registers the `DaprJobsClient` to be inj
var app = builder.Build();
```
-Sometimes the developer will need to configure the created client using the various configuration options detailed above. This is done through an overload that passes in the `DaprJobsClientBuiler` and exposes methods for configuring the necessary options.
+Sometimes the developer will need to configure the created client using the various configuration options detailed
+above. This is done through an overload that passes in the `DaprJobsClientBuiler` and exposes methods for configuring
+the necessary options.
```cs
var builder = WebApplication.CreateBuilder(args);
-builder.Services.AddDaprJobsClient(daprJobsClientBuilder => {
+builder.Services.AddDaprJobsClient((_, daprJobsClientBuilder) => {
//Set the API token
daprJobsClientBuilder.UseDaprApiToken("abc123");
//Specify a non-standard HTTP endpoint
@@ -91,7 +111,10 @@ builder.Services.AddDaprJobsClient(daprJobsClientBuilder => {
var app = builder.Build();
```
-Finally, it's possible that the developer may need to retrieve information from another service in order to populate these configuration values. That value may be provided from a `DaprClient` instance, a vendor-specific SDK or some local service, but as long as it's also registered in DI, it can be injected into this configuration operation via the last overload:
+Finally, it's possible that the developer may need to retrieve information from another service in order to populate
+these configuration values. That value may be provided from a `DaprClient` instance, a vendor-specific SDK or some
+local service, but as long as it's also registered in DI, it can be injected into this configuration operation via the
+last overload:
```cs
var builder = WebApplication.CreateBuilder(args);
@@ -113,9 +136,16 @@ var app = builder.Build();
## Understanding payload serialization on DaprJobsClient
-While there are many methods on the `DaprClient` that automatically serialize and deserialize data using the `System.Text.Json` serializer, this SDK takes a different philosophy. Instead, the relevant methods accept an optional payload of `ReadOnlyMemory` meaning that serialization is an exercise left to the developer and is not generally handled by the SDK.
+While there are many methods on the `DaprClient` that automatically serialize and deserialize data using the
+`System.Text.Json` serializer, this SDK takes a different philosophy. Instead, the relevant methods accept an optional
+payload of `ReadOnlyMemory` meaning that serialization is an exercise left to the developer and is not
+generally handled by the SDK.
-That said, there are some helper extension methods available for each of the scheduling methods. If you know that you want to use a type that's JSON-serializable, you can use the `Schedule*WithPayloadAsync` method for each scheduling type that accepts an `object` as a payload and an optional `JsonSerializerOptions` to use when serializing the value. This will convert the value to UTF-8 encoded bytes for you as a convenience. Here's an example of what this might look like when scheduling a Cron expression:
+That said, there are some helper extension methods available for each of the scheduling methods. If you know that you
+want to use a type that's JSON-serializable, you can use the `Schedule*WithPayloadAsync` method for each scheduling
+type that accepts an `object` as a payload and an optional `JsonSerializerOptions` to use when serializing the value.
+This will convert the value to UTF-8 encoded bytes for you as a convenience. Here's an example of what this might
+look like when scheduling a Cron expression:
```cs
public sealed record Doodad (string Name, int Value);
@@ -125,7 +155,9 @@ var doodad = new Doodad("Thing", 100);
await daprJobsClient.ScheduleCronJobWithPayloadAsync("myJob", "5 * * * *", doodad);
```
-In the same vein, if you have a plain string value, you can use an overload of the same method to serialize a string-typed payload and the JSON serialization step will be skipped and it'll only be encoded to an array of UTF-8 encoded bytes. Here's an exampe of what this might look like when scheduling a one-time job:
+In the same vein, if you have a plain string value, you can use an overload of the same method to serialize a
+string-typed payload and the JSON serialization step will be skipped and it'll only be encoded to an array of
+UTF-8 encoded bytes. Here's an example of what this might look like when scheduling a one-time job:
```cs
var now = DateTime.UtcNow;
@@ -133,7 +165,10 @@ var oneWeekFromNow = now.AddDays(7);
await daprJobsClient.ScheduleOneTimeJobWithPayloadAsync("myOtherJob", oneWeekFromNow, "This is a test!");
```
-The `JobDetails` type returns the data as a `ReadOnlyMemory?` so the developer has the freedom to deserialize as they wish, but there are again two helper extensions included that can deserialize this to either a JSON-compatible type or a string. Both methods assume that the developer encoded the originally scheduled job (perhaps using the helper serialization methods) as these methods will not force the bytes to represent something they're not.
+The `JobDetails` type returns the data as a `ReadOnlyMemory?` so the developer has the freedom to deserialize
+as they wish, but there are again two helper extensions included that can deserialize this to either a JSON-compatible
+type or a string. Both methods assume that the developer encoded the originally scheduled job (perhaps using the
+helper serialization methods) as these methods will not force the bytes to represent something they're not.
To deserialize the bytes to a string, the following helper method can be used:
```cs
@@ -143,7 +178,9 @@ if (jobDetails.Payload is not null)
}
```
-To deserialize JSON-encoded UTF-8 bytes to the corresponding type, the following helper method can be used. An overload argument is available that permits the developer to pass in their own `JsonSerializerOptions` to be applied during deserialization.
+To deserialize JSON-encoded UTF-8 bytes to the corresponding type, the following helper method can be used. An
+overload argument is available that permits the developer to pass in their own `JsonSerializerOptions` to be applied
+during deserialization.
```cs
public sealed record Doodad (string Name, int Value);
@@ -157,7 +194,12 @@ if (jobDetails.Payload is not null)
## Error handling
-Methods on `DaprJobsClient` will throw a `DaprJobsServiceException` if an issue is encountered between the SDK and the Jobs API service running on the Dapr sidecar. If a failure is encountered because of a poorly formatted request made to the Jobs API service through this SDK, a `DaprMalformedJobException` will be thrown. In case of illegal argument values, the appropriate standard exception will be thrown (e.g. `ArgumentOutOfRangeException` or `ArgumentNullException`) with the name of the offending argument. And for anything else, a `DaprException` will be thrown.
+Methods on `DaprJobsClient` will throw a `DaprJobsServiceException` if an issue is encountered between the SDK
+and the Jobs API service running on the Dapr sidecar. If a failure is encountered because of a poorly formatted
+request made to the Jobs API service through this SDK, a `DaprMalformedJobException` will be thrown. In case of
+illegal argument values, the appropriate standard exception will be thrown (e.g. `ArgumentOutOfRangeException`
+or `ArgumentNullException`) with the name of the offending argument. And for anything else, a `DaprException`
+will be thrown.
The most common cases of failure will be related to:
diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-messaging/_index.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-messaging/_index.md
new file mode 100644
index 000000000..7418f008a
--- /dev/null
+++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-messaging/_index.md
@@ -0,0 +1,17 @@
+---
+type: docs
+title: "Dapr Messaging .NET SDK"
+linkTitle: "Messaging"
+weight: 60000
+description: Get up and running with the Dapr Messaging .NET SDK
+---
+
+With the Dapr Messaging package, you can interact with the Dapr messaging APIs from a .NET application. In the
+v1.15 release, this package only contains the functionality corresponding to the
+[streaming PubSub capability](https://docs.dapr.io/developing-applications/building-blocks/pubsub/howto-publish-subscribe/#subscribe-to-topics).
+
+Future Dapr .NET SDK releases will migrate existing messaging capabilities out from Dapr.Client to this
+Dapr.Messaging package. This will be documented in the release notes, documentation and obsolete attributes in advance.
+
+To get started, walk through the [Dapr Messaging]({{< ref dotnet-messaging-pubsub-howto.md >}}) how-to guide and
+refer to [best practices documentation]({{< ref dotnet-messaging-pubsub-usage.md >}}) for additional guidance.
\ No newline at end of file
diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-messaging/dotnet-messaging-pubsub-howto.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-messaging/dotnet-messaging-pubsub-howto.md
new file mode 100644
index 000000000..b128d884e
--- /dev/null
+++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-messaging/dotnet-messaging-pubsub-howto.md
@@ -0,0 +1,268 @@
+---
+type: docs
+title: "How to: Author and manage Dapr streaming subscriptions in the .NET SDK"
+linkTitle: "How to: Author & manage streaming subscriptions"
+weight: 61000
+description: Learn how to author and manage Dapr streaming subscriptions using the .NET SDK
+---
+
+Let's create a subscription to a pub/sub topic or queue at using the streaming capability. We'll use the
+[simple example provided here](https://github.com/dapr/dotnet-sdk/tree/master/examples/Client/PublishSubscribe/StreamingSubscriptionExample),
+for the following demonstration and walk through it as an explainer of how you can configure message handlers at
+runtime and which do not require an endpoint to be pre-configured. In this guide, you will:
+
+- Deploy a .NET Web API application ([StreamingSubscriptionExample](https://github.com/dapr/dotnet-sdk/tree/master/examples/Client/PublishSubscribe/StreamingSubscriptionExample))
+- Utilize the Dapr .NET Messaging SDK to subscribe dynamically to a pub/sub topic.
+
+## Prerequisites
+- [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/)
+- [Initialized Dapr environment](https://docs.dapr.io/getting-started/install-dapr-selfhost)
+- [.NET 6](https://dotnet.microsoft.com/download/dotnet/6.0), [.NET 8](https://dotnet.microsoft.com/download/dotnet/8.0) or [.NET 9](https://dotnet.microsoft.com/download/dotnet/9.0) installed
+- [Dapr.Messaging](https://www.nuget.org/packages/Dapr.Messaging) NuGet package installed to your project
+
+{{% alert title="Note" color="primary" %}}
+
+Note that while .NET 6 is the minimum support version of .NET 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).
+
+```sh
+git clone https://github.com/dapr/dotnet-sdk.git
+```
+
+From the .NET SDK root directory, navigate to the Dapr streaming PubSub example.
+
+```sh
+cd examples/Client/PublishSubscribe
+```
+
+## Run the application locally
+
+To run the Dapr application, you need to start the .NET program and a Dapr sidecar. Navigate to the `StreamingSubscriptionExample` directory.
+
+```sh
+cd StreamingSubscriptionExample
+```
+
+We'll run a command that starts both the Dapr sidecar and the .NET program at the same time.
+
+```sh
+dapr run --app-id pubsubapp --dapr-grpc-port 4001 --dapr-http-port 3500 -- dotnet run
+```
+> Dapr listens for HTTP requests at `http://localhost:3500` and internal Jobs gRPC requests at `http://localhost:4001`.
+
+## Register the Dapr PubSub client with dependency injection
+The Dapr Messaging SDK provides an extension method to simplify the registration of the Dapr PubSub client. Before
+completing the dependency injection registration in `Program.cs`, add the following line:
+
+```csharp
+var builder = WebApplication.CreateBuilder(args);
+
+//Add anywhere between these two
+builder.Services.AddDaprPubSubClient(); //That's it
+
+var app = builder.Build();
+```
+
+It's possible that you may want to provide some configuration options to the Dapr PubSub client that
+should be present with each call to the sidecar such as a Dapr API token, or you want to use a non-standard
+HTTP or gRPC endpoint. This be possible through use of an overload of the registration method that allows configuration
+of a `DaprPublishSubscribeClientBuilder` instance:
+
+```csharp
+var builder = WebApplication.CreateBuilder(args);
+
+builder.Services.AddDaprPubSubClient((_, daprPubSubClientBuilder) => {
+ daprPubSubClientBuilder.UseDaprApiToken("abc123");
+ daprPubSubClientBuilder.UseHttpEndpoint("http://localhost:8512"); //Non-standard sidecar HTTP endpoint
+});
+
+var app = builder.Build();
+```
+
+Still, it's possible that whatever values you wish to inject need to be retrieved from some other source, itself registered as a dependency. There's one more overload you can use to inject an `IServiceProvider` into the configuration action method. In the following example, we register a fictional singleton that can retrieve secrets from somewhere and pass it into the configuration method for `AddDaprJobClient` so
+we can retrieve our Dapr API token from somewhere else for registration here:
+
+```csharp
+var builder = WebApplication.CreateBuilder(args);
+
+builder.Services.AddSingleton();
+builder.Services.AddDaprPubSubClient((serviceProvider, daprPubSubClientBuilder) => {
+ var secretRetriever = serviceProvider.GetRequiredService();
+ var daprApiToken = secretRetriever.GetSecret("DaprApiToken").Value;
+ daprPubSubClientBuilder.UseDaprApiToken(daprApiToken);
+
+ daprPubSubClientBuilder.UseHttpEndpoint("http://localhost:8512");
+});
+
+var app = builder.Build();
+```
+
+## Use the Dapr PubSub client using IConfiguration
+It's possible to configure the Dapr PubSub client using the values in your registered `IConfiguration` as well without
+explicitly specifying each of the value overrides using the `DaprPublishSubscribeClientBuilder` as demonstrated in the previous
+section. Rather, by populating an `IConfiguration` made available through dependency injection the `AddDaprPubSubClient()`
+registration will automatically use these values over their respective defaults.
+
+Start by populating the values in your configuration. This can be done in several different ways as demonstrated below.
+
+### Configuration via `ConfigurationBuilder`
+Application settings can be configured without using a configuration source and by instead populating the value in-memory
+using a `ConfigurationBuilder` instance:
+
+```csharp
+var builder = WebApplication.CreateBuilder();
+
+//Create the configuration
+var configuration = new ConfigurationBuilder()
+ .AddInMemoryCollection(new Dictionary {
+ { "DAPR_HTTP_ENDPOINT", "http://localhost:54321" },
+ { "DAPR_API_TOKEN", "abc123" }
+ })
+ .Build();
+
+builder.Configuration.AddConfiguration(configuration);
+builder.Services.AddDaprPubSubClient(); //This will automatically populate the HTTP endpoint and API token values from the IConfiguration
+```
+
+### Configuration via Environment Variables
+Application settings can be accessed from environment variables available to your application.
+
+The following environment variables will be used to populate both the HTTP endpoint and API token used to register the
+Dapr PubSub client.
+
+| Key | Value |
+|--------------------|------------------------|
+| DAPR_HTTP_ENDPOINT | http://localhost:54321 |
+| DAPR_API_TOKEN | abc123 |
+
+```csharp
+var builder = WebApplication.CreateBuilder();
+
+builder.Configuration.AddEnvironmentVariables();
+builder.Services.AddDaprPubSubClient();
+```
+
+The Dapr PubSub client will be configured to use both the HTTP endpoint `http://localhost:54321` and populate all outbound
+requests with the API token header `abc123`.
+
+### Configuration via prefixed Environment Variables
+However, in shared-host scenarios where there are multiple applications all running on the same machine without using
+containers or in development environments, it's not uncommon to prefix environment variables. The following example
+assumes that both the HTTP endpoint and the API token will be pulled from environment variables prefixed with the
+value "myapp_". The two environment variables used in this scenario are as follows:
+
+| Key | Value |
+|--------------------------|------------------------|
+| myapp_DAPR_HTTP_ENDPOINT | http://localhost:54321 |
+| myapp_DAPR_API_TOKEN | abc123 |
+
+These environment variables will be loaded into the registered configuration in the following example and made available
+without the prefix attached.
+
+```csharp
+var builder = WebApplication.CreateBuilder();
+
+builder.Configuration.AddEnvironmentVariables(prefix: "myapp_");
+builder.Services.AddDaprPubSubClient();
+```
+
+The Dapr PubSub client will be configured to use both the HTTP endpoint `http://localhost:54321` and populate all outbound
+requests with the API token header `abc123`.
+
+## Use the Dapr PubSub client without relying on dependency injection
+While the use of dependency injection simplifies the use of complex types in .NET and makes it easier to
+deal with complicated configurations, you're not required to register the `DaprPublishSubscribeClient` in this way.
+Rather, you can also elect to create an instance of it from a `DaprPublishSubscribeClientBuilder` instance as
+demonstrated below:
+
+```cs
+
+public class MySampleClass
+{
+ public void DoSomething()
+ {
+ var daprPubSubClientBuilder = new DaprPublishSubscribeClientBuilder();
+ var daprPubSubClient = daprPubSubClientBuilder.Build();
+
+ //Do something with the `daprPubSubClient`
+ }
+}
+```
+
+## Set up message handler
+The streaming subscription implementation in Dapr gives you greater control over handling backpressure from events by
+leaving the messages in the Dapr runtime until your application is ready to accept them. The .NET SDK supports a
+high-performance queue for maintaining a local cache of these messages in your application while processing is pending.
+These messages will persist in the queue until processing either times out for each one or a response action is taken
+for each (typically after processing succeeds or fails). Until this response action is received by the Dapr runtime,
+the messages will be persisted by Dapr and made available in case of a service failure.
+
+The various response actions available are as follows:
+| Response Action | Description |
+| --- | --- |
+| Retry | The event should be delivered again in the future. |
+| Drop | The event should be deleted (or forwarded to a dead letter queue, if configured) and not attempted again. |
+| Success | The event should be deleted as it was successfully processed. |
+
+The handler will receive only one message at a time and if a cancellation token is provided to the subscription,
+this token will be provided during the handler invocation.
+
+The handler must be configured to return a `Task` indicating one of these operations, even if from
+a try/catch block. If an exception is not caught by your handler, the subscription will use the response action configured
+in the options during subscription registration.
+
+The following demonstrates the sample message handler provided in the example:
+
+```csharp
+Task HandleMessageAsync(TopicMessage message, CancellationToken cancellationToken = default)
+{
+ try
+ {
+ //Do something with the message
+ Console.WriteLine(Encoding.UTF8.GetString(message.Data.Span));
+ return Task.FromResult(TopicResponseAction.Success);
+ }
+ catch
+ {
+ return Task.FromResult(TopicResponseAction.Retry);
+ }
+}
+```
+
+## Configure and subscribe to the PubSub topic
+Configuration of the streaming subscription requires the name of the PubSub component registered with Dapr, the name
+of the topic or queue being subscribed to, the `DaprSubscriptionOptions` providing the configuration for the subscription,
+the message handler and an optional cancellation token. The only required argument to the `DaprSubscriptionOptions` is
+the default `MessageHandlingPolicy` which consists of a per-event timeout and the `TopicResponseAction` to take when
+that timeout occurs.
+
+Other options are as follows:
+
+| Property Name | Description |
+|-----------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------|
+| Metadata | Additional subscription metadata |
+| DeadLetterTopic | The optional name of the dead-letter topic to send dropped messages to. |
+| MaximumQueuedMessages | By default, there is no maximum boundary enforced for the internal queue, but setting this |
+| property would impose an upper limit. | |
+| MaximumCleanupTimeout | When the subscription is disposed of or the token flags a cancellation request, this specifies |
+| the maximum amount of time available to process the remaining messages in the internal queue. | |
+
+Subscription is then configured as in the following example:
+```csharp
+var messagingClient = app.Services.GetRequiredService();
+
+var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(60)); //Override the default of 30 seconds
+var options = new DaprSubscriptionOptions(new MessageHandlingPolicy(TimeSpan.FromSeconds(10), TopicResponseAction.Retry));
+var subscription = await messagingClient.SubscribeAsync("pubsub", "mytopic", options, HandleMessageAsync, cancellationTokenSource.Token);
+```
+
+## Terminate and clean up subscription
+When you've finished with your subscription and wish to stop receiving new events, simply await a call to
+`DisposeAsync()` on your subscription instance. This will cause the client to unregister from additional events and
+proceed to finish processing all the events still leftover in the backpressure queue, if any, before disposing of any
+internal resources. This cleanup will be limited to the timeout interval provided in the `DaprSubscriptionOptions` when
+the subscription was registered and by default, this is set to 30 seconds.
\ No newline at end of file
diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-messaging/dotnet-messaging-pubsub-usage.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-messaging/dotnet-messaging-pubsub-usage.md
new file mode 100644
index 000000000..8b3359d0c
--- /dev/null
+++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-messaging/dotnet-messaging-pubsub-usage.md
@@ -0,0 +1,130 @@
+---
+type: docs
+title: "DaprPublishSubscribeClient usage"
+linkTitle: "DaprPublishSubscribeClient usage"
+weight: 69000
+description: Essential tips and advice for using DaprPublishSubscribeClient
+---
+
+## Lifetime management
+
+A `DaprPublishSubscribeClient` is a version of the Dapr client that is dedicated to interacting with the Dapr Messaging API.
+It can be registered alongside a `DaprClient` and other Dapr clients without issue.
+
+It maintains access to networking resources in the form of TCP sockets used to communicate with the Dapr sidecar and implements
+`IAsyncDisposable` to support the eager cleanup of resources.
+
+For best performance, create a single long-lived instance of `DaprPublishSubscribeClient` and provide access to that shared
+instance throughout your application. `DaprPublishSubscribeClient` instances are thread-safe and intended to be shared.
+
+This can be aided by utilizing the dependency injection functionality. The registration method supports registration using
+as a singleton, a scoped instance or as transient (meaning it's recreated every time it's injected), but also enables
+registration to utilize values from an `IConfiguration` or other injected service in a way that's impractical when
+creating the client from scratch in each of your classes.
+
+Avoid creating a `DaprPublishSubscribeClient` for each operation and disposing it when the operation is complete. It's
+intended that the `DaprPublishSubscribeClient` should only be disposed when you no longer wish to receive events on the
+subscription as disposing it will cancel the ongoing receipt of new events.
+
+## Configuring DaprPublishSubscribeClient via the DaprPublishSubscribeClientBuilder
+A `DaprPublishSubscribeClient` can be configured by invoking methods on the `DaprPublishSubscribeClientBuilder` class
+before calling `.Build()` to create the client itself. The settings for each `DaprPublishSubscribeClient` are separate
+and cannot be changed after calling `.Build()`.
+
+```cs
+var daprPubsubClient = new DaprPublishSubscribeClientBuilder()
+ .UseDaprApiToken("abc123") // Specify the API token used to authenticate to other Dapr sidecars
+ .Build();
+```
+
+The `DaprPublishSubscribeClientBuilder` contains settings for:
+
+- The HTTP endpoint of the Dapr sidecar
+- The gRPC endpoint of the Dapr sidecar
+- The `JsonSerializerOptions` object used to configure JSON serialization
+- The `GrpcChannelOptions` object used to configure gRPC
+- The API token used to authenticate requests to the sidecar
+- The factory method used to create the `HttpClient` instance used by the SDK
+- The timeout used for the `HttpClient` instance when making requests to the sidecar
+
+The SDK will read the following environment variables to configure the default values:
+
+- `DAPR_HTTP_ENDPOINT`: used to find the HTTP endpoint of the Dapr sidecar, example: `https://dapr-api.mycompany.com`
+- `DAPR_GRPC_ENDPOINT`: used to find the gRPC endpoint of the Dapr sidecar, example: `https://dapr-grpc-api.mycompany.com`
+- `DAPR_HTTP_PORT`: if `DAPR_HTTP_ENDPOINT` is not set, this is used to find the HTTP local endpoint of the Dapr sidecar
+- `DAPR_GRPC_PORT`: if `DAPR_GRPC_ENDPOINT` is not set, this is used to find the gRPC local endpoint of the Dapr sidecar
+- `DAPR_API_TOKEN`: used to set the API token
+
+### Configuring gRPC channel options
+Dapr's use of `CancellationToken` for cancellation relies on the configuration of the gRPC channel options. If you
+need to configure these options yourself, make sure to enable the [ThrowOperationCanceledOnCancellation setting](https://grpc.github.io/grpc/csharp-dotnet/api/Grpc.Net.Client.GrpcChannelOptions.html#Grpc_Net_Client_GrpcChannelOptions_ThrowOperationCanceledOnCancellation).
+
+```cs
+var daprPubsubClient = new DaprPublishSubscribeClientBuilder()
+ .UseGrpcChannelOptions(new GrpcChannelOptions { ... ThrowOperationCanceledOnCancellation = true })
+ .Build();
+```
+
+## Using cancellation with `DaprPublishSubscribeClient`
+
+The APIs on `DaprPublishSubscribeClient` perform asynchronous operations and accept an optional `CancellationToken`
+parameter. This follows a standard .NET practice for cancellable operations. Note that when cancellation occurs, there is
+no guarantee that the remote endpoint stops processing the request, only that the client has stopped waiting for completion.
+
+When an operation is cancelled, it will throw an `OperationCancelledException`.
+
+## Configuring `DaprPublishSubscribeClient` via dependency injection
+
+Using the built-in extension methods for registering the `DaprPublishSubscribeClient` in a dependency injection container
+can provide the benefit of registering the long-lived service a single time, centralize complex configuration and improve
+performance by ensuring similarly long-lived resources are re-purposed when possible (e.g. `HttpClient` instances).
+
+There are three overloads available to give the developer the greatest flexibility in configuring the client for their
+scenario. Each of these will register the `IHttpClientFactory` on your behalf if not already registered, and configure
+the `DaprPublishSubscribeClientBuilder` to use it when creating the `HttpClient` instance in order to re-use the same
+instance as much as possible and avoid socket exhaustion and other issues.
+
+In the first approach, there's no configuration done by the developer and the `DaprPublishSubscribeClient` is configured with
+the default settings.
+
+```cs
+var builder = WebApplication.CreateBuilder(args);
+
+builder.Services.DaprPublishSubscribeClient(); //Registers the `DaprPublishSubscribeClient` to be injected as needed
+var app = builder.Build();
+```
+
+Sometimes the developer will need to configure the created client using the various configuration options detailed above. This is done through an overload that passes in the `DaprJobsClientBuiler` and exposes methods for configuring the necessary options.
+
+```cs
+var builder = WebApplication.CreateBuilder(args);
+
+builder.Services.AddDaprJobsClient((_, daprPubSubClientBuilder) => {
+ //Set the API token
+ daprPubSubClientBuilder.UseDaprApiToken("abc123");
+ //Specify a non-standard HTTP endpoint
+ daprPubSubClientBuilder.UseHttpEndpoint("http://dapr.my-company.com");
+});
+
+var app = builder.Build();
+```
+
+Finally, it's possible that the developer may need to retrieve information from another service in order to populate these configuration values. That value may be provided from a `DaprClient` instance, a vendor-specific SDK or some local service, but as long as it's also registered in DI, it can be injected into this configuration operation via the last overload:
+
+```cs
+var builder = WebApplication.CreateBuilder(args);
+
+//Register a fictional service that retrieves secrets from somewhere
+builder.Services.AddSingleton();
+
+builder.Services.AddDaprPublishSubscribeClient((serviceProvider, daprPubSubClientBuilder) => {
+ //Retrieve an instance of the `SecretService` from the service provider
+ var secretService = serviceProvider.GetRequiredService();
+ var daprApiToken = secretService.GetSecret("DaprApiToken").Value;
+
+ //Configure the `DaprPublishSubscribeClientBuilder`
+ daprPubSubClientBuilder.UseDaprApiToken(daprApiToken);
+});
+
+var app = builder.Build();
+```
\ No newline at end of file
diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-troubleshooting/_index.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-troubleshooting/_index.md
index e7d8da774..5ffd7c7c6 100644
--- a/daprdocs/content/en/dotnet-sdk-docs/dotnet-troubleshooting/_index.md
+++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-troubleshooting/_index.md
@@ -2,6 +2,6 @@
type: docs
title: "How to troubleshoot and debug with the Dapr .NET SDK"
linkTitle: "Troubleshooting"
-weight: 100000
+weight: 120000
description: Tips, tricks, and guides for troubleshooting and debugging with the Dapr .NET SDKs
---
\ No newline at end of file
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 9be910234..e81efc340 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
@@ -20,13 +20,12 @@ In the .NET example project:
- [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/)
- [Initialized Dapr environment](https://docs.dapr.io/getting-started/install-dapr-selfhost/)
-- [.NET 7](https://dotnet.microsoft.com/download), [.NET 8](https://dotnet.microsoft.com/download) or [.NET 9](https://dotnet.microsoft.com/download) installed
+- [.NET 7](https://dotnet.microsoft.com/download/dotnet/7.0), [.NET 8](https://dotnet.microsoft.com/download/dotnet/8.0) or [.NET 9](https://dotnet.microsoft.com/download/dotnet/9.0) 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.
+Dapr.Workflows supports .NET 7 or newer in v1.15. However, following the release of Dapr v1.16, only
+.NET 8 and .NET 9 will be supported.
{{% /alert %}}
diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-workflow/dotnet-workflowclient-usage.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-workflow/dotnet-workflowclient-usage.md
index ac6a0f189..a376e6acb 100644
--- a/daprdocs/content/en/dotnet-sdk-docs/dotnet-workflow/dotnet-workflowclient-usage.md
+++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-workflow/dotnet-workflowclient-usage.md
@@ -74,4 +74,64 @@ builder.Services.AddDaprWorkflow(options => {
var app = builder.Build();
await app.RunAsync();
-```
\ No newline at end of file
+```
+
+## Injecting Services into Workflow Activities
+Workflow activities support the same dependency injection that developers have come to expect of modern C# applications. Assuming a proper
+registration at startup, any such type can be injected into the constructor of the workflow activity and available to utilize during
+the execution of the workflow. This makes it simple to add logging via an injected `ILogger` or access to other Dapr
+building blocks by injecting `DaprClient` or `DaprJobsClient`, for example.
+
+```csharp
+internal sealed class SquareNumberActivity : WorkflowActivity
+{
+ private readonly ILogger _logger;
+
+ public MyActivity(ILogger logger)
+ {
+ this._logger = logger;
+ }
+
+ public override Task RunAsync(WorkflowActivityContext context, int input)
+ {
+ this._logger.LogInformation("Squaring the value {number}", input);
+ var result = input * input;
+ this._logger.LogInformation("Got a result of {squareResult}", result);
+
+ return Task.FromResult(result);
+ }
+}
+```
+
+### Using ILogger in Workflow
+Because workflows must be deterministic, it is not possible to inject arbitrary services into them. For example,
+if you were able to inject a standard `ILogger` into a workflow and it needed to be replayed because of an error,
+subsequent replay from the event source log would result in the log recording additional operations that didn't actually
+take place a second or third time because their results were sourced from the log. This has the potential to introduce
+a significant amount of confusion. Rather, a replay-safe logger is made available for use within workflows. It will only
+log events the first time the workflow runs and will not log anything whenever the workflow is being replaced.
+
+This logger can be retrieved from a method present on the `WorkflowContext` available on your workflow instance and
+otherwise used precisely as you might otherwise use an `ILogger` instance.
+
+An end-to-end sample demonstrating this can be seen in the
+[.NET SDK repository](https://github.com/dapr/dotnet-sdk/blob/master/examples/Workflow/WorkflowConsoleApp/Workflows/OrderProcessingWorkflow.cs)
+but a brief extraction of this sample is available below.
+
+```csharp
+public class OrderProcessingWorkflow : Workflow
+{
+ public override async Task RunAsync(WorkflowContext context, OrderPayload order)
+ {
+ string orderId = context.InstanceId;
+ var logger = context.CreateReplaySafeLogger(); //Use this method to access the logger instance
+
+ logger.LogInformation("Received order {orderId} for {quantity} {name} at ${totalCost}", orderId, order.Quantity, order.Name, order.TotalCost);
+
+ //...
+ }
+}
+```
+
+
+
\ No newline at end of file
diff --git a/docs/RELEASE.md b/docs/RELEASE.md
new file mode 100644
index 000000000..b49c6227b
--- /dev/null
+++ b/docs/RELEASE.md
@@ -0,0 +1,69 @@
+# Dapr .NET SDK Release Process
+
+> This information is intended for SDK maintainers. SDK users can ignore this document.
+
+## Publish a SDK Release Candidate (RC)
+
+RC release versions canonically use the form `-rc` where `` represents the overall release version (e.g. `1.0`) and `` represents a specific iteration of RC release (e.g. `01`, `02`, ..., `0n`).
+
+Assume we intend to release `` (e.g. `1.0-rc01`) of the SDK.
+
+1. Create a release branch (if not already done) from `master`
+
+ ```bash
+ git checkout -b release-
+ ```
+
+1. Push the release branch to the `dotnet-sdk` repo (i.e. typically `origin`)
+
+ ```bash
+ git push origin v
+ ```
+
+1. Create a tag on the release branch for the RC version
+
+ ```bash
+ git tag v-rc
+ ```
+
+1. Push the tag to the `dotnet-sdk` repo (i.e. typically `origin`)
+
+ ```bash
+ git push origin v-rc
+ ```
+
+ > This final step will generate a build and automatically publish the resulting packages to NuGet.
+
+## Publish a SDK Release
+
+Official (i.e. supported) release versions canonically use the form `` where `` represents the overall release version (e.g. `1.0`).
+
+1. Create a release branch (if not already done) from `master`
+
+ ```bash
+ git checkout -b release-
+ ```
+
+1. Push the release branch to the `dotnet-sdk` repo (i.e. typically `origin`)
+
+ ```bash
+ git push origin v
+ ```
+
+1. Create a tag on the release branch for the release
+
+ ```bash
+ git tag v
+ ```
+
+1. Push the tag to the `dotnet-sdk` repo (i.e. typically `origin`)
+
+ ```bash
+ git push origin v
+ ```
+
+ > This final step will generate a build and automatically publish the resulting packages to NuGet.
+
+## NuGet Package Publishing
+
+Publishing to NuGet requires keys generated by a member of the Dapr organization. Such keys are added as a [GitHub Action secret](https://github.com/dapr/dotnet-sdk/settings/secrets/actions) with the name `NUGETORG_DAPR_API_KEY` These keys expire and therefore must be maintained and the GitHub Actions secret updated periodically.
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..6315db87a
--- /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.AddDaprConversationClient();
+
+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/Client/StateManagement/Program.cs b/examples/Client/StateManagement/Program.cs
index 24e37d004..e9ef36979 100644
--- a/examples/Client/StateManagement/Program.cs
+++ b/examples/Client/StateManagement/Program.cs
@@ -1,4 +1,4 @@
-// ------------------------------------------------------------------------
+// ------------------------------------------------------------------------
// Copyright 2021 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.
@@ -24,7 +24,8 @@ class Program
new StateStoreExample(),
new StateStoreTransactionsExample(),
new StateStoreETagsExample(),
- new BulkStateExample()
+ new BulkStateExample(),
+ new StateStoreBinaryExample()
};
static async Task Main(string[] args)
diff --git a/examples/Client/StateManagement/StateStoreBinaryExample.cs b/examples/Client/StateManagement/StateStoreBinaryExample.cs
new file mode 100644
index 000000000..edf23704e
--- /dev/null
+++ b/examples/Client/StateManagement/StateStoreBinaryExample.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Dapr.Client;
+using System.Threading.Tasks;
+using System.Threading;
+using Google.Protobuf;
+
+namespace Samples.Client
+{
+ public class StateStoreBinaryExample : Example
+ {
+
+ private static readonly string stateKeyName = "binarydata";
+ private static readonly string storeName = "statestore";
+
+ public override string DisplayName => "Using the State Store with binary data";
+
+ public override async Task RunAsync(CancellationToken cancellationToken)
+ {
+ using var client = new DaprClientBuilder().Build();
+
+ var state = "Test Binary Data";
+ // convert variable in to byte array
+ var stateBytes = Encoding.UTF8.GetBytes(state);
+ await client.SaveByteStateAsync(storeName, stateKeyName, stateBytes.AsMemory(), cancellationToken: cancellationToken);
+ Console.WriteLine("Saved State!");
+
+ var responseBytes = await client.GetByteStateAsync(storeName, stateKeyName, cancellationToken: cancellationToken);
+ var savedState = Encoding.UTF8.GetString(ByteString.CopyFrom(responseBytes.Span).ToByteArray());
+
+ if (savedState == null)
+ {
+ Console.WriteLine("State not found in store");
+ }
+ else
+ {
+ Console.WriteLine($"Got State: {savedState}");
+ }
+
+ await client.DeleteStateAsync(storeName, stateKeyName, cancellationToken: cancellationToken);
+ Console.WriteLine("Deleted State!");
+ }
+
+
+ }
+}
diff --git a/examples/Workflow/WorkflowConsoleApp/Workflows/OrderProcessingWorkflow.cs b/examples/Workflow/WorkflowConsoleApp/Workflows/OrderProcessingWorkflow.cs
index 3b8af5951..8c5e9d133 100644
--- a/examples/Workflow/WorkflowConsoleApp/Workflows/OrderProcessingWorkflow.cs
+++ b/examples/Workflow/WorkflowConsoleApp/Workflows/OrderProcessingWorkflow.cs
@@ -1,4 +1,5 @@
using Dapr.Workflow;
+using Microsoft.Extensions.Logging;
using WorkflowConsoleApp.Activities;
namespace WorkflowConsoleApp.Workflows
@@ -16,7 +17,10 @@ public class OrderProcessingWorkflow : Workflow
public override async Task RunAsync(WorkflowContext context, OrderPayload order)
{
string orderId = context.InstanceId;
+ var logger = context.CreateReplaySafeLogger();
+ logger.LogInformation("Received order {orderId} for {quantity} {name} at ${totalCost}", orderId, order.Quantity, order.Name, order.TotalCost);
+
// Notify the user that an order has come through
await context.CallActivityAsync(
nameof(NotifyActivity),
@@ -31,6 +35,8 @@ await context.CallActivityAsync(
// If there is insufficient inventory, fail and let the user know
if (!result.Success)
{
+ logger.LogError("Insufficient inventory for {orderName}", order.Name);
+
// End the workflow here since we don't have sufficient inventory
await context.CallActivityAsync(
nameof(NotifyActivity),
@@ -39,8 +45,10 @@ await context.CallActivityAsync(
}
// Require orders over a certain threshold to be approved
- if (order.TotalCost > 50000)
+ const int threshold = 50000;
+ if (order.TotalCost > threshold)
{
+ logger.LogInformation("Requesting manager approval since total cost {totalCost} exceeds threshold {threshold}", order.TotalCost, threshold);
// Request manager approval for the order
await context.CallActivityAsync(nameof(RequestApprovalActivity), order);
@@ -51,9 +59,13 @@ await context.CallActivityAsync(
ApprovalResult approvalResult = await context.WaitForExternalEventAsync(
eventName: "ManagerApproval",
timeout: TimeSpan.FromSeconds(30));
+
+ logger.LogInformation("Approval result: {approvalResult}", approvalResult);
context.SetCustomStatus($"Approval result: {approvalResult}");
if (approvalResult == ApprovalResult.Rejected)
{
+ logger.LogWarning("Order was rejected by approver");
+
// The order was rejected, end the workflow here
await context.CallActivityAsync(
nameof(NotifyActivity),
@@ -63,6 +75,8 @@ await context.CallActivityAsync(
}
catch (TaskCanceledException)
{
+ logger.LogError("Cancelling order because it didn't receive an approval");
+
// An approval timeout results in automatic order cancellation
await context.CallActivityAsync(
nameof(NotifyActivity),
@@ -72,6 +86,7 @@ await context.CallActivityAsync(
}
// There is enough inventory available so the user can purchase the item(s). Process their payment
+ logger.LogInformation("Processing payment as sufficient inventory is available");
await context.CallActivityAsync(
nameof(ProcessPaymentActivity),
new PaymentRequest(RequestId: orderId, order.Name, order.Quantity, order.TotalCost),
@@ -88,6 +103,7 @@ await context.CallActivityAsync(
catch (WorkflowTaskFailedException e)
{
// Let them know their payment processing failed
+ logger.LogError("Order {orderId} failed! Details: {errorMessage}", orderId, e.FailureDetails.ErrorMessage);
await context.CallActivityAsync(
nameof(NotifyActivity),
new Notification($"Order {orderId} Failed! Details: {e.FailureDetails.ErrorMessage}"));
@@ -95,6 +111,7 @@ await context.CallActivityAsync(
}
// Let them know their payment was processed
+ logger.LogError("Order {orderId} has completed!", orderId);
await context.CallActivityAsync(
nameof(NotifyActivity),
new Notification($"Order {orderId} has completed!"));
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.Workflow/WorkflowEngineClient.cs b/src/Dapr.AI/Conversation/Extensions/DaprAiConversationBuilder.cs
similarity index 54%
rename from src/Dapr.Workflow/WorkflowEngineClient.cs
rename to src/Dapr.AI/Conversation/Extensions/DaprAiConversationBuilder.cs
index c5869b3c6..876d223b1 100644
--- a/src/Dapr.Workflow/WorkflowEngineClient.cs
+++ b/src/Dapr.AI/Conversation/Extensions/DaprAiConversationBuilder.cs
@@ -1,5 +1,5 @@
// ------------------------------------------------------------------------
-// Copyright 2023 The Dapr Authors
+// 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
@@ -11,24 +11,25 @@
// limitations under the License.
// ------------------------------------------------------------------------
-namespace Dapr.Workflow
+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
{
- using System;
- using Microsoft.DurableTask.Client;
+ ///
+ /// The registered services on the builder.
+ ///
+ public IServiceCollection Services { get; }
///
- /// Deprecated. Use instead.
+ /// Used to initialize a new .
///
- [Obsolete($"Deprecated. Use {nameof(DaprWorkflowClient)} instead.")]
- public sealed class WorkflowEngineClient : DaprWorkflowClient
+ public DaprAiConversationBuilder(IServiceCollection services)
{
- ///
- /// Deprecated. Use instead.
- ///
- ///
- public WorkflowEngineClient(DurableTaskClient innerClient)
- : base(innerClient)
- {
- }
+ 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..2f049a906
--- /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 AddDaprConversationClient(this IServiceCollection services, Action? configure = null, ServiceLifetime lifetime = ServiceLifetime.Singleton)
+ {
+ ArgumentNullException.ThrowIfNull(services, nameof(services));
+
+ services.AddHttpClient();
+
+ var registration = new Func(provider =>
+ {
+ var configuration = provider.GetService();
+ var builder = new DaprConversationClientBuilder(configuration);
+
+ var httpClientFactory = provider.GetRequiredService();
+ builder.UseHttpClientFactory(httpClientFactory);
+
+ configure?.Invoke(provider, builder);
+
+ return builder.Build();
+ });
+
+ switch (lifetime)
+ {
+ case ServiceLifetime.Scoped:
+ services.TryAddScoped(registration);
+ break;
+ case ServiceLifetime.Transient:
+ services.TryAddTransient(registration);
+ break;
+ case ServiceLifetime.Singleton:
+ default:
+ services.TryAddSingleton(registration);
+ break;
+ }
+
+ return new DaprAiConversationBuilder(services);
+ }
+}
diff --git a/src/Dapr.AI/Conversation/Extensions/IDaprAiConversationBuilder.cs b/src/Dapr.AI/Conversation/Extensions/IDaprAiConversationBuilder.cs
new file mode 100644
index 000000000..30d3822d4
--- /dev/null
+++ b/src/Dapr.AI/Conversation/Extensions/IDaprAiConversationBuilder.cs
@@ -0,0 +1,23 @@
+// ------------------------------------------------------------------------
+// Copyright 2024 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+using Dapr.AI.Extensions;
+
+namespace Dapr.AI.Conversation.Extensions;
+
+///
+/// Provides a root builder for the Dapr AI conversational functionality facilitating a more fluent-style registration.
+///
+public interface IDaprAiConversationBuilder : IDaprAiServiceBuilder
+{
+}
diff --git a/src/Dapr.AI/Dapr.AI.csproj b/src/Dapr.AI/Dapr.AI.csproj
new file mode 100644
index 000000000..8220c5c4d
--- /dev/null
+++ b/src/Dapr.AI/Dapr.AI.csproj
@@ -0,0 +1,26 @@
+
+
+
+ net6;net8
+ enable
+ enable
+ Dapr.AI
+ Dapr AI SDK
+ Dapr AI SDK for performing operations associated with artificial intelligence.
+ alpha
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Dapr.AI/DaprAIClient.cs b/src/Dapr.AI/DaprAIClient.cs
new file mode 100644
index 000000000..a2fd2255f
--- /dev/null
+++ b/src/Dapr.AI/DaprAIClient.cs
@@ -0,0 +1,34 @@
+// ------------------------------------------------------------------------
+// Copyright 2024 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+using Dapr.AI.Conversation;
+
+namespace Dapr.AI;
+
+///
+/// The base implementation of a Dapr AI client.
+///
+public abstract class DaprAIClient
+{
+ ///
+ /// Sends various inputs to the large language model via the Conversational building block on the Dapr sidecar.
+ ///
+ /// The name of the Dapr conversation component.
+ /// The input values to send.
+ /// Optional options used to configure the conversation.
+ /// Cancellation token.
+ /// The response(s) provided by the LLM provider.
+ public abstract Task ConverseAsync(string daprConversationComponentName,
+ IReadOnlyList inputs, ConversationOptions? options = null,
+ CancellationToken cancellationToken = default);
+}
diff --git a/src/Dapr.AI/Extensions/IDaprAiServiceBuilder.cs b/src/Dapr.AI/Extensions/IDaprAiServiceBuilder.cs
new file mode 100644
index 000000000..8a0a80c2c
--- /dev/null
+++ b/src/Dapr.AI/Extensions/IDaprAiServiceBuilder.cs
@@ -0,0 +1,27 @@
+// ------------------------------------------------------------------------
+// Copyright 2024 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Dapr.AI.Extensions;
+
+///
+/// Responsible for registering Dapr AI service functionality.
+///
+public interface IDaprAiServiceBuilder
+{
+ ///
+ /// The registered services on the builder.
+ ///
+ public IServiceCollection Services { get; }
+}
diff --git a/src/Dapr.Client/DaprClient.cs b/src/Dapr.Client/DaprClient.cs
index 43c640a69..6be31a648 100644
--- a/src/Dapr.Client/DaprClient.cs
+++ b/src/Dapr.Client/DaprClient.cs
@@ -850,6 +850,80 @@ public abstract Task SaveStateAsync(
IReadOnlyDictionary metadata = default,
CancellationToken cancellationToken = default);
+
+ ///
+ /// Saves the provided associated with the provided to the Dapr state
+ /// store
+ ///
+ /// The name of the state store.
+ /// The state key.
+ /// The binary data that will be stored in the state store.
+ /// Options for performing save state operation.
+ /// A collection of metadata key-value pairs that will be provided to the state store. The valid metadata keys and values are determined by the type of state store used.
+ /// A that can be used to cancel the operation.
+ /// A that will complete when the operation has completed.
+ public abstract Task SaveByteStateAsync(
+ string storeName,
+ string key,
+ ReadOnlyMemory binaryValue,
+ StateOptions stateOptions = default,
+ IReadOnlyDictionary metadata = default,
+ CancellationToken cancellationToken = default);
+
+ ///
+ ///Saves the provided associated with the provided using the
+ /// to the Dapr state. State store implementation will allow the update only if the attached ETag matches with the latest ETag in the state store.
+ ///
+ /// The name of the state store.
+ /// The state key.
+ /// The binary data that will be stored in the state store.
+ /// An ETag.
+ /// Options for performing save state operation.
+ /// A collection of metadata key-value pairs that will be provided to the state store. The valid metadata keys and values are determined by the type of state store used.
+ /// A that can be used to cancel the operation.
+ /// A that will complete when the operation has completed.
+ public abstract Task TrySaveByteStateAsync(
+ string storeName,
+ string key,
+ ReadOnlyMemory binaryValue,
+ string etag,
+ StateOptions stateOptions = default,
+ IReadOnlyDictionary metadata = default,
+ CancellationToken cancellationToken = default);
+
+
+ ///
+ /// Gets the current binary value associated with the from the Dapr state store.
+ ///
+ /// The name of state store to read from.
+ /// The state key.
+ /// The consistency mode .
+ /// A collection of metadata key-value pairs that will be provided to the state store. The valid metadata keys and values are determined by the type of state store used.
+ /// A that can be used to cancel the operation.
+ /// A that will return the value when the operation has completed.
+ public abstract Task> GetByteStateAsync(
+ string storeName,
+ string key,
+ ConsistencyMode? consistencyMode = default,
+ IReadOnlyDictionary metadata = default,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// Gets the current binary value associated with the from the Dapr state store and an ETag.
+ ///
+ /// The name of the state store.
+ /// The state key.
+ /// The consistency mode .
+ /// A collection of metadata key-value pairs that will be provided to the state store. The valid metadata keys and values are determined by the type of state store used.
+ /// A that can be used to cancel the operation.
+ /// A that will return the value when the operation has completed. This wraps the read value and an ETag.
+ public abstract Task<(ReadOnlyMemory, string etag)> GetByteStateAndETagAsync(
+ string storeName,
+ string key,
+ ConsistencyMode? consistencyMode = default,
+ IReadOnlyDictionary metadata = default,
+ CancellationToken cancellationToken = default);
+
///
/// Tries to save the state associated with the provided using the
/// to the Dapr state. State store implementation will allow the update only if the attached ETag matches with the latest ETag in the state store.
diff --git a/src/Dapr.Client/DaprClientGrpc.cs b/src/Dapr.Client/DaprClientGrpc.cs
index c70aef77b..40df4767c 100644
--- a/src/Dapr.Client/DaprClientGrpc.cs
+++ b/src/Dapr.Client/DaprClientGrpc.cs
@@ -11,2257 +11,2449 @@
// 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
- this.httpClient.DefaultRequestHeaders.UserAgent.Add(UserAgent());
- }
+ ///
+ 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));
- #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));
+ 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, null, 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));
+
+ 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 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);
+ }
+
+ private async Task MakePublishRequest(
+ string pubsubName,
+ string topicName,
+ ByteString content,
+ Dictionary metadata,
+ string dataContentType,
+ CancellationToken cancellationToken)
+ {
+ var envelope = new Autogenerated.PublishEventRequest() { PubsubName = pubsubName, Topic = topicName, };
- ///
- public override Task PublishEventAsync(
- string pubsubName,
- string topicName,
- CancellationToken cancellationToken = default)
+ if (content != null)
{
- ArgumentVerifier.ThrowIfNullOrEmpty(pubsubName, nameof(pubsubName));
- ArgumentVerifier.ThrowIfNullOrEmpty(topicName, nameof(topicName));
- return MakePublishRequest(pubsubName, topicName, null, null, null, cancellationToken);
+ envelope.Data = content;
+ envelope.DataContentType = dataContentType ?? Constants.ContentTypeApplicationJson;
}
- public override Task PublishEventAsync(
- string pubsubName,
- string topicName,
- Dictionary metadata,
- CancellationToken cancellationToken = default)
+ if (metadata != null)
{
- ArgumentVerifier.ThrowIfNullOrEmpty(pubsubName, nameof(pubsubName));
- ArgumentVerifier.ThrowIfNullOrEmpty(topicName, nameof(topicName));
- ArgumentVerifier.ThrowIfNull(metadata, nameof(metadata));
- return MakePublishRequest(pubsubName, topicName, null, metadata, null, cancellationToken);
+ foreach (var kvp in metadata)
+ {
+ envelope.Metadata.Add(kvp.Key, kvp.Value);
+ }
}
- public override Task PublishByteEventAsync(
- string pubsubName,
- string topicName,
- ReadOnlyMemory data,
- string dataContentType = Constants.ContentTypeApplicationJson,
- Dictionary metadata = default,
- CancellationToken cancellationToken = default)
+ var options = CreateCallOptions(headers: null, cancellationToken);
+
+ try
{
- ArgumentVerifier.ThrowIfNullOrEmpty(pubsubName, nameof(pubsubName));
- ArgumentVerifier.ThrowIfNullOrEmpty(topicName, nameof(topicName));
- return MakePublishRequest(pubsubName, topicName, ByteString.CopyFrom(data.Span), metadata, dataContentType, cancellationToken);
+ 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);
+ }
+ }
- private async Task MakePublishRequest(
- string pubsubName,
- string topicName,
- ByteString content,
- Dictionary metadata,
- string dataContentType,
- CancellationToken cancellationToken)
+ ///
+ 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, };
+
+ Dictionary> entryMap = new Dictionary>();
+
+ for (int counter = 0; counter < events.Count; counter++)
{
- var envelope = new Autogenerated.PublishEventRequest()
+ var entry = new Autogenerated.BulkPublishRequestEntry()
{
- PubsubName = pubsubName,
- Topic = topicName,
+ 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 (content != null)
+ if (metadata != null)
+ {
+ foreach (var kvp in metadata)
{
- envelope.Data = content;
- envelope.DataContentType = dataContentType ?? Constants.ContentTypeApplicationJson;
+ envelope.Metadata.Add(kvp.Key, kvp.Value);
}
+ }
- if (metadata != null)
- {
- foreach (var kvp in metadata)
- {
- 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
- {
- await client.PublishEventAsync(envelope, options);
- }
- catch (RpcException ex)
+ List> failedEntries =
+ new List>();
+
+ foreach (var entry in response.FailedEntries)
{
- throw new DaprException("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);
+ }
+ }
- ///
- 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,
- };
-
- Dictionary> entryMap = new Dictionary>();
+ #endregion
- for (int counter = 0; counter < events.Count; counter++)
- {
- 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));
- }
-
- if (metadata != null)
- {
- foreach (var kvp in metadata)
- {
- envelope.Metadata.Add(kvp.Key, kvp.Value);
- }
- }
-
- var options = CreateCallOptions(headers: null, cancellationToken);
+ #region InvokeBinding Apis
- try
- {
- var response = await client.BulkPublishEventAlpha1Async(envelope, options);
+ ///
+ 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));
- 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);
+ var bytes = TypeConverters.ToJsonByteString(data, this.jsonSerializerOptions);
+ _ = await MakeInvokeBindingRequestAsync(bindingName, operation, bytes, metadata, cancellationToken);
+ }
- return bulkPublishResponse;
- }
- catch (RpcException ex)
- {
- throw new DaprException("Bulk Publish operation failed: the Dapr endpoint indicated a " +
- "failure. See InnerException for details.", ex);
- }
+ ///
+ 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);
+ 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);
}
- #endregion
+ }
+
+ 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);
+ }
- #region InvokeBinding Apis
+ private async Task MakeInvokeBindingRequestAsync(
+ string name,
+ string operation,
+ ByteString data,
+ IReadOnlyDictionary metadata = default,
+ CancellationToken cancellationToken = default)
+ {
+ var envelope = new Autogenerated.InvokeBindingRequest() { Name = name, Operation = operation };
- ///
- public override async Task InvokeBindingAsync(
- string bindingName,
- string operation,
- TRequest data,
- IReadOnlyDictionary metadata = default,
- CancellationToken cancellationToken = default)
+ if (data != null)
{
- ArgumentVerifier.ThrowIfNullOrEmpty(bindingName, nameof(bindingName));
- ArgumentVerifier.ThrowIfNullOrEmpty(operation, nameof(operation));
+ envelope.Data = data;
+ }
- var bytes = TypeConverters.ToJsonByteString(data, this.jsonSerializerOptions);
- _ = await MakeInvokeBindingRequestAsync(bindingName, operation, bytes, metadata, cancellationToken);
+ if (metadata != null)
+ {
+ foreach (var kvp in metadata)
+ {
+ envelope.Metadata.Add(kvp.Key, kvp.Value);
+ }
}
- ///
- public override async Task InvokeBindingAsync(
- string bindingName,
- string operation,
- TRequest data,
- IReadOnlyDictionary metadata = default,
- CancellationToken cancellationToken = default)
+ var options = CreateCallOptions(headers: null, cancellationToken);
+ try
+ {
+ return await client.InvokeBindingAsync(envelope, options);
+ }
+ catch (RpcException ex)
{
- ArgumentVerifier.ThrowIfNullOrEmpty(bindingName, nameof(bindingName));
- ArgumentVerifier.ThrowIfNullOrEmpty(operation, nameof(operation));
+ throw new DaprException(
+ "Binding operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex);
+ }
+ }
- var bytes = TypeConverters.ToJsonByteString(data, this.jsonSerializerOptions);
- var response = await MakeInvokeBindingRequestAsync(bindingName, operation, bytes, metadata, cancellationToken);
+ #endregion
- 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);
- }
+ #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);
+
+ 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);
}
- public override async Task InvokeBindingAsync(BindingRequest request, CancellationToken cancellationToken = default)
+ 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));
+
+ 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))
{
- 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);
+ throw new InvalidOperationException("The provided request URI is not a Dapr service invocation URI.");
}
- private async Task MakeInvokeBindingRequestAsync(
- string name,
- string operation,
- ByteString data,
- IReadOnlyDictionary metadata = default,
- CancellationToken cancellationToken = default)
+ // Note: we intentionally DO NOT validate the status code here.
+ // This method allows you to 'invoke' without exceptions on non-2xx.
+ try
{
- var envelope = new Autogenerated.InvokeBindingRequest()
- {
- Name = name,
- Operation = operation
- };
+ 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);
- if (data != null)
- {
- envelope.Data = data;
- }
+ throw new InvocationException(
+ appId: appId as string,
+ methodName: methodName as string,
+ innerException: ex,
+ response: null);
+ }
+ }
- if (metadata != null)
- {
- foreach (var kvp in metadata)
- {
- envelope.Metadata.Add(kvp.Key, kvp.Value);
- }
- }
+ ///
+ ///
+ /// 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
- 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);
- }
+ public async override Task InvokeMethodAsync(HttpRequestMessage request,
+ CancellationToken cancellationToken = default)
+ {
+ ArgumentVerifier.ThrowIfNull(request, nameof(request));
+
+ var response = await InvokeMethodWithResponseAsync(request, cancellationToken);
+ try
+ {
+ response.EnsureSuccessStatusCode();
}
- #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);
-
- 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);
- }
+ 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);
- return request;
+ 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));
- ///
- /// 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)
+ var response = await InvokeMethodWithResponseAsync(request, cancellationToken);
+ try
{
- ArgumentVerifier.ThrowIfNull(httpMethod, nameof(httpMethod));
- ArgumentVerifier.ThrowIfNullOrEmpty(appId, nameof(appId));
- ArgumentVerifier.ThrowIfNull(methodName, nameof(methodName));
+ 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);
- var request = CreateInvokeMethodRequest(httpMethod, appId, methodName, queryStringParameters);
- request.Content = JsonContent.Create(data, options: this.JsonSerializerOptions);
- return request;
+ throw new InvocationException(
+ appId: appId as string,
+ methodName: methodName as string,
+ innerException: ex,
+ response: response);
}
- public override async Task InvokeMethodWithResponseAsync(HttpRequestMessage request, CancellationToken cancellationToken = default)
+ try
{
- ArgumentVerifier.ThrowIfNull(request, nameof(request));
+ 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);
- if (!this.httpEndpoint.IsBaseOf(request.RequestUri))
- {
- throw new InvalidOperationException("The provided request URI is not a Dapr service invocation URI.");
- }
+ 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);
- // 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: response);
}
+ }
- ///
- ///
- /// 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 async Task InvokeMethodGrpcAsync(string appId, string methodName,
+ CancellationToken cancellationToken = default)
+ {
+ ArgumentVerifier.ThrowIfNullOrEmpty(appId, nameof(appId));
+ ArgumentVerifier.ThrowIfNullOrEmpty(methodName, nameof(methodName));
- public async override Task InvokeMethodAsync(HttpRequestMessage request, CancellationToken cancellationToken = default)
+ var envelope = new Autogenerated.InvokeServiceRequest()
{
- ArgumentVerifier.ThrowIfNull(request, nameof(request));
+ Id = appId, Message = new Autogenerated.InvokeRequest() { Method = 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);
- }
- }
+ var options = CreateCallOptions(headers: null, cancellationToken);
- public async override Task InvokeMethodAsync(HttpRequestMessage request, CancellationToken cancellationToken = default)
+ try
{
- ArgumentVerifier.ThrowIfNull(request, nameof(request));
+ _ = await this.Client.InvokeServiceAsync(envelope, options);
+ }
+ catch (RpcException ex)
+ {
+ throw new InvocationException(appId, methodName, ex);
+ }
+ }
- 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);
- }
+ public override async Task InvokeMethodGrpcAsync(string appId, string methodName, TRequest data,
+ CancellationToken cancellationToken = default)
+ {
+ ArgumentVerifier.ThrowIfNullOrEmpty(appId, nameof(appId));
+ ArgumentVerifier.ThrowIfNullOrEmpty(methodName, nameof(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)
+ var envelope = new Autogenerated.InvokeServiceRequest()
+ {
+ Id = appId,
+ Message = new Autogenerated.InvokeRequest()
{
- 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);
- }
+ Method = methodName, ContentType = Constants.ContentTypeApplicationGrpc, Data = Any.Pack(data),
+ },
+ };
+
+ var options = CreateCallOptions(headers: null, cancellationToken);
+
+ 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()
- {
- Id = appId,
- Message = new Autogenerated.InvokeRequest()
- {
- Method = methodName,
- },
- };
+ public override async Task InvokeMethodGrpcAsync(string appId, string methodName,
+ CancellationToken cancellationToken = default)
+ {
+ ArgumentVerifier.ThrowIfNullOrEmpty(appId, nameof(appId));
+ ArgumentVerifier.ThrowIfNullOrEmpty(methodName, nameof(methodName));
- var options = CreateCallOptions(headers: null, cancellationToken);
+ var envelope = new Autogenerated.InvokeServiceRequest()
+ {
+ Id = appId, Message = new Autogenerated.InvokeRequest() { Method = methodName, },
+ };
- try
- {
- _ = await this.Client.InvokeServiceAsync(envelope, options);
- }
- catch (RpcException ex)
- {
- throw new InvocationException(appId, methodName, ex);
- }
- }
+ var options = CreateCallOptions(headers: null, cancellationToken);
- public override async Task InvokeMethodGrpcAsync(string appId, string methodName, TRequest data, CancellationToken cancellationToken = default)
+ try
{
- ArgumentVerifier.ThrowIfNullOrEmpty(appId, nameof(appId));
- ArgumentVerifier.ThrowIfNullOrEmpty(methodName, nameof(methodName));
+ var response = await this.Client.InvokeServiceAsync(envelope, options);
+ return response.Data.Unpack();
+ }
+ catch (RpcException ex)
+ {
+ throw new InvocationException(appId, methodName, ex);
+ }
+ }
+
+ 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()
+ 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, ContentType = Constants.ContentTypeApplicationGrpc, Data = Any.Pack(data),
+ },
+ };
- 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
+ {
+ var response = await this.Client.InvokeServiceAsync(envelope, options);
+ return response.Data.Unpack();
}
-
- 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()
- {
- Id = appId,
- Message = new Autogenerated.InvokeRequest()
- {
- Method = methodName,
- },
- };
+ #endregion
- var options = CreateCallOptions(headers: null, cancellationToken);
+ #region State Apis
- try
- {
- var response = await this.Client.InvokeServiceAsync(envelope, options);
- return response.Data.Unpack();
- }
- catch (RpcException ex)
- {
- throw new InvocationException(appId, methodName, ex);
- }
+ ///
+ 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)
+ {
+ bulkResponse.Add(new BulkStateItem(item.Key, item.Value.ToStringUtf8(), item.Etag));
}
- public override async Task InvokeMethodGrpcAsync(string appId, string methodName, TRequest data, CancellationToken cancellationToken = default)
+ 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)
{
- ArgumentVerifier.ThrowIfNullOrEmpty(appId, nameof(appId));
- ArgumentVerifier.ThrowIfNullOrEmpty(methodName, nameof(methodName));
+ var deserializedValue = TypeConverters.FromJsonByteString(item.Value, this.JsonSerializerOptions);
+ bulkResponse.Add(new BulkStateItem(item.Key, deserializedValue, item.Etag));
+ }
- var envelope = new Autogenerated.InvokeServiceRequest()
- {
- Id = appId,
- Message = new Autogenerated.InvokeRequest()
- {
- Method = methodName,
- ContentType = Constants.ContentTypeApplicationGrpc,
- Data = Any.Pack(data),
- },
- };
+ return bulkResponse;
+ }
- var options = CreateCallOptions(headers: null, cancellationToken);
+ ///
+ /// 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");
- try
- {
- var response = await this.Client.InvokeServiceAsync(envelope, options);
- return response.Data.Unpack();
- }
- catch (RpcException ex)
+ var envelope = new Autogenerated.GetBulkStateRequest()
+ {
+ StoreName = storeName, Parallelism = parallelism ?? default
+ };
+
+ if (metadata != null)
+ {
+ foreach (var kvp in metadata)
{
- throw new InvocationException(appId, methodName, ex);
+ envelope.Metadata.Add(kvp.Key, kvp.Value);
}
}
- #endregion
+ envelope.Keys.AddRange(keys);
- #region State Apis
+ 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> GetBulkStateAsync(string storeName, IReadOnlyList keys, int? parallelism, IReadOnlyDictionary metadata = default, CancellationToken cancellationToken = default)
+ var bulkResponse = new List<(string Key, string Etag, ByteString Value)>();
+ foreach (var item in response.Items)
{
- var rawBulkState = await GetBulkStateRawAsync(storeName, keys, parallelism, metadata, cancellationToken);
+ bulkResponse.Add((item.Key, item.Etag, item.Data));
+ }
+
+ 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.GetStateRequest() { StoreName = storeName, Key = key, };
- var bulkResponse = new List();
- foreach (var item in rawBulkState)
+ if (metadata != null)
+ {
+ foreach (var kvp in metadata)
{
- bulkResponse.Add(new BulkStateItem(item.Key, item.Value.ToStringUtf8(), item.Etag));
+ envelope.Metadata.Add(kvp.Key, kvp.Value);
}
+ }
- return bulkResponse;
+ if (consistencyMode != null)
+ {
+ envelope.Consistency = GetStateConsistencyForConsistencyMode(consistencyMode.Value);
}
-
- ///
- public override async Task>> GetBulkStateAsync(
- string storeName,
- IReadOnlyList keys,
- int? parallelism,
- IReadOnlyDictionary metadata = default,
- CancellationToken cancellationToken = default)
+
+ var options = CreateCallOptions(headers: null, cancellationToken);
+ Autogenerated.GetStateResponse response;
+
+ try
{
- var rawBulkState = await GetBulkStateRawAsync(storeName, keys, parallelism, metadata, cancellationToken);
+ 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);
+ }
- 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));
- }
+ 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);
+ }
+ }
- return bulkResponse;
+ ///
+ 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");
}
- ///
- /// 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)
+ var envelope = new Autogenerated.SaveStateRequest() { StoreName = storeName, };
+
+ foreach (var item in items)
{
- ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName));
- if (keys.Count == 0)
- throw new ArgumentException("keys do not contain any elements");
+ var stateItem = new Autogenerated.StateItem() { Key = item.Key, };
- var envelope = new Autogenerated.GetBulkStateRequest()
+ if (item.ETag != null)
{
- StoreName = storeName,
- Parallelism = parallelism ?? default
- };
+ stateItem.Etag = new Autogenerated.Etag() { Value = item.ETag };
+ }
- if (metadata != null)
+ if (item.Metadata != null)
{
- foreach (var kvp in metadata)
+ foreach (var kvp in item.Metadata)
{
- envelope.Metadata.Add(kvp.Key, kvp.Value);
+ stateItem.Metadata.Add(kvp.Key, kvp.Value);
}
}
- envelope.Keys.AddRange(keys);
-
- var options = CreateCallOptions(headers: null, cancellationToken);
- Autogenerated.GetBulkStateResponse response;
-
- try
+ if (item.StateOptions != null)
{
- 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);
+ stateItem.Options = ToAutoGeneratedStateOptions(item.StateOptions);
}
- var bulkResponse = new List<(string Key, string Etag, ByteString Value)>();
- foreach (var item in response.Items)
+ if (item.Value != null)
{
- bulkResponse.Add((item.Key, item.Etag, item.Data));
+ stateItem.Value = TypeConverters.ToJsonByteString(item.Value, this.jsonSerializerOptions);
}
- return bulkResponse;
+ envelope.States.Add(stateItem);
+ }
+
+ try
+ {
+ await this.Client.SaveStateAsync(envelope, cancellationToken: cancellationToken);
}
-
- ///
- public override async Task GetStateAsync(
- string storeName,
- string key,
- ConsistencyMode? consistencyMode = default,
- IReadOnlyDictionary metadata = default,
- CancellationToken cancellationToken = default)
+ catch (RpcException ex)
{
- ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName));
- ArgumentVerifier.ThrowIfNullOrEmpty(key, nameof(key));
+ throw new DaprException(
+ "State operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex);
+ }
+ }
- var envelope = new Autogenerated.GetStateRequest()
- {
- StoreName = storeName,
- Key = key,
- };
+ ///
+ public override async Task SaveByteStateAsync(
+ string storeName,
+ string key,
+ ReadOnlyMemory value,
+ StateOptions stateOptions = default,
+ IReadOnlyDictionary metadata = default,
+ CancellationToken cancellationToken = default)
+ {
+ ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName));
+ ArgumentVerifier.ThrowIfNullOrEmpty(key, nameof(key));
+ _ = await this.MakeSaveByteStateCallAsync(
+ storeName,
+ key,
+ ByteString.CopyFrom(value.Span),
+ etag: null,
+ stateOptions,
+ metadata,
+ cancellationToken);
+ }
- if (metadata != null)
- {
- foreach (var kvp in metadata)
- {
- envelope.Metadata.Add(kvp.Key, kvp.Value);
- }
- }
+ ///
+ public override async Task TrySaveByteStateAsync(
+ string storeName,
+ string key,
+ ReadOnlyMemory value,
+ string etag,
+ StateOptions stateOptions = default,
+ IReadOnlyDictionary metadata = default,
+ CancellationToken cancellationToken = default)
+ {
+ ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName));
+ ArgumentVerifier.ThrowIfNullOrEmpty(key, nameof(key));
+ ArgumentVerifier.ThrowIfNull(etag, nameof(etag));
+ return await this.MakeSaveByteStateCallAsync(storeName, key, ByteString.CopyFrom(value.Span), etag,
+ stateOptions, metadata, cancellationToken);
+ }
- if (consistencyMode != null)
- {
- envelope.Consistency = GetStateConsistencyForConsistencyMode(consistencyMode.Value);
- }
+ // Method MakeSaveStateCallAsync to save binary value
+ private async Task MakeSaveByteStateCallAsync(
+ string storeName,
+ string key,
+ ByteString 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);
- Autogenerated.GetStateResponse response;
- 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);
- }
+ var stateItem = new Autogenerated.StateItem() { Key = key, };
- try
+ if (metadata != null)
+ {
+ foreach (var kvp in metadata)
{
- return TypeConverters.FromJsonByteString(response.Data, this.JsonSerializerOptions);
+ stateItem.Metadata.Add(kvp.Key, kvp.Value);
}
- catch (JsonException ex)
+ }
+
+ if (etag != null)
+ {
+ stateItem.Etag = new Autogenerated.Etag() { Value = etag };
+ }
+
+ if (stateOptions != null)
+ {
+ stateItem.Options = ToAutoGeneratedStateOptions(stateOptions);
+ }
+
+ if (value != null)
+ {
+
+ stateItem.Value = value;
+ }
+
+ 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);
+ }
+ }
+
+ ///
+ public override async Task> GetByteStateAsync(
+ 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 (consistencyMode != null)
+ {
+ envelope.Consistency = GetStateConsistencyForConsistencyMode(consistencyMode.Value);
+ }
+
+ if (metadata != null)
+ {
+ foreach (var kvp in metadata)
{
- throw new DaprException("State operation failed: the state payload could not be deserialized. See InnerException for details.", ex);
+ envelope.Metadata.Add(kvp.Key, kvp.Value);
}
}
- ///