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); } } - /// - public override async Task SaveBulkStateAsync(string storeName, IReadOnlyList> items, CancellationToken cancellationToken = default) + var options = CreateCallOptions(headers: null, cancellationToken); + try + { + var response = await client.GetStateAsync(envelope, options); + return response.Data.ToByteArray().AsMemory(); + } + catch (RpcException ex) { - ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); + throw new DaprException( + "State operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + } + } + + /// + public override async Task<(ReadOnlyMemory, string etag)> GetByteStateAndETagAsync( + string storeName, + string key, + ConsistencyMode? consistencyMode = default, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); + ArgumentVerifier.ThrowIfNullOrEmpty(key, nameof(key)); - if (items.Count == 0) + var envelope = new Autogenerated.GetStateRequest() { StoreName = storeName, Key = key }; + + if (metadata != null) + { + foreach (var kvp in metadata) { - throw new ArgumentException("items do not contain any elements"); + envelope.Metadata.Add(kvp.Key, kvp.Value); } + } - var envelope = new Autogenerated.SaveStateRequest() - { - StoreName = storeName, - }; + if (consistencyMode != null) + { + envelope.Consistency = GetStateConsistencyForConsistencyMode(consistencyMode.Value); + } - foreach (var item in items) - { - var stateItem = new Autogenerated.StateItem() - { - Key = item.Key, - }; + var options = CreateCallOptions(headers: null, cancellationToken); + Autogenerated.GetStateResponse response; - if (item.ETag != null) - { - stateItem.Etag = new Autogenerated.Etag() { Value = item.ETag }; - } + try + { + response = await client.GetStateAsync(envelope, options); + } + catch (RpcException ex) + { + throw new DaprException( + "State operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", + ex); + } - if (item.Metadata != null) - { - foreach (var kvp in item.Metadata) - { - stateItem.Metadata.Add(kvp.Key, kvp.Value); - } - } + try + { + return (response.Data.ToByteArray().AsMemory(), response.Etag); + } + catch (JsonException ex) + { + throw new DaprException( + "State operation failed: the state payload could not be deserialized. See InnerException for details.", + ex); + } + } - if (item.StateOptions != null) - { - stateItem.Options = ToAutoGeneratedStateOptions(item.StateOptions); - } + /// + public override async Task DeleteBulkStateAsync(string storeName, IReadOnlyList items, + CancellationToken cancellationToken = default) + { + var envelope = new Autogenerated.DeleteBulkStateRequest() { StoreName = storeName, }; - if (item.Value != null) - { - stateItem.Value = TypeConverters.ToJsonByteString(item.Value, this.jsonSerializerOptions); - } + foreach (var item in items) + { + var stateItem = new Autogenerated.StateItem() { Key = item.Key, }; - envelope.States.Add(stateItem); + if (item.ETag != null) + { + stateItem.Etag = new Autogenerated.Etag() { Value = item.ETag }; } - try + if (item.Metadata != null) { - await this.Client.SaveStateAsync(envelope, cancellationToken: cancellationToken); + foreach (var kvp in item.Metadata) + { + stateItem.Metadata.Add(kvp.Key, kvp.Value); + } } - catch (RpcException ex) + + if (item.StateOptions != null) { - throw new DaprException("State operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + stateItem.Options = ToAutoGeneratedStateOptions(item.StateOptions); } + envelope.States.Add(stateItem); } - /// - public override async Task DeleteBulkStateAsync(string storeName, IReadOnlyList items, CancellationToken cancellationToken = default) + try { - var envelope = new Autogenerated.DeleteBulkStateRequest() - { - StoreName = storeName, - }; - - foreach (var item in items) - { - var stateItem = new Autogenerated.StateItem() - { - Key = item.Key, - }; - - if (item.ETag != null) - { - stateItem.Etag = new Autogenerated.Etag() { Value = item.ETag }; - } + await this.Client.DeleteBulkStateAsync(envelope, cancellationToken: cancellationToken); + } + catch (RpcException ex) + { + throw new DaprException( + "State operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + } - if (item.Metadata != null) - { - foreach (var kvp in item.Metadata) - { - stateItem.Metadata.Add(kvp.Key, kvp.Value); - } - } + } - if (item.StateOptions != null) - { - stateItem.Options = ToAutoGeneratedStateOptions(item.StateOptions); - } + /// + public override async Task<(TValue value, string etag)> GetStateAndETagAsync( + string storeName, + string key, + ConsistencyMode? consistencyMode = default, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); + ArgumentVerifier.ThrowIfNullOrEmpty(key, nameof(key)); - envelope.States.Add(stateItem); - } + var envelope = new Autogenerated.GetStateRequest() { StoreName = storeName, Key = key }; - try - { - await this.Client.DeleteBulkStateAsync(envelope, cancellationToken: cancellationToken); - } - catch (RpcException ex) + if (metadata != null) + { + foreach (var kvp in metadata) { - throw new DaprException("State operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + envelope.Metadata.Add(kvp.Key, kvp.Value); } + } + if (consistencyMode != null) + { + envelope.Consistency = GetStateConsistencyForConsistencyMode(consistencyMode.Value); } - /// - public override async Task<(TValue value, string etag)> GetStateAndETagAsync( - string storeName, - string key, - ConsistencyMode? consistencyMode = default, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default) + var options = CreateCallOptions(headers: null, cancellationToken); + Autogenerated.GetStateResponse response; + + try { - ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); - ArgumentVerifier.ThrowIfNullOrEmpty(key, nameof(key)); + 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 envelope = new Autogenerated.GetStateRequest() - { - StoreName = storeName, - Key = key - }; + try + { + return (TypeConverters.FromJsonByteString(response.Data, this.JsonSerializerOptions), + response.Etag); + } + catch (JsonException ex) + { + throw new DaprException( + "State operation failed: the state payload could not be deserialized. See InnerException for details.", + ex); + } + } - if (metadata != null) - { - foreach (var kvp in metadata) - { - envelope.Metadata.Add(kvp.Key, kvp.Value); - } - } + /// + public override async Task SaveStateAsync( + string storeName, + string key, + TValue value, + StateOptions stateOptions = default, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); + ArgumentVerifier.ThrowIfNullOrEmpty(key, nameof(key)); + + _ = await this.MakeSaveStateCallAsync( + storeName, + key, + value, + etag: null, + stateOptions, + metadata, + cancellationToken); + } - if (consistencyMode != null) - { - envelope.Consistency = GetStateConsistencyForConsistencyMode(consistencyMode.Value); - } + /// + public override async Task TrySaveStateAsync( + string storeName, + string key, + TValue value, + string etag, + StateOptions stateOptions = default, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); + ArgumentVerifier.ThrowIfNullOrEmpty(key, nameof(key)); + // Not all state stores treat empty etag as invalid. Therefore, we will not verify an empty etag and + // rely on bubbling up the error if any from Dapr runtime + ArgumentVerifier.ThrowIfNull(etag, nameof(etag)); + + return await this.MakeSaveStateCallAsync(storeName, key, value, etag, stateOptions, metadata, + cancellationToken); + } - var options = CreateCallOptions(headers: null, cancellationToken); - Autogenerated.GetStateResponse response; + private async Task MakeSaveStateCallAsync( + string storeName, + string key, + TValue value, + string etag = default, + StateOptions stateOptions = default, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + var envelope = new Autogenerated.SaveStateRequest() { StoreName = storeName, }; - 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); - } - try - { - return (TypeConverters.FromJsonByteString(response.Data, this.JsonSerializerOptions), response.Etag); - } - catch (JsonException ex) + var stateItem = new Autogenerated.StateItem() { Key = key, }; + + 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); + stateItem.Metadata.Add(kvp.Key, kvp.Value); } } - /// - public override async Task SaveStateAsync( - string storeName, - string key, - TValue value, - StateOptions stateOptions = default, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default) - { - ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); - ArgumentVerifier.ThrowIfNullOrEmpty(key, nameof(key)); - - _ = await this.MakeSaveStateCallAsync( - storeName, - key, - value, - etag: null, - stateOptions, - metadata, - cancellationToken); + if (etag != null) + { + stateItem.Etag = new Autogenerated.Etag() { Value = etag }; } - /// - public override async Task TrySaveStateAsync( - string storeName, - string key, - TValue value, - string etag, - StateOptions stateOptions = default, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default) + if (stateOptions != null) { - ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); - ArgumentVerifier.ThrowIfNullOrEmpty(key, nameof(key)); - // Not all state stores treat empty etag as invalid. Therefore, we will not verify an empty etag and - // rely on bubbling up the error if any from Dapr runtime - ArgumentVerifier.ThrowIfNull(etag, nameof(etag)); - - return await this.MakeSaveStateCallAsync(storeName, key, value, etag, stateOptions, metadata, cancellationToken); + stateItem.Options = ToAutoGeneratedStateOptions(stateOptions); } - private async Task MakeSaveStateCallAsync( - string storeName, - string key, - TValue value, - string etag = default, - StateOptions stateOptions = default, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default) + if (value != null) { - var envelope = new Autogenerated.SaveStateRequest() - { - StoreName = storeName, - }; + stateItem.Value = TypeConverters.ToJsonByteString(value, this.jsonSerializerOptions); + } + envelope.States.Add(stateItem); - var stateItem = new Autogenerated.StateItem() - { - Key = key, - }; + 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); + } + } - if (metadata != null) - { - foreach (var kvp in metadata) - { - stateItem.Metadata.Add(kvp.Key, kvp.Value); - } - } - if (etag != null) - { - stateItem.Etag = new Autogenerated.Etag() { Value = etag }; - } + /// + public override async Task ExecuteStateTransactionAsync( + string storeName, + IReadOnlyList operations, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); + ArgumentVerifier.ThrowIfNull(operations, nameof(operations)); + if (operations.Count == 0) + { + throw new ArgumentException($"{nameof(operations)} does not contain any elements"); + } - if (stateOptions != null) - { - stateItem.Options = ToAutoGeneratedStateOptions(stateOptions); - } + await this.MakeExecuteStateTransactionCallAsync( + storeName, + operations, + metadata, + cancellationToken); + } + + private async Task MakeExecuteStateTransactionCallAsync( + string storeName, + IReadOnlyList states, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + var envelope = new Autogenerated.ExecuteStateTransactionRequest() { StoreName = storeName, }; - if (value != null) + foreach (var state in states) + { + var stateOperation = new Autogenerated.TransactionalStateOperation { - stateItem.Value = TypeConverters.ToJsonByteString(value, this.jsonSerializerOptions); - } + OperationType = state.OperationType.ToString().ToLower(), Request = ToAutogeneratedStateItem(state) + }; - envelope.States.Add(stateItem); + envelope.Operations.Add(stateOperation); - 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 ExecuteStateTransactionAsync( - string storeName, - IReadOnlyList operations, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default) + // Add metadata that applies to all operations if specified + if (metadata != null) { - ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); - ArgumentVerifier.ThrowIfNull(operations, nameof(operations)); - if (operations.Count == 0) + foreach (var kvp in metadata) { - throw new ArgumentException($"{nameof(operations)} does not contain any elements"); + envelope.Metadata.Add(kvp.Key, kvp.Value); } - - await this.MakeExecuteStateTransactionCallAsync( - storeName, - operations, - metadata, - cancellationToken); } - private async Task MakeExecuteStateTransactionCallAsync( - string storeName, - IReadOnlyList states, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default) + var options = CreateCallOptions(headers: null, cancellationToken); + try { - var envelope = new Autogenerated.ExecuteStateTransactionRequest() - { - StoreName = storeName, - }; - - foreach (var state in states) - { - var stateOperation = new Autogenerated.TransactionalStateOperation - { - OperationType = state.OperationType.ToString().ToLower(), - Request = ToAutogeneratedStateItem(state) - }; + await client.ExecuteStateTransactionAsync(envelope, options); + } + catch (RpcException ex) + { + throw new DaprException( + "State operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + } + } - envelope.Operations.Add(stateOperation); + private Autogenerated.StateItem ToAutogeneratedStateItem(StateTransactionRequest state) + { + var stateOperation = new Autogenerated.StateItem { Key = state.Key }; - } + if (state.Value != null) + { + stateOperation.Value = ByteString.CopyFrom(state.Value); + } - // Add metadata that applies to all operations if specified - if (metadata != null) - { - foreach (var kvp in metadata) - { - envelope.Metadata.Add(kvp.Key, kvp.Value); - } - } + if (state.ETag != null) + { + stateOperation.Etag = new Autogenerated.Etag() { Value = state.ETag }; + } - var options = CreateCallOptions(headers: null, cancellationToken); - try - { - await client.ExecuteStateTransactionAsync(envelope, options); - } - catch (RpcException ex) + if (state.Metadata != null) + { + foreach (var kvp in state.Metadata) { - throw new DaprException("State operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + stateOperation.Metadata.Add(kvp.Key, kvp.Value); } } - private Autogenerated.StateItem ToAutogeneratedStateItem(StateTransactionRequest state) + if (state.Options != null) { - var stateOperation = new Autogenerated.StateItem - { - Key = state.Key - }; + stateOperation.Options = ToAutoGeneratedStateOptions(state.Options); + } - if (state.Value != null) - { - stateOperation.Value = ByteString.CopyFrom(state.Value); - } + return stateOperation; + } - if (state.ETag != null) - { - stateOperation.Etag = new Autogenerated.Etag() { Value = state.ETag }; - } + /// + public override async Task DeleteStateAsync( + string storeName, + string key, + StateOptions stateOptions = default, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); + ArgumentVerifier.ThrowIfNullOrEmpty(key, nameof(key)); + + _ = await this.MakeDeleteStateCallAsync( + storeName, + key, + etag: null, + stateOptions, + metadata, + cancellationToken); + } - if (state.Metadata != null) - { - foreach (var kvp in state.Metadata) - { - stateOperation.Metadata.Add(kvp.Key, kvp.Value); - } - } + /// + public override async Task TryDeleteStateAsync( + string storeName, + string key, + string etag, + StateOptions stateOptions = default, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); + ArgumentVerifier.ThrowIfNullOrEmpty(key, nameof(key)); + // Not all state stores treat empty etag as invalid. Therefore, we will not verify an empty etag and + // rely on bubbling up the error if any from Dapr runtime + ArgumentVerifier.ThrowIfNull(etag, nameof(etag)); + + return await this.MakeDeleteStateCallAsync(storeName, key, etag, stateOptions, metadata, cancellationToken); + } + + private async Task MakeDeleteStateCallAsync( + string storeName, + string key, + string etag = default, + StateOptions stateOptions = default, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + var deleteStateEnvelope = new Autogenerated.DeleteStateRequest() { StoreName = storeName, Key = key, }; - if (state.Options != null) + if (metadata != null) + { + foreach (var kvp in metadata) { - stateOperation.Options = ToAutoGeneratedStateOptions(state.Options); + deleteStateEnvelope.Metadata.Add(kvp.Key, kvp.Value); } - - return stateOperation; } - - /// - public override async Task DeleteStateAsync( - string storeName, - string key, - StateOptions stateOptions = default, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default) + if (etag != null) { - ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); - ArgumentVerifier.ThrowIfNullOrEmpty(key, nameof(key)); - - _ = await this.MakeDeleteStateCallAsync( - storeName, - key, - etag: null, - stateOptions, - metadata, - cancellationToken); + deleteStateEnvelope.Etag = new Autogenerated.Etag() { Value = etag }; } - /// - public override async Task TryDeleteStateAsync( - string storeName, - string key, - string etag, - StateOptions stateOptions = default, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default) + if (stateOptions != null) { - ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); - ArgumentVerifier.ThrowIfNullOrEmpty(key, nameof(key)); - // Not all state stores treat empty etag as invalid. Therefore, we will not verify an empty etag and - // rely on bubbling up the error if any from Dapr runtime - ArgumentVerifier.ThrowIfNull(etag, nameof(etag)); + deleteStateEnvelope.Options = ToAutoGeneratedStateOptions(stateOptions); + } + + var options = CreateCallOptions(headers: null, cancellationToken); - return await this.MakeDeleteStateCallAsync(storeName, key, etag, stateOptions, metadata, cancellationToken); + try + { + await client.DeleteStateAsync(deleteStateEnvelope, options); + return true; + } + catch (RpcException rpc) when (etag != null && rpc.StatusCode == StatusCode.Aborted) + { + // This kind of failure indicates an ETag mismatch. Aborted doesn't seem like + // the right status code at first, but check the docs, it fits this use-case. + // + // When an ETag is used we surface this though the Try... pattern + return false; } + catch (RpcException ex) + { + throw new DaprException( + "State operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + } + } + + /// + public async override Task> QueryStateAsync( + string storeName, + string jsonQuery, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + var queryRequest = new Autogenerated.QueryStateRequest() { StoreName = storeName, Query = jsonQuery }; - private async Task MakeDeleteStateCallAsync( - string storeName, - string key, - string etag = default, - StateOptions stateOptions = default, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default) + if (metadata != null) { - var deleteStateEnvelope = new Autogenerated.DeleteStateRequest() + foreach (var kvp in metadata) { - StoreName = storeName, - Key = key, - }; + queryRequest.Metadata.Add(kvp.Key, kvp.Value); + } + } - if (metadata != null) + var options = CreateCallOptions(headers: null, cancellationToken); + + try + { + var items = new List>(); + var failedKeys = new List(); + var queryResponse = await client.QueryStateAlpha1Async(queryRequest, options); + foreach (var item in queryResponse.Results) { - foreach (var kvp in metadata) + if (!string.IsNullOrEmpty(item.Error)) { - deleteStateEnvelope.Metadata.Add(kvp.Key, kvp.Value); + // When we encounter an error, we record the key and prepare to throw an exception at the end of the results. + failedKeys.Add(item.Key); + continue; } - } - if (etag != null) - { - deleteStateEnvelope.Etag = new Autogenerated.Etag() { Value = etag }; + items.Add(new StateQueryItem(item.Key, + TypeConverters.FromJsonByteString(item.Data, this.JsonSerializerOptions), item.Etag, + item.Error)); } - if (stateOptions != null) + var results = new StateQueryResponse(items, queryResponse.Token, queryResponse.Metadata); + if (failedKeys.Count > 0) { - deleteStateEnvelope.Options = ToAutoGeneratedStateOptions(stateOptions); + // We encountered some bad keys so we throw instead of returning to alert the user. + throw new StateQueryException($"Encountered an error while processing state query results.", + results, failedKeys); } - var options = CreateCallOptions(headers: null, cancellationToken); - - try - { - await client.DeleteStateAsync(deleteStateEnvelope, options); - return true; - } - catch (RpcException rpc) when (etag != null && rpc.StatusCode == StatusCode.Aborted) - { - // This kind of failure indicates an ETag mismatch. Aborted doesn't seem like - // the right status code at first, but check the docs, it fits this use-case. - // - // When an ETag is used we surface this though the Try... pattern - return false; - } - catch (RpcException ex) - { - throw new DaprException("State operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); - } + return results; } - - /// - public async override Task> QueryStateAsync( - string storeName, - string jsonQuery, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default) + catch (RpcException ex) { - var queryRequest = new Autogenerated.QueryStateRequest() - { - StoreName = storeName, - Query = jsonQuery - }; + throw new DaprException( + "Query state operation failed: the Dapr endpointed indicated a failure. See InnerException for details.", + ex); + } + catch (JsonException ex) + { + throw new DaprException( + "State operation failed: the state payload could not be deserialized. See InnerException for details.", + ex); + } + } - if (metadata != null) - { - foreach (var kvp in metadata) - { - queryRequest.Metadata.Add(kvp.Key, kvp.Value); - } - } + #endregion - var options = CreateCallOptions(headers: null, cancellationToken); + #region Secret Apis - try - { - var items = new List>(); - var failedKeys = new List(); - var queryResponse = await client.QueryStateAlpha1Async(queryRequest, options); - foreach (var item in queryResponse.Results) - { - if (!string.IsNullOrEmpty(item.Error)) - { - // When we encounter an error, we record the key and prepare to throw an exception at the end of the results. - failedKeys.Add(item.Key); - continue; - } - items.Add(new StateQueryItem(item.Key, TypeConverters.FromJsonByteString(item.Data, this.JsonSerializerOptions), item.Etag, item.Error)); - } + /// + public async override Task> GetSecretAsync( + string storeName, + string key, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); + ArgumentVerifier.ThrowIfNullOrEmpty(key, nameof(key)); - var results = new StateQueryResponse(items, queryResponse.Token, queryResponse.Metadata); - if (failedKeys.Count > 0) - { - // We encountered some bad keys so we throw instead of returning to alert the user. - throw new StateQueryException($"Encountered an error while processing state query results.", results, failedKeys); - } + var envelope = new Autogenerated.GetSecretRequest() { StoreName = storeName, Key = key }; - return results; - } - catch (RpcException ex) - { - throw new DaprException("Query state operation failed: the Dapr endpointed indicated a failure. See InnerException for details.", ex); - } - catch (JsonException ex) + 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); } } - #endregion - #region Secret Apis - /// - public async override Task> GetSecretAsync( - string storeName, - string key, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default) - { - ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); - ArgumentVerifier.ThrowIfNullOrEmpty(key, nameof(key)); + var options = CreateCallOptions(headers: null, cancellationToken); + Autogenerated.GetSecretResponse response; - var envelope = new Autogenerated.GetSecretRequest() - { - StoreName = storeName, - Key = key - }; + try + { + response = await client.GetSecretAsync(envelope, options); + } + catch (RpcException ex) + { + throw new DaprException( + "Secret operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + } - if (metadata != null) - { - foreach (var kvp in metadata) - { - envelope.Metadata.Add(kvp.Key, kvp.Value); - } - } + return response.Data.ToDictionary(kv => kv.Key, kv => kv.Value); + } - var options = CreateCallOptions(headers: null, cancellationToken); - Autogenerated.GetSecretResponse response; + /// + public async override Task>> GetBulkSecretAsync( + string storeName, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + var envelope = new Autogenerated.GetBulkSecretRequest() { StoreName = storeName }; - try - { - response = await client.GetSecretAsync(envelope, options); - } - catch (RpcException ex) + if (metadata != null) + { + foreach (var kvp in metadata) { - throw new DaprException("Secret operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + envelope.Metadata.Add(kvp.Key, kvp.Value); } - - return response.Data.ToDictionary(kv => kv.Key, kv => kv.Value); } - /// - public async override Task>> GetBulkSecretAsync( - string storeName, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default) + var options = CreateCallOptions(headers: null, cancellationToken); + Autogenerated.GetBulkSecretResponse response; + + try { - var envelope = new Autogenerated.GetBulkSecretRequest() - { - StoreName = storeName - }; + response = await client.GetBulkSecretAsync(envelope, options); + } + catch (RpcException ex) + { + throw new DaprException( + "Bulk secret operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", + ex); + } - if (metadata != null) - { - foreach (var kvp in metadata) - { - envelope.Metadata.Add(kvp.Key, kvp.Value); - } - } + return response.Data.ToDictionary(r => r.Key, r => r.Value.Secrets.ToDictionary(s => s.Key, s => s.Value)); + } - var options = CreateCallOptions(headers: null, cancellationToken); - Autogenerated.GetBulkSecretResponse response; + #endregion - try - { - response = await client.GetBulkSecretAsync(envelope, options); - } - catch (RpcException ex) - { - throw new DaprException("Bulk secret operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); - } + #region Configuration API - return response.Data.ToDictionary(r => r.Key, r => r.Value.Secrets.ToDictionary(s => s.Key, s => s.Value)); - } - #endregion + /// + public async override Task GetConfiguration( + string storeName, + IReadOnlyList keys, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); - #region Configuration API - /// - public async override Task GetConfiguration( - string storeName, - IReadOnlyList keys, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default) - { - ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); + var request = new Autogenerated.GetConfigurationRequest() { StoreName = storeName }; - var request = new Autogenerated.GetConfigurationRequest() - { - StoreName = storeName - }; + if (keys != null && keys.Count > 0) + { + request.Keys.AddRange(keys); + } - if (keys != null && keys.Count > 0) + if (metadata != null) + { + foreach (var kvp in metadata) { - request.Keys.AddRange(keys); + request.Metadata.Add(kvp.Key, kvp.Value); } + } - if (metadata != null) - { - foreach (var kvp in metadata) - { - request.Metadata.Add(kvp.Key, kvp.Value); - } - } + var options = CreateCallOptions(headers: null, cancellationToken); + Autogenerated.GetConfigurationResponse response = new Autogenerated.GetConfigurationResponse(); + try + { + response = await client.GetConfigurationAsync(request, options); + } + catch (RpcException ex) + { + throw new DaprException( + "GetConfiguration operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", + ex); + } - var options = CreateCallOptions(headers: null, cancellationToken); - Autogenerated.GetConfigurationResponse response = new Autogenerated.GetConfigurationResponse(); - try - { - response = await client.GetConfigurationAsync(request, options); - } - catch (RpcException ex) - { - throw new DaprException("GetConfiguration operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); - } + var responseItems = response.Items.ToDictionary(item => item.Key, + item => new ConfigurationItem(item.Value.Value, item.Value.Version, item.Value.Metadata)); - var responseItems = response.Items.ToDictionary(item => item.Key, item => new ConfigurationItem(item.Value.Value, item.Value.Version, item.Value.Metadata)); + return new GetConfigurationResponse(responseItems); + } - return new GetConfigurationResponse(responseItems); - } + /// + public override Task SubscribeConfiguration( + string storeName, + IReadOnlyList keys, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); - /// - public override Task SubscribeConfiguration( - string storeName, - IReadOnlyList keys, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default) + Autogenerated.SubscribeConfigurationRequest request = new Autogenerated.SubscribeConfigurationRequest() { - ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); - - Autogenerated.SubscribeConfigurationRequest request = new Autogenerated.SubscribeConfigurationRequest() - { - StoreName = storeName - }; + StoreName = storeName + }; - if (keys != null && keys.Count > 0) - { - request.Keys.AddRange(keys); - } + if (keys != null && keys.Count > 0) + { + request.Keys.AddRange(keys); + } - if (metadata != null) + if (metadata != null) + { + foreach (var kvp in metadata) { - foreach (var kvp in metadata) - { - request.Metadata.Add(kvp.Key, kvp.Value); - } + request.Metadata.Add(kvp.Key, kvp.Value); } - - var options = CreateCallOptions(headers: null, cancellationToken: cancellationToken); - return Task.FromResult(new SubscribeConfigurationResponse(new DaprSubscribeConfigurationSource(client.SubscribeConfiguration(request, options)))); } - public override async Task UnsubscribeConfiguration( - string storeName, - string id, - CancellationToken cancellationToken = default) - { - ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); - ArgumentVerifier.ThrowIfNullOrEmpty(id, nameof(id)); + var options = CreateCallOptions(headers: null, cancellationToken: cancellationToken); + return Task.FromResult(new SubscribeConfigurationResponse( + new DaprSubscribeConfigurationSource(client.SubscribeConfiguration(request, options)))); + } - Autogenerated.UnsubscribeConfigurationRequest request = new Autogenerated.UnsubscribeConfigurationRequest() - { - StoreName = storeName, - Id = id - }; + public override async Task UnsubscribeConfiguration( + string storeName, + string id, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); + ArgumentVerifier.ThrowIfNullOrEmpty(id, nameof(id)); - var options = CreateCallOptions(headers: null, cancellationToken); - var resp = await client.UnsubscribeConfigurationAsync(request, options); - return new UnsubscribeConfigurationResponse(resp.Ok, resp.Message); - } + Autogenerated.UnsubscribeConfigurationRequest request = + new Autogenerated.UnsubscribeConfigurationRequest() { StoreName = storeName, Id = id }; - #endregion + var options = CreateCallOptions(headers: null, cancellationToken); + var resp = await client.UnsubscribeConfigurationAsync(request, options); + return new UnsubscribeConfigurationResponse(resp.Ok, resp.Message); + } + + #endregion - #region Cryptography + #region Cryptography - /// - [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public override async Task> EncryptAsync(string vaultResourceName, - ReadOnlyMemory plaintextBytes, string keyName, EncryptionOptions encryptionOptions, - CancellationToken cancellationToken = default) + /// + [Obsolete( + "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public override async Task> EncryptAsync(string vaultResourceName, + ReadOnlyMemory plaintextBytes, string keyName, EncryptionOptions encryptionOptions, + CancellationToken cancellationToken = default) + { + if (MemoryMarshal.TryGetArray(plaintextBytes, out var plaintextSegment) && plaintextSegment.Array != null) { - if (MemoryMarshal.TryGetArray(plaintextBytes, out var plaintextSegment) && plaintextSegment.Array != null) - { - var encryptionResult = await EncryptAsync(vaultResourceName, new MemoryStream(plaintextSegment.Array), keyName, encryptionOptions, - cancellationToken); - - var bufferedResult = new ArrayBufferWriter(); + var encryptionResult = await EncryptAsync(vaultResourceName, new MemoryStream(plaintextSegment.Array), + keyName, encryptionOptions, + cancellationToken); - await foreach (var item in encryptionResult.WithCancellation(cancellationToken)) - { - bufferedResult.Write(item.Span); - } - - return bufferedResult.WrittenMemory; + var bufferedResult = new ArrayBufferWriter(); + + await foreach (var item in encryptionResult.WithCancellation(cancellationToken)) + { + bufferedResult.Write(item.Span); } - throw new ArgumentException("The input instance doesn't have a valid underlying data store.", nameof(plaintextBytes)); + return bufferedResult.WrittenMemory; } - /// - [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public override async Task>> EncryptAsync(string vaultResourceName, Stream plaintextStream, - string keyName, EncryptionOptions encryptionOptions, CancellationToken cancellationToken = default) - { - ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); - ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); - ArgumentVerifier.ThrowIfNull(plaintextStream, nameof(plaintextStream)); - ArgumentVerifier.ThrowIfNull(encryptionOptions, nameof(encryptionOptions)); + throw new ArgumentException("The input instance doesn't have a valid underlying data store.", + nameof(plaintextBytes)); + } - var shouldOmitDecryptionKeyName = string.IsNullOrWhiteSpace(encryptionOptions.DecryptionKeyName); //Whitespace isn't likely a valid key name either + /// + [Obsolete( + "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public override async Task>> EncryptAsync(string vaultResourceName, + Stream plaintextStream, + string keyName, EncryptionOptions encryptionOptions, CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); + ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); + ArgumentVerifier.ThrowIfNull(plaintextStream, nameof(plaintextStream)); + ArgumentVerifier.ThrowIfNull(encryptionOptions, nameof(encryptionOptions)); + + var shouldOmitDecryptionKeyName = + string.IsNullOrWhiteSpace(encryptionOptions + .DecryptionKeyName); //Whitespace isn't likely a valid key name either + + var encryptRequestOptions = new Autogenerated.EncryptRequestOptions + { + ComponentName = vaultResourceName, + DataEncryptionCipher = encryptionOptions.EncryptionCipher.GetValueFromEnumMember(), + KeyName = keyName, + KeyWrapAlgorithm = encryptionOptions.KeyWrapAlgorithm.GetValueFromEnumMember(), + OmitDecryptionKeyName = shouldOmitDecryptionKeyName + }; + + if (!shouldOmitDecryptionKeyName) + { + ArgumentVerifier.ThrowIfNullOrEmpty(encryptionOptions.DecryptionKeyName, + nameof(encryptionOptions.DecryptionKeyName)); + encryptRequestOptions.DecryptionKeyName = encryptRequestOptions.DecryptionKeyName; + } + + var options = CreateCallOptions(headers: null, cancellationToken); + var duplexStream = client.EncryptAlpha1(options); + + //Run both operations at the same time, but return the output of the streaming values coming from the operation + var receiveResult = Task.FromResult(RetrieveEncryptedStreamAsync(duplexStream, cancellationToken)); + return await Task.WhenAll( + //Stream the plaintext data to the sidecar in chunks + SendPlaintextStreamAsync(plaintextStream, encryptionOptions.StreamingBlockSizeInBytes, + duplexStream, encryptRequestOptions, cancellationToken), + //At the same time, retrieve the encrypted response from the sidecar + receiveResult).ContinueWith(_ => receiveResult.Result, cancellationToken); + } - var encryptRequestOptions = new Autogenerated.EncryptRequestOptions - { - ComponentName = vaultResourceName, - DataEncryptionCipher = encryptionOptions.EncryptionCipher.GetValueFromEnumMember(), - KeyName = keyName, - KeyWrapAlgorithm = encryptionOptions.KeyWrapAlgorithm.GetValueFromEnumMember(), - OmitDecryptionKeyName = shouldOmitDecryptionKeyName - }; + /// + /// Sends the plaintext bytes in chunks to the sidecar to be encrypted. + /// + private async Task SendPlaintextStreamAsync(Stream plaintextStream, + int streamingBlockSizeInBytes, + AsyncDuplexStreamingCall duplexStream, + Autogenerated.EncryptRequestOptions encryptRequestOptions, + CancellationToken cancellationToken) + { + //Start with passing the metadata about the encryption request itself in the first message + await duplexStream.RequestStream.WriteAsync( + new Autogenerated.EncryptRequest { Options = encryptRequestOptions }, cancellationToken); - if (!shouldOmitDecryptionKeyName) - { - ArgumentVerifier.ThrowIfNullOrEmpty(encryptionOptions.DecryptionKeyName, nameof(encryptionOptions.DecryptionKeyName)); - encryptRequestOptions.DecryptionKeyName = encryptRequestOptions.DecryptionKeyName; - } + //Send the plaintext bytes in blocks in subsequent messages + await using (var bufferedStream = new BufferedStream(plaintextStream, streamingBlockSizeInBytes)) + { + var buffer = new byte[streamingBlockSizeInBytes]; + int bytesRead; + ulong sequenceNumber = 0; - var options = CreateCallOptions(headers: null, cancellationToken); - var duplexStream = client.EncryptAlpha1(options); - - //Run both operations at the same time, but return the output of the streaming values coming from the operation - var receiveResult = Task.FromResult(RetrieveEncryptedStreamAsync(duplexStream, cancellationToken)); - return await Task.WhenAll( - //Stream the plaintext data to the sidecar in chunks - SendPlaintextStreamAsync(plaintextStream, encryptionOptions.StreamingBlockSizeInBytes, - duplexStream, encryptRequestOptions, cancellationToken), - //At the same time, retrieve the encrypted response from the sidecar - receiveResult).ContinueWith(_ => receiveResult.Result, cancellationToken); - } - - /// - /// Sends the plaintext bytes in chunks to the sidecar to be encrypted. - /// - private async Task SendPlaintextStreamAsync(Stream plaintextStream, - int streamingBlockSizeInBytes, - AsyncDuplexStreamingCall duplexStream, - Autogenerated.EncryptRequestOptions encryptRequestOptions, - CancellationToken cancellationToken) - { - //Start with passing the metadata about the encryption request itself in the first message - await duplexStream.RequestStream.WriteAsync( - new Autogenerated.EncryptRequest {Options = encryptRequestOptions}, cancellationToken); - - //Send the plaintext bytes in blocks in subsequent messages - await using (var bufferedStream = new BufferedStream(plaintextStream, streamingBlockSizeInBytes)) + while ((bytesRead = + await bufferedStream.ReadAsync(buffer.AsMemory(0, streamingBlockSizeInBytes), + cancellationToken)) != + 0) { - var buffer = new byte[streamingBlockSizeInBytes]; - int bytesRead; - ulong sequenceNumber = 0; - - while ((bytesRead = - await bufferedStream.ReadAsync(buffer.AsMemory(0, streamingBlockSizeInBytes), cancellationToken)) != - 0) - { - await duplexStream.RequestStream.WriteAsync( - new Autogenerated.EncryptRequest + await duplexStream.RequestStream.WriteAsync( + new Autogenerated.EncryptRequest + { + Payload = new Autogenerated.StreamPayload { - Payload = new Autogenerated.StreamPayload - { - Data = ByteString.CopyFrom(buffer, 0, bytesRead), Seq = sequenceNumber - } - }, cancellationToken); - - //Increment the sequence number - sequenceNumber++; - } - } + Data = ByteString.CopyFrom(buffer, 0, bytesRead), Seq = sequenceNumber + } + }, cancellationToken); - //Send the completion message - await duplexStream.RequestStream.CompleteAsync(); + //Increment the sequence number + sequenceNumber++; + } } - /// - /// Retrieves the encrypted bytes from the encryption operation on the sidecar and returns as an enumerable stream. - /// - private async IAsyncEnumerable> RetrieveEncryptedStreamAsync(AsyncDuplexStreamingCall duplexStream, [EnumeratorCancellation] CancellationToken cancellationToken) + //Send the completion message + await duplexStream.RequestStream.CompleteAsync(); + } + + /// + /// Retrieves the encrypted bytes from the encryption operation on the sidecar and returns as an enumerable stream. + /// + private async IAsyncEnumerable> RetrieveEncryptedStreamAsync( + AsyncDuplexStreamingCall duplexStream, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + await foreach (var encryptResponse in duplexStream.ResponseStream.ReadAllAsync(cancellationToken) + .ConfigureAwait(false)) { - await foreach (var encryptResponse in duplexStream.ResponseStream.ReadAllAsync(cancellationToken) - .ConfigureAwait(false)) - { - yield return encryptResponse.Payload.Data.Memory; - } + yield return encryptResponse.Payload.Data.Memory; } - - /// - [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public override async Task>> DecryptAsync(string vaultResourceName, Stream ciphertextStream, string keyName, - DecryptionOptions decryptionOptions, CancellationToken cancellationToken = default) - { - ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); - ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); - ArgumentVerifier.ThrowIfNull(ciphertextStream, nameof(ciphertextStream)); - ArgumentVerifier.ThrowIfNull(decryptionOptions, nameof(decryptionOptions)); + } - var decryptRequestOptions = new Autogenerated.DecryptRequestOptions - { - ComponentName = vaultResourceName, - KeyName = keyName - }; + /// + [Obsolete( + "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public override async Task>> DecryptAsync(string vaultResourceName, + Stream ciphertextStream, string keyName, + DecryptionOptions decryptionOptions, CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); + ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); + ArgumentVerifier.ThrowIfNull(ciphertextStream, nameof(ciphertextStream)); + ArgumentVerifier.ThrowIfNull(decryptionOptions, nameof(decryptionOptions)); - var options = CreateCallOptions(headers: null, cancellationToken); - var duplexStream = client.DecryptAlpha1(options); + var decryptRequestOptions = new Autogenerated.DecryptRequestOptions + { + ComponentName = vaultResourceName, KeyName = keyName + }; + + var options = CreateCallOptions(headers: null, cancellationToken); + var duplexStream = client.DecryptAlpha1(options); - //Run both operations at the same time, but return the output of the streaming values coming from the operation - var receiveResult = Task.FromResult(RetrieveDecryptedStreamAsync(duplexStream, cancellationToken)); - return await Task.WhenAll( + //Run both operations at the same time, but return the output of the streaming values coming from the operation + var receiveResult = Task.FromResult(RetrieveDecryptedStreamAsync(duplexStream, cancellationToken)); + return await Task.WhenAll( //Stream the ciphertext data to the sidecar in chunks SendCiphertextStreamAsync(ciphertextStream, decryptionOptions.StreamingBlockSizeInBytes, duplexStream, decryptRequestOptions, cancellationToken), //At the same time, retrieve the decrypted response from the sidecar receiveResult) - //Return only the result of the `RetrieveEncryptedStreamAsync` method + //Return only the result of the `RetrieveEncryptedStreamAsync` method .ContinueWith(t => receiveResult.Result, cancellationToken); - } + } - /// - [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public override Task>> DecryptAsync(string vaultResourceName, - Stream ciphertextStream, string keyName, CancellationToken cancellationToken = default) => - DecryptAsync(vaultResourceName, ciphertextStream, keyName, new DecryptionOptions(), - cancellationToken); - - /// - /// Sends the ciphertext bytes in chunks to the sidecar to be decrypted. - /// - private async Task SendCiphertextStreamAsync(Stream ciphertextStream, - int streamingBlockSizeInBytes, - AsyncDuplexStreamingCall duplexStream, - Autogenerated.DecryptRequestOptions decryptRequestOptions, - CancellationToken cancellationToken) - { - //Start with passing the metadata about the decryption request itself in the first message - await duplexStream.RequestStream.WriteAsync( - new Autogenerated.DecryptRequest { Options = decryptRequestOptions }, cancellationToken); - - //Send the ciphertext bytes in blocks in subsequent messages - await using (var bufferedStream = new BufferedStream(ciphertextStream, streamingBlockSizeInBytes)) + /// + [Obsolete( + "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public override Task>> DecryptAsync(string vaultResourceName, + Stream ciphertextStream, string keyName, CancellationToken cancellationToken = default) => + DecryptAsync(vaultResourceName, ciphertextStream, keyName, new DecryptionOptions(), + cancellationToken); + + /// + /// Sends the ciphertext bytes in chunks to the sidecar to be decrypted. + /// + private async Task SendCiphertextStreamAsync(Stream ciphertextStream, + int streamingBlockSizeInBytes, + AsyncDuplexStreamingCall duplexStream, + Autogenerated.DecryptRequestOptions decryptRequestOptions, + CancellationToken cancellationToken) + { + //Start with passing the metadata about the decryption request itself in the first message + await duplexStream.RequestStream.WriteAsync( + new Autogenerated.DecryptRequest { Options = decryptRequestOptions }, cancellationToken); + + //Send the ciphertext bytes in blocks in subsequent messages + await using (var bufferedStream = new BufferedStream(ciphertextStream, streamingBlockSizeInBytes)) + { + var buffer = new byte[streamingBlockSizeInBytes]; + int bytesRead; + ulong sequenceNumber = 0; + + while ((bytesRead = + await bufferedStream.ReadAsync(buffer.AsMemory(0, streamingBlockSizeInBytes), + cancellationToken)) != 0) { - var buffer = new byte[streamingBlockSizeInBytes]; - int bytesRead; - ulong sequenceNumber = 0; - - while ((bytesRead = await bufferedStream.ReadAsync(buffer.AsMemory(0, streamingBlockSizeInBytes), cancellationToken)) != 0) - { - await duplexStream.RequestStream.WriteAsync(new Autogenerated.DecryptRequest + await duplexStream.RequestStream.WriteAsync( + new Autogenerated.DecryptRequest { Payload = new Autogenerated.StreamPayload { - Data = ByteString.CopyFrom(buffer, 0, bytesRead), - Seq = sequenceNumber + Data = ByteString.CopyFrom(buffer, 0, bytesRead), Seq = sequenceNumber } }, cancellationToken); - - //Increment the sequence number - sequenceNumber++; - } + + //Increment the sequence number + sequenceNumber++; } - - //Send the completion message - await duplexStream.RequestStream.CompleteAsync(); } - /// - /// Retrieves the decrypted bytes from the decryption operation on the sidecar and returns as an enumerable stream. - /// - private async IAsyncEnumerable> RetrieveDecryptedStreamAsync( - AsyncDuplexStreamingCall duplexStream, - [EnumeratorCancellation] CancellationToken cancellationToken) + //Send the completion message + await duplexStream.RequestStream.CompleteAsync(); + } + + /// + /// Retrieves the decrypted bytes from the decryption operation on the sidecar and returns as an enumerable stream. + /// + private async IAsyncEnumerable> RetrieveDecryptedStreamAsync( + AsyncDuplexStreamingCall duplexStream, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + await foreach (var decryptResponse in duplexStream.ResponseStream.ReadAllAsync(cancellationToken) + .ConfigureAwait(false)) { - await foreach (var decryptResponse in duplexStream.ResponseStream.ReadAllAsync(cancellationToken) - .ConfigureAwait(false)) - { - yield return decryptResponse.Payload.Data.Memory; - } + yield return decryptResponse.Payload.Data.Memory; } - - /// - [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public override async Task> DecryptAsync(string vaultResourceName, - ReadOnlyMemory ciphertextBytes, string keyName, DecryptionOptions decryptionOptions, - CancellationToken cancellationToken = default) - { - if (MemoryMarshal.TryGetArray(ciphertextBytes, out var ciphertextSegment) && ciphertextSegment.Array != null) - { - var decryptionResult = await DecryptAsync(vaultResourceName, new MemoryStream(ciphertextSegment.Array), - keyName, decryptionOptions, cancellationToken); - - var bufferedResult = new ArrayBufferWriter(); - await foreach (var item in decryptionResult.WithCancellation(cancellationToken)) - { - bufferedResult.Write(item.Span); - } + } - return bufferedResult.WrittenMemory; - } + /// + [Obsolete( + "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public override async Task> DecryptAsync(string vaultResourceName, + ReadOnlyMemory ciphertextBytes, string keyName, DecryptionOptions decryptionOptions, + CancellationToken cancellationToken = default) + { + if (MemoryMarshal.TryGetArray(ciphertextBytes, out var ciphertextSegment) && ciphertextSegment.Array != null) + { + var decryptionResult = await DecryptAsync(vaultResourceName, new MemoryStream(ciphertextSegment.Array), + keyName, decryptionOptions, cancellationToken); - throw new ArgumentException("The input instance doesn't have a valid underlying data store", nameof(ciphertextBytes)); - } - - /// - [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public override async Task> DecryptAsync(string vaultResourceName, - ReadOnlyMemory ciphertextBytes, string keyName, CancellationToken cancellationToken = default) => - await DecryptAsync(vaultResourceName, ciphertextBytes, keyName, - new DecryptionOptions(), cancellationToken); - - #region Subtle Crypto Implementation - - ///// - //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - //public override async Task<(string Name, string PublicKey)> GetKeyAsync(string vaultResourceName, string keyName, Autogenerated.SubtleGetKeyRequest.Types.KeyFormat keyFormat, - // CancellationToken cancellationToken = default) - //{ - // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); - // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); - - // var envelope = new Autogenerated.SubtleGetKeyRequest() - // { - // ComponentName = vaultResourceName, Format = keyFormat, Name = keyName - // }; - - // var options = CreateCallOptions(headers: null, cancellationToken); - // Autogenerated.SubtleGetKeyResponse response; - - // try - // { - // response = await client.SubtleGetKeyAlpha1Async(envelope, options); - // } - // catch (RpcException ex) - // { - // throw new DaprException( - // "Cryptography operation failed: the Dapr endpoint indicated a failure. See InnerException for details", ex); - // } - - // return (response.Name, response.PublicKey); - //} - - ///// - //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - //public override async Task<(byte[] CipherTextBytes, byte[] AuthenticationTag)> EncryptAsync(string vaultResourceName, byte[] plainTextBytes, string algorithm, - // string keyName, byte[] nonce, byte[] associatedData, CancellationToken cancellationToken = default) - //{ - // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); - // ArgumentVerifier.ThrowIfNullOrEmpty(algorithm, nameof(algorithm)); - // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); - - // var envelope = new Autogenerated.SubtleEncryptRequest - // { - // ComponentName = vaultResourceName, - // Algorithm = algorithm, - // KeyName = keyName, - // Nonce = ByteString.CopyFrom(nonce), - // Plaintext = ByteString.CopyFrom(plainTextBytes), - // AssociatedData = ByteString.CopyFrom(associatedData) - // }; - - // var options = CreateCallOptions(headers: null, cancellationToken); - // Autogenerated.SubtleEncryptResponse response; - - // try - // { - // response = await client.SubtleEncryptAlpha1Async(envelope, options); - // } - // catch (RpcException ex) - // { - // throw new DaprException( - // "Cryptography operation failed: the Dapr endpoint indicated a failure. See InnerException for details", - // ex); - // } - - // return (response.Ciphertext.ToByteArray(), response.Tag.ToByteArray() ?? Array.Empty()); - //} - - ///// - //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - //public override async Task DecryptAsync(string vaultResourceName, byte[] cipherTextBytes, string algorithm, string keyName, byte[] nonce, byte[] tag, - // byte[] associatedData, CancellationToken cancellationToken = default) - //{ - // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); - // ArgumentVerifier.ThrowIfNullOrEmpty(algorithm, nameof(algorithm)); - // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); - - // var envelope = new Autogenerated.SubtleDecryptRequest - // { - // ComponentName = vaultResourceName, - // Algorithm = algorithm, - // KeyName = keyName, - // Nonce = ByteString.CopyFrom(nonce), - // Ciphertext = ByteString.CopyFrom(cipherTextBytes), - // AssociatedData = ByteString.CopyFrom(associatedData), - // Tag = ByteString.CopyFrom(tag) - // }; - - // var options = CreateCallOptions(headers: null, cancellationToken); - // Autogenerated.SubtleDecryptResponse response; - - // try - // { - // response = await client.SubtleDecryptAlpha1Async(envelope, options); - // } - // catch (RpcException ex) - // { - // throw new DaprException( - // "Cryptography operation failed: the Dapr endpoint included a failure. See InnerException for details", ex); - // } - - // return response.Plaintext.ToByteArray(); - //} - - ///// - //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - //public override async Task<(byte[] WrappedKey, byte[] AuthenticationTag)> WrapKeyAsync(string vaultResourceName, byte[] plainTextKey, string keyName, - // string algorithm, byte[] nonce, byte[] associatedData, CancellationToken cancellationToken = default) - //{ - // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); - // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); - // ArgumentVerifier.ThrowIfNullOrEmpty(algorithm, nameof(algorithm)); - - // var envelope = new Autogenerated.SubtleWrapKeyRequest - // { - // ComponentName = vaultResourceName, - // Algorithm = algorithm, - // KeyName = keyName, - // Nonce = ByteString.CopyFrom(nonce), - // PlaintextKey = ByteString.CopyFrom(plainTextKey), - // AssociatedData = ByteString.CopyFrom(associatedData) - // }; - - // var options = CreateCallOptions(headers: null, cancellationToken); - // Autogenerated.SubtleWrapKeyResponse response; - - // try - // { - // response = await client.SubtleWrapKeyAlpha1Async(envelope, options); - // } - // catch (RpcException ex) - // { - // throw new DaprException( - // "Cryptography operation failed: the Dapr endpoint included a failure. See InnerException for details", - // ex); - // } - - // return (response.WrappedKey.ToByteArray(), response.Tag.ToByteArray() ?? Array.Empty()); - //} - - ///// - //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - //public override async Task UnwrapKeyAsync(string vaultResourceName, byte[] wrappedKey, string algorithm, - // string keyName, byte[] nonce, byte[] tag, byte[] associatedData, CancellationToken cancellationToken = default) - //{ - // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); - // ArgumentVerifier.ThrowIfNullOrEmpty(algorithm, nameof(algorithm)); - // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); - - // var envelope = new Autogenerated.SubtleUnwrapKeyRequest - // { - // ComponentName = vaultResourceName, - // WrappedKey = ByteString.CopyFrom(wrappedKey), - // AssociatedData = ByteString.CopyFrom(associatedData), - // Algorithm = algorithm, - // KeyName = keyName, - // Nonce = ByteString.CopyFrom(nonce), - // Tag = ByteString.CopyFrom(tag) - // }; - - // var options = CreateCallOptions(headers: null, cancellationToken); - // Autogenerated.SubtleUnwrapKeyResponse response; - - // try - // { - // response = await client.SubtleUnwrapKeyAlpha1Async(envelope, options); - // } - // catch (RpcException ex) - // { - // throw new DaprException( - // "Cryptography operation failed: the Dapr endpoint included a failure. See InnerException for details", - // ex); - // } - - // return response.PlaintextKey.ToByteArray(); - //} - - ///// - //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - //public override async Task SignAsync(string vaultResourceName, byte[] digest, string algorithm, string keyName, CancellationToken cancellationToken = default) - //{ - // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); - // ArgumentVerifier.ThrowIfNullOrEmpty(algorithm, nameof(algorithm)); - // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); - - // var envelope = new Autogenerated.SubtleSignRequest - // { - // ComponentName = vaultResourceName, - // Digest = ByteString.CopyFrom(digest), - // Algorithm = algorithm, - // KeyName = keyName - // }; - - // var options = CreateCallOptions(headers: null, cancellationToken); - // Autogenerated.SubtleSignResponse response; - - // try - // { - // response = await client.SubtleSignAlpha1Async(envelope, options); - // } - // catch (RpcException ex) - // { - // throw new DaprException( - // "Cryptography operation failed: the Dapr endpoint included a failure. See InnerException for details", - // ex); - // } - - // return response.Signature.ToByteArray(); - //} - - ///// - //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - //public override async Task VerifyAsync(string vaultResourceName, byte[] digest, byte[] signature, - // string algorithm, string keyName, CancellationToken cancellationToken = default) - //{ - // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); - // ArgumentVerifier.ThrowIfNullOrEmpty(algorithm, nameof(algorithm)); - // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); - - // var envelope = new Autogenerated.SubtleVerifyRequest - // { - // ComponentName = vaultResourceName, - // Algorithm = algorithm, - // KeyName = keyName, - // Signature = ByteString.CopyFrom(signature), - // Digest = ByteString.CopyFrom(digest) - // }; - - // var options = CreateCallOptions(headers: null, cancellationToken); - // Autogenerated.SubtleVerifyResponse response; - - // try - // { - // response = await client.SubtleVerifyAlpha1Async(envelope, options); - // } - // catch (RpcException ex) - // { - // throw new DaprException( - // "Cryptography operation failed: the Dapr endpoint included a failure. See InnerException for details", - // ex); - // } - - // return response.Valid; - //} - - #endregion - - - #endregion - - #region Distributed Lock API - /// - [Obsolete] - public async override Task Lock( - string storeName, - string resourceId, - string lockOwner, - Int32 expiryInSeconds, - CancellationToken cancellationToken = default) - { - ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); - ArgumentVerifier.ThrowIfNullOrEmpty(resourceId, nameof(resourceId)); - ArgumentVerifier.ThrowIfNullOrEmpty(lockOwner, nameof(lockOwner)); - - if (expiryInSeconds == 0 || expiryInSeconds < 0) + var bufferedResult = new ArrayBufferWriter(); + await foreach (var item in decryptionResult.WithCancellation(cancellationToken)) { - throw new ArgumentException("The value cannot be zero or less than zero: " + expiryInSeconds); + bufferedResult.Write(item.Span); } - var request = new Autogenerated.TryLockRequest() - { - StoreName = storeName, - ResourceId = resourceId, - LockOwner = lockOwner, - ExpiryInSeconds = expiryInSeconds - }; + return bufferedResult.WrittenMemory; + } - try - { - var options = CreateCallOptions(headers: null, cancellationToken); - var response = await client.TryLockAlpha1Async(request, options); - return new TryLockResponse() - { - StoreName = storeName, - ResourceId = resourceId, - LockOwner = lockOwner, - Success = response.Success - }; - } - catch (RpcException ex) - { - throw new DaprException("Lock operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); - } + throw new ArgumentException("The input instance doesn't have a valid underlying data store", + nameof(ciphertextBytes)); + } + + /// + [Obsolete( + "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public override async Task> DecryptAsync(string vaultResourceName, + ReadOnlyMemory ciphertextBytes, string keyName, CancellationToken cancellationToken = default) => + await DecryptAsync(vaultResourceName, ciphertextBytes, keyName, + new DecryptionOptions(), cancellationToken); + + #region Subtle Crypto Implementation + + ///// + //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public override async Task<(string Name, string PublicKey)> GetKeyAsync(string vaultResourceName, string keyName, Autogenerated.SubtleGetKeyRequest.Types.KeyFormat keyFormat, + // CancellationToken cancellationToken = default) + //{ + // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); + // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); + + // var envelope = new Autogenerated.SubtleGetKeyRequest() + // { + // ComponentName = vaultResourceName, Format = keyFormat, Name = keyName + // }; + + // var options = CreateCallOptions(headers: null, cancellationToken); + // Autogenerated.SubtleGetKeyResponse response; + + // try + // { + // response = await client.SubtleGetKeyAlpha1Async(envelope, options); + // } + // catch (RpcException ex) + // { + // throw new DaprException( + // "Cryptography operation failed: the Dapr endpoint indicated a failure. See InnerException for details", ex); + // } + + // return (response.Name, response.PublicKey); + //} + + ///// + //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public override async Task<(byte[] CipherTextBytes, byte[] AuthenticationTag)> EncryptAsync(string vaultResourceName, byte[] plainTextBytes, string algorithm, + // string keyName, byte[] nonce, byte[] associatedData, CancellationToken cancellationToken = default) + //{ + // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); + // ArgumentVerifier.ThrowIfNullOrEmpty(algorithm, nameof(algorithm)); + // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); + + // var envelope = new Autogenerated.SubtleEncryptRequest + // { + // ComponentName = vaultResourceName, + // Algorithm = algorithm, + // KeyName = keyName, + // Nonce = ByteString.CopyFrom(nonce), + // Plaintext = ByteString.CopyFrom(plainTextBytes), + // AssociatedData = ByteString.CopyFrom(associatedData) + // }; + + // var options = CreateCallOptions(headers: null, cancellationToken); + // Autogenerated.SubtleEncryptResponse response; + + // try + // { + // response = await client.SubtleEncryptAlpha1Async(envelope, options); + // } + // catch (RpcException ex) + // { + // throw new DaprException( + // "Cryptography operation failed: the Dapr endpoint indicated a failure. See InnerException for details", + // ex); + // } + + // return (response.Ciphertext.ToByteArray(), response.Tag.ToByteArray() ?? Array.Empty()); + //} + + ///// + //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public override async Task DecryptAsync(string vaultResourceName, byte[] cipherTextBytes, string algorithm, string keyName, byte[] nonce, byte[] tag, + // byte[] associatedData, CancellationToken cancellationToken = default) + //{ + // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); + // ArgumentVerifier.ThrowIfNullOrEmpty(algorithm, nameof(algorithm)); + // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); + + // var envelope = new Autogenerated.SubtleDecryptRequest + // { + // ComponentName = vaultResourceName, + // Algorithm = algorithm, + // KeyName = keyName, + // Nonce = ByteString.CopyFrom(nonce), + // Ciphertext = ByteString.CopyFrom(cipherTextBytes), + // AssociatedData = ByteString.CopyFrom(associatedData), + // Tag = ByteString.CopyFrom(tag) + // }; + + // var options = CreateCallOptions(headers: null, cancellationToken); + // Autogenerated.SubtleDecryptResponse response; + + // try + // { + // response = await client.SubtleDecryptAlpha1Async(envelope, options); + // } + // catch (RpcException ex) + // { + // throw new DaprException( + // "Cryptography operation failed: the Dapr endpoint included a failure. See InnerException for details", ex); + // } + + // return response.Plaintext.ToByteArray(); + //} + + ///// + //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public override async Task<(byte[] WrappedKey, byte[] AuthenticationTag)> WrapKeyAsync(string vaultResourceName, byte[] plainTextKey, string keyName, + // string algorithm, byte[] nonce, byte[] associatedData, CancellationToken cancellationToken = default) + //{ + // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); + // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); + // ArgumentVerifier.ThrowIfNullOrEmpty(algorithm, nameof(algorithm)); + + // var envelope = new Autogenerated.SubtleWrapKeyRequest + // { + // ComponentName = vaultResourceName, + // Algorithm = algorithm, + // KeyName = keyName, + // Nonce = ByteString.CopyFrom(nonce), + // PlaintextKey = ByteString.CopyFrom(plainTextKey), + // AssociatedData = ByteString.CopyFrom(associatedData) + // }; + + // var options = CreateCallOptions(headers: null, cancellationToken); + // Autogenerated.SubtleWrapKeyResponse response; + + // try + // { + // response = await client.SubtleWrapKeyAlpha1Async(envelope, options); + // } + // catch (RpcException ex) + // { + // throw new DaprException( + // "Cryptography operation failed: the Dapr endpoint included a failure. See InnerException for details", + // ex); + // } + + // return (response.WrappedKey.ToByteArray(), response.Tag.ToByteArray() ?? Array.Empty()); + //} + + ///// + //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public override async Task UnwrapKeyAsync(string vaultResourceName, byte[] wrappedKey, string algorithm, + // string keyName, byte[] nonce, byte[] tag, byte[] associatedData, CancellationToken cancellationToken = default) + //{ + // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); + // ArgumentVerifier.ThrowIfNullOrEmpty(algorithm, nameof(algorithm)); + // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); + + // var envelope = new Autogenerated.SubtleUnwrapKeyRequest + // { + // ComponentName = vaultResourceName, + // WrappedKey = ByteString.CopyFrom(wrappedKey), + // AssociatedData = ByteString.CopyFrom(associatedData), + // Algorithm = algorithm, + // KeyName = keyName, + // Nonce = ByteString.CopyFrom(nonce), + // Tag = ByteString.CopyFrom(tag) + // }; + + // var options = CreateCallOptions(headers: null, cancellationToken); + // Autogenerated.SubtleUnwrapKeyResponse response; + + // try + // { + // response = await client.SubtleUnwrapKeyAlpha1Async(envelope, options); + // } + // catch (RpcException ex) + // { + // throw new DaprException( + // "Cryptography operation failed: the Dapr endpoint included a failure. See InnerException for details", + // ex); + // } + + // return response.PlaintextKey.ToByteArray(); + //} + + ///// + //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public override async Task SignAsync(string vaultResourceName, byte[] digest, string algorithm, string keyName, CancellationToken cancellationToken = default) + //{ + // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); + // ArgumentVerifier.ThrowIfNullOrEmpty(algorithm, nameof(algorithm)); + // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); + + // var envelope = new Autogenerated.SubtleSignRequest + // { + // ComponentName = vaultResourceName, + // Digest = ByteString.CopyFrom(digest), + // Algorithm = algorithm, + // KeyName = keyName + // }; + + // var options = CreateCallOptions(headers: null, cancellationToken); + // Autogenerated.SubtleSignResponse response; + + // try + // { + // response = await client.SubtleSignAlpha1Async(envelope, options); + // } + // catch (RpcException ex) + // { + // throw new DaprException( + // "Cryptography operation failed: the Dapr endpoint included a failure. See InnerException for details", + // ex); + // } + + // return response.Signature.ToByteArray(); + //} + + ///// + //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public override async Task VerifyAsync(string vaultResourceName, byte[] digest, byte[] signature, + // string algorithm, string keyName, CancellationToken cancellationToken = default) + //{ + // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); + // ArgumentVerifier.ThrowIfNullOrEmpty(algorithm, nameof(algorithm)); + // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); + + // var envelope = new Autogenerated.SubtleVerifyRequest + // { + // ComponentName = vaultResourceName, + // Algorithm = algorithm, + // KeyName = keyName, + // Signature = ByteString.CopyFrom(signature), + // Digest = ByteString.CopyFrom(digest) + // }; + + // var options = CreateCallOptions(headers: null, cancellationToken); + // Autogenerated.SubtleVerifyResponse response; + + // try + // { + // response = await client.SubtleVerifyAlpha1Async(envelope, options); + // } + // catch (RpcException ex) + // { + // throw new DaprException( + // "Cryptography operation failed: the Dapr endpoint included a failure. See InnerException for details", + // ex); + // } + + // return response.Valid; + //} + + #endregion + + + #endregion + + #region Distributed Lock API + + /// + [Obsolete] + public async override Task Lock( + string storeName, + string resourceId, + string lockOwner, + Int32 expiryInSeconds, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); + ArgumentVerifier.ThrowIfNullOrEmpty(resourceId, nameof(resourceId)); + ArgumentVerifier.ThrowIfNullOrEmpty(lockOwner, nameof(lockOwner)); + + if (expiryInSeconds == 0 || expiryInSeconds < 0) + { + throw new ArgumentException("The value cannot be zero or less than zero: " + expiryInSeconds); } - /// - [Obsolete] - public async override Task Unlock( - string storeName, - string resourceId, - string lockOwner, - CancellationToken cancellationToken = default) + var request = new Autogenerated.TryLockRequest() + { + StoreName = storeName, ResourceId = resourceId, LockOwner = lockOwner, ExpiryInSeconds = expiryInSeconds + }; + + try { - ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); - ArgumentVerifier.ThrowIfNullOrEmpty(resourceId, nameof(resourceId)); - ArgumentVerifier.ThrowIfNullOrEmpty(lockOwner, nameof(lockOwner)); + var options = CreateCallOptions(headers: null, cancellationToken); - var request = new Autogenerated.UnlockRequest() + var response = await client.TryLockAlpha1Async(request, options); + return new TryLockResponse() { - StoreName = storeName, - ResourceId = resourceId, - LockOwner = lockOwner + StoreName = storeName, ResourceId = resourceId, LockOwner = lockOwner, Success = response.Success }; + } + catch (RpcException ex) + { + throw new DaprException( + "Lock operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + } + } - var options = CreateCallOptions(headers: null, cancellationToken); - Autogenerated.UnlockResponse response = new Autogenerated.UnlockResponse(); - try - { - response = await client.UnlockAlpha1Async(request, options); - } - catch (RpcException ex) - { - throw new DaprException("Lock operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); - } + /// + [Obsolete] + public async override Task Unlock( + string storeName, + string resourceId, + string lockOwner, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); + ArgumentVerifier.ThrowIfNullOrEmpty(resourceId, nameof(resourceId)); + ArgumentVerifier.ThrowIfNullOrEmpty(lockOwner, nameof(lockOwner)); - return new UnlockResponse(GetUnLockStatus(response.Status)); + var request = new Autogenerated.UnlockRequest() + { + StoreName = storeName, ResourceId = resourceId, LockOwner = lockOwner + }; + + var options = CreateCallOptions(headers: null, cancellationToken); + Autogenerated.UnlockResponse response = new Autogenerated.UnlockResponse(); + try + { + response = await client.UnlockAlpha1Async(request, options); + } + catch (RpcException ex) + { + throw new DaprException( + "Lock operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); } - #endregion + return new UnlockResponse(GetUnLockStatus(response.Status)); + } - #region Dapr Sidecar Methods + #endregion - /// - public override async Task CheckHealthAsync(CancellationToken cancellationToken = default) - { - var path = "/v1.0/healthz"; - var request = new HttpRequestMessage(HttpMethod.Get, new Uri(this.httpEndpoint, path)); + #region Dapr Sidecar Methods - if (this.apiTokenHeader is not null) - { - request.Headers.Add(this.apiTokenHeader.Value.Key, this.apiTokenHeader.Value.Value); - } + /// + public override async Task CheckHealthAsync(CancellationToken cancellationToken = default) + { + var path = "/v1.0/healthz"; + var request = new HttpRequestMessage(HttpMethod.Get, new Uri(this.httpEndpoint, path)); - try - { - using var response = await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); - return response.IsSuccessStatusCode; - } - catch (HttpRequestException) - { - return false; - } + if (this.apiTokenHeader is not null) + { + request.Headers.Add(this.apiTokenHeader.Value.Key, this.apiTokenHeader.Value.Value); } - /// - public override async Task CheckOutboundHealthAsync(CancellationToken cancellationToken = default) + try + { + using var response = + await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + return response.IsSuccessStatusCode; + } + catch (HttpRequestException) { - var path = "/v1.0/healthz/outbound"; - var request = new HttpRequestMessage(HttpMethod.Get, new Uri(this.httpEndpoint, path)); + return false; + } + } - if (this.apiTokenHeader is not null) - { - request.Headers.Add(this.apiTokenHeader.Value.Key, this.apiTokenHeader.Value.Value); - } + /// + public override async Task CheckOutboundHealthAsync(CancellationToken cancellationToken = default) + { + var path = "/v1.0/healthz/outbound"; + var request = new HttpRequestMessage(HttpMethod.Get, new Uri(this.httpEndpoint, path)); - try - { - using var response = await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); - return response.IsSuccessStatusCode; - } - catch (HttpRequestException) - { - return false; - } + if (this.apiTokenHeader is not null) + { + request.Headers.Add(this.apiTokenHeader.Value.Key, this.apiTokenHeader.Value.Value); } - /// - public override async Task WaitForSidecarAsync(CancellationToken cancellationToken = default) + try { - while (true) - { - var response = await CheckOutboundHealthAsync(cancellationToken); - if (response) - { - break; - } - await Task.Delay(TimeSpan.FromMilliseconds(500), cancellationToken); - } + using var response = + await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + return response.IsSuccessStatusCode; } - - /// - public async override Task ShutdownSidecarAsync(CancellationToken cancellationToken = default) + catch (HttpRequestException) { - await client.ShutdownAsync(new Autogenerated.ShutdownRequest(), CreateCallOptions(null, cancellationToken)); + return false; } + } - /// - public override async Task GetMetadataAsync(CancellationToken cancellationToken = default) + /// + public override async Task WaitForSidecarAsync(CancellationToken cancellationToken = default) + { + while (true) { - var options = CreateCallOptions(headers: null, cancellationToken); - try - { - var response = await client.GetMetadataAsync(new Autogenerated.GetMetadataRequest(), options); - return new DaprMetadata(response.Id ?? "", - response.ActorRuntime?.ActiveActors?.Select(c => new DaprActorMetadata(c.Type, c.Count)).ToList() ?? - new List(), - response.ExtendedMetadata?.ToDictionary(c => c.Key, c => c.Value) ?? - new Dictionary(), - response.RegisteredComponents?.Select(c => - new DaprComponentsMetadata(c.Name, c.Type, c.Version, c.Capabilities.ToArray())).ToList() ?? - new List()); - } - catch (RpcException ex) + var response = await CheckOutboundHealthAsync(cancellationToken); + if (response) { - throw new DaprException("Get metadata operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + break; } + + await Task.Delay(TimeSpan.FromMilliseconds(500), cancellationToken); } + } + + /// + public async override Task ShutdownSidecarAsync(CancellationToken cancellationToken = default) + { + await client.ShutdownAsync(new Autogenerated.ShutdownRequest(), CreateCallOptions(null, cancellationToken)); + } - /// - public override async Task SetMetadataAsync(string attributeName, string attributeValue, CancellationToken cancellationToken = default) + /// + public override async Task GetMetadataAsync(CancellationToken cancellationToken = default) + { + var options = CreateCallOptions(headers: null, cancellationToken); + try + { + var response = await client.GetMetadataAsync(new Autogenerated.GetMetadataRequest(), options); + return new DaprMetadata(response.Id ?? "", + response.ActorRuntime?.ActiveActors?.Select(c => new DaprActorMetadata(c.Type, c.Count)).ToList() ?? + new List(), + response.ExtendedMetadata?.ToDictionary(c => c.Key, c => c.Value) ?? + new Dictionary(), + response.RegisteredComponents?.Select(c => + new DaprComponentsMetadata(c.Name, c.Type, c.Version, c.Capabilities.ToArray())).ToList() ?? + new List()); + } + catch (RpcException ex) { - ArgumentVerifier.ThrowIfNullOrEmpty(attributeName, nameof(attributeName)); + throw new DaprException( + "Get metadata operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", + ex); + } + } - var envelope = new Autogenerated.SetMetadataRequest() - { - Key = attributeName, - Value = attributeValue - }; + /// + public override async Task SetMetadataAsync(string attributeName, string attributeValue, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(attributeName, nameof(attributeName)); - var options = CreateCallOptions(headers: null, cancellationToken); + var envelope = new Autogenerated.SetMetadataRequest() { Key = attributeName, Value = attributeValue }; - try - { - _ = await this.Client.SetMetadataAsync(envelope, options); - } - catch (RpcException ex) - { - throw new DaprException("Set metadata operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); - } - } - #endregion + var options = CreateCallOptions(headers: null, cancellationToken); - protected override void Dispose(bool disposing) + try { - if (disposing) - { - this.channel.Dispose(); - this.httpClient.Dispose(); - } + _ = await this.Client.SetMetadataAsync(envelope, options); + } + catch (RpcException ex) + { + throw new DaprException( + "Set metadata operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", + ex); } + } - #region Helper Methods + #endregion - private CallOptions CreateCallOptions(Metadata headers, CancellationToken cancellationToken) + protected override void Dispose(bool disposing) + { + if (disposing) { - var options = new CallOptions(headers: headers ?? new Metadata(), cancellationToken: cancellationToken); + this.channel.Dispose(); + this.httpClient.Dispose(); + } + } - options.Headers.Add("User-Agent", UserAgent().ToString()); + #region Helper Methods - // add token for dapr api token based authentication - if (this.apiTokenHeader is not null) - { - options.Headers.Add(this.apiTokenHeader.Value.Key, this.apiTokenHeader.Value.Value); - } + private CallOptions CreateCallOptions(Metadata headers, CancellationToken cancellationToken) + { + var options = new CallOptions(headers: headers ?? new Metadata(), cancellationToken: cancellationToken); - return options; - } + options.Headers.Add("User-Agent", UserAgent().ToString()); - /// - /// Makes Grpc call using the cancellationToken and handles Errors. - /// All common exception handling logic will reside here. - /// - /// - /// - /// - /// - private async Task MakeGrpcCallHandleError(Func> callFunc, CancellationToken cancellationToken = default) + // add token for dapr api token based authentication + if (this.apiTokenHeader is not null) { - var callOptions = CreateCallOptions(headers: null, cancellationToken); - return await callFunc.Invoke(callOptions); + options.Headers.Add(this.apiTokenHeader.Value.Key, this.apiTokenHeader.Value.Value); } - private Autogenerated.StateOptions ToAutoGeneratedStateOptions(StateOptions stateOptions) - { - var stateRequestOptions = new Autogenerated.StateOptions(); + return options; + } - if (stateOptions.Consistency != null) - { - stateRequestOptions.Consistency = GetStateConsistencyForConsistencyMode(stateOptions.Consistency.Value); - } + /// + /// Makes Grpc call using the cancellationToken and handles Errors. + /// All common exception handling logic will reside here. + /// + /// + /// + /// + /// + private async Task MakeGrpcCallHandleError( + Func> callFunc, CancellationToken cancellationToken = default) + { + var callOptions = CreateCallOptions(headers: null, cancellationToken); + return await callFunc.Invoke(callOptions); + } - if (stateOptions.Concurrency != null) - { - stateRequestOptions.Concurrency = GetStateConcurrencyForConcurrencyMode(stateOptions.Concurrency.Value); - } + private Autogenerated.StateOptions ToAutoGeneratedStateOptions(StateOptions stateOptions) + { + var stateRequestOptions = new Autogenerated.StateOptions(); - return stateRequestOptions; + if (stateOptions.Consistency != null) + { + stateRequestOptions.Consistency = GetStateConsistencyForConsistencyMode(stateOptions.Consistency.Value); } - private static Autogenerated.StateOptions.Types.StateConsistency GetStateConsistencyForConsistencyMode(ConsistencyMode consistencyMode) + if (stateOptions.Concurrency != null) { - return consistencyMode switch - { - ConsistencyMode.Eventual => Autogenerated.StateOptions.Types.StateConsistency.ConsistencyEventual, - ConsistencyMode.Strong => Autogenerated.StateOptions.Types.StateConsistency.ConsistencyStrong, - _ => throw new ArgumentException($"{consistencyMode} Consistency Mode is not supported.") - }; + stateRequestOptions.Concurrency = GetStateConcurrencyForConcurrencyMode(stateOptions.Concurrency.Value); } - private static Autogenerated.StateOptions.Types.StateConcurrency GetStateConcurrencyForConcurrencyMode(ConcurrencyMode concurrencyMode) + return stateRequestOptions; + } + + private static Autogenerated.StateOptions.Types.StateConsistency GetStateConsistencyForConsistencyMode( + ConsistencyMode consistencyMode) + { + return consistencyMode switch { - return concurrencyMode switch - { - ConcurrencyMode.FirstWrite => Autogenerated.StateOptions.Types.StateConcurrency.ConcurrencyFirstWrite, - ConcurrencyMode.LastWrite => Autogenerated.StateOptions.Types.StateConcurrency.ConcurrencyLastWrite, - _ => throw new ArgumentException($"{concurrencyMode} Concurrency Mode is not supported.") - }; - } + ConsistencyMode.Eventual => Autogenerated.StateOptions.Types.StateConsistency.ConsistencyEventual, + ConsistencyMode.Strong => Autogenerated.StateOptions.Types.StateConsistency.ConsistencyStrong, + _ => throw new ArgumentException($"{consistencyMode} Consistency Mode is not supported.") + }; + } - private static LockStatus GetUnLockStatus(Autogenerated.UnlockResponse.Types.Status status) + private static Autogenerated.StateOptions.Types.StateConcurrency GetStateConcurrencyForConcurrencyMode( + ConcurrencyMode concurrencyMode) + { + return concurrencyMode switch { - return status switch - { - Autogenerated.UnlockResponse.Types.Status.Success => LockStatus.Success, - Autogenerated.UnlockResponse.Types.Status.LockDoesNotExist => LockStatus.LockDoesNotExist, - Autogenerated.UnlockResponse.Types.Status.LockBelongsToOthers => LockStatus.LockBelongsToOthers, - Autogenerated.UnlockResponse.Types.Status.InternalError => LockStatus.InternalError, - _ => throw new ArgumentException($"{status} Status is not supported.") - }; - } + ConcurrencyMode.FirstWrite => Autogenerated.StateOptions.Types.StateConcurrency.ConcurrencyFirstWrite, + ConcurrencyMode.LastWrite => Autogenerated.StateOptions.Types.StateConcurrency.ConcurrencyLastWrite, + _ => throw new ArgumentException($"{concurrencyMode} Concurrency Mode is not supported.") + }; + } - #endregion Helper Methods + private static LockStatus GetUnLockStatus(Autogenerated.UnlockResponse.Types.Status status) + { + return status switch + { + Autogenerated.UnlockResponse.Types.Status.Success => LockStatus.Success, + Autogenerated.UnlockResponse.Types.Status.LockDoesNotExist => LockStatus.LockDoesNotExist, + Autogenerated.UnlockResponse.Types.Status.LockBelongsToOthers => LockStatus.LockBelongsToOthers, + Autogenerated.UnlockResponse.Types.Status.InternalError => LockStatus.InternalError, + _ => throw new ArgumentException($"{status} Status is not supported.") + }; } + + #endregion Helper Methods } diff --git a/src/Dapr.Common/AssemblyInfo.cs b/src/Dapr.Common/AssemblyInfo.cs index 5044876a9..3037485a9 100644 --- a/src/Dapr.Common/AssemblyInfo.cs +++ b/src/Dapr.Common/AssemblyInfo.cs @@ -16,9 +16,11 @@ [assembly: InternalsVisibleTo("Dapr.Actors, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.Actors.Generators, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.Actors.AspNetCore, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.AI, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.AspNetCore, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.Client, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.Jobs, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.Messaging, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.Extensions.Configuration, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.Workflow, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] @@ -27,6 +29,7 @@ [assembly: InternalsVisibleTo("Dapr.Actors.AspNetCore.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.Actors.Generators.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.Actors.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.AI.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.AspNetCore.IntegrationTest, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.AspNetCore.IntegrationTest.App, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.AspNetCore.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] @@ -40,3 +43,4 @@ [assembly: InternalsVisibleTo("Dapr.E2E.Test.App.ReentrantActors, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.Extensions.Configuration.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.Jobs.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.Messaging.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] diff --git a/src/Dapr.Common/DaprClientUtilities.cs b/src/Dapr.Common/DaprClientUtilities.cs new file mode 100644 index 000000000..1aa860bbe --- /dev/null +++ b/src/Dapr.Common/DaprClientUtilities.cs @@ -0,0 +1,65 @@ +using System.Net.Http.Headers; +using System.Reflection; +using Grpc.Core; + +namespace Dapr.Common; + +internal static class DaprClientUtilities +{ + /// + /// Provisions the gRPC call options used to provision the various Dapr clients. + /// + /// The Dapr API token, if any. + /// The assembly the user agent is built from. + /// Cancellation token. + /// The gRPC call options. + internal static CallOptions ConfigureGrpcCallOptions(Assembly assembly, string? daprApiToken, CancellationToken cancellationToken = default) + { + var callOptions = new CallOptions(headers: new Metadata(), cancellationToken: cancellationToken); + + //Add the user-agent header to the gRPC call options + var assemblyVersion = assembly + .GetCustomAttributes() + .FirstOrDefault()? + .InformationalVersion; + var userAgent = new ProductInfoHeaderValue("dapr-sdk-dotnet", $"v{assemblyVersion}").ToString(); + callOptions.Headers!.Add("User-Agent", userAgent); + + //Add the API token to the headers as well if it's populated + if (daprApiToken is not null) + { + var apiTokenHeader = GetDaprApiTokenHeader(daprApiToken); + if (apiTokenHeader is not null) + { + callOptions.Headers.Add(apiTokenHeader.Value.Key, apiTokenHeader.Value.Value); + } + } + + return callOptions; + } + + /// + /// Used to create the user-agent from the assembly attributes. + /// + /// The assembly the client is being built for. + /// The header value containing the user agent information. + public static ProductInfoHeaderValue GetUserAgent(Assembly assembly) + { + var assemblyVersion = assembly + .GetCustomAttributes() + .FirstOrDefault()? + .InformationalVersion; + return new ProductInfoHeaderValue("dapr-sdk-dotnet", $"v{assemblyVersion}"); + } + + /// + /// Used to provision the header used for the Dapr API token on the HTTP or gRPC connection. + /// + /// The value of the Dapr API token. + /// If a Dapr API token exists, the key/value pair to use for the header; otherwise null. + public static KeyValuePair? GetDaprApiTokenHeader(string? daprApiToken) => + string.IsNullOrWhiteSpace(daprApiToken) + ? null + : new KeyValuePair("dapr-api-token", daprApiToken); +} + diff --git a/src/Dapr.Common/DaprGenericClientBuilder.cs b/src/Dapr.Common/DaprGenericClientBuilder.cs index 254953241..7a7abf025 100644 --- a/src/Dapr.Common/DaprGenericClientBuilder.cs +++ b/src/Dapr.Common/DaprGenericClientBuilder.cs @@ -1,4 +1,18 @@ -using System.Text.Json; +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System.Reflection; +using System.Text.Json; using Grpc.Net.Client; using Microsoft.Extensions.Configuration; @@ -170,8 +184,9 @@ public DaprGenericClientBuilder UseTimeout(TimeSpan timeout) /// Builds out the inner DaprClient that provides the core shape of the /// runtime gRPC client used by the consuming package. /// + /// The assembly the dependencies are being built for. /// - protected (GrpcChannel channel, HttpClient httpClient, Uri httpEndpoint) BuildDaprClientDependencies() + protected (GrpcChannel channel, HttpClient httpClient, Uri httpEndpoint, string daprApiToken) BuildDaprClientDependencies(Assembly assembly) { var grpcEndpoint = new Uri(this.GrpcEndpoint); if (grpcEndpoint.Scheme != "http" && grpcEndpoint.Scheme != "https") @@ -185,21 +200,47 @@ public DaprGenericClientBuilder UseTimeout(TimeSpan timeout) AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); } - var httpEndpoint = new Uri(this.HttpEndpoint); + var httpEndpoint = new Uri(this.HttpEndpoint); if (httpEndpoint.Scheme != "http" && httpEndpoint.Scheme != "https") { throw new InvalidOperationException("The HTTP endpoint must use http or https."); } - var channel = GrpcChannel.ForAddress(this.GrpcEndpoint, this.GrpcChannelOptions); + //Configure the HTTP client + var httpClient = ConfigureHttpClient(assembly); + this.GrpcChannelOptions.HttpClient = httpClient; + + var channel = GrpcChannel.ForAddress(this.GrpcEndpoint, this.GrpcChannelOptions); + return (channel, httpClient, httpEndpoint, this.DaprApiToken); + } + /// + /// Configures the HTTP client. + /// + /// The assembly the user agent is built from. + /// The HTTP client to interact with the Dapr runtime with. + private HttpClient ConfigureHttpClient(Assembly assembly) + { var httpClient = HttpClientFactory is not null ? HttpClientFactory() : new HttpClient(); + + //Set the timeout as necessary if (this.Timeout > TimeSpan.Zero) { httpClient.Timeout = this.Timeout; } + + //Set the user agent + var userAgent = DaprClientUtilities.GetUserAgent(assembly); + httpClient.DefaultRequestHeaders.Add("User-Agent", userAgent.ToString()); + + //Set the API token + var apiTokenHeader = DaprClientUtilities.GetDaprApiTokenHeader(this.DaprApiToken); + if (apiTokenHeader is not null) + { + httpClient.DefaultRequestHeaders.Add(apiTokenHeader.Value.Key, apiTokenHeader.Value.Value); + } - return (channel, httpClient, httpEndpoint); + return httpClient; } /// diff --git a/src/Dapr.Common/Extensions/EnumExtensions.cs b/src/Dapr.Common/Extensions/EnumExtensions.cs index ff9b43706..0216c9258 100644 --- a/src/Dapr.Common/Extensions/EnumExtensions.cs +++ b/src/Dapr.Common/Extensions/EnumExtensions.cs @@ -1,4 +1,17 @@ -using System.Reflection; +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System.Reflection; using System.Runtime.Serialization; namespace Dapr.Common.Extensions; diff --git a/src/Dapr.Common/JsonConverters/GenericEnumJsonConverter.cs b/src/Dapr.Common/JsonConverters/GenericEnumJsonConverter.cs new file mode 100644 index 000000000..be79c101a --- /dev/null +++ b/src/Dapr.Common/JsonConverters/GenericEnumJsonConverter.cs @@ -0,0 +1,70 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System.Text.Json; +using System.Text.Json.Serialization; +using Dapr.Common.Extensions; + +namespace Dapr.Common.JsonConverters; + +/// +/// A JsonConverter used to convert from an enum to a string and vice versa, but using the Enum extension written to pull +/// the value from the [EnumMember] attribute, if present. +/// +/// The enum type to convert. +internal sealed class GenericEnumJsonConverter : JsonConverter where T : struct, Enum +{ + private static readonly Dictionary enumMemberCache = new(); + + static GenericEnumJsonConverter() + { + foreach (var enumValue in Enum.GetValues()) + { + var enumMemberValue = enumValue.GetValueFromEnumMember(); + enumMemberCache[enumMemberValue] = enumValue; + } + } + + /// Reads and converts the JSON to type . + /// The reader. + /// The type to convert. + /// An object that specifies serialization options to use. + /// The converted value. + public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + //Get the string value from the JSON reader + var value = reader.GetString(); + + //Try pulling the value from the cache + if (value is not null && enumMemberCache.TryGetValue(value, out var enumValue)) + { + return enumValue; + } + + //If no match found, throw an exception + throw new JsonException($"Invalid valid for {typeToConvert.Name}: {value}"); + } + + /// Writes a specified value as JSON. + /// The writer to write to. + /// The value to convert to JSON. + /// An object that specifies serialization options to use. + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + //Get the value from the EnumMember attribute, if any + var enumMemberValue = value.GetValueFromEnumMember(); + + //Write the value to the JSON writer + writer.WriteStringValue(enumMemberValue); + } +} diff --git a/src/Dapr.Jobs/DaprJobsClientBuilder.cs b/src/Dapr.Jobs/DaprJobsClientBuilder.cs index 390d52236..509486a1e 100644 --- a/src/Dapr.Jobs/DaprJobsClientBuilder.cs +++ b/src/Dapr.Jobs/DaprJobsClientBuilder.cs @@ -12,6 +12,7 @@ // ------------------------------------------------------------------------ using Dapr.Common; +using Microsoft.Extensions.Configuration; using Autogenerated = Dapr.Client.Autogen.Grpc.v1; namespace Dapr.Jobs; @@ -21,17 +22,22 @@ namespace Dapr.Jobs; /// public sealed class DaprJobsClientBuilder : DaprGenericClientBuilder { + /// + /// Used to initialize a new instance of . + /// + /// An optional instance of . + public DaprJobsClientBuilder(IConfiguration? configuration = null) : base(configuration) + { + } + /// /// Builds the client instance from the properties of the builder. /// /// The Dapr client instance. public override DaprJobsClient Build() { - var daprClientDependencies = this.BuildDaprClientDependencies(); - + var daprClientDependencies = this.BuildDaprClientDependencies(typeof(DaprJobsClient).Assembly); var client = new Autogenerated.Dapr.DaprClient(daprClientDependencies.channel); - var apiTokenHeader = this.DaprApiToken is not null ? DaprJobsClient.GetDaprApiTokenHeader(this.DaprApiToken) : null; - - return new DaprJobsGrpcClient(client, daprClientDependencies.httpClient, apiTokenHeader); + return new DaprJobsGrpcClient(client, daprClientDependencies.httpClient, daprClientDependencies.daprApiToken); } } diff --git a/src/Dapr.Jobs/DaprJobsGrpcClient.cs b/src/Dapr.Jobs/DaprJobsGrpcClient.cs index f23ef67fd..b548290df 100644 --- a/src/Dapr.Jobs/DaprJobsGrpcClient.cs +++ b/src/Dapr.Jobs/DaprJobsGrpcClient.cs @@ -11,8 +11,7 @@ // limitations under the License. // ------------------------------------------------------------------------ -using System.Net.Http.Headers; -using System.Reflection; +using Dapr.Common; using Dapr.Jobs.Models; using Dapr.Jobs.Models.Responses; using Google.Protobuf; @@ -28,29 +27,35 @@ namespace Dapr.Jobs; internal sealed class DaprJobsGrpcClient : DaprJobsClient { /// - /// Present only for testing purposes. + /// The HTTP client used by the client for calling the Dapr runtime. /// - internal readonly HttpClient httpClient; - + /// + /// Property exposed for testing purposes. + /// + internal readonly HttpClient HttpClient; /// - /// Used to populate options headers with API token value. + /// The Dapr API token value. /// - internal readonly KeyValuePair? apiTokenHeader; - - private readonly Autogenerated.Dapr.DaprClient client; - private readonly string userAgent = UserAgent().ToString(); - - // property exposed for testing purposes - internal Autogenerated.Dapr.DaprClient Client => client; - + /// + /// Property exposed for testing purposes. + /// + internal readonly string? DaprApiToken; + /// + /// The autogenerated Dapr client. + /// + /// + /// Property exposed for testing purposes. + /// + internal Autogenerated.Dapr.DaprClient Client { get; } + internal DaprJobsGrpcClient( Autogenerated.Dapr.DaprClient innerClient, HttpClient httpClient, - KeyValuePair? apiTokenHeader) + string? daprApiToken) { - this.client = innerClient; - this.httpClient = httpClient; - this.apiTokenHeader = apiTokenHeader; + this.Client = innerClient; + this.HttpClient = httpClient; + this.DaprApiToken = daprApiToken; } /// @@ -107,11 +112,11 @@ public override async Task ScheduleJobAsync(string jobName, DaprJobSchedule sche var envelope = new Autogenerated.ScheduleJobRequest { Job = job }; - var callOptions = CreateCallOptions(headers: null, cancellationToken); + var grpcCallOptions = DaprClientUtilities.ConfigureGrpcCallOptions(typeof(DaprJobsClient).Assembly, this.DaprApiToken, cancellationToken); try { - await client.ScheduleJobAlpha1Async(envelope, callOptions); + await Client.ScheduleJobAlpha1Async(envelope, grpcCallOptions).ConfigureAwait(false); } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { @@ -146,8 +151,8 @@ public override async Task GetJobAsync(string jobName, Cancellat try { var envelope = new Autogenerated.GetJobRequest { Name = jobName }; - var callOptions = CreateCallOptions(headers: null, cancellationToken); - var response = await client.GetJobAlpha1Async(envelope, callOptions); + var grpcCallOptions = DaprClientUtilities.ConfigureGrpcCallOptions(typeof(DaprJobsClient).Assembly, this.DaprApiToken, cancellationToken); + var response = await Client.GetJobAlpha1Async(envelope, grpcCallOptions); return new DaprJobDetails(new DaprJobSchedule(response.Job.Schedule)) { DueTime = response.Job.DueTime is not null ? DateTime.Parse(response.Job.DueTime) : null, @@ -190,8 +195,8 @@ public override async Task DeleteJobAsync(string jobName, CancellationToken canc try { var envelope = new Autogenerated.DeleteJobRequest { Name = jobName }; - var callOptions = CreateCallOptions(headers: null, cancellationToken); - await client.DeleteJobAlpha1Async(envelope, callOptions); + var grpcCallOptions = DaprClientUtilities.ConfigureGrpcCallOptions(typeof(DaprJobsClient).Assembly, this.DaprApiToken, cancellationToken); + await Client.DeleteJobAlpha1Async(envelope, grpcCallOptions); } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { @@ -213,36 +218,7 @@ protected override void Dispose(bool disposing) { if (disposing) { - this.httpClient.Dispose(); - } - } - - private CallOptions CreateCallOptions(Metadata? headers, CancellationToken cancellationToken) - { - var callOptions = new CallOptions(headers: headers ?? new Metadata(), cancellationToken: cancellationToken); - - callOptions.Headers!.Add("User-Agent", this.userAgent); - - if (apiTokenHeader is not null) - { - callOptions.Headers.Add(apiTokenHeader.Value.Key, apiTokenHeader.Value.Value); + this.HttpClient.Dispose(); } - - return callOptions; - } - - /// - /// Returns the value for the User-Agent. - /// - /// A containing the value to use for the User-Agent. - private static ProductInfoHeaderValue UserAgent() - { - var assembly = typeof(DaprJobsClient).Assembly; - var assemblyVersion = assembly - .GetCustomAttributes() - .FirstOrDefault()? - .InformationalVersion; - - return new ProductInfoHeaderValue("dapr-sdk-dotnet", $"v{assemblyVersion}"); } } diff --git a/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs b/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs index 93265837b..03540aae1 100644 --- a/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs +++ b/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs @@ -11,6 +11,7 @@ // limitations under the License. // ------------------------------------------------------------------------ +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -25,27 +26,30 @@ public static class DaprJobsServiceCollectionExtensions /// Adds Dapr Jobs client support to the service collection. /// /// The . - /// Optionally allows greater configuration of the . + /// Optionally allows greater configuration of the using injected services. /// The lifetime of the registered services. - public static IServiceCollection AddDaprJobsClient(this IServiceCollection serviceCollection, Action? configure = null, ServiceLifetime lifetime = ServiceLifetime.Singleton) + /// + public static IServiceCollection AddDaprJobsClient(this IServiceCollection serviceCollection, Action? configure = null, ServiceLifetime lifetime = ServiceLifetime.Singleton) + { ArgumentNullException.ThrowIfNull(serviceCollection, nameof(serviceCollection)); //Register the IHttpClientFactory implementation serviceCollection.AddHttpClient(); - + var registration = new Func(serviceProvider => { var httpClientFactory = serviceProvider.GetRequiredService(); + var configuration = serviceProvider.GetService(); - var builder = new DaprJobsClientBuilder(); + var builder = new DaprJobsClientBuilder(configuration); builder.UseHttpClientFactory(httpClientFactory); - configure?.Invoke(builder); + configure?.Invoke(serviceProvider, builder); return builder.Build(); }); - + switch (lifetime) { case ServiceLifetime.Scoped: @@ -62,33 +66,4 @@ public static IServiceCollection AddDaprJobsClient(this IServiceCollection servi return serviceCollection; } - - /// - /// Adds Dapr Jobs client support to the service collection. - /// - /// The . - /// Optionally allows greater configuration of the using injected services. - /// The lifetime of the registered services. - /// - public static IServiceCollection AddDaprJobsClient(this IServiceCollection serviceCollection, Action? configure, ServiceLifetime lifetime = ServiceLifetime.Singleton) - { - ArgumentNullException.ThrowIfNull(serviceCollection, nameof(serviceCollection)); - - //Register the IHttpClientFactory implementation - serviceCollection.AddHttpClient(); - - serviceCollection.TryAddSingleton(serviceProvider => - { - var httpClientFactory = serviceProvider.GetRequiredService(); - - var builder = new DaprJobsClientBuilder(); - builder.UseHttpClientFactory(httpClientFactory); - - configure?.Invoke(serviceProvider, builder); - - return builder.Build(); - }); - - return serviceCollection; - } } diff --git a/src/Dapr.Messaging/AssemblyInfo.cs b/src/Dapr.Messaging/AssemblyInfo.cs new file mode 100644 index 000000000..4e2e7a0a7 --- /dev/null +++ b/src/Dapr.Messaging/AssemblyInfo.cs @@ -0,0 +1,18 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Dapr.Messaging.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] + + diff --git a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClientBuilder.cs b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClientBuilder.cs index b94bc5cdf..829bab75d 100644 --- a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClientBuilder.cs +++ b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClientBuilder.cs @@ -39,9 +39,8 @@ public DaprPublishSubscribeClientBuilder(IConfiguration? configuration = null) : /// public override DaprPublishSubscribeClient Build() { - var daprClientDependencies = BuildDaprClientDependencies(); + var daprClientDependencies = BuildDaprClientDependencies(typeof(DaprPublishSubscribeClient).Assembly); var client = new Autogenerated.Dapr.DaprClient(daprClientDependencies.channel); - - return new DaprPublishSubscribeGrpcClient(client); + return new DaprPublishSubscribeGrpcClient(client, daprClientDependencies.httpClient, daprClientDependencies.daprApiToken); } } diff --git a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs index df6ccdcfe..33ef05494 100644 --- a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs +++ b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs @@ -20,14 +20,36 @@ namespace Dapr.Messaging.PublishSubscribe; /// internal sealed class DaprPublishSubscribeGrpcClient : DaprPublishSubscribeClient { - private readonly P.DaprClient daprClient; + /// + /// The HTTP client used by the client for calling the Dapr runtime. + /// + /// + /// Property exposed for testing purposes. + /// + internal readonly HttpClient HttpClient; + /// + /// The Dapr API token value. + /// + /// + /// Property exposed for testing purposes. + /// + internal readonly string? DaprApiToken; + /// + /// The autogenerated Dapr client. + /// + /// + /// Property exposed for testing purposes. + /// + private readonly P.DaprClient Client; /// /// Creates a new instance of a /// - public DaprPublishSubscribeGrpcClient(P.DaprClient client) + public DaprPublishSubscribeGrpcClient(P.DaprClient client, HttpClient httpClient, string? daprApiToken) { - daprClient = client; + this.Client = client; + this.HttpClient = httpClient; + this.DaprApiToken = daprApiToken; } /// @@ -41,7 +63,7 @@ public DaprPublishSubscribeGrpcClient(P.DaprClient client) /// public override async Task SubscribeAsync(string pubSubName, string topicName, DaprSubscriptionOptions options, TopicMessageHandler messageHandler, CancellationToken cancellationToken = default) { - var receiver = new PublishSubscribeReceiver(pubSubName, topicName, options, messageHandler, daprClient); + var receiver = new PublishSubscribeReceiver(pubSubName, topicName, options, messageHandler, this.Client); await receiver.SubscribeAsync(cancellationToken); return receiver; } diff --git a/src/Dapr.Messaging/PublishSubscribe/Extensions/PublishSubscribeServiceCollectionExtensions.cs b/src/Dapr.Messaging/PublishSubscribe/Extensions/PublishSubscribeServiceCollectionExtensions.cs index fe9b7c417..3d9e3ee8d 100644 --- a/src/Dapr.Messaging/PublishSubscribe/Extensions/PublishSubscribeServiceCollectionExtensions.cs +++ b/src/Dapr.Messaging/PublishSubscribe/Extensions/PublishSubscribeServiceCollectionExtensions.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; namespace Dapr.Messaging.PublishSubscribe.Extensions; @@ -25,8 +26,9 @@ public static IServiceCollection AddDaprPubSubClient(this IServiceCollection ser var registration = new Func(serviceProvider => { var httpClientFactory = serviceProvider.GetRequiredService(); + var configuration = serviceProvider.GetService(); - var builder = new DaprPublishSubscribeClientBuilder(); + var builder = new DaprPublishSubscribeClientBuilder(configuration); builder.UseHttpClientFactory(httpClientFactory); configure?.Invoke(serviceProvider, builder); diff --git a/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs b/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs index 886d57006..4b0d608ff 100644 --- a/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs +++ b/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs @@ -77,6 +77,11 @@ internal sealed class PublishSubscribeReceiver : IAsyncDisposable /// private bool isDisposed; + // Internal property for testing purposes + internal Task TopicMessagesChannelCompletion => topicMessagesChannel.Reader.Completion; + // Internal property for testing purposes + internal Task AcknowledgementsChannelCompletion => acknowledgementsChannel.Reader.Completion; + /// /// Constructs a new instance of a instance. /// @@ -115,20 +120,40 @@ internal async Task SubscribeAsync(CancellationToken cancellationToken = default var stream = await GetStreamAsync(cancellationToken); - //Retrieve the messages from the sidecar and write to the messages channel - var fetchMessagesTask = FetchDataFromSidecarAsync(stream, topicMessagesChannel.Writer, cancellationToken); + //Retrieve the messages from the sidecar and write to the messages channel - start without awaiting so this isn't blocking + _ = FetchDataFromSidecarAsync(stream, topicMessagesChannel.Writer, cancellationToken) + .ContinueWith(HandleTaskCompletion, null, cancellationToken, TaskContinuationOptions.OnlyOnFaulted, + TaskScheduler.Default); //Process the messages as they're written to either channel - var acknowledgementProcessorTask = ProcessAcknowledgementChannelMessagesAsync(stream, cancellationToken); - var topicMessageProcessorTask = ProcessTopicChannelMessagesAsync(cancellationToken); + _ = ProcessAcknowledgementChannelMessagesAsync(stream, cancellationToken).ContinueWith(HandleTaskCompletion, + null, cancellationToken, TaskContinuationOptions.OnlyOnFaulted, TaskScheduler.Default); + _ = ProcessTopicChannelMessagesAsync(cancellationToken).ContinueWith(HandleTaskCompletion, null, + cancellationToken, + TaskContinuationOptions.OnlyOnFaulted, TaskScheduler.Default); + } - try - { - await Task.WhenAll(fetchMessagesTask, acknowledgementProcessorTask, topicMessageProcessorTask); - } - catch (OperationCanceledException) + /// + /// Exposed for testing purposes only. + /// + /// The test message to write. + internal async Task WriteMessageToChannelAsync(TopicMessage message) + { + await topicMessagesChannel.Writer.WriteAsync(message); + } + + //Exposed for testing purposes only + internal async Task WriteAcknowledgementToChannelAsync(TopicAcknowledgement acknowledgement) + { + await acknowledgementsChannel.Writer.WriteAsync(acknowledgement); + } + + //Exposed for testing purposes only + internal static void HandleTaskCompletion(Task task, object? state) + { + if (task.Exception != null) { - // Will be cleaned up during DisposeAsync + throw task.Exception; } } @@ -251,13 +276,21 @@ await stream.RequestStream.WriteAsync( //Each time a message is received from the stream, push it into the topic messages channel await foreach (var response in stream.ResponseStream.ReadAllAsync(cancellationToken)) { + //https://github.com/dapr/dotnet-sdk/issues/1412 reports that this is sometimes null + //Skip the initial response - we only want to pass along TopicMessage payloads to developers + if (response?.EventMessage is null) + { + continue; + } + var message = new TopicMessage(response.EventMessage.Id, response.EventMessage.Source, response.EventMessage.Type, response.EventMessage.SpecVersion, response.EventMessage.DataContentType, response.EventMessage.Topic, response.EventMessage.PubsubName) { Path = response.EventMessage.Path, - Extensions = response.EventMessage.Extensions.Fields.ToDictionary(f => f.Key, kvp => kvp.Value) + Extensions = response.EventMessage.Extensions.Fields.ToDictionary(f => f.Key, kvp => kvp.Value), + Data = response.EventMessage.Data.ToByteArray() }; try @@ -308,6 +341,6 @@ public async ValueTask DisposeAsync() /// /// The identifier of the message. /// The action to take on the message in the acknowledgement request. - private sealed record TopicAcknowledgement(string MessageId, TopicEventResponse.Types.TopicEventResponseStatus Action); + internal sealed record TopicAcknowledgement(string MessageId, TopicEventResponse.Types.TopicEventResponseStatus Action); } diff --git a/src/Dapr.Protos/Protos/dapr/proto/common/v1/common.proto b/src/Dapr.Protos/Protos/dapr/proto/common/v1/common.proto index 0eb882b89..fc5e99835 100644 --- a/src/Dapr.Protos/Protos/dapr/proto/common/v1/common.proto +++ b/src/Dapr.Protos/Protos/dapr/proto/common/v1/common.proto @@ -157,4 +157,4 @@ message ConfigurationItem { // the metadata which will be passed to/from configuration store component. map metadata = 3; -} \ No newline at end of file +} diff --git a/src/Dapr.Protos/Protos/dapr/proto/runtime/v1/appcallback.proto b/src/Dapr.Protos/Protos/dapr/proto/runtime/v1/appcallback.proto index 51dee5539..144e8c87a 100644 --- a/src/Dapr.Protos/Protos/dapr/proto/runtime/v1/appcallback.proto +++ b/src/Dapr.Protos/Protos/dapr/proto/runtime/v1/appcallback.proto @@ -340,4 +340,4 @@ message ListInputBindingsResponse { // HealthCheckResponse is the message with the response to the health check. // This message is currently empty as used as placeholder. -message HealthCheckResponse {} \ No newline at end of file +message HealthCheckResponse {} diff --git a/src/Dapr.Protos/Protos/dapr/proto/runtime/v1/dapr.proto b/src/Dapr.Protos/Protos/dapr/proto/runtime/v1/dapr.proto index ecf0f76f7..d1d8658f3 100644 --- a/src/Dapr.Protos/Protos/dapr/proto/runtime/v1/dapr.proto +++ b/src/Dapr.Protos/Protos/dapr/proto/runtime/v1/dapr.proto @@ -151,25 +151,39 @@ service Dapr { rpc SubtleVerifyAlpha1(SubtleVerifyRequest) returns (SubtleVerifyResponse); // Starts a new instance of a workflow - rpc StartWorkflowAlpha1 (StartWorkflowRequest) returns (StartWorkflowResponse) {} + rpc StartWorkflowAlpha1 (StartWorkflowRequest) returns (StartWorkflowResponse) { + option deprecated = true; + } // Gets details about a started workflow instance - rpc GetWorkflowAlpha1 (GetWorkflowRequest) returns (GetWorkflowResponse) {} + rpc GetWorkflowAlpha1 (GetWorkflowRequest) returns (GetWorkflowResponse) { + option deprecated = true; + } // Purge Workflow - rpc PurgeWorkflowAlpha1 (PurgeWorkflowRequest) returns (google.protobuf.Empty) {} + rpc PurgeWorkflowAlpha1 (PurgeWorkflowRequest) returns (google.protobuf.Empty) { + option deprecated = true; + } // Terminates a running workflow instance - rpc TerminateWorkflowAlpha1 (TerminateWorkflowRequest) returns (google.protobuf.Empty) {} + rpc TerminateWorkflowAlpha1 (TerminateWorkflowRequest) returns (google.protobuf.Empty) { + option deprecated = true; + } // Pauses a running workflow instance - rpc PauseWorkflowAlpha1 (PauseWorkflowRequest) returns (google.protobuf.Empty) {} + rpc PauseWorkflowAlpha1 (PauseWorkflowRequest) returns (google.protobuf.Empty) { + option deprecated = true; + } // Resumes a paused workflow instance - rpc ResumeWorkflowAlpha1 (ResumeWorkflowRequest) returns (google.protobuf.Empty) {} + rpc ResumeWorkflowAlpha1 (ResumeWorkflowRequest) returns (google.protobuf.Empty) { + option deprecated = true; + } // Raise an event to a running workflow instance - rpc RaiseEventWorkflowAlpha1 (RaiseEventWorkflowRequest) returns (google.protobuf.Empty) {} + rpc RaiseEventWorkflowAlpha1 (RaiseEventWorkflowRequest) returns (google.protobuf.Empty) { + option deprecated = true; + } // Starts a new instance of a workflow rpc StartWorkflowBeta1 (StartWorkflowRequest) returns (StartWorkflowResponse) {} @@ -191,6 +205,7 @@ service Dapr { // Raise an event to a running workflow instance rpc RaiseEventWorkflowBeta1 (RaiseEventWorkflowRequest) returns (google.protobuf.Empty) {} + // Shutdown the sidecar rpc Shutdown (ShutdownRequest) returns (google.protobuf.Empty) {} @@ -202,6 +217,9 @@ service Dapr { // Delete a job rpc DeleteJobAlpha1(DeleteJobRequest) returns (DeleteJobResponse) {} + + // Converse with a LLM service + rpc ConverseAlpha1(ConversationRequest) returns (ConversationResponse) {} } // InvokeServiceRequest represents the request message for Service invocation. @@ -1206,7 +1224,8 @@ message Job { // // Systemd timer style cron accepts 6 fields: // seconds | minutes | hours | day of month | month | day of week - // 0-59 | 0-59 | 0-23 | 1-31 | 1-12/jan-dec | 0-7/sun-sat + // 0-59 | 0-59 | 0-23 | 1-31 | 1-12/jan-dec | 0-6/sun-sat + // // "0 30 * * * *" - every hour on the half hour // "0 15 3 * * *" - every day at 03:15 @@ -1274,4 +1293,56 @@ message DeleteJobRequest { // DeleteJobResponse is the message response to delete the job by name. message DeleteJobResponse { // Empty -} \ No newline at end of file +} + +// ConversationRequest is the request object for Conversation. +message ConversationRequest { + // The name of Conversation component + string name = 1; + + // The ID of an existing chat (like in ChatGPT) + optional string contextID = 2; + + // Inputs for the conversation, support multiple input in one time. + repeated ConversationInput inputs = 3; + + // Parameters for all custom fields. + map parameters = 4; + + // The metadata passing to conversation components. + map metadata = 5; + + // Scrub PII data that comes back from the LLM + optional bool scrubPII = 6; + + // Temperature for the LLM to optimize for creativity or predictability + optional double temperature = 7; +} + +message ConversationInput { + // The message to send to the llm + string message = 1; + + // The role to set for the message + optional string role = 2; + + // Scrub PII data that goes into the LLM + optional bool scrubPII = 3; +} + +// ConversationResult is the result for one input. +message ConversationResult { + // Result for the one conversation input. + string result = 1; + // Parameters for all custom fields. + map parameters = 2; +} + +// ConversationResponse is the response for Conversation. +message ConversationResponse { + // The ID of an existing chat (like in ChatGPT) + optional string contextID = 1; + + // An array of results. + repeated ConversationResult outputs = 2; +} diff --git a/src/Dapr.Workflow/DaprWorkflowContext.cs b/src/Dapr.Workflow/DaprWorkflowContext.cs index be08ef421..55c965955 100644 --- a/src/Dapr.Workflow/DaprWorkflowContext.cs +++ b/src/Dapr.Workflow/DaprWorkflowContext.cs @@ -11,6 +11,8 @@ // limitations under the License. // ------------------------------------------------------------------------ +using Microsoft.Extensions.Logging; + namespace Dapr.Workflow { using System; @@ -34,7 +36,7 @@ internal DaprWorkflowContext(TaskOrchestrationContext innerContext) public override DateTime CurrentUtcDateTime => this.innerContext.CurrentUtcDateTime; public override bool IsReplaying => this.innerContext.IsReplaying; - + public override Task CallActivityAsync(string name, object? input = null, WorkflowTaskOptions? options = null) { return WrapExceptions(this.innerContext.CallActivityAsync(name, input, options?.ToDurableTaskOptions())); @@ -95,6 +97,25 @@ public override Guid NewGuid() return this.innerContext.NewGuid(); } + /// + /// Returns an instance of that is replay-safe, meaning that the logger only + /// writes logs when the orchestrator is not replaying previous history. + /// + /// The logger's category name. + /// An instance of that is replay-safe. + public override ILogger CreateReplaySafeLogger(string categoryName) => + this.innerContext.CreateReplaySafeLogger(categoryName); + + /// + /// The type to derive the category name from. + public override ILogger CreateReplaySafeLogger(Type type) => + this.innerContext.CreateReplaySafeLogger(type); + + /// + /// The type to derive category name from. + public override ILogger CreateReplaySafeLogger() => + this.innerContext.CreateReplaySafeLogger(); + static async Task WrapExceptions(Task task) { try diff --git a/src/Dapr.Workflow/IWorkflowContext.cs b/src/Dapr.Workflow/IWorkflowContext.cs new file mode 100644 index 000000000..7bbbb4f94 --- /dev/null +++ b/src/Dapr.Workflow/IWorkflowContext.cs @@ -0,0 +1,21 @@ +namespace Dapr.Workflow; + +/// +/// Provides functionality available to orchestration code. +/// +public interface IWorkflowContext +{ + /// + /// Gets a value indicating whether the orchestration or operation is currently replaying itself. + /// + /// + /// This property is useful when there is logic that needs to run only when *not* replaying. For example, + /// certain types of application logging may become too noisy when duplicated as part of replay. The + /// application code could check to see whether the function is being replayed and then issue + /// the log statements when this value is false. + /// + /// + /// true if the orchestration or operation is currently being replayed; otherwise false. + /// + bool IsReplaying { get; } +} diff --git a/src/Dapr.Workflow/WorkflowContext.cs b/src/Dapr.Workflow/WorkflowContext.cs index 98b8be96b..afc544ed5 100644 --- a/src/Dapr.Workflow/WorkflowContext.cs +++ b/src/Dapr.Workflow/WorkflowContext.cs @@ -11,6 +11,8 @@ // limitations under the License. // ------------------------------------------------------------------------ +using Microsoft.Extensions.Logging; + namespace Dapr.Workflow { using System; @@ -21,13 +23,13 @@ namespace Dapr.Workflow /// Context object used by workflow implementations to perform actions such as scheduling activities, durable timers, waiting for /// external events, and for getting basic information about the current workflow instance. /// - public abstract class WorkflowContext + public abstract class WorkflowContext : IWorkflowContext { /// /// Gets the name of the current workflow. /// public abstract string Name { get; } - + /// /// Gets the instance ID of the current workflow. /// @@ -271,6 +273,22 @@ public virtual Task CallChildWorkflowAsync( { return this.CallChildWorkflowAsync(workflowName, input, options); } + + /// + /// Returns an instance of that is replay-safe, meaning that the logger only + /// writes logs when the orchestrator is not replaying previous history. + /// + /// The logger's category name. + /// An instance of that is replay-safe. + public abstract ILogger CreateReplaySafeLogger(string categoryName); + + /// + /// The type to derive the category name from. + public abstract ILogger CreateReplaySafeLogger(Type type); + + /// + /// The type to derive category name from. + public abstract ILogger CreateReplaySafeLogger(); /// /// Restarts the workflow with a new input and clears its history. diff --git a/src/Dapr.Workflow/WorkflowLoggingService.cs b/src/Dapr.Workflow/WorkflowLoggingService.cs index 331156f3e..115db817f 100644 --- a/src/Dapr.Workflow/WorkflowLoggingService.cs +++ b/src/Dapr.Workflow/WorkflowLoggingService.cs @@ -29,10 +29,9 @@ internal sealed class WorkflowLoggingService : IHostedService private static readonly HashSet registeredWorkflows = new(); private static readonly HashSet registeredActivities = new(); - public WorkflowLoggingService(ILogger logger, IConfiguration configuration) + public WorkflowLoggingService(ILogger logger) { this.logger = logger; - } public Task StartAsync(CancellationToken cancellationToken) { diff --git a/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs b/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs index 209e4edc0..f45d21efa 100644 --- a/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs +++ b/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs @@ -35,50 +35,38 @@ public static IServiceCollection AddDaprWorkflow( Action configure, ServiceLifetime lifetime = ServiceLifetime.Singleton) { - if (serviceCollection == null) - { - throw new ArgumentNullException(nameof(serviceCollection)); - } + ArgumentNullException.ThrowIfNull(serviceCollection, nameof(serviceCollection)); serviceCollection.AddDaprClient(lifetime: lifetime); serviceCollection.AddHttpClient(); serviceCollection.AddHostedService(); - + switch (lifetime) { case ServiceLifetime.Singleton: -#pragma warning disable CS0618 // Type or member is obsolete - keeping around temporarily - replaced by DaprWorkflowClient - serviceCollection.TryAddSingleton(); -#pragma warning restore CS0618 // Type or member is obsolete serviceCollection.TryAddSingleton(); serviceCollection.TryAddSingleton(); break; case ServiceLifetime.Scoped: -#pragma warning disable CS0618 // Type or member is obsolete - keeping around temporarily - replaced by DaprWorkflowClient - serviceCollection.TryAddScoped(); -#pragma warning restore CS0618 // Type or member is obsolete serviceCollection.TryAddScoped(); serviceCollection.TryAddScoped(); break; case ServiceLifetime.Transient: -#pragma warning disable CS0618 // Type or member is obsolete - keeping around temporarily - replaced by DaprWorkflowClient - serviceCollection.TryAddTransient(); -#pragma warning restore CS0618 // Type or member is obsolete serviceCollection.TryAddTransient(); serviceCollection.TryAddTransient(); break; default: throw new ArgumentOutOfRangeException(nameof(lifetime), lifetime, null); } - + serviceCollection.AddOptions().Configure(configure); - + //Register the factory and force resolution so the Durable Task client and worker can be registered using (var scope = serviceCollection.BuildServiceProvider().CreateScope()) { var httpClientFactory = scope.ServiceProvider.GetRequiredService(); var configuration = scope.ServiceProvider.GetService(); - + var factory = new DaprWorkflowClientBuilderFactory(configuration, httpClientFactory); factory.CreateClientBuilder(serviceCollection, configure); } diff --git a/test/Dapr.AI.Test/Conversation/DaprConversationClientBuilderTest.cs b/test/Dapr.AI.Test/Conversation/DaprConversationClientBuilderTest.cs new file mode 100644 index 000000000..901c4b656 --- /dev/null +++ b/test/Dapr.AI.Test/Conversation/DaprConversationClientBuilderTest.cs @@ -0,0 +1,33 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.AI.Conversation; + +namespace Dapr.AI.Test.Conversation; + +public class DaprConversationClientBuilderTest +{ + [Fact] + public void Build_WithDefaultConfiguration_ShouldReturnNewInstanceOfDaprConversationClient() + { + // Arrange + var conversationClientBuilder = new DaprConversationClientBuilder(); + + // Act + var client = conversationClientBuilder.Build(); + + // Assert + Assert.NotNull(client); + Assert.IsType(client); + } +} diff --git a/test/Dapr.AI.Test/Conversation/Extensions/DaprAiConversationBuilderExtensionsTest.cs b/test/Dapr.AI.Test/Conversation/Extensions/DaprAiConversationBuilderExtensionsTest.cs new file mode 100644 index 000000000..2ee321895 --- /dev/null +++ b/test/Dapr.AI.Test/Conversation/Extensions/DaprAiConversationBuilderExtensionsTest.cs @@ -0,0 +1,182 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using Dapr.AI.Conversation; +using Dapr.AI.Conversation.Extensions; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Dapr.AI.Test.Conversation.Extensions; + +public class DaprAiConversationBuilderExtensionsTest +{ + [Fact] + public void AddDaprConversationClient_FromIConfiguration() + { + const string apiToken = "abc123"; + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary { { "DAPR_API_TOKEN", apiToken } }) + .Build(); + + var services = new ServiceCollection(); + services.AddSingleton(configuration); + + services.AddDaprConversationClient(); + + var app = services.BuildServiceProvider(); + + var conversationClient = app.GetRequiredService() as DaprConversationClient; + + Assert.NotNull(conversationClient!.DaprApiToken); + Assert.Equal(apiToken, conversationClient.DaprApiToken); + } + + [Fact] + public void AddDaprConversationClient_RegistersDaprClientOnlyOnce() + { + var services = new ServiceCollection(); + + var clientBuilder = new Action((sp, builder) => + { + builder.UseDaprApiToken("abc"); + }); + + services.AddDaprConversationClient(); //Sets a default API token value of an empty string + services.AddDaprConversationClient(clientBuilder); //Sets the API token value + + var serviceProvider = services.BuildServiceProvider(); + var daprConversationClient = serviceProvider.GetService(); + + Assert.NotNull(daprConversationClient!.HttpClient); + Assert.False(daprConversationClient.HttpClient.DefaultRequestHeaders.TryGetValues("dapr-api-token", out var _)); + } + + [Fact] + public void AddDaprConversationClient_RegistersUsingDependencyFromIServiceProvider() + { + var services = new ServiceCollection(); + services.AddSingleton(); + services.AddDaprConversationClient((provider, builder) => + { + var configProvider = provider.GetRequiredService(); + var apiToken = configProvider.GetApiTokenValue(); + builder.UseDaprApiToken(apiToken); + }); + + var serviceProvider = services.BuildServiceProvider(); + var client = serviceProvider.GetRequiredService(); + + //Validate it's set on the GrpcClient - note that it doesn't get set on the HttpClient + Assert.NotNull(client); + Assert.NotNull(client.DaprApiToken); + Assert.Equal("abcdef", client.DaprApiToken); + Assert.NotNull(client.HttpClient); + + if (!client.HttpClient.DefaultRequestHeaders.TryGetValues("dapr-api-token", out var daprApiToken)) + { + Assert.Fail(); + } + Assert.Equal("abcdef", daprApiToken.FirstOrDefault()); + } + + [Fact] + public void AddDaprConversationClient_WithoutConfigure_ShouldAddServices() + { + var services = new ServiceCollection(); + var builder = services.AddDaprConversationClient(); + Assert.NotNull(builder); + } + + [Fact] + public void AddDaprConversationClient_RegistersIHttpClientFactory() + { + var services = new ServiceCollection(); + services.AddDaprConversationClient(); + var serviceProvider = services.BuildServiceProvider(); + + var httpClientFactory = serviceProvider.GetService(); + Assert.NotNull(httpClientFactory); + + var daprConversationClient = serviceProvider.GetService(); + Assert.NotNull(daprConversationClient); + } + + [Fact] + public void AddDaprConversationClient_NullServices_ShouldThrowException() + { + IServiceCollection services = null; + Assert.Throws(() => services.AddDaprConversationClient()); + } + + [Fact] + public void AddDaprConversationClient_ShouldRegisterSingleton_WhenLifetimeIsSingleton() + { + var services = new ServiceCollection(); + + services.AddDaprConversationClient((_, _) => { }, ServiceLifetime.Singleton); + var serviceProvider = services.BuildServiceProvider(); + + var daprConversationClient1 = serviceProvider.GetService(); + var daprConversationClient2 = serviceProvider.GetService(); + + Assert.NotNull(daprConversationClient1); + Assert.NotNull(daprConversationClient2); + + Assert.Same(daprConversationClient1, daprConversationClient2); + } + + [Fact] + public async Task AddDaprConversationClient_ShouldRegisterScoped_WhenLifetimeIsScoped() + { + var services = new ServiceCollection(); + + services.AddDaprConversationClient((_, _) => { }, ServiceLifetime.Scoped); + var serviceProvider = services.BuildServiceProvider(); + + await using var scope1 = serviceProvider.CreateAsyncScope(); + var daprConversationClient1 = scope1.ServiceProvider.GetService(); + + await using var scope2 = serviceProvider.CreateAsyncScope(); + var daprConversationClient2 = scope2.ServiceProvider.GetService(); + + Assert.NotNull(daprConversationClient1); + Assert.NotNull(daprConversationClient2); + Assert.NotSame(daprConversationClient1, daprConversationClient2); + } + + [Fact] + public void AddDaprConversationClient_ShouldRegisterTransient_WhenLifetimeIsTransient() + { + var services = new ServiceCollection(); + + services.AddDaprConversationClient((_, _) => { }, ServiceLifetime.Transient); + var serviceProvider = services.BuildServiceProvider(); + + var daprConversationClient1 = serviceProvider.GetService(); + var daprConversationClient2 = serviceProvider.GetService(); + + Assert.NotNull(daprConversationClient1); + Assert.NotNull(daprConversationClient2); + Assert.NotSame(daprConversationClient1, daprConversationClient2); + } + + private class TestSecretRetriever + { + public string GetApiTokenValue() => "abcdef"; + } +} diff --git a/test/Dapr.AI.Test/Dapr.AI.Test.csproj b/test/Dapr.AI.Test/Dapr.AI.Test.csproj new file mode 100644 index 000000000..f937f64e2 --- /dev/null +++ b/test/Dapr.AI.Test/Dapr.AI.Test.csproj @@ -0,0 +1,28 @@ + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + diff --git a/test/Dapr.Client.Test/StateApiTest.cs b/test/Dapr.Client.Test/StateApiTest.cs index f6ecb5d80..12fd0e3de 100644 --- a/test/Dapr.Client.Test/StateApiTest.cs +++ b/test/Dapr.Client.Test/StateApiTest.cs @@ -11,24 +11,26 @@ // limitations under the License. // ------------------------------------------------------------------------ + +using System; +using System.Collections.Generic; +using System.Net; +using System.Text.Json; +using System.Threading.Tasks; +using Autogenerated = Dapr.Client.Autogen.Grpc.v1; +using FluentAssertions; +using Google.Protobuf; +using Grpc.Core; +using Moq; +using StateConsistency = Dapr.Client.Autogen.Grpc.v1.StateOptions.Types.StateConsistency; +using StateConcurrency = Dapr.Client.Autogen.Grpc.v1.StateOptions.Types.StateConcurrency; +using Xunit; +using System.Threading; +using System.Net.Http; +using System.Text; + namespace Dapr.Client.Test { - using System; - using System.Collections.Generic; - using System.Net; - using System.Text.Json; - using System.Threading.Tasks; - using Autogenerated = Dapr.Client.Autogen.Grpc.v1; - using FluentAssertions; - using Google.Protobuf; - using Grpc.Core; - using Moq; - using StateConsistency = Dapr.Client.Autogen.Grpc.v1.StateOptions.Types.StateConsistency; - using StateConcurrency = Dapr.Client.Autogen.Grpc.v1.StateOptions.Types.StateConcurrency; - using Xunit; - using System.Threading; - using System.Net.Http; - public class StateApiTest { [Fact] @@ -36,10 +38,7 @@ public async Task GetStateAsync_CanReadState() { await using var client = TestClient.CreateForDaprClient(); - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.GetStateAsync("testStore", "test"); - }); + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetStateAsync("testStore", "test")); request.Dismiss(); @@ -58,14 +57,11 @@ public async Task GetBulkStateAsync_CanReadState() { await using var client = TestClient.CreateForDaprClient(); - var key = "test"; - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.GetBulkStateAsync("testStore", new List() { key }, null); - }); + const string key = "test"; + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetBulkStateAsync("testStore", new List() { key }, null)); // Create Response & Respond - var data = "value"; + const string data = "value"; var envelope = MakeGetBulkStateResponse(key, data); var state = await request.CompleteWithMessageAsync(envelope); @@ -78,11 +74,8 @@ public async Task GetBulkStateAsync_CanReadDeserializedState() { await using var client = TestClient.CreateForDaprClient(); - var key = "test"; - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.GetBulkStateAsync("testStore", new List() {key}, null); - }); + const string key = "test"; + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetBulkStateAsync("testStore", new List() {key}, null)); // Create Response & Respond const string size = "small"; @@ -102,11 +95,8 @@ public async Task GetBulkStateAsync_WrapsRpcException() { await using var client = TestClient.CreateForDaprClient(); - var key = "test"; - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.GetBulkStateAsync("testStore", new List() { key }, null); - }); + const string key = "test"; + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetBulkStateAsync("testStore", new List() { key }, null)); // Create Response & Respond var ex = await Assert.ThrowsAsync(async () => @@ -121,15 +111,12 @@ public async Task GetBulkStateAsync_ValidateRequest() { await using var client = TestClient.CreateForDaprClient(); - var key = "test"; + const string key = "test"; var metadata = new Dictionary { { "partitionKey", "mypartition" } }; - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.GetBulkStateAsync("testStore", new List() { key }, null, metadata: metadata); - }); + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetBulkStateAsync("testStore", new List() { key }, null, metadata: metadata)); request.Dismiss(); @@ -144,10 +131,7 @@ public async Task GetStateAndEtagAsync_CanReadState() { await using var client = TestClient.CreateForDaprClient(); - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.GetStateAndETagAsync("testStore", "test"); - }); + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetStateAndETagAsync("testStore", "test")); // Create Response & Respond var data = new Widget() { Size = "small", Color = "yellow", }; @@ -165,10 +149,7 @@ public async Task GetStateAndETagAsync_WrapsRpcException() { await using var client = TestClient.CreateForDaprClient(); - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.GetStateAndETagAsync("testStore", "test"); - }); + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetStateAndETagAsync("testStore", "test")); // Create Response & Respond var ex = await Assert.ThrowsAsync(async () => @@ -183,10 +164,7 @@ public async Task GetStateAndETagAsync_WrapsJsonException() { await using var client = TestClient.CreateForDaprClient(); - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.GetStateAndETagAsync("testStore", "test"); - }); + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetStateAndETagAsync("testStore", "test")); // Create Response & Respond var envelope = new Autogenerated.GetStateResponse() @@ -206,10 +184,7 @@ public async Task GetStateAsync_CanReadEmptyState_ReturnsDefault() { await using var client = TestClient.CreateForDaprClient(); - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.GetStateAsync("testStore", "test", ConsistencyMode.Eventual); - }); + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetStateAsync("testStore", "test", ConsistencyMode.Eventual)); // Create Response & Respond var envelope = MakeGetStateResponse(null); @@ -226,10 +201,7 @@ public async Task GetStateAsync_ValidateRequest(ConsistencyMode consistencyMode, { await using var client = TestClient.CreateForDaprClient(); - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.GetStateAsync("testStore", "test", consistencyMode); - }); + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetStateAsync("testStore", "test", consistencyMode)); // Get Request & Validate var envelope = await request.GetRequestEnvelopeAsync(); @@ -253,10 +225,7 @@ public async Task GetStateAndEtagAsync_ValidateRequest() { { "partitionKey", "mypartition" } }; - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.GetStateAsync("testStore", "test", metadata: metadata); - }); + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetStateAsync("testStore", "test", metadata: metadata)); // Get Request & Validate var envelope = await request.GetRequestEnvelopeAsync(); @@ -276,10 +245,7 @@ public async Task GetStateAsync_WrapsRpcException() { await using var client = TestClient.CreateForDaprClient(); - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.GetStateAsync("testStore", "test"); - }); + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetStateAsync("testStore", "test")); // Create Response & Respond var ex = await Assert.ThrowsAsync(async () => @@ -294,10 +260,7 @@ public async Task GetStateAsync_WrapsJsonException() { await using var client = TestClient.CreateForDaprClient(); - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.GetStateAsync("testStore", "test"); - }); + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetStateAsync("testStore", "test")); // Create Response & Respond var stateResponse = new Autogenerated.GetStateResponse() @@ -467,10 +430,10 @@ public async Task ExecuteStateTransactionAsync_CanSaveState() }; var state1 = new StateTransactionRequest("stateKey1", JsonSerializer.SerializeToUtf8Bytes(stateValue1), StateOperationType.Upsert, "testEtag", metadata1, options1); - var stateValue2 = 100; + const int stateValue2 = 100; var state2 = new StateTransactionRequest("stateKey2", JsonSerializer.SerializeToUtf8Bytes(stateValue2), StateOperationType.Delete); - var stateValue3 = "teststring"; + const string stateValue3 = "teststring"; var state3 = new StateTransactionRequest("stateKey3", JsonSerializer.SerializeToUtf8Bytes(stateValue3), StateOperationType.Upsert); var states = new List @@ -619,10 +582,7 @@ public async Task GetStateEntryAsync_CanReadState() { await using var client = TestClient.CreateForDaprClient(); - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.GetStateEntryAsync("testStore", "test"); - }); + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetStateEntryAsync("testStore", "test")); // Create Response & Respond var data = new Widget() { Size = "small", Color = "yellow", }; @@ -639,10 +599,7 @@ public async Task GetStateEntryAsync_CanReadEmptyState_ReturnsDefault() { await using var client = TestClient.CreateForDaprClient(); - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.GetStateEntryAsync("testStore", "test"); - }); + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetStateEntryAsync("testStore", "test")); // Create Response & Respond var envelope = MakeGetStateResponse(null); @@ -657,10 +614,7 @@ public async Task GetStateEntryAsync_CanSaveState() { await using var client = TestClient.CreateForDaprClient(); - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.GetStateEntryAsync("testStore", "test"); - }); + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetStateEntryAsync("testStore", "test")); // Create Response & Respond var data = new Widget() { Size = "small", Color = "yellow", }; @@ -699,10 +653,7 @@ public async Task GetStateEntryAsync_CanDeleteState() { await using var client = TestClient.CreateForDaprClient(); - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.GetStateEntryAsync("testStore", "test"); - }); + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetStateEntryAsync("testStore", "test")); // Create Response & Respond var data = new Widget() { Size = "small", Color = "yellow", }; @@ -805,10 +756,7 @@ public async Task TrySaveStateAsync_ValidateOptions( { "key1", "value1" }, { "key2", "value2" } }; - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.TrySaveStateAsync("testStore", "test", widget, "Test_Etag", stateOptions, metadata); - }); + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.TrySaveStateAsync("testStore", "test", widget, "Test_Etag", stateOptions, metadata)); request.Dismiss(); @@ -1021,10 +969,7 @@ public async Task TryDeleteStateAsync_ValidateOptions( Consistency = consistencyMode }; - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.TryDeleteStateAsync("testStore", "test", "Test_Etag", stateOptions); - }); + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.TryDeleteStateAsync("testStore", "test", "Test_Etag", stateOptions)); request.Dismiss(); @@ -1042,8 +987,8 @@ public async Task DeleteBulkStateAsync_ValidateRequest() { await using var client = TestClient.CreateForDaprClient(); - var key = "test"; - var etag = "etag"; + const string key = "test"; + const string etag = "etag"; var metadata = new Dictionary { { "partitionKey", "mypartition" } @@ -1069,11 +1014,8 @@ public async Task QueryStateAsync_ValidateResult() { await using var client = TestClient.CreateForDaprClient(); - var queryJson = "{'query':{'filter':{ 'EQ': {'value':'test'}}}}"; - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.QueryStateAsync("testStore", queryJson, new Dictionary()); - }); + const string queryJson = "{'query':{'filter':{ 'EQ': {'value':'test'}}}}"; + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.QueryStateAsync("testStore", queryJson, new Dictionary())); // Validate request. var envelope = await request.GetRequestEnvelopeAsync(); @@ -1099,11 +1041,8 @@ public async Task QueryStateAsync_EncountersError_ValidatePartialResult() { await using var client = TestClient.CreateForDaprClient(); - var queryJson = "{'query':{'filter':{ 'EQ': {'value':'test'}}}}"; - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.QueryStateAsync("testStore", queryJson, new Dictionary()); - }); + const string queryJson = "{'query':{'filter':{ 'EQ': {'value':'test'}}}}"; + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.QueryStateAsync("testStore", queryJson, new Dictionary())); // Validate request. var envelope = await request.GetRequestEnvelopeAsync(); @@ -1175,14 +1114,371 @@ private Autogenerated.GetBulkStateResponse MakeGetBulkStateResponse(string ke private Autogenerated.QueryStateItem MakeQueryStateItem(string key, T data, string etag = default, string error = default) { - var wireItem = new Autogenerated.QueryStateItem(); - wireItem.Key = key; - wireItem.Data = ByteString.CopyFromUtf8(JsonSerializer.Serialize(data)); - wireItem.Etag = etag ?? string.Empty; - wireItem.Error = error ?? string.Empty; + var wireItem = new Autogenerated.QueryStateItem + { + Key = key, Data = ByteString.CopyFromUtf8(JsonSerializer.Serialize(data)), Etag = etag ?? string.Empty, + Error = error ?? string.Empty + }; return wireItem; } + [Theory] + [InlineData(ConsistencyMode.Eventual, ConcurrencyMode.FirstWrite, StateConsistency.ConsistencyEventual, StateConcurrency.ConcurrencyFirstWrite)] + [InlineData(ConsistencyMode.Eventual, ConcurrencyMode.LastWrite, StateConsistency.ConsistencyEventual, StateConcurrency.ConcurrencyLastWrite)] + [InlineData(ConsistencyMode.Strong, ConcurrencyMode.FirstWrite, StateConsistency.ConsistencyStrong, StateConcurrency.ConcurrencyFirstWrite)] + [InlineData(ConsistencyMode.Strong, ConcurrencyMode.LastWrite, StateConsistency.ConsistencyStrong, StateConcurrency.ConcurrencyLastWrite)] + public async Task SaveByteStateAsync_ValidateOptions( + ConsistencyMode consistencyMode, + ConcurrencyMode concurrencyMode, + StateConsistency expectedConsistency, + StateConcurrency expectedConcurrency) + { + await using var client = TestClient.CreateForDaprClient(); + + const string data = "Test binary data"; + var stateBytes = Encoding.UTF8.GetBytes(data); + var stateOptions = new StateOptions + { + Concurrency = concurrencyMode, + Consistency = consistencyMode + }; + + var metadata = new Dictionary + { + { "key1", "value1" }, + { "key2", "value2" } + }; + + var request = await client.CaptureGrpcRequestAsync(async daprClient => + { + await daprClient.SaveByteStateAsync("testStore", "test", stateBytes.AsMemory(), stateOptions, metadata); + }); + + request.Dismiss(); + + // Get Request and validate + var envelope = await request.GetRequestEnvelopeAsync(); + envelope.StoreName.Should().Be("testStore"); + envelope.States.Count.Should().Be(1); + var state = envelope.States[0]; + state.Key.Should().Be("test"); + state.Metadata.Count.Should().Be(2); + state.Metadata.Keys.Contains("key1").Should().BeTrue(); + state.Metadata.Keys.Contains("key2").Should().BeTrue(); + state.Metadata["key1"].Should().Be("value1"); + state.Metadata["key2"].Should().Be("value2"); + state.Options.Concurrency.Should().Be(expectedConcurrency); + state.Options.Consistency.Should().Be(expectedConsistency); + + var stateBinaryData = state.Value.ToStringUtf8(); + stateBinaryData.Should().Be(data); + } + + [Fact] + public async Task SaveByteStateAsync_CanSaveState() + { + await using var client = TestClient.CreateForDaprClient(); + + const string data = "Test binary data"; + var stateBytes = Encoding.UTF8.GetBytes(data); + var request = await client.CaptureGrpcRequestAsync(async daprClient => + { + await daprClient.SaveByteStateAsync("testStore", "test", stateBytes.AsMemory()); + }); + + request.Dismiss(); + + // Get Request and validate + var envelope = await request.GetRequestEnvelopeAsync(); + envelope.StoreName.Should().Be("testStore"); + envelope.States.Count.Should().Be(1); + var state = envelope.States[0]; + state.Key.Should().Be("test"); + + var stateBinaryData = state.Value.ToStringUtf8(); + stateBinaryData.Should().Be(data); + } + + [Fact] + public async Task SaveByteStateAsync_CanClearState() + { + await using var client = TestClient.CreateForDaprClient(); + + var request = await client.CaptureGrpcRequestAsync(async daprClient => + { + await daprClient.SaveByteStateAsync("testStore", "test", null); + }); + + request.Dismiss(); + + // Get Request and validate + var envelope = await request.GetRequestEnvelopeAsync(); + + envelope.StoreName.Should().Be("testStore"); + envelope.States.Count.Should().Be(1); + var state = envelope.States[0]; + state.Key.Should().Be("test"); + state.Value.Should().Equal(ByteString.Empty); + } + + [Fact] + public async Task SaveByteStateAsync_WithCancelledToken() + { + await using var client = TestClient.CreateForDaprClient(); + + var cts = new CancellationTokenSource(); + cts.Cancel(); + + await Assert.ThrowsAsync(async () => + { + await client.InnerClient.SaveByteStateAsync("testStore", "test", null, cancellationToken: cts.Token); + }); + } + + [Theory] + [InlineData(ConsistencyMode.Eventual, ConcurrencyMode.FirstWrite, StateConsistency.ConsistencyEventual, StateConcurrency.ConcurrencyFirstWrite)] + [InlineData(ConsistencyMode.Eventual, ConcurrencyMode.LastWrite, StateConsistency.ConsistencyEventual, StateConcurrency.ConcurrencyLastWrite)] + [InlineData(ConsistencyMode.Strong, ConcurrencyMode.FirstWrite, StateConsistency.ConsistencyStrong, StateConcurrency.ConcurrencyFirstWrite)] + [InlineData(ConsistencyMode.Strong, ConcurrencyMode.LastWrite, StateConsistency.ConsistencyStrong, StateConcurrency.ConcurrencyLastWrite)] + public async Task TrySaveByteStateAsync_ValidateOptions( + ConsistencyMode consistencyMode, + ConcurrencyMode concurrencyMode, + StateConsistency expectedConsistency, + StateConcurrency expectedConcurrency) + { + await using var client = TestClient.CreateForDaprClient(); + const string data = "Test binary data"; + var stateBytes = Encoding.UTF8.GetBytes(data); + var stateOptions = new StateOptions + { + Concurrency = concurrencyMode, + Consistency = consistencyMode + }; + + var metadata = new Dictionary + { + { "key1", "value1" }, + { "key2", "value2" } + }; + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.TrySaveByteStateAsync("testStore", "test", stateBytes.AsMemory(), "Test_Etag", stateOptions, metadata)); + + request.Dismiss(); + + // Get Request and validate + var envelope = await request.GetRequestEnvelopeAsync(); + envelope.StoreName.Should().Be("testStore"); + envelope.States.Count.Should().Be(1); + var state = envelope.States[0]; + state.Etag.Value.Should().Be("Test_Etag"); + state.Metadata.Count.Should().Be(2); + state.Metadata.Keys.Contains("key1").Should().BeTrue(); + state.Metadata.Keys.Contains("key2").Should().BeTrue(); + state.Metadata["key1"].Should().Be("value1"); + state.Metadata["key2"].Should().Be("value2"); + state.Options.Concurrency.Should().Be(expectedConcurrency); + state.Options.Consistency.Should().Be(expectedConsistency); + + var stateBinaryData = state.Value.ToStringUtf8(); + stateBinaryData.Should().Be(data); + } + + [Fact] + public async Task TrySaveByteStateAsync_ValidateNonETagErrorThrowsException() + { + var client = new MockClient(); + + var response = client.CallStateApi() + .Build(); + const string data = "Test binary data"; + var stateBytes = Encoding.UTF8.GetBytes(data); + var rpcException = new RpcException(new Status(StatusCode.Internal, "Network Error")); + + // Setup the mock client to throw an Rpc Exception with the expected details info + client.Mock + .Setup(m => m.SaveStateAsync(It.IsAny(), It.IsAny())) + .Throws(rpcException); + + var ex = await Assert.ThrowsAsync(async () => + { + await client.DaprClient.TrySaveByteStateAsync("test", "test", stateBytes.AsMemory(), "someETag"); + }); + Assert.Same(rpcException, ex.InnerException); + } + + [Fact] + public async Task TrySaveByteStateAsync_ValidateETagRelatedExceptionReturnsFalse() + { + var client = new MockClient(); + + var response = client.CallStateApi() + .Build(); + const string data = "Test binary data"; + var stateBytes = Encoding.UTF8.GetBytes(data); + var rpcException = new RpcException(new Status(StatusCode.Aborted, $"failed saving state in state store testStore")); + // Setup the mock client to throw an Rpc Exception with the expected details info + client.Mock + .Setup(m => m.SaveStateAsync(It.IsAny(), It.IsAny())) + .Throws(rpcException); + + var operationResult = await client.DaprClient.TrySaveByteStateAsync("testStore", "test", stateBytes.AsMemory(), "invalidETag"); + Assert.False(operationResult); + } + + [Fact] + public async Task TrySaveByteStateAsync_NullEtagThrowsArgumentException() + { + var client = new MockClient(); + const string data = "Test binary data"; + var stateBytes = Encoding.UTF8.GetBytes(data); + var response = client.CallStateApi() + .Build(); + + await FluentActions.Awaiting(async () => await client.DaprClient.TrySaveByteStateAsync("test", "test", stateBytes.AsMemory(), null)) + .Should().ThrowAsync(); + } + + [Fact] + public async Task TrySaveByteStateAsync_EmptyEtagDoesNotThrow() + { + var client = new MockClient(); + const string data = "Test binary data"; + var stateBytes = Encoding.UTF8.GetBytes(data); + var response = client.CallStateApi() + .Build(); + + // Setup the mock client to return success + client.Mock + .Setup(m => m.SaveStateAsync(It.IsAny(), It.IsAny())) + .Returns(response); + + var result = await client.DaprClient.TrySaveByteStateAsync("test", "test", stateBytes.AsMemory(), ""); + Assert.True(result); + } + [Fact] + public async Task GetByteStateAsync_CanReadEmptyState_ReturnsDefault() + { + await using var client = TestClient.CreateForDaprClient(); + + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetByteStateAsync("testStore", "test", ConsistencyMode.Eventual)); + + // Create Response & Respond to request + var envelope = MakeGetByteStateResponse(null); + var state = await request.CompleteWithMessageAsync(envelope); + + // Get response and validate + state.ToArray().Should().BeNullOrEmpty(); + } + + [Theory] + [InlineData(ConsistencyMode.Eventual, StateConsistency.ConsistencyEventual)] + [InlineData(ConsistencyMode.Strong, StateConsistency.ConsistencyStrong)] + public async Task GetByteStateAsync_ValidateRequest(ConsistencyMode consistencyMode, StateConsistency expectedConsistencyMode) + { + await using var client = TestClient.CreateForDaprClient(); + + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetByteStateAsync("testStore", "test", consistencyMode)); + + // Get Request & Validate + var envelope = await request.GetRequestEnvelopeAsync(); + envelope.StoreName.Should().Be("testStore"); + envelope.Key.Should().Be("test"); + envelope.Consistency.Should().Be(expectedConsistencyMode); + var binaryData = Encoding.ASCII.GetBytes("test data"); + // Create Response & Respond + var state = await request.CompleteWithMessageAsync(MakeGetByteStateResponse(binaryData.AsMemory())); + var stateStr = ByteString.CopyFrom(state.Span).ToByteArray(); + // Get response and validate + stateStr.Should().BeEquivalentTo(binaryData); + } + + [Fact] + public async Task GetByteStateAndEtagAsync_ValidateRequest() + { + await using var client = TestClient.CreateForDaprClient(); + + var metadata = new Dictionary + { + { "partitionKey", "mypartition" } + }; + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetByteStateAndETagAsync("testStore", "test", metadata: metadata)); + + // Get Request & Validate + var envelope = await request.GetRequestEnvelopeAsync(); + envelope.StoreName.Should().Be("testStore"); + envelope.Key.Should().Be("test"); + envelope.Metadata.Should().BeEquivalentTo(metadata); + var binaryData = Encoding.ASCII.GetBytes("test data"); + // Create Response & Respond + var (state, etag) = await request.CompleteWithMessageAsync((MakeGetByteStateResponse(binaryData.AsMemory()))); + var stateStr = ByteString.CopyFrom(state.Span).ToByteArray(); + // Get response and validate + stateStr.Should().BeEquivalentTo(binaryData); + } + [Fact] + public async Task GetByteStateAsync_WrapsRpcException() + { + await using var client = TestClient.CreateForDaprClient(); + + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetByteStateAsync("testStore", "test")); + + // Create Response & Respond + var ex = await Assert.ThrowsAsync(async () => + { + await request.CompleteAsync(new HttpResponseMessage(HttpStatusCode.NotAcceptable)); + }); + Assert.IsType(ex.InnerException); + } + + [Fact] + public async Task GetByteStateAndEtagAsync_CanReadState() + { + await using var client = TestClient.CreateForDaprClient(); + + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetByteStateAndETagAsync("testStore", "test")); + + // Create Response & Respond + var binaryData = Encoding.ASCII.GetBytes("test data"); + var envelope = MakeGetByteStateResponse(binaryData.AsMemory(), "Test_Etag"); + var (state, etag) = await request.CompleteWithMessageAsync(envelope); + var stateStr = ByteString.CopyFrom(state.Span).ToByteArray(); + // Get response and validate + stateStr.Should().BeEquivalentTo(binaryData); + etag.Should().Be("Test_Etag"); + } + + [Fact] + public async Task GetByteStateAndETagAsync_WrapsRpcException() + { + await using var client = TestClient.CreateForDaprClient(); + + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetByteStateAndETagAsync("testStore", "test")); + + // Create Response & Respond + var ex = await Assert.ThrowsAsync(async () => + { + await request.CompleteAsync(new HttpResponseMessage(HttpStatusCode.NotAcceptable)); + }); + Assert.IsType(ex.InnerException); + } + + private Autogenerated.GetStateResponse MakeGetByteStateResponse(ReadOnlyMemory state, string etag = null) + { + + var response = new Autogenerated.GetStateResponse(); + + // convert to byte string if state is not null + if (!state.Span.IsEmpty) + { + response.Data = ByteString.CopyFrom(state.Span); + } + + if (etag != null) + { + response.Etag = etag; + } + + return response; + } private class Widget { public string Size { get; set; } diff --git a/test/Dapr.Common.Test/Extensions/EnumExtensionsTest.cs b/test/Dapr.Common.Test/Extensions/EnumExtensionsTest.cs index 84e2998d6..e7b2d014b 100644 --- a/test/Dapr.Common.Test/Extensions/EnumExtensionsTest.cs +++ b/test/Dapr.Common.Test/Extensions/EnumExtensionsTest.cs @@ -12,14 +12,14 @@ public void GetValueFromEnumMember_RedResolvesAsExpected() var value = TestEnum.Red.GetValueFromEnumMember(); Assert.Equal("red", value); } - + [Fact] public void GetValueFromEnumMember_YellowResolvesAsExpected() { var value = TestEnum.Yellow.GetValueFromEnumMember(); Assert.Equal("YELLOW", value); } - + [Fact] public void GetValueFromEnumMember_BlueResolvesAsExpected() { @@ -27,6 +27,7 @@ public void GetValueFromEnumMember_BlueResolvesAsExpected() Assert.Equal("Blue", value); } } + public enum TestEnum { [EnumMember(Value = "red")] @@ -35,4 +36,3 @@ public enum TestEnum Yellow, Blue } - diff --git a/test/Dapr.Common.Test/JsonConverters/GenericEnumJsonConverterTest.cs b/test/Dapr.Common.Test/JsonConverters/GenericEnumJsonConverterTest.cs new file mode 100644 index 000000000..065a74220 --- /dev/null +++ b/test/Dapr.Common.Test/JsonConverters/GenericEnumJsonConverterTest.cs @@ -0,0 +1,52 @@ +using System.Runtime.Serialization; +using System.Text.Json; +using System.Text.Json.Serialization; +using Dapr.Common.JsonConverters; +using Xunit; + +namespace Dapr.Common.Test.JsonConverters; + +public class GenericEnumJsonConverterTest +{ + [Fact] + public void ShouldSerializeWithEnumMemberAttribute() + { + var testValue = new TestType("ColorTest", Color.Red); + var serializedValue = JsonSerializer.Serialize(testValue); + Assert.Equal("{\"Name\":\"ColorTest\",\"Color\":\"definitely-not-red\"}", serializedValue); + } + + [Fact] + public void ShouldSerializeWithoutEnumMemberAttribute() + { + var testValue = new TestType("ColorTest", Color.Green); + var serializedValue = JsonSerializer.Serialize(testValue); + Assert.Equal("{\"Name\":\"ColorTest\",\"Color\":\"Green\"}", serializedValue); + } + + [Fact] + public void ShouldDeserializeWithEnumMemberAttribute() + { + const string json = "{\"Name\":\"ColorTest\",\"Color\":\"definitely-not-red\"}"; + var deserializedValue = JsonSerializer.Deserialize(json); + Assert.Equal("ColorTest", deserializedValue.Name); + Assert.Equal(Color.Red, deserializedValue.Color); + } + + [Fact] + public void ShouldDeserializeWithoutEnumMemberAttribute() + { + const string json = "{\"Name\":\"ColorTest\",\"Color\":\"Green\"}"; + var deserializedValue = JsonSerializer.Deserialize(json); + Assert.Equal("ColorTest", deserializedValue.Name); + Assert.Equal(Color.Green, deserializedValue.Color); + } + + private record TestType(string Name, Color Color); + + [JsonConverter(typeof(GenericEnumJsonConverter))] + private enum Color { + [EnumMember(Value="definitely-not-red")] + Red, + Green }; +} diff --git a/test/Dapr.Jobs.Test/Extensions/DaprJobsServiceCollectionExtensionsTests.cs b/test/Dapr.Jobs.Test/Extensions/DaprJobsServiceCollectionExtensionsTests.cs index 281477d4e..28a8a0681 100644 --- a/test/Dapr.Jobs.Test/Extensions/DaprJobsServiceCollectionExtensionsTests.cs +++ b/test/Dapr.Jobs.Test/Extensions/DaprJobsServiceCollectionExtensionsTests.cs @@ -12,31 +12,59 @@ // ------------------------------------------------------------------------ using System; +using System.Collections.Generic; +using System.Linq; using System.Net.Http; using System.Threading.Tasks; using Dapr.Jobs.Extensions; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace Dapr.Jobs.Test.Extensions; public class DaprJobsServiceCollectionExtensionsTest { + [Fact] + + public void AddDaprJobsClient_FromIConfiguration() + { + const string apiToken = "abc123"; + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary { { "DAPR_API_TOKEN", apiToken } }) + .Build(); + + var services = new ServiceCollection(); + services.AddSingleton(configuration); + + services.AddDaprJobsClient(); + + var app = services.BuildServiceProvider(); + + var jobsClient = app.GetRequiredService() as DaprJobsGrpcClient; + + Assert.NotNull(jobsClient!.DaprApiToken); + Assert.Equal(apiToken, jobsClient.DaprApiToken); + } + [Fact] public void AddDaprJobsClient_RegistersDaprClientOnlyOnce() { var services = new ServiceCollection(); - var clientBuilder = new Action(builder => - builder.UseDaprApiToken("abc")); + + var clientBuilder = new Action((sp, builder) => + { + builder.UseDaprApiToken("abc"); + }); services.AddDaprJobsClient(); //Sets a default API token value of an empty string services.AddDaprJobsClient(clientBuilder); //Sets the API token value var serviceProvider = services.BuildServiceProvider(); var daprJobClient = serviceProvider.GetService() as DaprJobsGrpcClient; - - Assert.Null(daprJobClient!.apiTokenHeader); - Assert.False(daprJobClient.httpClient.DefaultRequestHeaders.TryGetValues("dapr-api-token", out var _)); + + Assert.NotNull(daprJobClient!.HttpClient); + Assert.False(daprJobClient.HttpClient.DefaultRequestHeaders.TryGetValues("dapr-api-token", out var _)); } [Fact] @@ -63,8 +91,8 @@ public void AddDaprJobsClient_RegistersUsingDependencyFromIServiceProvider() services.AddDaprJobsClient((provider, builder) => { var configProvider = provider.GetRequiredService(); - var daprApiToken = configProvider.GetApiTokenValue(); - builder.UseDaprApiToken(daprApiToken); + var apiToken = configProvider.GetApiTokenValue(); + builder.UseDaprApiToken(apiToken); }); var serviceProvider = services.BuildServiceProvider(); @@ -72,10 +100,15 @@ public void AddDaprJobsClient_RegistersUsingDependencyFromIServiceProvider() //Validate it's set on the GrpcClient - note that it doesn't get set on the HttpClient Assert.NotNull(client); - Assert.NotNull(client.apiTokenHeader); - Assert.True(client.apiTokenHeader.HasValue); - Assert.Equal("dapr-api-token", client.apiTokenHeader.Value.Key); - Assert.Equal("abcdef", client.apiTokenHeader.Value.Value); + Assert.NotNull(client.DaprApiToken); + Assert.Equal("abcdef", client.DaprApiToken); + Assert.NotNull(client.HttpClient); + + if (!client.HttpClient.DefaultRequestHeaders.TryGetValues("dapr-api-token", out var daprApiToken)) + { + Assert.Fail(); + } + Assert.Equal("abcdef", daprApiToken.FirstOrDefault()); } [Fact] @@ -83,7 +116,7 @@ public void RegisterJobsClient_ShouldRegisterSingleton_WhenLifetimeIsSingleton() { var services = new ServiceCollection(); - services.AddDaprJobsClient(options => { }, ServiceLifetime.Singleton); + services.AddDaprJobsClient((_, _) => { }, ServiceLifetime.Singleton); var serviceProvider = services.BuildServiceProvider(); var daprJobsClient1 = serviceProvider.GetService(); @@ -100,7 +133,7 @@ public async Task RegisterJobsClient_ShouldRegisterScoped_WhenLifetimeIsScoped() { var services = new ServiceCollection(); - services.AddDaprJobsClient(options => { }, ServiceLifetime.Scoped); + services.AddDaprJobsClient((_, _) => { }, ServiceLifetime.Scoped); var serviceProvider = services.BuildServiceProvider(); await using var scope1 = serviceProvider.CreateAsyncScope(); @@ -119,7 +152,7 @@ public void RegisterJobsClient_ShouldRegisterTransient_WhenLifetimeIsTransient() { var services = new ServiceCollection(); - services.AddDaprJobsClient(options => { }, ServiceLifetime.Transient); + services.AddDaprJobsClient((_, _) => { }, ServiceLifetime.Transient); var serviceProvider = services.BuildServiceProvider(); var daprJobsClient1 = serviceProvider.GetService(); diff --git a/test/Dapr.Messaging.Test/Dapr.Messaging.Test.csproj b/test/Dapr.Messaging.Test/Dapr.Messaging.Test.csproj index 8f39e1713..0b10230f7 100644 --- a/test/Dapr.Messaging.Test/Dapr.Messaging.Test.csproj +++ b/test/Dapr.Messaging.Test/Dapr.Messaging.Test.csproj @@ -25,6 +25,7 @@ + diff --git a/test/Dapr.Messaging.Test/Extensions/PublishSubscribeServiceCollectionExtensionsTests.cs b/test/Dapr.Messaging.Test/Extensions/PublishSubscribeServiceCollectionExtensionsTests.cs index d239fb86d..d8e218d52 100644 --- a/test/Dapr.Messaging.Test/Extensions/PublishSubscribeServiceCollectionExtensionsTests.cs +++ b/test/Dapr.Messaging.Test/Extensions/PublishSubscribeServiceCollectionExtensionsTests.cs @@ -1,20 +1,90 @@ -using Dapr.Messaging.PublishSubscribe; +// ------------------------------------------------------------------------ +// 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.Messaging.PublishSubscribe; using Dapr.Messaging.PublishSubscribe.Extensions; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Moq; namespace Dapr.Messaging.Test.Extensions; public sealed class PublishSubscribeServiceCollectionExtensionsTests { [Fact] - public void AddDaprPubSubClient_RegistersIHttpClientFactory() + public void AddDaprMessagingClient_FromIConfiguration() { + const string apiToken = "abc123"; + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + {"DAPR_API_TOKEN", apiToken } + }) + .Build(); + var services = new ServiceCollection(); + services.AddSingleton(configuration); + + services.AddDaprPubSubClient(); + + var app = services.BuildServiceProvider(); + + var pubSubClient = app.GetRequiredService() as DaprPublishSubscribeGrpcClient; + + Assert.NotNull(pubSubClient!); + Assert.Equal(apiToken, pubSubClient.DaprApiToken); + } + + [Fact] + public void AddDaprPubSubClient_RegistersIHttpClientFactory() + { + var services = new ServiceCollection(); services.AddDaprPubSubClient(); var serviceProvider = services.BuildServiceProvider(); + var daprClient = serviceProvider.GetService(); + Assert.NotNull(daprClient); + } + + [Fact] + public void AddDaprPubSubClient_CallsConfigureAction() + { + var services = new ServiceCollection(); + + var configureCalled = false; + + services.AddDaprPubSubClient(Configure); + + var serviceProvider = services.BuildServiceProvider(); + var daprClient = serviceProvider.GetService(); + Assert.NotNull(daprClient); + Assert.True(configureCalled); + return; + + void Configure(IServiceProvider sp, DaprPublishSubscribeClientBuilder builder) + { + configureCalled = true; + } + } + [Fact] + public void AddDaprPubSubClient_RegistersServicesCorrectly() + { + var services = new ServiceCollection(); + services.AddDaprPubSubClient(); + var serviceProvider = services.BuildServiceProvider(); + var httpClientFactory = serviceProvider.GetService(); Assert.NotNull(httpClientFactory); diff --git a/test/Dapr.Messaging.Test/PublishSubscribe/MessageHandlingPolicyTest.cs b/test/Dapr.Messaging.Test/PublishSubscribe/MessageHandlingPolicyTest.cs index 0efb5e879..6efdd6397 100644 --- a/test/Dapr.Messaging.Test/PublishSubscribe/MessageHandlingPolicyTest.cs +++ b/test/Dapr.Messaging.Test/PublishSubscribe/MessageHandlingPolicyTest.cs @@ -1,4 +1,17 @@ -using Dapr.Messaging.PublishSubscribe; +// ------------------------------------------------------------------------ +// 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.Messaging.PublishSubscribe; namespace Dapr.Messaging.Test.PublishSubscribe { diff --git a/test/Dapr.Messaging.Test/PublishSubscribe/PublishSubscribeReceiverTests.cs b/test/Dapr.Messaging.Test/PublishSubscribe/PublishSubscribeReceiverTests.cs new file mode 100644 index 000000000..f8070aa66 --- /dev/null +++ b/test/Dapr.Messaging.Test/PublishSubscribe/PublishSubscribeReceiverTests.cs @@ -0,0 +1,206 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System.Threading.Channels; +using Dapr.AppCallback.Autogen.Grpc.v1; +using Dapr.Messaging.PublishSubscribe; +using Grpc.Core; +using Moq; +using P = Dapr.Client.Autogen.Grpc.v1; + +namespace Dapr.Messaging.Test.PublishSubscribe; + +public class PublishSubscribeReceiverTests +{ + [Fact] + public void SubscribeAsync_ShouldNotBlock() + { + const string pubSubName = "testPubSub"; + const string topicName = "testTopic"; + var options = + new DaprSubscriptionOptions(new MessageHandlingPolicy(TimeSpan.FromSeconds(5), TopicResponseAction.Success)) + { + MaximumQueuedMessages = 100, MaximumCleanupTimeout = TimeSpan.FromSeconds(1) + }; + + var messageHandler = new TopicMessageHandler((message, token) => Task.FromResult(TopicResponseAction.Success)); + + //Mock the daprClient + var mockDaprClient = new Mock(); + + //Create a mock AsyncDuplexStreamingCall + var mockRequestStream = new Mock>(); + var mockResponseStream = new Mock>(); + var mockCall = + new AsyncDuplexStreamingCall( + mockRequestStream.Object, mockResponseStream.Object, Task.FromResult(new Metadata()), + () => new Status(), () => new Metadata(), () => { }); + + //Setup the mock to return the mock call + mockDaprClient.Setup(client => + client.SubscribeTopicEventsAlpha1(null, null, It.IsAny())) + .Returns(mockCall); + + var receiver = new PublishSubscribeReceiver(pubSubName, topicName, options, messageHandler, mockDaprClient.Object); + var stopwatch = System.Diagnostics.Stopwatch.StartNew(); + var subscribeTask = receiver.SubscribeAsync(); + stopwatch.Stop(); + + Assert.True(stopwatch.ElapsedMilliseconds < 100, "SubscribeAsync should return immediately and not block"); + } + + [Fact] + public void Constructor_ShouldInitializeCorrectly() + { + const string pubSubName = "testPubSub"; + const string topicName = "testTopic"; + var options = + new DaprSubscriptionOptions(new MessageHandlingPolicy(TimeSpan.FromSeconds(5), TopicResponseAction.Success)) + { + MaximumQueuedMessages = 100, MaximumCleanupTimeout = TimeSpan.FromSeconds(1) + }; + + var messageHandler = new TopicMessageHandler((message, token) => Task.FromResult(TopicResponseAction.Success)); + + //Mock the daprClient + var mockDaprClient = new Mock(); + var receiver = + new PublishSubscribeReceiver(pubSubName, topicName, options, messageHandler, mockDaprClient.Object); + Assert.NotNull(receiver); + } + + [Fact] + public async Task ProcessTopicChannelMessagesAsync_ShouldProcessMessages() + { + const string pubSubName = "testPubSub"; + const string topicName = "testTopic"; + var options = + new DaprSubscriptionOptions(new MessageHandlingPolicy(TimeSpan.FromSeconds(5), TopicResponseAction.Success)) + { + MaximumQueuedMessages = 100, MaximumCleanupTimeout = TimeSpan.FromSeconds(1) + }; + + // Mock the message handler + var mockMessageHandler = new Mock(); + mockMessageHandler + .Setup(handler => handler(It.IsAny(), It.IsAny())) + .ReturnsAsync(TopicResponseAction.Success); + + //Mock the daprClient + var mockDaprClient = new Mock(); + // Create a mock AsyncDuplexStreamingCall + var mockRequestStream = new Mock>(); + var mockResponseStream = new Mock>(); + var mockCall = new AsyncDuplexStreamingCall( + mockRequestStream.Object, mockResponseStream.Object, Task.FromResult(new Metadata()), () => new Status(), () => new Metadata(), () => { }); + + //Set up the mock to return the mock call + mockDaprClient.Setup(client => client.SubscribeTopicEventsAlpha1(null, null, It.IsAny())) + .Returns(mockCall); + var receiver = new PublishSubscribeReceiver(pubSubName, topicName, options, mockMessageHandler.Object, mockDaprClient.Object); + + await receiver.SubscribeAsync(); + + //Write a message to the channel + var message = new TopicMessage("id", "source", "type", "specVersion", "dataContentType", topicName, pubSubName); + await receiver.WriteMessageToChannelAsync(message); + + //Allow some time for the message to be processed + await Task.Delay(100); + + mockMessageHandler.Verify(handler => handler(It.IsAny(), It.IsAny()), + Times.Once); + } + + [Fact] + public async Task SubscribeAsync_ShouldProcessAcknowledgements() + { + const string pubSubName = "testPubSub"; + const string topicName = "testTopic"; + var options = new DaprSubscriptionOptions(new MessageHandlingPolicy(TimeSpan.FromSeconds(30), TopicResponseAction.Success)) + { + MaximumQueuedMessages = 100 // Example value, adjust as needed + }; + + // Mock the message handler + var mockMessageHandler = new Mock(); + mockMessageHandler + .Setup(handler => handler(It.IsAny(), It.IsAny())) + .ReturnsAsync(TopicResponseAction.Success); + + // Mock the DaprClient + var mockDaprClient = new Mock(); + + // Create a mock AsyncDuplexStreamingCall + var mockRequestStream = new Mock>(); + var mockResponseStream = new Mock>(); + var mockCall = new AsyncDuplexStreamingCall( + mockRequestStream.Object, mockResponseStream.Object, Task.FromResult(new Metadata()), () => new Status(), () => new Metadata(), () => { }); + + // Setup the mock to return the mock call + mockDaprClient.Setup(client => client.SubscribeTopicEventsAlpha1(null, null, It.IsAny())) + .Returns(mockCall); + + var receiver = new PublishSubscribeReceiver(pubSubName, topicName, options, mockMessageHandler.Object, mockDaprClient.Object); + + await receiver.SubscribeAsync(); + + // Use reflection to access the private acknowledgementsChannel and write an acknowledgement + var acknowledgementsChannelField = typeof(PublishSubscribeReceiver).GetField("acknowledgementsChannel", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + if (acknowledgementsChannelField is null) + Assert.Fail(); + var acknowledgementsChannel = (Channel)acknowledgementsChannelField.GetValue(receiver)!; + + var acknowledgement = new PublishSubscribeReceiver.TopicAcknowledgement("id", TopicEventResponse.Types.TopicEventResponseStatus.Success); + await acknowledgementsChannel.Writer.WriteAsync(acknowledgement); + + // Allow some time for the acknowledgement to be processed + await Task.Delay(100); + + // Verify that the request stream's WriteAsync method was called twice (initial request + acknowledgement) + mockRequestStream.Verify(stream => stream.WriteAsync(It.IsAny(), It.IsAny()), Times.Exactly(2)); + } + + [Fact] + public async Task DisposeAsync_ShouldCompleteChannels() + { + const string pubSubName = "testPubSub"; + const string topicName = "testTopic"; + var options = + new DaprSubscriptionOptions(new MessageHandlingPolicy(TimeSpan.FromSeconds(5), TopicResponseAction.Success)) + { + MaximumQueuedMessages = 100, MaximumCleanupTimeout = TimeSpan.FromSeconds(1) + }; + + var messageHandler = new TopicMessageHandler((message, topic) => Task.FromResult(TopicResponseAction.Success)); + var daprClient = new Mock(); + var receiver = new PublishSubscribeReceiver(pubSubName, topicName, options, messageHandler, daprClient.Object); + + await receiver.DisposeAsync(); + + Assert.True(receiver.TopicMessagesChannelCompletion.IsCompleted); + Assert.True(receiver.AcknowledgementsChannelCompletion.IsCompleted); + } + + [Fact] + public void HandleTaskCompletion_ShouldThrowException_WhenTaskHasException() + { + var task = Task.FromException(new InvalidOperationException("Test exception")); + + var exception = Assert.Throws(() => + PublishSubscribeReceiver.HandleTaskCompletion(task, null)); + + Assert.IsType(exception.InnerException); + Assert.Equal("Test exception", exception.InnerException.Message); + } +}