From d4f9a5819899f17b39c4050546b0b9de61e1957c Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sun, 16 Mar 2025 23:49:22 -0500 Subject: [PATCH 01/22] Drop .NET 6 and .NET 7 support (#1482) * Updated Directory.Build.props to drop .NET 6 and .NET 7 targets * Removed explicit .NET targets in favor of using Directory.Build.props * Added Directory.Build.props for all examples, updated examples to favor targeting this file instead * Updated README to include missing packages and reflect a few modernization updates (including the new target versions) * Fixed file numbering in doc * Fixed comment on global.json to reflect use of the 9.0.100 minimum SDK version * Removed .net 6 and 7 targets from GitHub build script * Updated integration test build script to remove .NET 7 and .NET 6 targets Signed-off-by: Whit Waldo --- .github/workflows/itests.yml | 18 +++++------------- .github/workflows/sdk_build.yml | 16 ++++------------ CONTRIBUTING.md | 12 ++++++------ README.md | 13 +++++++++++-- .../ConversationalAI/ConversationalAI.csproj | 1 - examples/Actor/ActorClient/ActorClient.csproj | 1 - examples/Actor/DemoActor/DemoActor.csproj | 4 ---- examples/Actor/IDemoActor/IDemoActor.csproj | 4 ---- .../ControllerSample/ControllerSample.csproj | 4 ---- .../GrpcServiceSample/GrpcServiceSample.csproj | 1 - .../RoutingSample/RoutingSample.csproj | 4 ---- ...cretStoreConfigurationProviderSample.csproj | 1 - .../ConfigurationApi/ConfigurationApi.csproj | 6 ------ .../Client/Cryptography/Cryptography.csproj | 1 - .../DistributedLock/DistributedLock.csproj | 7 ------- .../BulkPublishEventExample.csproj | 1 - .../PublishEventExample.csproj | 1 - .../StreamingSubscriptionExample.csproj | 1 - .../ServiceInvocation/ServiceInvocation.csproj | 1 - .../StateManagement/StateManagement.csproj | 1 - examples/Directory.Build.props | 1 + .../ActorClient/ActorClient.csproj | 2 -- .../ActorCommon/ActorCommon.csproj | 2 -- .../ActorService/ActorService.csproj | 2 -- examples/Jobs/JobsSample/JobsSample.csproj | 1 - .../WorkflowAsyncOperations.csproj | 1 - .../WorkflowConsoleApp.csproj | 1 - .../WorkflowExternalInteraction.csproj | 1 - .../WorkflowFanOutFanIn.csproj | 1 - .../WorkflowMonitor/WorkflowMonitor.csproj | 1 - .../WorkflowSubworkflow.csproj | 1 - .../WorkflowTaskChaining.csproj | 1 - .../WorkflowUnitTest/WorkflowUnitTest.csproj | 1 - global.json | 2 +- src/Dapr.AI/Dapr.AI.csproj | 1 - src/Dapr.Common/Dapr.Common.csproj | 1 - src/Dapr.Protos/Dapr.Protos.csproj | 1 - src/Dapr.Workflow/Dapr.Workflow.csproj | 4 ---- src/Directory.Build.props | 2 +- test/Directory.Build.props | 2 +- 40 files changed, 30 insertions(+), 97 deletions(-) diff --git a/.github/workflows/itests.yml b/.github/workflows/itests.yml index 0c96fa9c..2ebbd04e 100644 --- a/.github/workflows/itests.yml +++ b/.github/workflows/itests.yml @@ -20,18 +20,8 @@ jobs: strategy: fail-fast: false matrix: - dotnet-version: ['6.0', '7.0', '8.0', '9.0'] + dotnet-version: ['8.0', '9.0'] include: - - dotnet-version: '6.0' - display-name: '.NET 6.0' - framework: 'net6' - prefix: 'net6' - install-version: '6.0.x' - - dotnet-version: '7.0' - display-name: '.NET 7.0' - framework: 'net7' - prefix: 'net7' - install-version: '7.0.x' - dotnet-version: '8.0' display-name: '.NET 8.0' framework: 'net8' @@ -50,7 +40,7 @@ jobs: GOPROXY: https://proxy.golang.org DAPR_CLI_VER: 1.15.0 DAPR_RUNTIME_VER: 1.15.3 - DAPR_INSTALL_URL: https://raw.githubusercontent.com/dapr/cli/release-1.14/install/install.sh + DAPR_INSTALL_URL: https://raw.githubusercontent.com/dapr/cli/release-1.15/install/install.sh DAPR_CLI_REF: '' steps: - name: Set up Dapr CLI @@ -124,9 +114,11 @@ jobs: with: dotnet-version: '9.0.x' dotnet-quality: 'ga' + - name: Restore dependencies + run: dotnet restore - name: Build # disable deterministic builds, just for test run. Deterministic builds break coverage for some reason - run: dotnet build --configuration release /p:GITHUB_ACTIONS=false + run: dotnet build --configuration release --no-restore /p:GITHUB_ACTIONS=false - name: Run General Tests id: tests continue-on-error: true # proceed if tests fail, the report step will report the failure with more details. diff --git a/.github/workflows/sdk_build.yml b/.github/workflows/sdk_build.yml index b6e26353..7fc5eb67 100644 --- a/.github/workflows/sdk_build.yml +++ b/.github/workflows/sdk_build.yml @@ -28,8 +28,10 @@ jobs: with: dotnet-version: 9.0.x dotnet-quality: 'ga' + - name: Restore dependencies + run: dotnet restore - name: Build - run: dotnet build --configuration release + run: dotnet build --configuration release --no-restore - name: Generate Packages run: dotnet pack --configuration release - name: Upload packages @@ -44,18 +46,8 @@ jobs: strategy: fail-fast: false matrix: - dotnet-version: ['6.0', '7.0', '8.0', '9.0'] + dotnet-version: ['8.0', '9.0'] include: - - dotnet-version: '6.0' - display-name: '.NET 6.0' - framework: 'net6' - prefix: 'net6' - install-version: '6.0.x' - - dotnet-version: '7.0' - display-name: '.NET 7.0' - framework: 'net7' - prefix: 'net7' - install-version: '7.0.x' - dotnet-version: '8.0' display-name: '.NET 8.0' framework: 'net8' diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7712340a..041f58f6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -54,13 +54,13 @@ This section describes the guidelines for contributing code / docs to Dapr. All contributions come through pull requests. To submit a proposed change, we recommend following this workflow: 1. Make sure there's an issue (bug or proposal) raised, which sets the expectations for the contribution you are about to make. -1. Fork the relevant repo and create a new branch -1. Create your change +2. Fork the relevant repo and create a new branch +3. Create your change - Code changes require tests -1. Update relevant documentation for the change -1. Commit and open a PR -1. Wait for the CI process to finish and make sure all checks are green -1. A maintainer of the project will be assigned, and you can expect a review within a few days +4. Update relevant documentation for the change +5. Commit and open a PR +6. Wait for the CI process to finish and make sure all checks are green +7. A maintainer of the project will be assigned, and you can expect a review within a few days #### Use work-in-progress PRs for early feedback diff --git a/README.md b/README.md index 948516fe..faec504b 100644 --- a/README.md +++ b/README.md @@ -44,12 +44,21 @@ This repo builds the following packages: - Dapr.AspNetCore - Dapr.Actors - Dapr.Actors.AspNetCore +- Dapr.Actors.Generators +- Dapr.AI +- Dapr.Jobs +- Dapr.Messaging - Dapr.Extensions.Configuration - Dapr.Workflow +It also builds the following packages which are not intended for public use and contain common types used in the packages above: +- Dapr.Common +- Dapr.Protos + + ### Prerequisites -Each project is a normal C# project. At minimum, you need [.NET 6.0 SDK](https://dotnet.microsoft.com/download/dotnet/6.0) to build, test, and generate NuGet packages. +Each project is a normal C# project. At minimum, you need [.NET 8.0 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) to build, test, and generate NuGet packages. Also make sure to reference the [.NET SDK contribution guide](https://docs.dapr.io/contributing/sdk-contrib/dotnet-contributing/) @@ -59,7 +68,7 @@ On macOS or Linux we recommend [Visual Studio Code](https://code.visualstudio.co **Windows:** -On Windows, we recommend installing [the latest Visual Studio 2019](https://www.visualstudio.com/vs/) which will set you up with all the .NET build tools and allow you to open the solution files. Community Edition is free and can be used to build everything here. +On Windows, we recommend installing [the latest Visual Studio 2022](https://www.visualstudio.com/vs/) which will set you up with all the .NET build tools and allow you to open the solution files. Community Edition is free and can be used to build everything here. Make sure you [update Visual Studio to the most recent release](https://docs.microsoft.com/visualstudio/install/update-visual-studio). diff --git a/examples/AI/ConversationalAI/ConversationalAI.csproj b/examples/AI/ConversationalAI/ConversationalAI.csproj index 976265a5..182363d2 100644 --- a/examples/AI/ConversationalAI/ConversationalAI.csproj +++ b/examples/AI/ConversationalAI/ConversationalAI.csproj @@ -1,7 +1,6 @@  - net8.0 enable enable diff --git a/examples/Actor/ActorClient/ActorClient.csproj b/examples/Actor/ActorClient/ActorClient.csproj index 0d1d94f5..43aa659b 100644 --- a/examples/Actor/ActorClient/ActorClient.csproj +++ b/examples/Actor/ActorClient/ActorClient.csproj @@ -2,7 +2,6 @@ Exe - net6 diff --git a/examples/Actor/DemoActor/DemoActor.csproj b/examples/Actor/DemoActor/DemoActor.csproj index 24a42ee0..4ed29d66 100644 --- a/examples/Actor/DemoActor/DemoActor.csproj +++ b/examples/Actor/DemoActor/DemoActor.csproj @@ -1,9 +1,5 @@  - - net6 - - true true diff --git a/examples/Actor/IDemoActor/IDemoActor.csproj b/examples/Actor/IDemoActor/IDemoActor.csproj index 9f774479..8b8a84b2 100644 --- a/examples/Actor/IDemoActor/IDemoActor.csproj +++ b/examples/Actor/IDemoActor/IDemoActor.csproj @@ -1,9 +1,5 @@  - - net6 - - diff --git a/examples/AspNetCore/ControllerSample/ControllerSample.csproj b/examples/AspNetCore/ControllerSample/ControllerSample.csproj index 6dbe750a..061510f9 100644 --- a/examples/AspNetCore/ControllerSample/ControllerSample.csproj +++ b/examples/AspNetCore/ControllerSample/ControllerSample.csproj @@ -1,9 +1,5 @@  - - net6 - - diff --git a/examples/AspNetCore/GrpcServiceSample/GrpcServiceSample.csproj b/examples/AspNetCore/GrpcServiceSample/GrpcServiceSample.csproj index 2319f6a5..5e1a27ed 100644 --- a/examples/AspNetCore/GrpcServiceSample/GrpcServiceSample.csproj +++ b/examples/AspNetCore/GrpcServiceSample/GrpcServiceSample.csproj @@ -1,7 +1,6 @@ - net6 true diff --git a/examples/AspNetCore/RoutingSample/RoutingSample.csproj b/examples/AspNetCore/RoutingSample/RoutingSample.csproj index 6dbe750a..061510f9 100644 --- a/examples/AspNetCore/RoutingSample/RoutingSample.csproj +++ b/examples/AspNetCore/RoutingSample/RoutingSample.csproj @@ -1,9 +1,5 @@  - - net6 - - diff --git a/examples/AspNetCore/SecretStoreConfigurationProviderSample/SecretStoreConfigurationProviderSample.csproj b/examples/AspNetCore/SecretStoreConfigurationProviderSample/SecretStoreConfigurationProviderSample.csproj index 01fbc207..ce47841b 100644 --- a/examples/AspNetCore/SecretStoreConfigurationProviderSample/SecretStoreConfigurationProviderSample.csproj +++ b/examples/AspNetCore/SecretStoreConfigurationProviderSample/SecretStoreConfigurationProviderSample.csproj @@ -2,7 +2,6 @@ Exe - net6 diff --git a/examples/Client/ConfigurationApi/ConfigurationApi.csproj b/examples/Client/ConfigurationApi/ConfigurationApi.csproj index 761ebb38..d74bcdd5 100644 --- a/examples/Client/ConfigurationApi/ConfigurationApi.csproj +++ b/examples/Client/ConfigurationApi/ConfigurationApi.csproj @@ -1,10 +1,4 @@ - - - net6 - - - diff --git a/examples/Client/Cryptography/Cryptography.csproj b/examples/Client/Cryptography/Cryptography.csproj index 525c3856..1a1b1eeb 100644 --- a/examples/Client/Cryptography/Cryptography.csproj +++ b/examples/Client/Cryptography/Cryptography.csproj @@ -2,7 +2,6 @@ Exe - net6.0 enable enable latest diff --git a/examples/Client/DistributedLock/DistributedLock.csproj b/examples/Client/DistributedLock/DistributedLock.csproj index 4c04fb90..6c001948 100644 --- a/examples/Client/DistributedLock/DistributedLock.csproj +++ b/examples/Client/DistributedLock/DistributedLock.csproj @@ -1,14 +1,7 @@ - - - - net6 - - - diff --git a/examples/Client/PublishSubscribe/BulkPublishEventExample/BulkPublishEventExample.csproj b/examples/Client/PublishSubscribe/BulkPublishEventExample/BulkPublishEventExample.csproj index b1e7647c..bccce505 100644 --- a/examples/Client/PublishSubscribe/BulkPublishEventExample/BulkPublishEventExample.csproj +++ b/examples/Client/PublishSubscribe/BulkPublishEventExample/BulkPublishEventExample.csproj @@ -2,7 +2,6 @@ Exe - net6 Samples.Client enable diff --git a/examples/Client/PublishSubscribe/PublishEventExample/PublishEventExample.csproj b/examples/Client/PublishSubscribe/PublishEventExample/PublishEventExample.csproj index 52b77a3e..93500fa1 100644 --- a/examples/Client/PublishSubscribe/PublishEventExample/PublishEventExample.csproj +++ b/examples/Client/PublishSubscribe/PublishEventExample/PublishEventExample.csproj @@ -2,7 +2,6 @@ Exe - net6 Samples.Client enable diff --git a/examples/Client/PublishSubscribe/StreamingSubscriptionExample/StreamingSubscriptionExample.csproj b/examples/Client/PublishSubscribe/StreamingSubscriptionExample/StreamingSubscriptionExample.csproj index 4ad620d0..77e84dbb 100644 --- a/examples/Client/PublishSubscribe/StreamingSubscriptionExample/StreamingSubscriptionExample.csproj +++ b/examples/Client/PublishSubscribe/StreamingSubscriptionExample/StreamingSubscriptionExample.csproj @@ -2,7 +2,6 @@ Exe - net6.0 enable enable diff --git a/examples/Client/ServiceInvocation/ServiceInvocation.csproj b/examples/Client/ServiceInvocation/ServiceInvocation.csproj index 7b165835..e292e4ae 100644 --- a/examples/Client/ServiceInvocation/ServiceInvocation.csproj +++ b/examples/Client/ServiceInvocation/ServiceInvocation.csproj @@ -2,7 +2,6 @@ Exe - net6 Samples.Client enable diff --git a/examples/Client/StateManagement/StateManagement.csproj b/examples/Client/StateManagement/StateManagement.csproj index 7b165835..e292e4ae 100644 --- a/examples/Client/StateManagement/StateManagement.csproj +++ b/examples/Client/StateManagement/StateManagement.csproj @@ -2,7 +2,6 @@ Exe - net6 Samples.Client enable diff --git a/examples/Directory.Build.props b/examples/Directory.Build.props index 00ec8e6c..95483b7a 100644 --- a/examples/Directory.Build.props +++ b/examples/Directory.Build.props @@ -2,6 +2,7 @@ + net8;net9 $(RepoRoot)bin\$(Configuration)\examples\$(MSBuildProjectName)\ diff --git a/examples/GeneratedActor/ActorClient/ActorClient.csproj b/examples/GeneratedActor/ActorClient/ActorClient.csproj index 88f75663..18969e41 100644 --- a/examples/GeneratedActor/ActorClient/ActorClient.csproj +++ b/examples/GeneratedActor/ActorClient/ActorClient.csproj @@ -2,8 +2,6 @@ Exe - net6 - 10.0 enable enable diff --git a/examples/GeneratedActor/ActorCommon/ActorCommon.csproj b/examples/GeneratedActor/ActorCommon/ActorCommon.csproj index 2cbc61e2..498e5e50 100644 --- a/examples/GeneratedActor/ActorCommon/ActorCommon.csproj +++ b/examples/GeneratedActor/ActorCommon/ActorCommon.csproj @@ -1,8 +1,6 @@ - net6 - 10.0 enable enable diff --git a/examples/GeneratedActor/ActorService/ActorService.csproj b/examples/GeneratedActor/ActorService/ActorService.csproj index a7410436..29b94ca7 100644 --- a/examples/GeneratedActor/ActorService/ActorService.csproj +++ b/examples/GeneratedActor/ActorService/ActorService.csproj @@ -1,8 +1,6 @@ - net6 - 10.0 enable enable diff --git a/examples/Jobs/JobsSample/JobsSample.csproj b/examples/Jobs/JobsSample/JobsSample.csproj index 4663d1d5..642c299e 100644 --- a/examples/Jobs/JobsSample/JobsSample.csproj +++ b/examples/Jobs/JobsSample/JobsSample.csproj @@ -1,7 +1,6 @@ - net8.0 enable enable diff --git a/examples/Workflow/WorkflowAsyncOperations/WorkflowAsyncOperations.csproj b/examples/Workflow/WorkflowAsyncOperations/WorkflowAsyncOperations.csproj index a1350fa7..be70a1b2 100644 --- a/examples/Workflow/WorkflowAsyncOperations/WorkflowAsyncOperations.csproj +++ b/examples/Workflow/WorkflowAsyncOperations/WorkflowAsyncOperations.csproj @@ -2,7 +2,6 @@ Exe - net6.0 enable enable diff --git a/examples/Workflow/WorkflowConsoleApp/WorkflowConsoleApp.csproj b/examples/Workflow/WorkflowConsoleApp/WorkflowConsoleApp.csproj index 25c03a41..9b38483c 100644 --- a/examples/Workflow/WorkflowConsoleApp/WorkflowConsoleApp.csproj +++ b/examples/Workflow/WorkflowConsoleApp/WorkflowConsoleApp.csproj @@ -6,7 +6,6 @@ Exe - net6 enable 612,618 diff --git a/examples/Workflow/WorkflowExternalInteraction/WorkflowExternalInteraction.csproj b/examples/Workflow/WorkflowExternalInteraction/WorkflowExternalInteraction.csproj index 4aae25c4..59d76d46 100644 --- a/examples/Workflow/WorkflowExternalInteraction/WorkflowExternalInteraction.csproj +++ b/examples/Workflow/WorkflowExternalInteraction/WorkflowExternalInteraction.csproj @@ -2,7 +2,6 @@ Exe - net6.0 enable enable diff --git a/examples/Workflow/WorkflowFanOutFanIn/WorkflowFanOutFanIn.csproj b/examples/Workflow/WorkflowFanOutFanIn/WorkflowFanOutFanIn.csproj index af3a1b2c..ba92ee6e 100644 --- a/examples/Workflow/WorkflowFanOutFanIn/WorkflowFanOutFanIn.csproj +++ b/examples/Workflow/WorkflowFanOutFanIn/WorkflowFanOutFanIn.csproj @@ -2,7 +2,6 @@ Exe - net8.0 enable enable diff --git a/examples/Workflow/WorkflowMonitor/WorkflowMonitor.csproj b/examples/Workflow/WorkflowMonitor/WorkflowMonitor.csproj index 91ded8af..ba92ee6e 100644 --- a/examples/Workflow/WorkflowMonitor/WorkflowMonitor.csproj +++ b/examples/Workflow/WorkflowMonitor/WorkflowMonitor.csproj @@ -2,7 +2,6 @@ Exe - net6.0 enable enable diff --git a/examples/Workflow/WorkflowSubworkflow/WorkflowSubworkflow.csproj b/examples/Workflow/WorkflowSubworkflow/WorkflowSubworkflow.csproj index af3a1b2c..ba92ee6e 100644 --- a/examples/Workflow/WorkflowSubworkflow/WorkflowSubworkflow.csproj +++ b/examples/Workflow/WorkflowSubworkflow/WorkflowSubworkflow.csproj @@ -2,7 +2,6 @@ Exe - net8.0 enable enable diff --git a/examples/Workflow/WorkflowTaskChaining/WorkflowTaskChaining.csproj b/examples/Workflow/WorkflowTaskChaining/WorkflowTaskChaining.csproj index 91ded8af..ba92ee6e 100644 --- a/examples/Workflow/WorkflowTaskChaining/WorkflowTaskChaining.csproj +++ b/examples/Workflow/WorkflowTaskChaining/WorkflowTaskChaining.csproj @@ -2,7 +2,6 @@ Exe - net6.0 enable enable diff --git a/examples/Workflow/WorkflowUnitTest/WorkflowUnitTest.csproj b/examples/Workflow/WorkflowUnitTest/WorkflowUnitTest.csproj index dec14a71..d0f391a2 100644 --- a/examples/Workflow/WorkflowUnitTest/WorkflowUnitTest.csproj +++ b/examples/Workflow/WorkflowUnitTest/WorkflowUnitTest.csproj @@ -1,7 +1,6 @@  - net6.0 enable false diff --git a/global.json b/global.json index 139cca3e..b86bbe43 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,5 @@ { - "_comment": "This policy allows the 8.0.100 SDK or patches in that family.", + "_comment": "This policy allows the 9.0.100 SDK or patches in that family.", "sdk": { "version": "9.0.100", "rollForward": "latestFeature" diff --git a/src/Dapr.AI/Dapr.AI.csproj b/src/Dapr.AI/Dapr.AI.csproj index 8220c5c4..87ae8bb8 100644 --- a/src/Dapr.AI/Dapr.AI.csproj +++ b/src/Dapr.AI/Dapr.AI.csproj @@ -1,7 +1,6 @@  - net6;net8 enable enable Dapr.AI diff --git a/src/Dapr.Common/Dapr.Common.csproj b/src/Dapr.Common/Dapr.Common.csproj index dd34d844..dac090d3 100644 --- a/src/Dapr.Common/Dapr.Common.csproj +++ b/src/Dapr.Common/Dapr.Common.csproj @@ -1,7 +1,6 @@  - net6;net7;net8;net9 enable enable diff --git a/src/Dapr.Protos/Dapr.Protos.csproj b/src/Dapr.Protos/Dapr.Protos.csproj index 5331f229..8a8804b2 100644 --- a/src/Dapr.Protos/Dapr.Protos.csproj +++ b/src/Dapr.Protos/Dapr.Protos.csproj @@ -1,7 +1,6 @@  - net6;net7;net8;net9 enable enable This package contains the reference protos used by develop services using Dapr. diff --git a/src/Dapr.Workflow/Dapr.Workflow.csproj b/src/Dapr.Workflow/Dapr.Workflow.csproj index f24d41e4..08859546 100644 --- a/src/Dapr.Workflow/Dapr.Workflow.csproj +++ b/src/Dapr.Workflow/Dapr.Workflow.csproj @@ -1,14 +1,10 @@  - - - net6;net7;net8;net9 enable Dapr.Workflow Dapr Workflow Authoring SDK Dapr Workflow SDK for building workflows as code with Dapr - 0.3.0 alpha diff --git a/src/Directory.Build.props b/src/Directory.Build.props index a74833a3..e7b1b91f 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -3,7 +3,7 @@ - net6;net8;net9 + net8;net9 $(RepoRoot)bin\$(Configuration)\prod\$(MSBuildProjectName)\ $(OutputPath)$(MSBuildProjectName).xml diff --git a/test/Directory.Build.props b/test/Directory.Build.props index e3a49b72..39b6b97e 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -2,7 +2,7 @@ - net6;net7;net8;net9 + net8;net9 $(RepoRoot)bin\$(Configuration)\test\$(MSBuildProjectName)\ From affa79775673fdb3264186678013c5322aba9c3f Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 17 Mar 2025 02:49:49 -0500 Subject: [PATCH 02/22] Additional commits to drop .NET 6 and 7 (#1486) * Updated all documentation referring to .NET 6 and .NET 7 to refer instead to .NET 8 and higher. * Updated minimum langversion to reflect C# 12 (released with .NET 8) Signed-off-by: Whit Waldo --- .../dotnet-sdk-contributing/dotnet-contributing.md | 13 ++----------- daprdocs/content/en/dotnet-sdk-docs/_index.md | 10 +--------- .../dotnet-actors/dotnet-actors-howto.md | 10 +--------- .../dotnet-ai/dotnet-ai-conversation-howto.md | 9 +-------- .../dotnet-development-dapr-aspire.md | 8 +++----- .../dotnet-jobs/dotnet-jobs-howto.md | 8 +------- .../dotnet-messaging-pubsub-howto.md | 8 +------- .../dotnet-workflow/dotnet-workflow-howto.md | 9 +-------- examples/Actor/README.md | 2 +- examples/AspNetCore/ControllerSample/README.md | 2 +- examples/AspNetCore/GrpcServiceSample/README.md | 2 +- examples/AspNetCore/RoutingSample/README.md | 2 +- .../README.md | 2 +- examples/Client/ConfigurationApi/README.md | 2 +- examples/Client/DistributedLock/README.md | 4 ++-- .../BulkPublishEventExample/README.md | 2 +- .../PublishSubscribe/PublishEventExample/README.md | 2 +- examples/Client/ServiceInvocation/README.md | 2 +- examples/Client/StateManagement/README.md | 2 +- examples/GeneratedActor/README.md | 2 +- examples/Workflow/README.md | 2 +- properties/dapr_managed_netcore.props | 2 +- 22 files changed, 26 insertions(+), 79 deletions(-) diff --git a/daprdocs/content/en/dotnet-sdk-contributing/dotnet-contributing.md b/daprdocs/content/en/dotnet-sdk-contributing/dotnet-contributing.md index f0378b43..81d2983c 100644 --- a/daprdocs/content/en/dotnet-sdk-contributing/dotnet-contributing.md +++ b/daprdocs/content/en/dotnet-sdk-contributing/dotnet-contributing.md @@ -101,17 +101,8 @@ squashing the PR locally and resubmitting to ensure that the sign-off statement # 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 | +supported .NET SDK. As of v1.16, this means that both .NET 8 and .NET 9 are supported. The latest language version available +is [C# version 12](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-version-history#c-version-12) 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 ce80b3ea..361cb06d 100644 --- a/daprdocs/content/en/dotnet-sdk-docs/_index.md +++ b/daprdocs/content/en/dotnet-sdk-docs/_index.md @@ -18,15 +18,7 @@ Dapr offers a variety of packages to help with the development of .NET applicati - [Dapr CLI]({{< ref install-dapr-cli.md >}}) installed - Initialized [Dapr environment]({{< ref install-dapr-selfhost.md >}}) -- [.NET 6](https://dotnet.microsoft.com/download), [.NET 8](https://dotnet.microsoft.com/download) or [.NET 9](https://dotnet.microsoft.com/download) installed - -{{% alert title="Note" color="primary" %}} - -Note that while .NET 6 is generally supported as the minimum .NET requirement across the Dapr .NET SDK packages -and .NET 7 is the minimally supported version of .NET by Dapr.Workflows in Dapr v1.15, only .NET 8 and .NET 9 will -continue to be supported by Dapr in v1.16 and later. - -{{% /alert %}} +- [.NET 8](https://dotnet.microsoft.com/download) or [.NET 9](https://dotnet.microsoft.com/download) installed ## Installation diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-actors/dotnet-actors-howto.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-actors/dotnet-actors-howto.md index aba62bf0..8c74f04d 100644 --- a/daprdocs/content/en/dotnet-sdk-docs/dotnet-actors/dotnet-actors-howto.md +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-actors/dotnet-actors-howto.md @@ -45,15 +45,7 @@ This project contains the implementation of the actor client which calls MyActor - [Dapr CLI]({{< ref install-dapr-cli.md >}}) installed. - Initialized [Dapr environment]({{< ref install-dapr-selfhost.md >}}). -- [.NET 6](https://dotnet.microsoft.com/download), [.NET 8](https://dotnet.microsoft.com/download) or [.NET 9](https://dotnet.microsoft.com/download) installed - -{{% alert title="Note" color="primary" %}} - -Note that while .NET 6 is generally supported as the minimum .NET requirement across the Dapr .NET SDK packages -and .NET 7 is the minimally supported version of .NET by Dapr.Workflows in Dapr v1.15, only .NET 8 and .NET 9 will -continue to be supported by Dapr in v1.16 and later. - -{{% /alert %}} +- [.NET 8](https://dotnet.microsoft.com/download) or [.NET 9](https://dotnet.microsoft.com/download) installed ## Step 0: Prepare 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 index 9d8d869d..8826f2af 100644 --- 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 @@ -7,17 +7,10 @@ description: Learn how to create and use the Dapr Conversational AI client using --- ## 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 +- [.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: 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 index 238a6d5a..953af088 100644 --- 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 @@ -24,13 +24,11 @@ Amazon AWS, deployment is currently outside the scope of this guide. More inform 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). +- Both the Dapr .NET SDK and .NET Aspire are 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 +- Install and initialize Dapr v1.16 or later ## Using .NET Aspire via CLI 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 be4d2705..6fe122c9 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 @@ -18,15 +18,9 @@ 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/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 +- [.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 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). 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 index b128d884..5b748b61 100644 --- 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 @@ -17,15 +17,9 @@ runtime and which do not require an endpoint to be pre-configured. In this guide ## 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 +- [.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). 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 e81efc34..dd5e2fb0 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,14 +20,7 @@ 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/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" %}} - -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 %}} +- [.NET 8](https://dotnet.microsoft.com/download/dotnet/8.0) or [.NET 9](https://dotnet.microsoft.com/download/dotnet/9.0) installed ## Set up the environment diff --git a/examples/Actor/README.md b/examples/Actor/README.md index 89b6bf0b..34cd496e 100644 --- a/examples/Actor/README.md +++ b/examples/Actor/README.md @@ -4,7 +4,7 @@ The Actor example shows how to create a virtual actor (`DemoActor`) and invoke i ## Prerequisites -- [.NET 6+](https://dotnet.microsoft.com/download) installed +- [.NET 8+](https://dotnet.microsoft.com/download) installed - [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/) - [Initialized Dapr environment](https://docs.dapr.io/getting-started/install-dapr-selfhost/) - [Dapr .NET SDK](https://github.com/dapr/dotnet-sdk/) diff --git a/examples/AspNetCore/ControllerSample/README.md b/examples/AspNetCore/ControllerSample/README.md index 3b2ca02b..cf2afc06 100644 --- a/examples/AspNetCore/ControllerSample/README.md +++ b/examples/AspNetCore/ControllerSample/README.md @@ -12,7 +12,7 @@ The application also registers for pub/sub with the `deposit`, `multideposit` an ## Prerequisitess -- [.NET 6+](https://dotnet.microsoft.com/download) installed +- [.NET 8+](https://dotnet.microsoft.com/download) installed - [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/) - [Initialized Dapr environment](https://docs.dapr.io/getting-started/install-dapr-selfhost/) - [Dapr .NET SDK](https://docs.dapr.io/developing-applications/sdks/dotnet/) diff --git a/examples/AspNetCore/GrpcServiceSample/README.md b/examples/AspNetCore/GrpcServiceSample/README.md index d08e96cd..4e0a615e 100644 --- a/examples/AspNetCore/GrpcServiceSample/README.md +++ b/examples/AspNetCore/GrpcServiceSample/README.md @@ -11,7 +11,7 @@ The application also registers for pub/sub with the `deposit` and `withdraw` top ## Prerequisitess -- [.NET 6+](https://dotnet.microsoft.com/download) installed +- [.NET 8+](https://dotnet.microsoft.com/download) installed - [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/) - [Initialized Dapr environment](https://docs.dapr.io/getting-started/install-dapr-selfhost/) - [Dapr .NET SDK](https://docs.dapr.io/developing-applications/sdks/dotnet/) diff --git a/examples/AspNetCore/RoutingSample/README.md b/examples/AspNetCore/RoutingSample/README.md index 901c51c4..8f486c45 100644 --- a/examples/AspNetCore/RoutingSample/README.md +++ b/examples/AspNetCore/RoutingSample/README.md @@ -12,7 +12,7 @@ The application also registers for pub/sub with the `deposit`, `multideposit`, a ## Prerequisites -- [.NET 6+](https://dotnet.microsoft.com/download) installed +- [.NET 8+](https://dotnet.microsoft.com/download) installed - [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/) - [Initialized Dapr environment](https://docs.dapr.io/getting-started/install-dapr-selfhost/) - [Dapr .NET SDK](https://docs.dapr.io/developing-applications/sdks/dotnet/) diff --git a/examples/AspNetCore/SecretStoreConfigurationProviderSample/README.md b/examples/AspNetCore/SecretStoreConfigurationProviderSample/README.md index a5d60c2f..2eb9b73f 100644 --- a/examples/AspNetCore/SecretStoreConfigurationProviderSample/README.md +++ b/examples/AspNetCore/SecretStoreConfigurationProviderSample/README.md @@ -2,7 +2,7 @@ ## Prerequisites -- [.NET 6+](https://dotnet.microsoft.com/download) installed +- [.NET 8+](https://dotnet.microsoft.com/download) installed - [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/) - [Initialized Dapr environment](https://docs.dapr.io/getting-started/install-dapr-selfhost/) - [Dapr .NET SDK](https://docs.dapr.io/developing-applications/sdks/dotnet/) diff --git a/examples/Client/ConfigurationApi/README.md b/examples/Client/ConfigurationApi/README.md index d73a29f9..bfc24dee 100644 --- a/examples/Client/ConfigurationApi/README.md +++ b/examples/Client/ConfigurationApi/README.md @@ -9,7 +9,7 @@ It demonstrates the following APIs: ## Prerequisites -- [.NET 6+](https://dotnet.microsoft.com/download) installed +- [.NET 8+](https://dotnet.microsoft.com/download) installed - [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/) - [Initialized Dapr environment](https://docs.dapr.io/getting-started/install-dapr-selfhost/) - [Dapr .NET SDK](https://docs.dapr.io/developing-applications/sdks/dotnet/) diff --git a/examples/Client/DistributedLock/README.md b/examples/Client/DistributedLock/README.md index 6a1af3b3..26098335 100644 --- a/examples/Client/DistributedLock/README.md +++ b/examples/Client/DistributedLock/README.md @@ -2,13 +2,13 @@ ## Prerequisites -- [.NET 6+](https://dotnet.microsoft.com/download) installed +- [.NET 8+](https://dotnet.microsoft.com/download) installed - [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/) - [Initialized Dapr environment](https://docs.dapr.io/getting-started/install-dapr-selfhost/) - [Dapr .NET SDK](https://docs.dapr.io/developing-applications/sdks/dotnet/) ## Distributed Lock API -Dapr 1.8 introduces the Distributed Lock API. This API can be used to prevent multiple processes from accessing the same resource. In Dapr, locks are scoped to a specific App ID. +Dapr 1.8 introduced the Distributed Lock API. This API can be used to prevent multiple processes from accessing the same resource. In Dapr, locks are scoped to a specific App ID. For this example, we will be running multiple instances of the same application to demonstrate an event driven consumer pattern. This example also includes a simple generator that creates some data that can be processed. diff --git a/examples/Client/PublishSubscribe/BulkPublishEventExample/README.md b/examples/Client/PublishSubscribe/BulkPublishEventExample/README.md index 39d206fa..652e59d0 100644 --- a/examples/Client/PublishSubscribe/BulkPublishEventExample/README.md +++ b/examples/Client/PublishSubscribe/BulkPublishEventExample/README.md @@ -2,7 +2,7 @@ ## Prerequisites -- [.NET 6+](https://dotnet.microsoft.com/download) installed +- [.NET 8+](https://dotnet.microsoft.com/download) installed - [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/) - [Initialized Dapr environment](https://docs.dapr.io/getting-started/install-dapr-selfhost/) - [Dapr .NET SDK](https://docs.dapr.io/developing-applications/sdks/dotnet/) diff --git a/examples/Client/PublishSubscribe/PublishEventExample/README.md b/examples/Client/PublishSubscribe/PublishEventExample/README.md index 9f3af565..efc93143 100644 --- a/examples/Client/PublishSubscribe/PublishEventExample/README.md +++ b/examples/Client/PublishSubscribe/PublishEventExample/README.md @@ -2,7 +2,7 @@ ## Prerequisites -- [.NET 6+](https://dotnet.microsoft.com/download) installed +- [.NET 8+](https://dotnet.microsoft.com/download) installed - [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/) - [Initialized Dapr environment](https://docs.dapr.io/getting-started/install-dapr-selfhost/) - [Dapr .NET SDK](https://docs.dapr.io/developing-applications/sdks/dotnet/) diff --git a/examples/Client/ServiceInvocation/README.md b/examples/Client/ServiceInvocation/README.md index ede5a506..5bddd15e 100644 --- a/examples/Client/ServiceInvocation/README.md +++ b/examples/Client/ServiceInvocation/README.md @@ -2,7 +2,7 @@ ## Prerequisites -- [.NET 6+](https://dotnet.microsoft.com/download) installed +- [.NET 8+](https://dotnet.microsoft.com/download) installed - [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/) - [Initialized Dapr environment](https://docs.dapr.io/getting-started/install-dapr-selfhost/) - [Dapr .NET SDK](https://docs.dapr.io/developing-applications/sdks/dotnet/) diff --git a/examples/Client/StateManagement/README.md b/examples/Client/StateManagement/README.md index fb266a24..3344e43e 100644 --- a/examples/Client/StateManagement/README.md +++ b/examples/Client/StateManagement/README.md @@ -2,7 +2,7 @@ ## Prerequisites -- [.NET 6+](https://dotnet.microsoft.com/download) installed +- [.NET 8+](https://dotnet.microsoft.com/download) installed - [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/) - [Initialized Dapr environment](https://docs.dapr.io/getting-started/install-dapr-selfhost/) - [Dapr .NET SDK](https://docs.dapr.io/developing-applications/sdks/dotnet/) diff --git a/examples/GeneratedActor/README.md b/examples/GeneratedActor/README.md index cd595b30..fcda4aaa 100644 --- a/examples/GeneratedActor/README.md +++ b/examples/GeneratedActor/README.md @@ -4,7 +4,7 @@ An example of generating a strongly-typed actor client. ## Prerequisites -- [.NET 6+](https://dotnet.microsoft.com/download) installed +- [.NET 8+](https://dotnet.microsoft.com/download) installed - [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/) - [Initialized Dapr environment](https://docs.dapr.io/getting-started/install-dapr-selfhost/) - [Dapr .NET SDK](https://docs.dapr.io/developing-applications/sdks/dotnet/) diff --git a/examples/Workflow/README.md b/examples/Workflow/README.md index 1af85576..4bdfa67a 100644 --- a/examples/Workflow/README.md +++ b/examples/Workflow/README.md @@ -4,7 +4,7 @@ This Dapr workflow example shows how to create a Dapr workflow (`Workflow`) and ## Prerequisites -- [.NET 6+](https://dotnet.microsoft.com/download) installed +- [.NET 8+](https://dotnet.microsoft.com/download) installed - [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/) - [Initialized Dapr environment](https://docs.dapr.io/getting-started/install-dapr-selfhost/) - [Dapr .NET SDK](https://github.com/dapr/dotnet-sdk/) diff --git a/properties/dapr_managed_netcore.props b/properties/dapr_managed_netcore.props index 6e8c01bf..4dab746e 100644 --- a/properties/dapr_managed_netcore.props +++ b/properties/dapr_managed_netcore.props @@ -3,7 +3,7 @@ Debug - 10.0 + 12.0 true 4 false From 51cc2f5f6b6a4464735fb27bbc7dacdbc4f8f137 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 1 Apr 2025 05:47:50 -0500 Subject: [PATCH 03/22] Consolidate client registration implementation (#1503) * Implemented generic Dapr client builder method to use across all Dapr packages to reduce code duplication throughout. * Implemented changes to Dapr.AI to use generic client builder extension. * Updating IDaprClient to implement IDisposable instead of putting it on each client type * Modified Conversation client to utilize the GrpcClient pattern of the other clients for consistency. * Updated Dapr.Jobs to use generic builder extensions * Refactored Dapr.Messaging for consistency with other Dapr clients (e.g. implements IDisposable, primary constructors, use of generic service registration extension) * Tweak to use same import reference as other clients for consistency * Renamed to AddDaprConversationClient to remain consistent with 1.14 naming - no need to introduce a breaking change here. Signed-off-by: Whit Waldo --- .../Conversation/DaprConversationClient.cs | 75 ++------ .../DaprConversationClientBuilder.cs | 6 +- .../DaprConversationGrpcClient.cs | 95 ++++++++++ .../Extensions/DaprAiConversationBuilder.cs | 12 +- .../DaprAiConversationBuilderExtensions.cs | 46 +---- .../Extensions/IDaprAiConversationBuilder.cs | 4 +- src/Dapr.AI/DaprAIClient.cs | 30 +-- .../Extensions/IDaprAiServiceBuilder.cs | 9 +- src/Dapr.Common/Dapr.Common.csproj | 1 + src/Dapr.Common/DaprGenericClientBuilder.cs | 4 +- .../Extensions/DaprClientBuilderExtensions.cs | 82 ++++++++ src/Dapr.Common/IDaprClient.cs | 19 ++ src/Dapr.Common/IDaprServiceBuilder.cs | 27 +++ src/Dapr.Jobs/DaprJobsClient.cs | 28 ++- src/Dapr.Jobs/DaprJobsGrpcClient.cs | 34 +--- src/Dapr.Jobs/Extensions/DaprJobsBuilder.cs | 28 +++ .../DaprJobsServiceCollectionExtensions.cs | 47 +---- src/Dapr.Jobs/IDaprJobsBuilder.cs | 22 +++ src/Dapr.Messaging/IDaprMessagingBuilder.cs | 21 +++ .../DaprPublishSubscribeClient.cs | 49 ++++- .../DaprPublishSubscribeGrpcClient.cs | 55 ++---- .../Extensions/DaprPubSubBuilder.cs | 28 +++ ...ishSubscribeServiceCollectionExtensions.cs | 45 +---- .../PublishSubscribe/IDaprPubSubBuilder.cs | 19 ++ .../DaprConversationClientBuilderTest.cs | 2 +- ...DaprAiConversationBuilderExtensionsTest.cs | 12 +- test/Dapr.Common.Test/Dapr.Common.Test.csproj | 1 + .../DaprGenericClientBuilderTest.cs | 9 +- .../DaprClientBuilderExtensionsTests.cs | 178 ++++++++++++++++++ ...aprJobsServiceCollectionExtensionsTests.cs | 16 +- 30 files changed, 707 insertions(+), 297 deletions(-) create mode 100644 src/Dapr.AI/Conversation/DaprConversationGrpcClient.cs create mode 100644 src/Dapr.Common/Extensions/DaprClientBuilderExtensions.cs create mode 100644 src/Dapr.Common/IDaprClient.cs create mode 100644 src/Dapr.Common/IDaprServiceBuilder.cs create mode 100644 src/Dapr.Jobs/Extensions/DaprJobsBuilder.cs create mode 100644 src/Dapr.Jobs/IDaprJobsBuilder.cs create mode 100644 src/Dapr.Messaging/IDaprMessagingBuilder.cs create mode 100644 src/Dapr.Messaging/PublishSubscribe/Extensions/DaprPubSubBuilder.cs create mode 100644 src/Dapr.Messaging/PublishSubscribe/IDaprPubSubBuilder.cs create mode 100644 test/Dapr.Common.Test/Extensions/DaprClientBuilderExtensionsTests.cs diff --git a/src/Dapr.AI/Conversation/DaprConversationClient.cs b/src/Dapr.AI/Conversation/DaprConversationClient.cs index ed33e677..b081427b 100644 --- a/src/Dapr.AI/Conversation/DaprConversationClient.cs +++ b/src/Dapr.AI/Conversation/DaprConversationClient.cs @@ -11,16 +11,26 @@ // limitations under the License. // ------------------------------------------------------------------------ -using Dapr.Common; -using Dapr.Common.Extensions; -using P = Dapr.Client.Autogen.Grpc.v1; +using Autogenerated = Dapr.Client.Autogen.Grpc.v1.Dapr; namespace Dapr.AI.Conversation; /// +/// /// Used to interact with the Dapr conversation building block. +/// Use to create a or register +/// for use with dependency injection via +/// DaprJobsServiceCollectionExtensions.AddDaprJobsClient. +/// +/// +/// Implementations of implement because the +/// client accesses network resources. For best performance, create a single long-lived client instance +/// and share it for the lifetime of the application. This is done for you if created via the DI extensions. Avoid +/// creating a disposing a client instance for each operation that the application performs - this can lead to socket +/// exhaustion and other problems. +/// /// -public sealed class DaprConversationClient : DaprAIClient +public abstract class DaprConversationClient : DaprAIClient { /// /// The HTTP client used by the client for calling the Dapr runtime. @@ -42,7 +52,7 @@ public sealed class DaprConversationClient : DaprAIClient /// /// Property exposed for testing purposes. /// - internal P.Dapr.DaprClient Client { get; } + internal Autogenerated.DaprClient Client { get; } /// /// Used to initialize a new instance of a . @@ -50,7 +60,7 @@ public sealed class DaprConversationClient : DaprAIClient /// 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, + protected DaprConversationClient(Autogenerated.DaprClient client, HttpClient httpClient, string? daprApiToken = null) { @@ -67,54 +77,7 @@ public sealed class DaprConversationClient : DaprAIClient /// 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) - { - if (options.ConversationId 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, - Content = input.Content, - 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); - } + public abstract Task ConverseAsync(string daprConversationComponentName, + IReadOnlyList inputs, ConversationOptions? options = null, + CancellationToken cancellationToken = default); } diff --git a/src/Dapr.AI/Conversation/DaprConversationClientBuilder.cs b/src/Dapr.AI/Conversation/DaprConversationClientBuilder.cs index 5e0a0825..88cf3b2a 100644 --- a/src/Dapr.AI/Conversation/DaprConversationClientBuilder.cs +++ b/src/Dapr.AI/Conversation/DaprConversationClientBuilder.cs @@ -25,11 +25,11 @@ public sealed class DaprConversationClientBuilder : DaprGenericClientBuilder /// Used to initialize a new instance of the . /// - /// + /// An optional to configure the client with. public DaprConversationClientBuilder(IConfiguration? configuration = null) : base(configuration) { } - + /// /// Builds the client instance from the properties of the builder. /// @@ -41,6 +41,6 @@ public sealed class DaprConversationClientBuilder : DaprGenericClientBuilder +/// 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. +internal sealed class DaprConversationGrpcClient(Autogenerated.Dapr.DaprClient client, HttpClient httpClient, string? daprApiToken = null) : DaprConversationClient(client, httpClient, 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 Autogenerated.ConversationRequest + { + Name = daprConversationComponentName + }; + + if (options is not null) + { + if (options.ConversationId 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 Autogenerated.ConversationInput + { + ScrubPII = input.ScrubPII, + Content = input.Content, + 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); + } + + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + this.HttpClient.Dispose(); + } + } +} diff --git a/src/Dapr.AI/Conversation/Extensions/DaprAiConversationBuilder.cs b/src/Dapr.AI/Conversation/Extensions/DaprAiConversationBuilder.cs index 876d223b..4ba23625 100644 --- a/src/Dapr.AI/Conversation/Extensions/DaprAiConversationBuilder.cs +++ b/src/Dapr.AI/Conversation/Extensions/DaprAiConversationBuilder.cs @@ -18,18 +18,10 @@ namespace Dapr.AI.Conversation.Extensions; /// /// Used by the fluent registration builder to configure a Dapr AI conversational manager. /// -public sealed class DaprAiConversationBuilder : IDaprAiConversationBuilder +public sealed class DaprAiConversationBuilder(IServiceCollection services) : IDaprAiConversationBuilder { /// /// The registered services on the builder. /// - public IServiceCollection Services { get; } - - /// - /// Used to initialize a new . - /// - public DaprAiConversationBuilder(IServiceCollection services) - { - Services = services; - } + public IServiceCollection Services { get; } = services; } diff --git a/src/Dapr.AI/Conversation/Extensions/DaprAiConversationBuilderExtensions.cs b/src/Dapr.AI/Conversation/Extensions/DaprAiConversationBuilderExtensions.cs index 2f049a90..42bd9180 100644 --- a/src/Dapr.AI/Conversation/Extensions/DaprAiConversationBuilderExtensions.cs +++ b/src/Dapr.AI/Conversation/Extensions/DaprAiConversationBuilderExtensions.cs @@ -11,9 +11,8 @@ // limitations under the License. // ------------------------------------------------------------------------ -using Microsoft.Extensions.Configuration; +using Dapr.Common.Extensions; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; namespace Dapr.AI.Conversation.Extensions; @@ -23,42 +22,11 @@ namespace Dapr.AI.Conversation.Extensions; public static class DaprAiConversationBuilderExtensions { /// - /// Registers the necessary functionality for the Dapr AI conversation functionality. + /// 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); - } + public static IDaprAiConversationBuilder AddDaprConversationClient( + this IServiceCollection services, + Action? configure = null, + ServiceLifetime lifetime = ServiceLifetime.Singleton) => services + .AddDaprClient(configure, lifetime); } diff --git a/src/Dapr.AI/Conversation/Extensions/IDaprAiConversationBuilder.cs b/src/Dapr.AI/Conversation/Extensions/IDaprAiConversationBuilder.cs index 30d3822d..3afe6355 100644 --- a/src/Dapr.AI/Conversation/Extensions/IDaprAiConversationBuilder.cs +++ b/src/Dapr.AI/Conversation/Extensions/IDaprAiConversationBuilder.cs @@ -18,6 +18,4 @@ 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 -{ -} +public interface IDaprAiConversationBuilder : IDaprAiServiceBuilder; diff --git a/src/Dapr.AI/DaprAIClient.cs b/src/Dapr.AI/DaprAIClient.cs index a2fd2255..ae029ece 100644 --- a/src/Dapr.AI/DaprAIClient.cs +++ b/src/Dapr.AI/DaprAIClient.cs @@ -11,24 +11,32 @@ // limitations under the License. // ------------------------------------------------------------------------ -using Dapr.AI.Conversation; +using Dapr.Common; namespace Dapr.AI; /// /// The base implementation of a Dapr AI client. /// -public abstract class DaprAIClient +public abstract class DaprAIClient : IDaprClient { + private bool disposed; + + /// + public void Dispose() + { + if (!this.disposed) + { + Dispose(disposing: true); + this.disposed = true; + } + } + /// - /// Sends various inputs to the large language model via the Conversational building block on the Dapr sidecar. + /// Disposes the resources associated with the object. /// - /// 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); + /// true if called by a call to the Dispose method; otherwise false. + protected virtual void Dispose(bool disposing) + { + } } diff --git a/src/Dapr.AI/Extensions/IDaprAiServiceBuilder.cs b/src/Dapr.AI/Extensions/IDaprAiServiceBuilder.cs index 8a0a80c2..d7d84d1f 100644 --- a/src/Dapr.AI/Extensions/IDaprAiServiceBuilder.cs +++ b/src/Dapr.AI/Extensions/IDaprAiServiceBuilder.cs @@ -11,6 +11,7 @@ // limitations under the License. // ------------------------------------------------------------------------ +using Dapr.Common; using Microsoft.Extensions.DependencyInjection; namespace Dapr.AI.Extensions; @@ -18,10 +19,4 @@ namespace Dapr.AI.Extensions; /// /// Responsible for registering Dapr AI service functionality. /// -public interface IDaprAiServiceBuilder -{ - /// - /// The registered services on the builder. - /// - public IServiceCollection Services { get; } -} +public interface IDaprAiServiceBuilder : IDaprServiceBuilder; diff --git a/src/Dapr.Common/Dapr.Common.csproj b/src/Dapr.Common/Dapr.Common.csproj index dac090d3..03d23f9b 100644 --- a/src/Dapr.Common/Dapr.Common.csproj +++ b/src/Dapr.Common/Dapr.Common.csproj @@ -6,6 +6,7 @@ + diff --git a/src/Dapr.Common/DaprGenericClientBuilder.cs b/src/Dapr.Common/DaprGenericClientBuilder.cs index 7a7abf02..8ff59490 100644 --- a/src/Dapr.Common/DaprGenericClientBuilder.cs +++ b/src/Dapr.Common/DaprGenericClientBuilder.cs @@ -21,7 +21,7 @@ namespace Dapr.Common; /// /// Builder for building a generic Dapr client. /// -public abstract class DaprGenericClientBuilder where TClientBuilder : class +public abstract class DaprGenericClientBuilder where TClientBuilder : class, IDaprClient { /// /// Initializes a new instance of the class. @@ -186,7 +186,7 @@ public abstract class DaprGenericClientBuilder where TClientBuil /// /// The assembly the dependencies are being built for. /// - protected (GrpcChannel channel, HttpClient httpClient, Uri httpEndpoint, string daprApiToken) BuildDaprClientDependencies(Assembly assembly) + protected internal (GrpcChannel channel, HttpClient httpClient, Uri httpEndpoint, string daprApiToken) BuildDaprClientDependencies(Assembly assembly) { var grpcEndpoint = new Uri(this.GrpcEndpoint); if (grpcEndpoint.Scheme != "http" && grpcEndpoint.Scheme != "https") diff --git a/src/Dapr.Common/Extensions/DaprClientBuilderExtensions.cs b/src/Dapr.Common/Extensions/DaprClientBuilderExtensions.cs new file mode 100644 index 00000000..1070133c --- /dev/null +++ b/src/Dapr.Common/Extensions/DaprClientBuilderExtensions.cs @@ -0,0 +1,82 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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 Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Autogenerated = Dapr.Client.Autogen.Grpc.v1.Dapr; + +namespace Dapr.Common.Extensions; + +/// +/// Generic extension used to build out type-specific Dapr clients. +/// +internal static class DaprClientBuilderExtensions +{ + /// + /// Registers the necessary base functionality for a Dapr client. + /// + /// The type of the client builder interface. + /// The abstract Dapr client type being created. + /// The concrete Dapr client type being created. + /// The type of the static builder used to build the Dapr ot client. + /// The collection of services to which the Dapr client and associated services are being registered. + /// An optional method used to provide additional configurations to the client builder. + /// The registered lifetime of the Dapr client. + /// The collection of DI-registered services. + //internal static TBuilderInterface AddDaprClient( + internal static TServiceBuilder AddDaprClient( + this IServiceCollection services, + Action? configure = null, + ServiceLifetime lifetime = ServiceLifetime.Singleton) + where TClient : class, IDaprClient + where TConcreteClient : TClient + where TServiceBuilder : class, IDaprServiceBuilder + where TClientBuilder : DaprGenericClientBuilder + { + ArgumentNullException.ThrowIfNull(services, nameof(services)); + + //Ensure that TConcreteClient is a concrete class + if (typeof(TConcreteClient).IsInterface || typeof(TConcreteClient).IsAbstract) + { + throw new ArgumentException($"{typeof(TConcreteClient).Name} must be a concrete class", + nameof(TConcreteClient)); + } + + //Ensure that TServiceBuilder is a concrete class + if (typeof(TServiceBuilder).IsInterface || typeof(TServiceBuilder).IsAbstract) + { + throw new ArgumentException($"{typeof(TServiceBuilder).Name} must be a concrete class", + nameof(TServiceBuilder)); + } + + services.AddHttpClient(); + + var registration = new Func(provider => + { + var configuration = provider.GetService(); + var builder = (TClientBuilder)Activator.CreateInstance(typeof(TClientBuilder), configuration)!; + + builder.UseDaprApiToken(DaprDefaults.GetDefaultDaprApiToken(configuration)); + configure?.Invoke(provider, builder); + var (channel, httpClient, _, daprApiToken) = + builder.BuildDaprClientDependencies(Assembly.GetExecutingAssembly()); + var daprClient = new Autogenerated.DaprClient(channel); + return (TClient)Activator.CreateInstance(typeof(TConcreteClient), daprClient, httpClient, daprApiToken)!; + }); + + services.Add(new ServiceDescriptor(typeof(TClient), registration, lifetime)); + + return (TServiceBuilder)Activator.CreateInstance(typeof(TServiceBuilder), services)!; + } +} diff --git a/src/Dapr.Common/IDaprClient.cs b/src/Dapr.Common/IDaprClient.cs new file mode 100644 index 00000000..001cc17c --- /dev/null +++ b/src/Dapr.Common/IDaprClient.cs @@ -0,0 +1,19 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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.Common; + +/// +/// Base interface for any of the specific Dapr clients. +/// +public interface IDaprClient : IDisposable; diff --git a/src/Dapr.Common/IDaprServiceBuilder.cs b/src/Dapr.Common/IDaprServiceBuilder.cs new file mode 100644 index 00000000..18379d91 --- /dev/null +++ b/src/Dapr.Common/IDaprServiceBuilder.cs @@ -0,0 +1,27 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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.Common; + +/// +/// Responsible for registering Dapr services with dependency injection. +/// +public interface IDaprServiceBuilder +{ + /// + /// The registered services on the builder. + /// + public IServiceCollection Services { get; } +} diff --git a/src/Dapr.Jobs/DaprJobsClient.cs b/src/Dapr.Jobs/DaprJobsClient.cs index 4dd4abd7..2aecf816 100644 --- a/src/Dapr.Jobs/DaprJobsClient.cs +++ b/src/Dapr.Jobs/DaprJobsClient.cs @@ -11,8 +11,10 @@ // limitations under the License. // ------------------------------------------------------------------------ +using Dapr.Common; using Dapr.Jobs.Models; using Dapr.Jobs.Models.Responses; +using Autogenerated = Dapr.Client.Autogen.Grpc.v1.Dapr; namespace Dapr.Jobs; @@ -31,10 +33,34 @@ namespace Dapr.Jobs; /// exhaustion and other problems. /// /// -public abstract class DaprJobsClient : IDisposable +public abstract class DaprJobsClient(Autogenerated.DaprClient client, HttpClient httpClient, string? daprApiToken = null) : IDaprClient { private bool disposed; + /// + /// The HTTP client used by the client for calling the Dapr runtime. + /// + /// + /// Property exposed for testing purposes. + /// + internal readonly HttpClient HttpClient = httpClient; + + /// + /// The Dapr API token value. + /// + /// + /// Property exposed for testing purposes. + /// + internal readonly string? DaprApiToken = daprApiToken; + + /// + /// The autogenerated Dapr client. + /// + /// + /// Property exposed for testing purposes. + /// + internal Autogenerated.DaprClient Client { get; } = client; + /// /// Schedules a job with Dapr. /// diff --git a/src/Dapr.Jobs/DaprJobsGrpcClient.cs b/src/Dapr.Jobs/DaprJobsGrpcClient.cs index 8743aa35..182db48c 100644 --- a/src/Dapr.Jobs/DaprJobsGrpcClient.cs +++ b/src/Dapr.Jobs/DaprJobsGrpcClient.cs @@ -24,40 +24,8 @@ namespace Dapr.Jobs; /// /// A client for interacting with the Dapr endpoints. /// -internal sealed class DaprJobsGrpcClient : DaprJobsClient +internal sealed class DaprJobsGrpcClient(Autogenerated.Dapr.DaprClient client, HttpClient httpClient, string? daprApiToken = null) : DaprJobsClient(client, httpClient, daprApiToken: daprApiToken) { - /// - /// 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 Autogenerated.Dapr.DaprClient Client { get; } - - internal DaprJobsGrpcClient( - Autogenerated.Dapr.DaprClient innerClient, - HttpClient httpClient, - string? daprApiToken) - { - this.Client = innerClient; - this.HttpClient = httpClient; - this.DaprApiToken = daprApiToken; - } - /// /// Schedules a job with Dapr. /// diff --git a/src/Dapr.Jobs/Extensions/DaprJobsBuilder.cs b/src/Dapr.Jobs/Extensions/DaprJobsBuilder.cs new file mode 100644 index 00000000..51bdb898 --- /dev/null +++ b/src/Dapr.Jobs/Extensions/DaprJobsBuilder.cs @@ -0,0 +1,28 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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.Jobs.Extensions; + +/// +/// Used by the fluent registration builder to configure a Dapr Jobs client. +/// +/// +public sealed class DaprJobsBuilder(IServiceCollection services) : IDaprJobsBuilder +{ + /// + /// The registered services on the builder. + /// + public IServiceCollection Services { get; } = services; +} diff --git a/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs b/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs index 03540aae..7eed80ab 100644 --- a/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs +++ b/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs @@ -11,9 +11,8 @@ // limitations under the License. // ------------------------------------------------------------------------ -using Microsoft.Extensions.Configuration; +using Dapr.Common.Extensions; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; namespace Dapr.Jobs.Extensions; @@ -25,45 +24,13 @@ public static class DaprJobsServiceCollectionExtensions /// /// Adds Dapr Jobs client support to the service collection. /// - /// The . + /// 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) - - { - 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(configuration); - builder.UseHttpClientFactory(httpClientFactory); - - configure?.Invoke(serviceProvider, builder); - - return builder.Build(); - }); - - switch (lifetime) - { - case ServiceLifetime.Scoped: - serviceCollection.TryAddScoped(registration); - break; - case ServiceLifetime.Transient: - serviceCollection.TryAddTransient(registration); - break; - case ServiceLifetime.Singleton: - default: - serviceCollection.TryAddSingleton(registration); - break; - } - - return serviceCollection; - } + public static IDaprJobsBuilder AddDaprJobsClient( + this IServiceCollection services, + Action? configure = null, + ServiceLifetime lifetime = ServiceLifetime.Singleton) => + services.AddDaprClient(configure, lifetime); } diff --git a/src/Dapr.Jobs/IDaprJobsBuilder.cs b/src/Dapr.Jobs/IDaprJobsBuilder.cs new file mode 100644 index 00000000..d5211c5e --- /dev/null +++ b/src/Dapr.Jobs/IDaprJobsBuilder.cs @@ -0,0 +1,22 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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.DependencyInjection; + +namespace Dapr.Jobs; + +/// +/// Responsible for registering Dapr Jobs service functionality. +/// +public interface IDaprJobsBuilder : IDaprServiceBuilder; diff --git a/src/Dapr.Messaging/IDaprMessagingBuilder.cs b/src/Dapr.Messaging/IDaprMessagingBuilder.cs new file mode 100644 index 00000000..79e5bf3f --- /dev/null +++ b/src/Dapr.Messaging/IDaprMessagingBuilder.cs @@ -0,0 +1,21 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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; + +namespace Dapr.Messaging; + +/// +/// Provides a root builder for the Dapr Messaging operations facilitating a more fluent-style registration. +/// +public interface IDaprMessagingBuilder : IDaprServiceBuilder; diff --git a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClient.cs b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClient.cs index 8fbec2df..cbf25ea4 100644 --- a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClient.cs +++ b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClient.cs @@ -11,13 +11,42 @@ // limitations under the License. // ------------------------------------------------------------------------ +using Dapr.Common; +using P = Dapr.Client.Autogen.Grpc.v1; + namespace Dapr.Messaging.PublishSubscribe; /// /// The base implementation of a Dapr pub/sub client. /// -public abstract class DaprPublishSubscribeClient +public abstract class DaprPublishSubscribeClient(P.Dapr.DaprClient client, HttpClient httpClient, string? daprApiToken = null) : IDaprClient { + private bool disposed; + + /// + /// The HTTP client used by the client for calling the Dapr runtime. + /// + /// + /// Property exposed for testing purposes. + /// + internal protected readonly HttpClient HttpClient = httpClient; + + /// + /// The Dapr API token value. + /// + /// + /// Property exposed for testing purposes. + /// + internal protected readonly string? DaprApiToken = daprApiToken; + + /// + /// The autogenerated Dapr client. + /// + /// + /// Property exposed for testing purposes. + /// + internal protected readonly P.Dapr.DaprClient Client = client; + /// /// Dynamically subscribes to a Publish/Subscribe component and topic. /// @@ -28,4 +57,22 @@ public abstract class DaprPublishSubscribeClient /// Cancellation token. /// public abstract Task SubscribeAsync(string pubSubName, string topicName, DaprSubscriptionOptions options, TopicMessageHandler messageHandler, CancellationToken cancellationToken = default); + + /// + public void Dispose() + { + if (!this.disposed) + { + Dispose(disposing: true); + this.disposed = true; + } + } + + /// + /// Disposes the resources associated with the object. + /// + /// true if called by a call to the Dispose method; otherwise false. + protected virtual void Dispose(bool disposing) + { + } } diff --git a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs index 33ef0549..ace670df 100644 --- a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs +++ b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs @@ -18,40 +18,11 @@ namespace Dapr.Messaging.PublishSubscribe; /// /// A client for interacting with the Dapr endpoints. /// -internal sealed class DaprPublishSubscribeGrpcClient : DaprPublishSubscribeClient +internal sealed class DaprPublishSubscribeGrpcClient( + P.DaprClient client, + HttpClient httpClient, + string? daprApiToken = null) : DaprPublishSubscribeClient(client, httpClient, daprApiToken) { - /// - /// 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, HttpClient httpClient, string? daprApiToken) - { - this.Client = client; - this.HttpClient = httpClient; - this.DaprApiToken = daprApiToken; - } - /// /// Dynamically subscribes to a Publish/Subscribe component and topic. /// @@ -61,11 +32,25 @@ internal sealed class DaprPublishSubscribeGrpcClient : DaprPublishSubscribeClien /// The delegate reflecting the action to take upon messages received by the subscription. /// Cancellation token. /// - public override async Task SubscribeAsync(string pubSubName, string topicName, DaprSubscriptionOptions options, TopicMessageHandler messageHandler, CancellationToken cancellationToken = default) + public override async Task SubscribeAsync( + string pubSubName, + string topicName, + DaprSubscriptionOptions options, + TopicMessageHandler messageHandler, + CancellationToken cancellationToken = default) { - var receiver = new PublishSubscribeReceiver(pubSubName, topicName, options, messageHandler, this.Client); + var receiver = new PublishSubscribeReceiver(pubSubName, topicName, options, messageHandler, Client); await receiver.SubscribeAsync(cancellationToken); return receiver; } + + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + this.HttpClient.Dispose(); + } + } } diff --git a/src/Dapr.Messaging/PublishSubscribe/Extensions/DaprPubSubBuilder.cs b/src/Dapr.Messaging/PublishSubscribe/Extensions/DaprPubSubBuilder.cs new file mode 100644 index 00000000..2772df2f --- /dev/null +++ b/src/Dapr.Messaging/PublishSubscribe/Extensions/DaprPubSubBuilder.cs @@ -0,0 +1,28 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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.Messaging.PublishSubscribe.Extensions; + +/// +/// Used by the fluent registration builder to configure a Dapr Publish/Subscribe client. +/// +/// +public sealed class DaprPubSubBuilder(IServiceCollection services) : IDaprPubSubBuilder +{ + /// + /// The registered services on the builder. + /// + public IServiceCollection Services { get; } = services; +} diff --git a/src/Dapr.Messaging/PublishSubscribe/Extensions/PublishSubscribeServiceCollectionExtensions.cs b/src/Dapr.Messaging/PublishSubscribe/Extensions/PublishSubscribeServiceCollectionExtensions.cs index 3d9e3ee8..954940e5 100644 --- a/src/Dapr.Messaging/PublishSubscribe/Extensions/PublishSubscribeServiceCollectionExtensions.cs +++ b/src/Dapr.Messaging/PublishSubscribe/Extensions/PublishSubscribeServiceCollectionExtensions.cs @@ -1,6 +1,5 @@ -using Microsoft.Extensions.Configuration; +using Dapr.Common.Extensions; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; namespace Dapr.Messaging.PublishSubscribe.Extensions; @@ -16,40 +15,10 @@ public static class PublishSubscribeServiceCollectionExtensions /// Optionally allows greater configuration of the using injected services. /// The lifetime of the registered services. /// - public static IServiceCollection AddDaprPubSubClient(this IServiceCollection services, Action? configure = null, ServiceLifetime lifetime = ServiceLifetime.Singleton) - { - ArgumentNullException.ThrowIfNull(services, nameof(services)); - - //Register the IHttpClientFactory implementation - services.AddHttpClient(); - - var registration = new Func(serviceProvider => - { - var httpClientFactory = serviceProvider.GetRequiredService(); - var configuration = serviceProvider.GetService(); - - var builder = new DaprPublishSubscribeClientBuilder(configuration); - builder.UseHttpClientFactory(httpClientFactory); - - configure?.Invoke(serviceProvider, builder); - - return builder.Build(); - }); - - switch (lifetime) - { - case ServiceLifetime.Scoped: - services.TryAddScoped(registration); - break; - case ServiceLifetime.Transient: - services.TryAddTransient(registration); - break; - default: - case ServiceLifetime.Singleton: - services.TryAddSingleton(registration); - break; - } - - return services; - } + public static IDaprPubSubBuilder AddDaprPubSubClient( + this IServiceCollection services, + Action? configure = null, + ServiceLifetime lifetime = ServiceLifetime.Singleton) => + services.AddDaprClient( + configure, lifetime); } diff --git a/src/Dapr.Messaging/PublishSubscribe/IDaprPubSubBuilder.cs b/src/Dapr.Messaging/PublishSubscribe/IDaprPubSubBuilder.cs new file mode 100644 index 00000000..23e6df1f --- /dev/null +++ b/src/Dapr.Messaging/PublishSubscribe/IDaprPubSubBuilder.cs @@ -0,0 +1,19 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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.Messaging.PublishSubscribe; + +/// +/// Provides a Dapr client builder specific for Publish/Subscribe operations. +/// +public interface IDaprPubSubBuilder : IDaprMessagingBuilder; diff --git a/test/Dapr.AI.Test/Conversation/DaprConversationClientBuilderTest.cs b/test/Dapr.AI.Test/Conversation/DaprConversationClientBuilderTest.cs index 901c4b65..6546a080 100644 --- a/test/Dapr.AI.Test/Conversation/DaprConversationClientBuilderTest.cs +++ b/test/Dapr.AI.Test/Conversation/DaprConversationClientBuilderTest.cs @@ -28,6 +28,6 @@ public class DaprConversationClientBuilderTest // Assert Assert.NotNull(client); - Assert.IsType(client); + Assert.IsAssignableFrom(client); } } diff --git a/test/Dapr.AI.Test/Conversation/Extensions/DaprAiConversationBuilderExtensionsTest.cs b/test/Dapr.AI.Test/Conversation/Extensions/DaprAiConversationBuilderExtensionsTest.cs index 2ee32189..20f550f5 100644 --- a/test/Dapr.AI.Test/Conversation/Extensions/DaprAiConversationBuilderExtensionsTest.cs +++ b/test/Dapr.AI.Test/Conversation/Extensions/DaprAiConversationBuilderExtensionsTest.cs @@ -47,18 +47,16 @@ public class DaprAiConversationBuilderExtensionsTest } [Fact] - public void AddDaprConversationClient_RegistersDaprClientOnlyOnce() + public void AddDaprConversationClient_RegistersDaprClient_UsesMostRecentRegistration() { var services = new ServiceCollection(); - var clientBuilder = new Action((sp, builder) => + services.AddDaprConversationClient((_, builder) => { - builder.UseDaprApiToken("abc"); - }); - + builder.UseDaprApiToken("abc123"); + }); //Sets the API token value 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(); diff --git a/test/Dapr.Common.Test/Dapr.Common.Test.csproj b/test/Dapr.Common.Test/Dapr.Common.Test.csproj index 22120719..73037105 100644 --- a/test/Dapr.Common.Test/Dapr.Common.Test.csproj +++ b/test/Dapr.Common.Test/Dapr.Common.Test.csproj @@ -7,6 +7,7 @@ + all diff --git a/test/Dapr.Common.Test/DaprGenericClientBuilderTest.cs b/test/Dapr.Common.Test/DaprGenericClientBuilderTest.cs index d28b4005..a783a127 100644 --- a/test/Dapr.Common.Test/DaprGenericClientBuilderTest.cs +++ b/test/Dapr.Common.Test/DaprGenericClientBuilderTest.cs @@ -83,12 +83,19 @@ public class DaprGenericClientBuilderTest Assert.Equal(timeout, builder.Timeout); } - private class SampleDaprGenericClientBuilder : DaprGenericClientBuilder + private sealed class SampleDaprGenericClientBuilder : DaprGenericClientBuilder, IDaprClient { public override SampleDaprGenericClientBuilder Build() { // Implementation throw new NotImplementedException(); } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + } } } diff --git a/test/Dapr.Common.Test/Extensions/DaprClientBuilderExtensionsTests.cs b/test/Dapr.Common.Test/Extensions/DaprClientBuilderExtensionsTests.cs new file mode 100644 index 00000000..91659df7 --- /dev/null +++ b/test/Dapr.Common.Test/Extensions/DaprClientBuilderExtensionsTests.cs @@ -0,0 +1,178 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +#nullable enable +using System; +using System.Net.Http; +using Dapr.Common.Extensions; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Xunit; +using Autogenerated = Dapr.Client.Autogen.Grpc.v1; + +namespace Dapr.Common.Test.Extensions; + +public sealed class DaprClientBuilderExtensionsTests +{ + [Fact] + public void AddDaprClient_CallsConfigureAction() + { + var services = new ServiceCollection(); + var configurationMock = new Mock(); + var serviceProviderMock = new Mock(); + serviceProviderMock.Setup(sp => sp.GetService(typeof(IConfiguration))).Returns(configurationMock.Object); + + var configureCalled = false; + + services.AddDaprClient(( + _, + _) => + { + configureCalled = true; + }); + + var serviceProvider = services.BuildServiceProvider(); + var client = serviceProvider.GetRequiredService(); + + Assert.NotNull(client); + Assert.True(configureCalled); + } + + [Fact] + public void AddDaprClient_ThrowsIfServicesIsNull() + { +#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. + IServiceCollection services = null; +#pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type. + Assert.Throws(() => + // ReSharper disable once AssignNullToNotNullAttribute +#pragma warning disable CS8604 // Possible null reference argument. + services.AddDaprClient( +#pragma warning restore CS8604 // Possible null reference argument. + (_, _) => { }, ServiceLifetime.Singleton)); + } + + [Fact] + public void AddDaprClient_RegistersClientWithServiceCollection_Singleton() + { + var services = new ServiceCollection(); + var configurationMock = new Mock(); + var serviceProviderMock = new Mock(); + serviceProviderMock.Setup(sp => sp.GetService(typeof(IConfiguration))).Returns(configurationMock.Object); + + services.AddDaprClient(( + _, + _) => + { + }, ServiceLifetime.Singleton); + + var serviceProvider = services.BuildServiceProvider(); + var client1 = serviceProvider.GetRequiredService(); + var client2 = serviceProvider.GetRequiredService(); + + Assert.NotNull(client1); + Assert.NotNull(client2); + Assert.Same(client1, client2); //Singletons should return the same instance + } + + [Fact] + public void AddDaprClient_RegistersClientWithServiceCollection_Scoped() + { + var services = new ServiceCollection(); + var configurationMock = new Mock(); + var serviceProviderMock = new Mock(); + serviceProviderMock.Setup(sp => sp.GetService(typeof(IConfiguration))).Returns(configurationMock.Object); + + services.AddDaprClient(( + _, + _) => + { + }, ServiceLifetime.Transient); + + var serviceProvider = services.BuildServiceProvider(); + var client1 = serviceProvider.GetRequiredService(); + var client2 = serviceProvider.GetRequiredService(); + + Assert.NotNull(client1); + Assert.NotNull(client2); + Assert.NotSame(client1, client2); //Transient should return different instances + } + + [Fact] + public void AddDaprClient_RegistersClientWithServiceCollection_Transient() + { + var services = new ServiceCollection(); + var configurationMock = new Mock(); + var serviceProviderMock = new Mock(); + serviceProviderMock.Setup(sp => sp.GetService(typeof(IConfiguration))).Returns(configurationMock.Object); + + services.AddDaprClient(( + _, + _) => + { + }, ServiceLifetime.Scoped); + + var serviceProvider = services.BuildServiceProvider(); + + using var scope1 = serviceProvider.CreateScope(); + var client1 = scope1.ServiceProvider.GetRequiredService(); + var client2 = scope1.ServiceProvider.GetRequiredService(); + + Assert.NotNull(client1); + Assert.NotNull(client2); + Assert.Same(client1, client2); //Transient should return the same instance within the same scope + + using (var scope2 = serviceProvider.CreateScope()) + { + var client3 = scope2.ServiceProvider.GetRequiredService(); + Assert.NotNull(client3); + Assert.NotSame(client1, client3); //Scoped should return different instances across different scopes + } + } + + private interface IDaprTestServiceBuilder : IDaprServiceBuilder; + + private sealed class DaprTestBuilder(IServiceCollection services) : IDaprTestServiceBuilder + { + /// + /// The registered services on the builder. + /// + public IServiceCollection Services { get; } = services; + } + + private sealed class DaprTestClientBuilder(IConfiguration? configuration = null) : DaprGenericClientBuilder(configuration) + { + public override DaprTestClient Build() + { + throw new NotImplementedException(); + } + } + + private abstract class DaprTestClient(Autogenerated.Dapr.DaprClient client, HttpClient httpClient, string? daprApiToken = null) : IDaprClient + { + internal readonly HttpClient HttpClient = httpClient; + internal readonly string? DaprApiToken = daprApiToken; + internal Autogenerated.Dapr.DaprClient Client { get; } = client; + + public void Dispose() + { + // TODO release managed resources here + } + } + + private sealed class DaprTestGrpcClient( + Autogenerated.Dapr.DaprClient client, + HttpClient httpClient, + string? daprApiToken = null) : DaprTestClient(client, httpClient, daprApiToken); +} diff --git a/test/Dapr.Jobs.Test/Extensions/DaprJobsServiceCollectionExtensionsTests.cs b/test/Dapr.Jobs.Test/Extensions/DaprJobsServiceCollectionExtensionsTests.cs index 28a8a068..814e5279 100644 --- a/test/Dapr.Jobs.Test/Extensions/DaprJobsServiceCollectionExtensionsTests.cs +++ b/test/Dapr.Jobs.Test/Extensions/DaprJobsServiceCollectionExtensionsTests.cs @@ -47,21 +47,19 @@ public class DaprJobsServiceCollectionExtensionsTest } [Fact] - public void AddDaprJobsClient_RegistersDaprClientOnlyOnce() + public void AddDaprJobsClient_DaprClientRegistration_UseMostRecentVersion() { var services = new ServiceCollection(); - - var clientBuilder = new Action((sp, builder) => + services.AddDaprJobsClient((_, builder) => { - builder.UseDaprApiToken("abc"); - }); - + //Sets the API token value + builder.UseDaprApiToken("abcd1234"); + }); 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; + var daprJobClient = serviceProvider.GetRequiredService() as DaprJobsGrpcClient; Assert.NotNull(daprJobClient!.HttpClient); Assert.False(daprJobClient.HttpClient.DefaultRequestHeaders.TryGetValues("dapr-api-token", out var _)); From c0d70ff24d18a91c80e904dfc3272231329a5512 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 1 Apr 2025 15:24:27 -0500 Subject: [PATCH 04/22] Quality of Life Improvements (#1504) * Removed unnecessary comment * Update to use file-level namespaces * Using target-typed new expression * QoL improvements to code base - primary constructors on classes and records, use of collection initializers, target-typed new expressions, etc. * Applied all manner of QoL improvements to Dapr.Client * Fixed break in example caused by nullability annotation change * Refactored where streaming subscription example is located in solution to align with packages * Refactored actor example to modernize * Modernized example Signed-off-by: Whit Waldo --- all.sln | 7 +- examples/Actor/ActorClient/ActorClient.csproj | 2 +- examples/Actor/ActorClient/Program.cs | 249 +- .../DemoActor.Interfaces.csproj} | 5 + .../IBankActor.cs | 38 +- .../Actor/DemoActor.Interfaces/IDemoActor.cs | 129 + examples/Actor/DemoActor/BankService.cs | 27 +- examples/Actor/DemoActor/DemoActor.cs | 325 +-- examples/Actor/DemoActor/DemoActor.csproj | 3 +- examples/Actor/DemoActor/Program.cs | 30 +- examples/Actor/DemoActor/Startup.cs | 59 - examples/Actor/IDemoActor/IDemoActor.cs | 149 - .../AspNetCore/ControllerSample/Account.cs | 25 +- .../Controllers/SampleController.cs | 529 ++-- .../ControllerSample/CustomTopicAttribute.cs | 53 +- .../AspNetCore/ControllerSample/Program.cs | 55 +- .../AspNetCore/ControllerSample/Startup.cs | 117 +- .../ControllerSample/Transaction.cs | 33 +- .../ControllerSample/TransactionV2.cs | 43 +- .../GrpcServiceSample/Models/Account.cs | 27 +- .../Models/GetAccountInput.cs | 19 +- .../GrpcServiceSample/Models/Transaction.cs | 31 +- .../AspNetCore/GrpcServiceSample/Program.cs | 61 +- .../Services/BankingService.cs | 299 +- .../AspNetCore/GrpcServiceSample/Startup.cs | 75 +- examples/AspNetCore/RoutingSample/Account.cs | 25 +- examples/AspNetCore/RoutingSample/Program.cs | 55 +- examples/AspNetCore/RoutingSample/Startup.cs | 453 ++- .../AspNetCore/RoutingSample/Transaction.cs | 25 +- .../Program.cs | 103 +- .../Startup.cs | 105 +- .../Controllers/ConfigurationController.cs | 119 +- examples/Client/ConfigurationApi/Program.cs | 63 +- examples/Client/ConfigurationApi/Startup.cs | 89 +- examples/Client/Cryptography/Example.cs | 15 +- .../EncryptDecryptFileStreamExample.cs | 101 +- .../Examples/EncryptDecryptStringExample.cs | 37 +- examples/Client/Cryptography/Program.cs | 55 +- .../Controllers/BindingController.cs | 155 +- .../Client/DistributedLock/Model/StateData.cs | 22 +- examples/Client/DistributedLock/Program.cs | 33 +- .../Services/GeneratorService.cs | 43 +- examples/Client/DistributedLock/Startup.cs | 71 +- .../BulkPublishEventExample.cs | 59 +- .../BulkPublishEventExample/Example.cs | 15 +- .../BulkPublishEventExample/Program.cs | 47 +- .../PublishEventExample/Example.cs | 15 +- .../PublishEventExample/Program.cs | 47 +- .../PublishBytesExample.cs | 25 +- .../PublishEventExample.cs | 23 +- examples/Client/ServiceInvocation/Example.cs | 15 +- .../InvokeServiceGrpcExample.cs | 45 +- .../InvokeServiceHttpClientExample.cs | 63 +- .../InvokeServiceHttpExample.cs | 73 +- examples/Client/ServiceInvocation/Program.cs | 51 +- .../StateManagement/BulkStateExample.cs | 69 +- examples/Client/StateManagement/Example.cs | 15 +- examples/Client/StateManagement/Program.cs | 55 +- .../StateStoreBinaryExample.cs | 61 +- .../StateManagement/StateStoreETagsExample.cs | 93 +- .../StateManagement/StateStoreExample.cs | 59 +- .../StateStoreTransactionsExample.cs | 69 +- .../ActorClient/IGenericClientActor.cs | 21 +- .../Activities/NotifyActivity.cs | 33 +- .../Activities/ProcessPaymentActivity.cs | 55 +- .../Activities/RequestApprovalActivity.cs | 33 +- .../Activities/ReserveInventoryActivity.cs | 91 +- .../Activities/UpdateInventoryActivity.cs | 99 +- .../Workflow/WorkflowConsoleApp/Models.cs | 27 +- .../Workflows/OrderProcessingWorkflow.cs | 193 +- .../WorkflowUnitTest/OrderProcessingTests.cs | 135 +- .../DaprConversationClientBuilder.cs | 11 +- .../ActorsEndpointRouteBuilderExtensions.cs | 303 +- .../ActorsServiceCollectionExtensions.cs | 157 +- .../DependencyInjectionActorActivator.cs | 145 +- ...ependencyInjectionActorActivatorFactory.cs | 29 +- src/Dapr.Actors.Generators/Constants.cs | 59 +- ...CancellationTokensMustBeTheLastArgument.cs | 39 +- ...tOptionallyFollowedByACancellationToken.cs | 39 +- .../DiagnosticsException.cs | 35 +- .../Extensions/IEnumerableExtensions.cs | 57 +- .../Helpers/SyntaxFactoryHelpers.cs | 275 +- .../Models/ActorClientDescriptor.cs | 67 +- src/Dapr.Actors.Generators/Templates.cs | 81 +- src/Dapr.Actors/ActorId.cs | 291 +- .../ActorMethodInvocationException.cs | 69 +- src/Dapr.Actors/ActorReentrancyConfig.cs | 67 +- .../ActorReentrancyContextAccessor.cs | 63 +- src/Dapr.Actors/ActorReference.cs | 149 +- src/Dapr.Actors/Builder/ActorCodeBuilder.cs | 333 ++- .../Builder/ActorCodeBuilderNames.cs | 191 +- .../Builder/ActorMethodDispatcherBase.cs | 449 ++- .../Builder/ActorProxyGenerator.cs | 41 +- .../Builder/ActorProxyGeneratorBuildResult.cs | 29 +- .../Builder/ActorProxyGeneratorBuilder.cs | 975 ++++--- src/Dapr.Actors/Builder/BuildResult.cs | 19 +- .../Builder/CodeBuilderAttribute.cs | 71 +- src/Dapr.Actors/Builder/CodeBuilderContext.cs | 47 +- src/Dapr.Actors/Builder/CodeBuilderModule.cs | 45 +- src/Dapr.Actors/Builder/CodeBuilderUtils.cs | 395 ++- src/Dapr.Actors/Builder/ICodeBuilder.cs | 59 +- src/Dapr.Actors/Builder/ICodeBuilderNames.cs | 189 +- src/Dapr.Actors/Builder/IProxyActivator.cs | 25 +- src/Dapr.Actors/Builder/InterfaceDetails.cs | 59 +- .../Builder/InterfaceDetailsStore.cs | 131 +- src/Dapr.Actors/Builder/MethodBodyTypes.cs | 19 +- .../Builder/MethodBodyTypesBuildResult.cs | 93 +- .../Builder/MethodBodyTypesBuilder.cs | 201 +- .../Builder/MethodDispatcherBuildResult.cs | 25 +- .../Builder/MethodDispatcherBuilder.cs | 831 +++--- src/Dapr.Actors/Client/ActorProxy.cs | 601 ++-- src/Dapr.Actors/Client/ActorProxyFactory.cs | 159 +- src/Dapr.Actors/Client/ActorProxyOptions.cs | 103 +- src/Dapr.Actors/Client/IActorProxy.cs | 31 +- src/Dapr.Actors/Client/IActorProxyFactory.cs | 79 +- src/Dapr.Actors/ClientSettings.cs | 39 +- src/Dapr.Actors/Common/CRC64.cs | 229 +- src/Dapr.Actors/Common/IdUtil.cs | 165 +- .../ActorDataContractSurrogate.cs | 85 +- .../Communication/ActorInvokeException.cs | 157 +- .../Communication/ActorInvokeExceptionData.cs | 79 +- .../Communication/ActorLogicalCallContext.cs | 51 +- ...geBodyDataContractSerializationProvider.cs | 423 ++- .../ActorMessageBodyJsonConverter.cs | 141 +- ...torMessageBodyJsonSerializationProvider.cs | 259 +- .../ActorMessageHeaderSerializer.cs | 175 +- .../ActorMessageSerializersManager.cs | 183 +- .../Communication/ActorMethodDispatcherMap.cs | 65 +- .../Communication/ActorRequestMessage.cs | 41 +- .../Communication/ActorRequestMessageBody.cs | 47 +- .../ActorRequestMessageHeader.cs | 143 +- .../Communication/ActorResponseMessage.cs | 45 +- .../Communication/ActorResponseMessageBody.cs | 35 +- .../ActorResponseMessageHeader.cs | 111 +- .../Communication/ActorStateResponse.cs | 63 +- src/Dapr.Actors/Communication/CacheEntry.cs | 27 +- .../Client/ActorNonRemotingClient.cs | 51 +- .../Client/ActorRemotingClient.cs | 83 +- .../DataContractMessageFactory.cs | 25 +- .../Communication/DisposableStream.cs | 125 +- .../Communication/IActorMessageBodyFactory.cs | 45 +- .../IActorMessageBodySerializationProvider.cs | 83 +- .../IActorMessageHeaderSerializer.cs | 63 +- .../Communication/IActorRequestMessage.cs | 29 +- .../Communication/IActorRequestMessageBody.cs | 47 +- .../IActorRequestMessageBodySerializer.cs | 41 +- .../IActorRequestMessageHeader.cs | 97 +- .../Communication/IActorResponseMessage.cs | 31 +- .../IActorResponseMessageBody.cs | 39 +- .../IActorResponseMessageBodySerializer.cs | 41 +- .../IActorResponseMessageHeader.cs | 49 +- src/Dapr.Actors/Constants.cs | 109 +- src/Dapr.Actors/DaprHttpInteractor.cs | 913 +++--- .../Description/ActorInterfaceDescription.cs | 103 +- .../Description/InterfaceDescription.cs | 397 ++- .../Description/MethodArgumentDescription.cs | 161 +- .../Description/MethodDescription.cs | 163 +- .../Description/MethodReturnCheck.cs | 13 +- src/Dapr.Actors/Description/TypeUtility.cs | 33 +- src/Dapr.Actors/Helper.cs | 39 +- src/Dapr.Actors/IActor.cs | 15 +- src/Dapr.Actors/IActorReference.cs | 33 +- src/Dapr.Actors/IDaprInteractor.cs | 187 +- src/Dapr.Actors/JsonSerializerDefaults.cs | 23 +- src/Dapr.Actors/Runtime/Actor.cs | 1081 ++++--- src/Dapr.Actors/Runtime/ActorActivator.cs | 65 +- .../Runtime/ActorActivatorFactory.cs | 25 +- .../Runtime/ActorActivatorState.cs | 37 +- src/Dapr.Actors/Runtime/ActorAttribute.cs | 29 +- src/Dapr.Actors/Runtime/ActorCallType.cs | 43 +- src/Dapr.Actors/Runtime/ActorHost.cs | 293 +- src/Dapr.Actors/Runtime/ActorManager.cs | 689 +++-- src/Dapr.Actors/Runtime/ActorMethodContext.cs | 99 +- src/Dapr.Actors/Runtime/ActorMethodInfoMap.cs | 57 +- src/Dapr.Actors/Runtime/ActorRegistration.cs | 77 +- .../Runtime/ActorRegistrationCollection.cs | 131 +- src/Dapr.Actors/Runtime/ActorReminder.cs | 407 ++- .../Runtime/ActorReminderOptions.cs | 75 +- src/Dapr.Actors/Runtime/ActorReminderToken.cs | 87 +- src/Dapr.Actors/Runtime/ActorRuntime.cs | 347 ++- .../Runtime/ActorRuntimeOptions.cs | 417 ++- src/Dapr.Actors/Runtime/ActorStateChange.cs | 127 +- src/Dapr.Actors/Runtime/ActorStateManager.cs | 989 ++++--- src/Dapr.Actors/Runtime/ActorTestOptions.cs | 155 +- src/Dapr.Actors/Runtime/ActorTimer.cs | 311 +- src/Dapr.Actors/Runtime/ActorTimerManager.cs | 71 +- src/Dapr.Actors/Runtime/ActorTimerOptions.cs | 75 +- src/Dapr.Actors/Runtime/ActorTimerToken.cs | 87 +- .../Runtime/ActorTypeExtensions.cs | 169 +- .../Runtime/ActorTypeInformation.cs | 279 +- src/Dapr.Actors/Runtime/ConditionalValue.cs | 53 +- src/Dapr.Actors/Runtime/DaprStateProvider.cs | 273 +- .../Runtime/DefaultActorActivator.cs | 87 +- .../Runtime/DefaultActorActivatorFactory.cs | 25 +- .../Runtime/DefaultActorTimerManager.cs | 191 +- .../Runtime/IActorContextualState.cs | 15 +- src/Dapr.Actors/Runtime/IActorReminder.cs | 67 +- src/Dapr.Actors/Runtime/IActorStateManager.cs | 629 ++-- .../Runtime/IActorStateSerializer.cs | 39 +- src/Dapr.Actors/Runtime/IRemindable.cs | 43 +- src/Dapr.Actors/Runtime/StateChangeKind.cs | 43 +- src/Dapr.Actors/Runtime/TimerInfo.cs | 299 +- .../Serialization/ActorIdJsonConverter.cs | 75 +- src/Dapr.AspNetCore/BulkMessageModel.cs | 117 +- .../BulkSubscribeAppResponse.cs | 33 +- .../BulkSubscribeAppResponseEntry.cs | 47 +- .../BulkSubscribeAppResponseStatus.cs | 40 +- src/Dapr.AspNetCore/BulkSubscribeAttribute.cs | 107 +- src/Dapr.AspNetCore/BulkSubscribeMessage.cs | 77 +- .../BulkSubscribeMessageEntry.cs | 95 +- .../BulkSubscribeTopicOptions.cs | 37 +- .../CloudEventPropertyNames.cs | 15 +- src/Dapr.AspNetCore/CloudEventsMiddleware.cs | 505 ++-- .../CloudEventsMiddlewareOptions.cs | 115 +- .../DaprApplicationBuilderExtensions.cs | 71 +- .../DaprAuthenticationBuilderExtensions.cs | 59 +- .../DaprAuthenticationHandler.cs | 105 +- .../DaprAuthenticationOptions.cs | 33 +- .../DaprAuthorizationOptionsExtensions.cs | 35 +- ...DaprEndpointConventionBuilderExtensions.cs | 303 +- .../DaprEndpointRouteBuilderExtensions.cs | 371 ++- .../DaprMvcBuilderExtensions.cs | 73 +- src/Dapr.AspNetCore/FromStateAttribute.cs | 99 +- src/Dapr.AspNetCore/FromStateBindingSource.cs | 27 +- src/Dapr.AspNetCore/IBulkSubscribeMetadata.cs | 37 +- .../IDeadLetterTopicMetadata.cs | 20 +- src/Dapr.AspNetCore/IOriginalTopicMetadata.cs | 43 +- .../IOwnedOriginalTopicMetadata.cs | 31 +- src/Dapr.AspNetCore/IRawTopicMetadata.cs | 19 +- src/Dapr.AspNetCore/ITopicMetadata.cs | 43 +- .../StateEntryApplicationModelProvider.cs | 83 +- src/Dapr.AspNetCore/StateEntryModelBinder.cs | 179 +- .../StateEntryModelBinderProvider.cs | 79 +- src/Dapr.AspNetCore/SubscribeOptions.cs | 19 +- src/Dapr.AspNetCore/Subscription.cs | 183 +- src/Dapr.AspNetCore/TopicAttribute.cs | 267 +- src/Dapr.AspNetCore/TopicMetadataAttribute.cs | 85 +- src/Dapr.AspNetCore/TopicOptions.cs | 85 +- src/Dapr.Client/BindingRequest.cs | 66 +- src/Dapr.Client/BindingResponse.cs | 55 +- src/Dapr.Client/BulkPublishEntry.cs | 66 +- src/Dapr.Client/BulkPublishResponse.cs | 27 +- .../BulkPublishResponseFailedEntry.cs | 38 +- src/Dapr.Client/BulkStateItem.cs | 117 +- src/Dapr.Client/CloudEvent.cs | 85 +- src/Dapr.Client/ConcurrencyMode.cs | 27 +- src/Dapr.Client/ConfigurationItem.cs | 51 +- src/Dapr.Client/ConfigurationSource.cs | 23 +- src/Dapr.Client/ConsistencyMode.cs | 27 +- src/Dapr.Client/Constants.cs | 15 +- src/Dapr.Client/CryptographyEnums.cs | 111 +- src/Dapr.Client/CryptographyOptions.cs | 107 +- src/Dapr.Client/DaprApiException.cs | 199 +- src/Dapr.Client/DaprClient.cs | 2573 ++++++++--------- src/Dapr.Client/DaprClientBuilder.cs | 341 ++- src/Dapr.Client/DaprClientGrpc.cs | 59 +- src/Dapr.Client/DaprError.cs | 31 +- src/Dapr.Client/DaprMetadata.cs | 168 +- .../DaprSubscribeConfigurationSource.cs | 121 +- src/Dapr.Client/DeleteBulkStateItem.cs | 61 +- src/Dapr.Client/Extensions/EnumExtensions.cs | 35 +- src/Dapr.Client/Extensions/HttpExtensions.cs | 51 +- src/Dapr.Client/GetConfigurationResponse.cs | 35 +- src/Dapr.Client/InvocationException.cs | 89 +- src/Dapr.Client/InvocationHandler.cs | 232 +- src/Dapr.Client/InvocationInterceptor.cs | 217 +- src/Dapr.Client/SaveStateItem.cs | 72 +- src/Dapr.Client/StartWorkflowResponse.cs | 15 +- src/Dapr.Client/StateEntry.cs | 229 +- src/Dapr.Client/StateOperationType.cs | 27 +- src/Dapr.Client/StateOptions.cs | 27 +- src/Dapr.Client/StateQueryException.cs | 43 +- src/Dapr.Client/StateQueryResponse.cs | 116 +- src/Dapr.Client/StateTransactionRequest.cs | 93 +- .../SubscribeConfigurationResponse.cs | 45 +- src/Dapr.Client/TryLockResponse.cs | 73 +- src/Dapr.Client/TypeConverters.cs | 76 +- src/Dapr.Client/UnlockResponse.cs | 71 +- .../UnsubscribeConfigurationResponse.cs | 38 +- src/Dapr.Common/ArgumentVerifier.cs | 63 +- src/Dapr.Common/DaprDefaults.cs | 207 +- src/Dapr.Common/DaprException.cs | 61 +- .../Exceptions/DaprExceptionExtensions.cs | 63 +- .../Exceptions/DaprExtendedErrorConstants.cs | 39 +- .../Exceptions/DaprExtendedErrorDetail.cs | 252 +- .../Exceptions/DaprExtendedErrorInfo.cs | 23 +- .../Exceptions/DaprExtendedErrorType.cs | 127 +- .../Exceptions/ExtendedErrorDetailFactory.cs | 197 +- .../DaprConfigurationStoreExtension.cs | 147 +- .../DaprConfigurationStoreProvider.cs | 167 +- .../DaprConfigurationStoreSource.cs | 77 +- .../DaprSecretDescriptor.cs | 101 +- .../DaprSecretStoreConfigurationExtensions.cs | 389 ++- .../DaprSecretStoreConfigurationProvider.cs | 411 ++- .../DaprSecretStoreConfigurationSource.cs | 105 +- src/Dapr.Jobs/DaprJobsClientBuilder.cs | 11 +- .../Extensions/DaprSerializationExtensions.cs | 2 +- .../Iso8601DateTimeJsonConverter.cs | 3 +- .../DaprPublishSubscribeClientBuilder.cs | 11 +- .../DaprWorkflowActivityContext.cs | 37 +- src/Dapr.Workflow/DaprWorkflowClient.cs | 530 ++-- .../DaprWorkflowClientBuilderFactory.cs | 14 +- src/Dapr.Workflow/DaprWorkflowContext.cs | 233 +- src/Dapr.Workflow/Workflow.cs | 231 +- src/Dapr.Workflow/WorkflowActivity.cs | 143 +- src/Dapr.Workflow/WorkflowActivityContext.cs | 31 +- src/Dapr.Workflow/WorkflowContext.cs | 631 ++-- src/Dapr.Workflow/WorkflowLoggingService.cs | 90 +- src/Dapr.Workflow/WorkflowRetryPolicy.cs | 150 +- src/Dapr.Workflow/WorkflowRuntimeOptions.cs | 316 +- src/Dapr.Workflow/WorkflowRuntimeStatus.cs | 67 +- src/Dapr.Workflow/WorkflowState.cs | 291 +- .../WorkflowTaskFailedException.cs | 35 +- .../WorkflowTaskFailureDetails.cs | 105 +- src/Dapr.Workflow/WorkflowTaskOptions.cs | 77 +- .../DependencyInjectionActor.cs | 41 +- .../Program.cs | 53 +- .../Startup.cs | 73 +- .../ActivationTests.cs | 115 +- .../AppWebApplicationFactory.cs | 43 +- .../HostingTests.cs | 393 ++- .../ActorHostingTest.cs | 147 +- ...torsEndpointRouteBuilderExtensionsTests.cs | 129 +- .../DependencyInjectionActorActivatorTests.cs | 321 +- .../Extensions/IEnumerableExtensionsTests.cs | 89 +- .../Helpers/SyntaxFactoryHelpersTests.cs | 221 +- .../Dapr.Actors.Test/ActorStateManagerTest.cs | 343 ++- test/Dapr.Actors.Test/ActorUnitTestTests.cs | 299 +- test/Dapr.Actors.Test/ApiTokenTests.cs | 177 +- .../DaprFormatTimeSpanTests.cs | 243 +- .../DaprHttpInteractorTest.cs | 821 +++--- .../Dapr.Actors.Test/DaprStateProviderTest.cs | 213 +- .../ActorInterfaceDescriptionTests.cs | 271 +- .../Description/InterfaceDescriptionTests.cs | 451 ++- .../MethodArgumentDescriptionTests.cs | 193 +- .../Description/MethodDescriptionTests.cs | 209 +- .../Runtime/ActorManagerTests.cs | 433 ++- .../Runtime/ActorReminderInfoTests.cs | 47 +- .../Runtime/ActorRuntimeOptionsTests.cs | 261 +- .../Runtime/ActorRuntimeTests.cs | 889 +++--- .../Runtime/ActorTypeInformationTests.cs | 123 +- .../Runtime/DefaultActorActivatorTests.cs | 185 +- .../CustomTopicAttribute.cs | 35 +- .../DaprController.cs | 315 +- .../Program.cs | 33 +- .../Startup.cs | 129 +- .../UserInfo.cs | 15 +- .../Widget.cs | 13 +- .../AppWebApplicationFactory.cs | 43 +- .../AuthenticationTest.cs | 83 +- .../CloudEventsIntegrationTest.cs | 185 +- .../ControllerIntegrationTest.cs | 291 +- .../RoutingIntegrationTest.cs | 43 +- .../StateTestClient.cs | 199 +- .../SubscribeEndpointTest.cs | 217 +- .../CloudEventsMiddlewareTest.cs | 793 +++-- .../DaprClientBuilderTest.cs | 200 +- .../DaprMvcBuilderExtensionsTest.cs | 119 +- .../StateEntryApplicationModelProviderTest.cs | 145 +- .../StateEntryModelBinderTest.cs | 351 ++- test/Dapr.Client.Test/ArgumentVerifierTest.cs | 71 +- .../BulkPublishEventApiTest.cs | 591 ++-- test/Dapr.Client.Test/ConfigurationApiTest.cs | 231 +- .../ConfigurationSourceTest.cs | 161 +- test/Dapr.Client.Test/CryptographyApiTest.cs | 169 +- test/Dapr.Client.Test/DaprApiTokenTest.cs | 89 +- ...lientTest.CreateInvokableHttpClientTest.cs | 65 +- .../DaprClientTest.InvokeMethodAsync.cs | 1453 +++++----- .../DaprClientTest.InvokeMethodGrpcAsync.cs | 795 +++-- test/Dapr.Client.Test/DaprClientTest.cs | 159 +- .../DistributedLockApiTest.cs | 139 +- .../Extensions/EnumExtensionTest.cs | 55 +- .../Extensions/HttpExtensionTest.cs | 111 +- .../InvocationHandlerTests.cs | 437 ++- test/Dapr.Client.Test/InvokeBindingApiTest.cs | 585 ++-- test/Dapr.Client.Test/MockClient.cs | 223 +- test/Dapr.Client.Test/PublishEventApiTest.cs | 575 ++-- test/Dapr.Client.Test/SecretApiTest.cs | 683 +++-- test/Dapr.Client.Test/StateApiTest.cs | 2548 ++++++++-------- test/Dapr.Client.Test/TryLockResponseTest.cs | 81 +- test/Dapr.Client.Test/TypeConvertersTest.cs | 49 +- .../DaprExtendedErrorInfoTest.cs | 1335 +++++---- .../ExceptionTesting/IExceptionActor.cs | 9 +- test/Dapr.E2E.Test.Actors/IPingActor.cs | 11 +- .../ISerializationActor.cs | 27 +- .../Reentrancy/CallRecord.cs | 17 +- .../Reentrancy/IReentrantActor.cs | 13 +- .../Reentrancy/ReentrantCallOptions.cs | 13 +- test/Dapr.E2E.Test.Actors/Reentrancy/State.cs | 11 +- .../RegressionActor/IRegression762Actor.cs | 15 +- .../RegressionActor/StateCall.cs | 15 +- .../Reminders/IReminderActor.cs | 21 +- .../Reminders/StartReminderOptions.cs | 11 +- test/Dapr.E2E.Test.Actors/Reminders/State.cs | 15 +- .../Dapr.E2E.Test.Actors/State/IStateActor.cs | 15 +- .../Timers/ITimerActor.cs | 39 +- .../Timers/StartTimerOptions.cs | 11 +- test/Dapr.E2E.Test.Actors/Timers/State.cs | 15 +- .../WeaklyTypedTesting/DerivedResponse.cs | 11 +- .../IWeaklyTypedTestingActor.cs | 15 +- .../WeaklyTypedTesting/ResponseBase.cs | 14 +- .../MessageRepository.cs | 41 +- test/Dapr.E2E.Test.App.Grpc/Program.cs | 65 +- .../Services/MessagerService.cs | 55 +- test/Dapr.E2E.Test.App.Grpc/Startup.cs | 51 +- .../Actors/ReentrantActor.cs | 89 +- .../Program.cs | 29 +- .../Startup.cs | 87 +- test/Dapr.E2E.Test.App/Account.cs | 25 +- .../Actors/ExceptionActor.cs | 27 +- .../Actors/Regression762Actor.cs | 69 +- .../Dapr.E2E.Test.App/Actors/ReminderActor.cs | 173 +- .../Actors/SerializationActor.cs | 43 +- test/Dapr.E2E.Test.App/Actors/StateActor.cs | 51 +- test/Dapr.E2E.Test.App/Actors/TimerActor.cs | 101 +- .../Actors/WeaklyTypedTestingActor.cs | 55 +- .../Controllers/TestController.cs | 111 +- test/Dapr.E2E.Test.App/Program.cs | 37 +- test/Dapr.E2E.Test.App/Startup.cs | 297 +- test/Dapr.E2E.Test.App/Transaction.cs | 33 +- test/Dapr.E2E.Test/AVeryCoolWorkAroundTest.cs | 17 +- test/Dapr.E2E.Test/ActorRuntimeChecker.cs | 41 +- .../Actors/E2ETests.CustomSerializerTests.cs | 175 +- .../Actors/E2ETests.ExceptionTests.cs | 41 +- .../Actors/E2ETests.ReentrantTests.cs | 109 +- .../Actors/E2ETests.Regression762Tests.cs | 169 +- .../Actors/E2ETests.ReminderTests.cs | 315 +- .../Actors/E2ETests.StateTests.cs | 201 +- .../Actors/E2ETests.TimerTests.cs | 105 +- .../Actors/E2ETests.WeaklyTypedTests.cs | 63 +- test/Dapr.E2E.Test/DaprCommand.cs | 307 +- test/Dapr.E2E.Test/DaprRunConfiguration.cs | 21 +- test/Dapr.E2E.Test/DaprTestApp.cs | 281 +- test/Dapr.E2E.Test/DaprTestAppFixture.cs | 171 +- test/Dapr.E2E.Test/DaprTestAppLifecycle.cs | 81 +- test/Dapr.E2E.Test/E2ETests.cs | 127 +- test/Dapr.E2E.Test/HttpAssert.cs | 65 +- .../E2ETests.GrpcProxyInvocationTests.cs | 127 +- .../E2ETests.ServiceInvocationTests.cs | 137 +- .../DaprConfigurationStoreProviderTest.cs | 387 ++- ...aprSecretStoreConfigurationProviderTest.cs | 1671 ++++++----- .../TestHttpClient.cs | 157 +- .../MessageHandlingPolicyTest.cs | 95 +- .../WorkflowActivityTest.cs | 67 +- test/Shared/AppCallbackClient.cs | 97 +- 445 files changed, 33843 insertions(+), 34787 deletions(-) rename examples/Actor/{IDemoActor/IDemoActor.csproj => DemoActor.Interfaces/DemoActor.Interfaces.csproj} (57%) rename examples/Actor/{IDemoActor => DemoActor.Interfaces}/IBankActor.cs (55%) create mode 100644 examples/Actor/DemoActor.Interfaces/IDemoActor.cs delete mode 100644 examples/Actor/DemoActor/Startup.cs delete mode 100644 examples/Actor/IDemoActor/IDemoActor.cs diff --git a/all.sln b/all.sln index 9a163b1d..eabc532e 100644 --- a/all.sln +++ b/all.sln @@ -54,7 +54,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControllerSample", "example EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Actor", "Actor", "{02374BD0-BF0B-40F8-A04A-C4C4D61D4992}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IDemoActor", "examples\Actor\IDemoActor\IDemoActor.csproj", "{7957E852-1291-4FAA-9034-FB66CE817FF1}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DemoActor.Interfaces", "examples\Actor\DemoActor.Interfaces\DemoActor.Interfaces.csproj", "{7957E852-1291-4FAA-9034-FB66CE817FF1}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DemoActor", "examples\Actor\DemoActor\DemoActor.csproj", "{626D74DD-4F37-4F74-87A3-5A6888684F5E}" EndProject @@ -155,6 +155,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JobsSample", "examples\Jobs EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Workflow.Test", "test\Dapr.Workflow.Test\Dapr.Workflow.Test.csproj", "{E90114C6-86FC-43B8-AE5C-D9273CF21FE4}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Messaging", "Messaging", "{8DB002D2-19E9-4342-A86B-025A367DF3D1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -471,12 +473,13 @@ Global {00359961-0C50-4BB1-A794-8B06DE991639} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9} {4E04EB35-7FD2-4FDB-B09A-F75CE24053B9} = {DD020B34-460F-455F-8D17-CF4A949F100B} {0EAE36A1-B578-4F13-A113-7A477ECA1BDA} = {27C5D71D-0721-4221-9286-B94AB07B58CF} - {290D1278-F613-4DF3-9DF5-F37E38CDC363} = {0EF6EA64-D7C3-420D-9890-EAE8D54A57E6} {C8BB6A85-A7EA-40C0-893D-F36F317829B3} = {27C5D71D-0721-4221-9286-B94AB07B58CF} {BF9828E9-5597-4D42-AA6E-6E6C12214204} = {DD020B34-460F-455F-8D17-CF4A949F100B} {D9697361-232F-465D-A136-4561E0E88488} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78} {9CAF360E-5AD3-4C4F-89A0-327EEB70D673} = {D9697361-232F-465D-A136-4561E0E88488} {E90114C6-86FC-43B8-AE5C-D9273CF21FE4} = {DD020B34-460F-455F-8D17-CF4A949F100B} + {8DB002D2-19E9-4342-A86B-025A367DF3D1} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78} + {290D1278-F613-4DF3-9DF5-F37E38CDC363} = {8DB002D2-19E9-4342-A86B-025A367DF3D1} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40} diff --git a/examples/Actor/ActorClient/ActorClient.csproj b/examples/Actor/ActorClient/ActorClient.csproj index 43aa659b..9631e517 100644 --- a/examples/Actor/ActorClient/ActorClient.csproj +++ b/examples/Actor/ActorClient/ActorClient.csproj @@ -6,7 +6,7 @@ - + diff --git a/examples/Actor/ActorClient/Program.cs b/examples/Actor/ActorClient/Program.cs index 950869b2..b578d488 100644 --- a/examples/Actor/ActorClient/Program.cs +++ b/examples/Actor/ActorClient/Program.cs @@ -11,146 +11,123 @@ // limitations under the License. // ------------------------------------------------------------------------ -using Dapr.Actors.Communication; +using System; +using System.Threading; +using System.Threading.Tasks; +using Dapr.Actors; +using Dapr.Actors.Client; using IDemoActor; -namespace ActorClient +var data = new MyData("ValueA", "ValueB"); + +// Create an actor Id. +var actorId = new ActorId("abc"); + +// Make strongly typed Actor calls with Remoting. +// DemoActor is the type registered with Dapr runtime in the service. +var proxy = ActorProxy.Create(actorId, "DemoActor"); + +Console.WriteLine("Making call using actor proxy to save data."); +await proxy.SaveData(data, TimeSpan.FromMinutes(10)); +Console.WriteLine("Making call using actor proxy to get data."); +var receivedData = await proxy.GetData(); +Console.WriteLine($"Received data is {receivedData}."); + +// Making some more calls to test methods. +try { - using System; - using System.Threading; - using System.Threading.Tasks; - using Dapr.Actors; - using Dapr.Actors.Client; + Console.WriteLine("Making calls to an actor method which has no argument and no return type."); + await proxy.TestNoArgumentNoReturnType(); +} +catch (Exception ex) +{ + Console.WriteLine($"ERROR: Got exception while making call to method with No Argument & No Return Type. Exception: {ex}"); +} - /// - /// Actor Client class. - /// - public class Program +try +{ + await proxy.TestThrowException(); +} +catch (ActorMethodInvocationException ex) +{ + if (ex.InnerException is ActorInvokeException invokeEx && invokeEx.ActualExceptionType is "System.NotImplementedException") { - /// - /// Entry point. - /// - /// Arguments. - /// A representing the asynchronous operation. - public static async Task Main(string[] args) - { - var data = new MyData() - { - PropertyA = "ValueA", - PropertyB = "ValueB", - }; - - // Create an actor Id. - var actorId = new ActorId("abc"); - - // Make strongly typed Actor calls with Remoting. - // DemoActor is the type registered with Dapr runtime in the service. - var proxy = ActorProxy.Create(actorId, "DemoActor"); - - Console.WriteLine("Making call using actor proxy to save data."); - await proxy.SaveData(data, TimeSpan.FromMinutes(10)); - Console.WriteLine("Making call using actor proxy to get data."); - var receivedData = await proxy.GetData(); - Console.WriteLine($"Received data is {receivedData}."); - - // Making some more calls to test methods. - try - { - Console.WriteLine("Making calls to an actor method which has no argument and no return type."); - await proxy.TestNoArgumentNoReturnType(); - } - catch (Exception ex) - { - Console.WriteLine($"ERROR: Got exception while making call to method with No Argument & No Return Type. Exception: {ex}"); - } - - try - { - await proxy.TestThrowException(); - } - catch (ActorMethodInvocationException ex) - { - if (ex.InnerException is ActorInvokeException invokeEx && invokeEx.ActualExceptionType is "System.NotImplementedException") - { - Console.WriteLine($"Got Correct Exception from actor method invocation."); - } - else - { - Console.WriteLine($"Got Incorrect Exception from actor method invocation. Exception {ex.InnerException}"); - } - } - - // Making calls without Remoting, this shows method invocation using InvokeMethodAsync methods, the method name and its payload is provided as arguments to InvokeMethodAsync methods. - Console.WriteLine("Making calls without Remoting."); - var nonRemotingProxy = ActorProxy.Create(actorId, "DemoActor"); - await nonRemotingProxy.InvokeMethodAsync("TestNoArgumentNoReturnType"); - await nonRemotingProxy.InvokeMethodAsync("SaveData", data); - await nonRemotingProxy.InvokeMethodAsync("GetData"); - - Console.WriteLine("Registering the timer and reminder"); - await proxy.RegisterTimer(); - await proxy.RegisterReminder(); - Console.WriteLine("Waiting so the timer and reminder can be triggered"); - await Task.Delay(6000); - - Console.WriteLine("Making call using actor proxy to get data after timer and reminder triggered"); - receivedData = await proxy.GetData(); - Console.WriteLine($"Received data is {receivedData}."); - - Console.WriteLine("Getting details of the registered reminder"); - var reminder = await proxy.GetReminder(); - Console.WriteLine($"Received reminder is {reminder}."); - - Console.WriteLine("Deregistering timer. Timers would any way stop if the actor is deactivated as part of Dapr garbage collection."); - await proxy.UnregisterTimer(); - Console.WriteLine("Deregistering reminder. Reminders are durable and would not stop until an explicit deregistration or the actor is deleted."); - await proxy.UnregisterReminder(); - - Console.WriteLine("Registering reminder with repetitions - The reminder will repeat 3 times."); - await proxy.RegisterReminderWithRepetitions(3); - Console.WriteLine("Waiting so the reminder can be triggered"); - await Task.Delay(5000); - Console.WriteLine("Getting details of the registered reminder"); - reminder = await proxy.GetReminder(); - Console.WriteLine($"Received reminder is {reminder?.ToString() ?? "None"} (expecting None)."); - Console.WriteLine("Registering reminder with ttl and repetitions, i.e. reminder stops when either condition is met - The reminder will repeat 2 times."); - await proxy.RegisterReminderWithTtlAndRepetitions(TimeSpan.FromSeconds(5), 2); - Console.WriteLine("Getting details of the registered reminder"); - reminder = await proxy.GetReminder(); - Console.WriteLine($"Received reminder is {reminder}."); - Console.WriteLine("Deregistering reminder. Reminders are durable and would not stop until an explicit deregistration or the actor is deleted."); - await proxy.UnregisterReminder(); - - Console.WriteLine("Registering reminder and Timer with TTL - The reminder will self delete after 10 seconds."); - await proxy.RegisterReminderWithTtl(TimeSpan.FromSeconds(10)); - await proxy.RegisterTimerWithTtl(TimeSpan.FromSeconds(10)); - Console.WriteLine("Getting details of the registered reminder"); - reminder = await proxy.GetReminder(); - Console.WriteLine($"Received reminder is {reminder}."); - - // Track the reminder. - var timer = new Timer(async state => Console.WriteLine($"Received data: {await proxy.GetData()}"), null, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5)); - await Task.Delay(TimeSpan.FromSeconds(21)); - await timer.DisposeAsync(); - - Console.WriteLine("Creating a Bank Actor"); - var bank = ActorProxy.Create(ActorId.CreateRandom(), "DemoActor"); - while (true) - { - var balance = await bank.GetAccountBalance(); - Console.WriteLine($"Balance for account '{balance.AccountId}' is '{balance.Balance:c}'."); - - Console.WriteLine($"Withdrawing '{10m:c}'..."); - try - { - await bank.Withdraw(new WithdrawRequest() { Amount = 10m, }); - } - catch (ActorMethodInvocationException ex) - { - Console.WriteLine("Overdraft: " + ex.Message); - break; - } - } - } + Console.WriteLine($"Got Correct Exception from actor method invocation."); + } + else + { + Console.WriteLine($"Got Incorrect Exception from actor method invocation. Exception {ex.InnerException}"); + } +} + +// Making calls without Remoting, this shows method invocation using InvokeMethodAsync methods, the method name and its payload is provided as arguments to InvokeMethodAsync methods. +Console.WriteLine("Making calls without Remoting."); +var nonRemotingProxy = ActorProxy.Create(actorId, "DemoActor"); +await nonRemotingProxy.InvokeMethodAsync("TestNoArgumentNoReturnType"); +await nonRemotingProxy.InvokeMethodAsync("SaveData", data); +await nonRemotingProxy.InvokeMethodAsync("GetData"); + +Console.WriteLine("Registering the timer and reminder"); +await proxy.RegisterTimer(); +await proxy.RegisterReminder(); +Console.WriteLine("Waiting so the timer and reminder can be triggered"); +await Task.Delay(6000); + +Console.WriteLine("Making call using actor proxy to get data after timer and reminder triggered"); +receivedData = await proxy.GetData(); +Console.WriteLine($"Received data is {receivedData}."); + +Console.WriteLine("Getting details of the registered reminder"); +var reminder = await proxy.GetReminder(); +Console.WriteLine($"Received reminder is {reminder}."); + +Console.WriteLine("Deregistering timer. Timers would any way stop if the actor is deactivated as part of Dapr garbage collection."); +await proxy.UnregisterTimer(); +Console.WriteLine("Deregistering reminder. Reminders are durable and would not stop until an explicit deregistration or the actor is deleted."); +await proxy.UnregisterReminder(); + +Console.WriteLine("Registering reminder with repetitions - The reminder will repeat 3 times."); +await proxy.RegisterReminderWithRepetitions(3); +Console.WriteLine("Waiting so the reminder can be triggered"); +await Task.Delay(5000); +Console.WriteLine("Getting details of the registered reminder"); +reminder = await proxy.GetReminder(); +Console.WriteLine($"Received reminder is {reminder?.ToString() ?? "None"} (expecting None)."); +Console.WriteLine("Registering reminder with ttl and repetitions, i.e. reminder stops when either condition is met - The reminder will repeat 2 times."); +await proxy.RegisterReminderWithTtlAndRepetitions(TimeSpan.FromSeconds(5), 2); +Console.WriteLine("Getting details of the registered reminder"); +reminder = await proxy.GetReminder(); +Console.WriteLine($"Received reminder is {reminder}."); +Console.WriteLine("Deregistering reminder. Reminders are durable and would not stop until an explicit deregistration or the actor is deleted."); +await proxy.UnregisterReminder(); + +Console.WriteLine("Registering reminder and Timer with TTL - The reminder will self delete after 10 seconds."); +await proxy.RegisterReminderWithTtl(TimeSpan.FromSeconds(10)); +await proxy.RegisterTimerWithTtl(TimeSpan.FromSeconds(10)); +Console.WriteLine("Getting details of the registered reminder"); +reminder = await proxy.GetReminder(); +Console.WriteLine($"Received reminder is {reminder}."); + +// Track the reminder. +var timer = new Timer(async state => Console.WriteLine($"Received data: {await proxy.GetData()}"), null, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5)); +await Task.Delay(TimeSpan.FromSeconds(21)); +await timer.DisposeAsync(); + +Console.WriteLine("Creating a Bank Actor"); +var bank = ActorProxy.Create(ActorId.CreateRandom(), "DemoActor"); +while (true) +{ + var balance = await bank.GetAccountBalance(); + Console.WriteLine($"Balance for account '{balance.AccountId}' is '{balance.Balance:c}'."); + + Console.WriteLine($"Withdrawing '{10m:c}'..."); + try + { + await bank.Withdraw(new WithdrawRequest(10m)); + } + catch (ActorMethodInvocationException ex) + { + Console.WriteLine($"Overdraft: {ex.Message}"); + break; } } diff --git a/examples/Actor/IDemoActor/IDemoActor.csproj b/examples/Actor/DemoActor.Interfaces/DemoActor.Interfaces.csproj similarity index 57% rename from examples/Actor/IDemoActor/IDemoActor.csproj rename to examples/Actor/DemoActor.Interfaces/DemoActor.Interfaces.csproj index 8b8a84b2..f4955942 100644 --- a/examples/Actor/IDemoActor/IDemoActor.csproj +++ b/examples/Actor/DemoActor.Interfaces/DemoActor.Interfaces.csproj @@ -1,5 +1,10 @@  + + IDemoActor + enable + + diff --git a/examples/Actor/IDemoActor/IBankActor.cs b/examples/Actor/DemoActor.Interfaces/IBankActor.cs similarity index 55% rename from examples/Actor/IDemoActor/IBankActor.cs rename to examples/Actor/DemoActor.Interfaces/IBankActor.cs index c495f027..92d84742 100644 --- a/examples/Actor/IDemoActor/IBankActor.cs +++ b/examples/Actor/DemoActor.Interfaces/IBankActor.cs @@ -15,32 +15,18 @@ using System; using System.Threading.Tasks; using Dapr.Actors; -namespace IDemoActor +namespace IDemoActor; + +public interface IBankActor : IActor { - public interface IBankActor : IActor - { - Task GetAccountBalance(); + Task GetAccountBalance(); - Task Withdraw(WithdrawRequest withdraw); - } - - public class AccountBalance - { - public string AccountId { get; set; } - - public decimal Balance { get; set; } - } - - public class WithdrawRequest - { - public decimal Amount { get; set; } - } - - public class OverdraftException : Exception - { - public OverdraftException(decimal balance, decimal amount) - : base($"Your current balance is {balance:c} - that's not enough to withdraw {amount:c}.") - { - } - } + Task Withdraw(WithdrawRequest withdraw); } + +public sealed record AccountBalance(string AccountId, decimal Balance); + +public sealed record WithdrawRequest(decimal Amount); + +public class OverdraftException(decimal balance, decimal amount) + : Exception($"Your current balance is {balance:c} - that's not enough to withdraw {amount:c}."); diff --git a/examples/Actor/DemoActor.Interfaces/IDemoActor.cs b/examples/Actor/DemoActor.Interfaces/IDemoActor.cs new file mode 100644 index 00000000..c5fefdb9 --- /dev/null +++ b/examples/Actor/DemoActor.Interfaces/IDemoActor.cs @@ -0,0 +1,129 @@ +// ------------------------------------------------------------------------ +// 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. +// 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.Threading.Tasks; +using Dapr.Actors; + +namespace IDemoActor; + +/// +/// Interface for Actor method. +/// +public interface IDemoActor : IActor +{ + /// + /// Method to save data. + /// + /// DAta to save. + /// TTL of state key. + /// A task that represents the asynchronous save operation. + Task SaveData(MyData data, TimeSpan ttl); + + /// + /// Method to get data. + /// + /// A task that represents the asynchronous save operation. + Task GetData(); + + /// + /// A test method which throws exception. + /// + /// A task that represents the asynchronous save operation. + Task TestThrowException(); + + /// + /// A test method which validates calls for methods with no arguments and no return types. + /// + /// A task that represents the asynchronous save operation. + Task TestNoArgumentNoReturnType(); + + /// + /// Registers a reminder. + /// + /// A task that represents the asynchronous save operation. + Task RegisterReminder(); + + /// + /// Registers a reminder. + /// + /// TimeSpan that dictates when the reminder expires. + /// A task that represents the asynchronous save operation. + Task RegisterReminderWithTtl(TimeSpan ttl); + + /// + /// Unregisters the registered reminder. + /// + /// Task representing the operation. + Task UnregisterReminder(); + + /// + /// Registers a timer. + /// + /// A task that represents the asynchronous save operation. + Task RegisterTimer(); + + /// + /// Registers a timer. + /// + /// Optional TimeSpan that dictates when the timer expires. + /// A task that represents the asynchronous save operation. + Task RegisterTimerWithTtl(TimeSpan ttl); + + /// + /// Registers a reminder with repetitions. + /// + /// The number of repetitions for which the reminder should be invoked. + /// A task that represents the asynchronous save operation. + Task RegisterReminderWithRepetitions(int repetitions); + + /// + /// Registers a reminder with ttl and repetitions. + /// + /// TimeSpan that dictates when the timer expires. + /// The number of repetitions for which the reminder should be invoked. + /// A task that represents the asynchronous save operation. + Task RegisterReminderWithTtlAndRepetitions(TimeSpan ttl, int repetitions); + + /// + /// Gets the registered reminder. + /// + /// A task that returns the reminder after completion. + Task GetReminder(); + + /// + /// Unregisters the registered timer. + /// + /// A task that represents the asynchronous save operation. + Task UnregisterTimer(); +} + +/// +/// Data Used by the Sample Actor. +/// +public sealed record MyData(string? PropertyA, string? PropertyB) +{ + /// + public override string ToString() => $"PropertyA: {PropertyA ?? "null"}, PropertyB: {PropertyB ?? "null"}"; +} + +public class ActorReminderData +{ + public string? Name { get; set; } + + public TimeSpan DueTime { get; set; } + + public TimeSpan Period { get; set; } + + public override string ToString() => $"Name: {this.Name}, DueTime: {this.DueTime}, Period: {this.Period}"; +} diff --git a/examples/Actor/DemoActor/BankService.cs b/examples/Actor/DemoActor/BankService.cs index a24eaded..22d2ac44 100644 --- a/examples/Actor/DemoActor/BankService.cs +++ b/examples/Actor/DemoActor/BankService.cs @@ -13,24 +13,23 @@ using IDemoActor; -namespace DemoActor +namespace DemoActor; + +public sealed class BankService { - public class BankService + // Allow overdraft of up to 50 (of whatever currency). + private const decimal OverdraftThreshold = -50m; + + public decimal Withdraw(decimal balance, decimal amount) { - // Allow overdraft of up to 50 (of whatever currency). - private readonly decimal OverdraftThreshold = -50m; + // Imagine putting some complex auditing logic here in addition to the basics. - public decimal Withdraw(decimal balance, decimal amount) + var updated = balance - amount; + if (updated < OverdraftThreshold) { - // Imagine putting some complex auditing logic here in addition to the basics. - - var updated = balance - amount; - if (updated < OverdraftThreshold) - { - throw new OverdraftException(balance, amount); - } - - return updated; + throw new OverdraftException(balance, amount); } + + return updated; } } diff --git a/examples/Actor/DemoActor/DemoActor.cs b/examples/Actor/DemoActor/DemoActor.cs index b5ef53e9..3e5de369 100644 --- a/examples/Actor/DemoActor/DemoActor.cs +++ b/examples/Actor/DemoActor/DemoActor.cs @@ -17,195 +17,182 @@ using System.Threading.Tasks; using Dapr.Actors.Runtime; using IDemoActor; -namespace DemoActor +namespace DemoActor; + +// The following example showcases a few features of Actors +// +// Every actor should inherit from the Actor type, and must implement one or more actor interfaces. +// In this case the actor interfaces are DemoActor.Interfaces and IBankActor. +// +// For Actors to use Reminders, it must derive from IRemindable. +// If you don't intend to use Reminder feature, you can skip implementing IRemindable and reminder +// specific methods which are shown in the code below. +public class DemoActor(ActorHost host, BankService bank) : Actor(host), IDemoActor.IDemoActor, IBankActor, IRemindable { - // The following example showcases a few features of Actors - // - // Every actor should inherit from the Actor type, and must implement one or more actor interfaces. - // In this case the actor interfaces are IDemoActor and IBankActor. - // - // For Actors to use Reminders, it must derive from IRemindable. - // If you don't intend to use Reminder feature, you can skip implementing IRemindable and reminder - // specific methods which are shown in the code below. - public class DemoActor : Actor, IDemoActor.IDemoActor, IBankActor, IRemindable + private const string StateName = "my_data"; + + public async Task SaveData(MyData data, TimeSpan ttl) { - private const string StateName = "my_data"; + Console.WriteLine($"This is Actor id {this.Id} with data {data}."); - private readonly BankService bank; + // Set State using StateManager, state is saved after the method execution. + await this.StateManager.SetStateAsync(StateName, data, ttl); + } - public DemoActor(ActorHost host, BankService bank) - : base(host) - { - // BankService is provided by dependency injection. - // See Program.cs - this.bank = bank; - } + public Task GetData() + { + // Get state using StateManager. + return this.StateManager.GetStateAsync(StateName); + } - public async Task SaveData(MyData data, TimeSpan ttl) - { - Console.WriteLine($"This is Actor id {this.Id} with data {data}."); + public Task TestThrowException() + { + throw new NotImplementedException(); + } - // Set State using StateManager, state is saved after the method execution. - await this.StateManager.SetStateAsync(StateName, data, ttl); - } + public Task TestNoArgumentNoReturnType() + { + return Task.CompletedTask; + } - public Task GetData() - { - // Get state using StateManager. - return this.StateManager.GetStateAsync(StateName); - } + public async Task RegisterReminder() + { + await this.RegisterReminderAsync("TestReminder", null, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5)); + } - public Task TestThrowException() - { - throw new NotImplementedException(); - } - - public Task TestNoArgumentNoReturnType() - { - return Task.CompletedTask; - } - - public async Task RegisterReminder() - { - await this.RegisterReminderAsync("TestReminder", null, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5)); - } - - public async Task RegisterReminderWithTtl(TimeSpan ttl) - { - await this.RegisterReminderAsync("TestReminder", null, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5), ttl); - } + public async Task RegisterReminderWithTtl(TimeSpan ttl) + { + await this.RegisterReminderAsync("TestReminder", null, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5), ttl); + } - public async Task RegisterReminderWithRepetitions(int repetitions) - { - await this.RegisterReminderAsync("TestReminder", null, TimeSpan.FromSeconds(0), TimeSpan.FromSeconds(1), repetitions); - } + public async Task RegisterReminderWithRepetitions(int repetitions) + { + await this.RegisterReminderAsync("TestReminder", null, TimeSpan.FromSeconds(0), TimeSpan.FromSeconds(1), repetitions); + } - public async Task RegisterReminderWithTtlAndRepetitions(TimeSpan ttl, int repetitions) - { - await this.RegisterReminderAsync("TestReminder", null, TimeSpan.FromSeconds(0), TimeSpan.FromSeconds(1), repetitions, ttl); - } + public async Task RegisterReminderWithTtlAndRepetitions(TimeSpan ttl, int repetitions) + { + await this.RegisterReminderAsync("TestReminder", null, TimeSpan.FromSeconds(0), TimeSpan.FromSeconds(1), repetitions, ttl); + } - public async Task GetReminder() - { - var reminder = await this.GetReminderAsync("TestReminder"); + public async Task GetReminder() + { + var reminder = await this.GetReminderAsync("TestReminder"); - return reminder is not null - ? new ActorReminderData - { - Name = reminder.Name, - Period = reminder.Period, - DueTime = reminder.DueTime - } - : null; - } + return reminder is not null + ? new ActorReminderData + { + Name = reminder.Name, + Period = reminder.Period, + DueTime = reminder.DueTime + } + : null; + } - public Task UnregisterReminder() + public Task UnregisterReminder() + { + return this.UnregisterReminderAsync("TestReminder"); + } + + public async Task ReceiveReminderAsync(string reminderName, byte[] state, TimeSpan dueTime, TimeSpan period) + { + // This method is invoked when an actor reminder is fired. + var actorState = await this.StateManager.GetStateAsync(StateName); + var updatedActorState = actorState with { - return this.UnregisterReminderAsync("TestReminder"); - } + PropertyB = $"Reminder triggered at '{DateTime.Now:yyyy-MM-ddTHH:mm:ss}'" + }; + await this.StateManager.SetStateAsync(StateName, updatedActorState, ttl: TimeSpan.FromMinutes(5)); + } - public async Task ReceiveReminderAsync(string reminderName, byte[] state, TimeSpan dueTime, TimeSpan period) + class TimerParams + { + public int IntParam { get; set; } + public string? StringParam { get; set; } + } + + /// + public Task RegisterTimer() + { + var timerParams = new TimerParams { - // This method is invoked when an actor reminder is fired. - var actorState = await this.StateManager.GetStateAsync(StateName); - actorState.PropertyB = $"Reminder triggered at '{DateTime.Now:yyyy-MM-ddTHH:mm:ss}'"; - await this.StateManager.SetStateAsync(StateName, actorState, ttl: TimeSpan.FromMinutes(5)); - } + IntParam = 100, + StringParam = "timer test", + }; - class TimerParams + var serializedTimerParams = JsonSerializer.SerializeToUtf8Bytes(timerParams); + return this.RegisterTimerAsync("TestTimer", nameof(this.TimerCallback), serializedTimerParams, TimeSpan.FromSeconds(3), TimeSpan.FromSeconds(3)); + } + + public Task RegisterTimerWithTtl(TimeSpan ttl) + { + var timerParams = new TimerParams { - public int IntParam { get; set; } - public string StringParam { get; set; } - } + IntParam = 100, + StringParam = "timer test", + }; - /// - public Task RegisterTimer() + var serializedTimerParams = JsonSerializer.SerializeToUtf8Bytes(timerParams); + return this.RegisterTimerAsync("TestTimer", nameof(this.TimerCallback), serializedTimerParams, TimeSpan.FromSeconds(3), TimeSpan.FromSeconds(3), ttl); + } + + public Task UnregisterTimer() + { + return this.UnregisterTimerAsync("TestTimer"); + } + + // This method is called whenever an actor is activated. + // An actor is activated the first time any of its methods are invoked. + protected override Task OnActivateAsync() + { + // Provides opportunity to perform some optional setup. + return Task.CompletedTask; + } + + // This method is called whenever an actor is deactivated after a period of inactivity. + protected override Task OnDeactivateAsync() + { + // Provides Opportunity to perform optional cleanup. + return Task.CompletedTask; + } + + /// + /// This method is called when the timer is triggered based on its registration. + /// It updates the PropertyA value. + /// + /// Timer input data. + /// A task that represents the asynchronous operation. + public async Task TimerCallback(byte[] data) + { + var state = await this.StateManager.GetStateAsync(StateName); + var updatedState = state with { PropertyA = $"Timer triggered at '{DateTime.Now:yyyyy-MM-ddTHH:mm:s}'" }; + await this.StateManager.SetStateAsync(StateName, updatedState, ttl: TimeSpan.FromMinutes(5)); + var timerParams = JsonSerializer.Deserialize(data); + if (timerParams != null) { - var timerParams = new TimerParams - { - IntParam = 100, - StringParam = "timer test", - }; - - var serializedTimerParams = JsonSerializer.SerializeToUtf8Bytes(timerParams); - return this.RegisterTimerAsync("TestTimer", nameof(this.TimerCallback), serializedTimerParams, TimeSpan.FromSeconds(3), TimeSpan.FromSeconds(3)); - } - - public Task RegisterTimerWithTtl(TimeSpan ttl) - { - var timerParams = new TimerParams - { - IntParam = 100, - StringParam = "timer test", - }; - - var serializedTimerParams = JsonSerializer.SerializeToUtf8Bytes(timerParams); - return this.RegisterTimerAsync("TestTimer", nameof(this.TimerCallback), serializedTimerParams, TimeSpan.FromSeconds(3), TimeSpan.FromSeconds(3), ttl); - } - - public Task UnregisterTimer() - { - return this.UnregisterTimerAsync("TestTimer"); - } - - // This method is called whenever an actor is activated. - // An actor is activated the first time any of its methods are invoked. - protected override Task OnActivateAsync() - { - // Provides opportunity to perform some optional setup. - return Task.CompletedTask; - } - - // This method is called whenever an actor is deactivated after a period of inactivity. - protected override Task OnDeactivateAsync() - { - // Provides Opportunity to perform optional cleanup. - return Task.CompletedTask; - } - - /// - /// This method is called when the timer is triggered based on its registration. - /// It updates the PropertyA value. - /// - /// Timer input data. - /// A task that represents the asynchronous operation. - public async Task TimerCallback(byte[] data) - { - var state = await this.StateManager.GetStateAsync(StateName); - state.PropertyA = $"Timer triggered at '{DateTime.Now:yyyyy-MM-ddTHH:mm:s}'"; - await this.StateManager.SetStateAsync(StateName, state, ttl: TimeSpan.FromMinutes(5)); - var timerParams = JsonSerializer.Deserialize(data); - Console.WriteLine("Timer parameter1: " + timerParams.IntParam); - Console.WriteLine("Timer parameter2: " + timerParams.StringParam); - } - - public async Task GetAccountBalance() - { - var starting = new AccountBalance() - { - AccountId = this.Id.GetId(), - Balance = 100m, // Start new accounts with 100, we're pretty generous. - }; - - var balance = await this.StateManager.GetOrAddStateAsync("balance", starting); - return balance; - } - - public async Task Withdraw(WithdrawRequest withdraw) - { - var starting = new AccountBalance() - { - AccountId = this.Id.GetId(), - Balance = 100m, // Start new accounts with 100, we're pretty generous. - }; - - var balance = await this.StateManager.GetOrAddStateAsync("balance", starting); - - // Throws Overdraft exception if the account doesn't have enough money. - var updated = this.bank.Withdraw(balance.Balance, withdraw.Amount); - - balance.Balance = updated; - await this.StateManager.SetStateAsync("balance", balance); + Console.WriteLine($"Timer parameter1: {timerParams.IntParam}"); + Console.WriteLine($"Timer parameter2: {timerParams.StringParam ?? ""}"); } } + + public async Task GetAccountBalance() + { + var starting = new AccountBalance(this.Id.GetId(), 100m); // Start new accounts with 100 million; we're pretty generous + + var balance = await this.StateManager.GetOrAddStateAsync("balance", starting); + return balance; + } + + public async Task Withdraw(WithdrawRequest withdraw) + { + var starting = new AccountBalance(this.Id.GetId(), 100m); // Start new accounts with 100 million; we're pretty generous. + + var balance = await this.StateManager.GetOrAddStateAsync("balance", starting); + + // Throws Overdraft exception if the account doesn't have enough money. + var updated = bank.Withdraw(balance.Balance, withdraw.Amount); + + balance = balance with { Balance = updated }; + await this.StateManager.SetStateAsync("balance", balance); + } } diff --git a/examples/Actor/DemoActor/DemoActor.csproj b/examples/Actor/DemoActor/DemoActor.csproj index 4ed29d66..f6bf9e8e 100644 --- a/examples/Actor/DemoActor/DemoActor.csproj +++ b/examples/Actor/DemoActor/DemoActor.csproj @@ -4,6 +4,7 @@ true true demo-actor + enable @@ -14,7 +15,7 @@ - + diff --git a/examples/Actor/DemoActor/Program.cs b/examples/Actor/DemoActor/Program.cs index 1d538b47..575bc718 100644 --- a/examples/Actor/DemoActor/Program.cs +++ b/examples/Actor/DemoActor/Program.cs @@ -11,23 +11,23 @@ // limitations under the License. // ------------------------------------------------------------------------ -using Microsoft.AspNetCore.Hosting; +using DemoActor; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -namespace DemoActor +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddSingleton(); +builder.Services.AddActors(options => { - public class Program - { - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } + options.Actors.RegisterActor(); +}); - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }); - } +var app = builder.Build(); +if (app.Environment.IsDevelopment()) +{ + app.UseDeveloperExceptionPage(); } + +app.UseRouting(); +app.MapActorsHandlers(); diff --git a/examples/Actor/DemoActor/Startup.cs b/examples/Actor/DemoActor/Startup.cs deleted file mode 100644 index f1165e3c..00000000 --- a/examples/Actor/DemoActor/Startup.cs +++ /dev/null @@ -1,59 +0,0 @@ -// ------------------------------------------------------------------------ -// 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. -// 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.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - -namespace DemoActor -{ - public class Startup - { - public Startup(IConfiguration configuration) - { - this.Configuration = configuration; - } - - public IConfiguration Configuration { get; } - - public void ConfigureServices(IServiceCollection services) - { - services.AddSingleton(); - services.AddActors(options => - { - options.Actors.RegisterActor(); - }); - } - - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - else - { - app.UseHsts(); - } - - app.UseRouting(); - - app.UseEndpoints(endpoints => - { - endpoints.MapActorsHandlers(); - }); - } - } -} diff --git a/examples/Actor/IDemoActor/IDemoActor.cs b/examples/Actor/IDemoActor/IDemoActor.cs deleted file mode 100644 index 6f2d3280..00000000 --- a/examples/Actor/IDemoActor/IDemoActor.cs +++ /dev/null @@ -1,149 +0,0 @@ -// ------------------------------------------------------------------------ -// 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. -// 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.Threading.Tasks; -using Dapr.Actors; - -namespace IDemoActor -{ - /// - /// Interface for Actor method. - /// - public interface IDemoActor : IActor - { - /// - /// Method to save data. - /// - /// DAta to save. - /// TTL of state key. - /// A task that represents the asynchronous save operation. - Task SaveData(MyData data, TimeSpan ttl); - - /// - /// Method to get data. - /// - /// A task that represents the asynchronous save operation. - Task GetData(); - - /// - /// A test method which throws exception. - /// - /// A task that represents the asynchronous save operation. - Task TestThrowException(); - - /// - /// A test method which validates calls for methods with no arguments and no return types. - /// - /// A task that represents the asynchronous save operation. - Task TestNoArgumentNoReturnType(); - - /// - /// Registers a reminder. - /// - /// A task that represents the asynchronous save operation. - Task RegisterReminder(); - - /// - /// Registers a reminder. - /// - /// TimeSpan that dictates when the reminder expires. - /// A task that represents the asynchronous save operation. - Task RegisterReminderWithTtl(TimeSpan ttl); - - /// - /// Unregisters the registered reminder. - /// - /// Task representing the operation. - Task UnregisterReminder(); - - /// - /// Registers a timer. - /// - /// A task that represents the asynchronous save operation. - Task RegisterTimer(); - - /// - /// Registers a timer. - /// - /// Optional TimeSpan that dictates when the timer expires. - /// A task that represents the asynchronous save operation. - Task RegisterTimerWithTtl(TimeSpan ttl); - - /// - /// Registers a reminder with repetitions. - /// - /// The number of repetitions for which the reminder should be invoked. - /// A task that represents the asynchronous save operation. - Task RegisterReminderWithRepetitions(int repetitions); - - /// - /// Registers a reminder with ttl and repetitions. - /// - /// TimeSpan that dictates when the timer expires. - /// The number of repetitions for which the reminder should be invoked. - /// A task that represents the asynchronous save operation. - Task RegisterReminderWithTtlAndRepetitions(TimeSpan ttl, int repetitions); - - /// - /// Gets the registered reminder. - /// - /// The name of the reminder. - /// A task that returns the reminder after completion. - Task GetReminder(); - - /// - /// Unregisters the registered timer. - /// - /// A task that represents the asynchronous save operation. - Task UnregisterTimer(); - } - - /// - /// Data Used by the Sample Actor. - /// - public class MyData - { - /// - /// Gets or sets the value for PropertyA. - /// - public string PropertyA { get; set; } - - /// - /// Gets or sets the value for PropertyB. - /// - public string PropertyB { get; set; } - - /// - public override string ToString() - { - var propAValue = this.PropertyA ?? "null"; - var propBValue = this.PropertyB ?? "null"; - return $"PropertyA: {propAValue}, PropertyB: {propBValue}"; - } - } - - public class ActorReminderData - { - public string Name { get; set; } - - public TimeSpan DueTime { get; set; } - - public TimeSpan Period { get; set; } - - public override string ToString() - { - return $"Name: {this.Name}, DueTime: {this.DueTime}, Period: {this.Period}"; - } - } -} diff --git a/examples/AspNetCore/ControllerSample/Account.cs b/examples/AspNetCore/ControllerSample/Account.cs index ed2bddd2..bcd0d350 100644 --- a/examples/AspNetCore/ControllerSample/Account.cs +++ b/examples/AspNetCore/ControllerSample/Account.cs @@ -11,21 +11,20 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace ControllerSample +namespace ControllerSample; + +/// +/// Class representing an Account for samples. +/// +public class Account { /// - /// Class representing an Account for samples. + /// Gets or sets account id. /// - public class Account - { - /// - /// Gets or sets account id. - /// - public string Id { get; set; } + public string Id { get; set; } - /// - /// Gets or sets account balance. - /// - public decimal Balance { get; set; } - } + /// + /// Gets or sets account balance. + /// + public decimal Balance { get; set; } } \ No newline at end of file diff --git a/examples/AspNetCore/ControllerSample/Controllers/SampleController.cs b/examples/AspNetCore/ControllerSample/Controllers/SampleController.cs index 5b339288..195c5ea5 100644 --- a/examples/AspNetCore/ControllerSample/Controllers/SampleController.cs +++ b/examples/AspNetCore/ControllerSample/Controllers/SampleController.cs @@ -13,286 +13,285 @@ using System.Linq; -namespace ControllerSample.Controllers +namespace ControllerSample.Controllers; + +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using Dapr; +using Dapr.AspNetCore; +using Dapr.Client; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +/// +/// Sample showing Dapr integration with controller. +/// +[ApiController] +public class SampleController : ControllerBase { - using System; - using System.Collections.Generic; - using System.Text; - using System.Text.Json; - using System.Threading.Tasks; - using Dapr; - using Dapr.AspNetCore; - using Dapr.Client; - using Microsoft.AspNetCore.Mvc; - using Microsoft.Extensions.Logging; + /// + /// SampleController Constructor with logger injection + /// + /// + public SampleController(ILogger logger) + { + this.logger = logger; + } /// - /// Sample showing Dapr integration with controller. + /// State store name. /// - [ApiController] - public class SampleController : ControllerBase + public const string StoreName = "statestore"; + + private readonly ILogger logger; + + /// + /// Gets the account information as specified by the id. + /// + /// Account information for the id from Dapr state store. + /// Account information. + [HttpGet("{account}")] + public ActionResult Get([FromState(StoreName)] StateEntry account) { - /// - /// SampleController Constructor with logger injection - /// - /// - public SampleController(ILogger logger) + if (account.Value is null) { - this.logger = logger; + return this.NotFound(); } - /// - /// State store name. - /// - public const string StoreName = "statestore"; + return account.Value; + } - private readonly ILogger logger; + /// + /// Method for depositing to account as specified in transaction. + /// + /// Transaction info. + /// State client to interact with Dapr runtime. + /// A representing the result of the asynchronous operation. + /// "pubsub", the first parameter into the Topic attribute, is name of the default pub/sub configured by the Dapr CLI. + [Topic("pubsub", "deposit", "amountDeadLetterTopic", false)] + [HttpPost("deposit")] + public async Task> Deposit(Transaction transaction, [FromServices] DaprClient daprClient) + { + // Example reading cloudevent properties from the headers + var headerEntries = Request.Headers.Aggregate("", (current, header) => current + ($"------- Header: {header.Key} : {header.Value}" + Environment.NewLine)); - /// - /// Gets the account information as specified by the id. - /// - /// Account information for the id from Dapr state store. - /// Account information. - [HttpGet("{account}")] - public ActionResult Get([FromState(StoreName)] StateEntry account) + logger.LogInformation(headerEntries); + + logger.LogInformation("Enter deposit"); + var state = await daprClient.GetStateEntryAsync(StoreName, transaction.Id); + state.Value ??= new Account() { Id = transaction.Id, }; + logger.LogInformation("Id is {0}, the amount to be deposited is {1}", transaction.Id, transaction.Amount); + + if (transaction.Amount < 0m) { - if (account.Value is null) - { - return this.NotFound(); - } - - return account.Value; - } - - /// - /// Method for depositing to account as specified in transaction. - /// - /// Transaction info. - /// State client to interact with Dapr runtime. - /// A representing the result of the asynchronous operation. - /// "pubsub", the first parameter into the Topic attribute, is name of the default pub/sub configured by the Dapr CLI. - [Topic("pubsub", "deposit", "amountDeadLetterTopic", false)] - [HttpPost("deposit")] - public async Task> Deposit(Transaction transaction, [FromServices] DaprClient daprClient) - { - // Example reading cloudevent properties from the headers - var headerEntries = Request.Headers.Aggregate("", (current, header) => current + ($"------- Header: {header.Key} : {header.Value}" + Environment.NewLine)); - - logger.LogInformation(headerEntries); - - logger.LogInformation("Enter deposit"); - var state = await daprClient.GetStateEntryAsync(StoreName, transaction.Id); - state.Value ??= new Account() { Id = transaction.Id, }; - logger.LogInformation("Id is {0}, the amount to be deposited is {1}", transaction.Id, transaction.Amount); - - if (transaction.Amount < 0m) - { - return BadRequest(new { statusCode = 400, message = "bad request" }); - } - - state.Value.Balance += transaction.Amount; - logger.LogInformation("Balance for Id {0} is {1}", state.Value.Id, state.Value.Balance); - await state.SaveAsync(); - return state.Value; - } - - /// - /// Method for depositing multiple times to the account as specified in transaction. - /// - /// List of entries of type BulkMessageModel received from dapr. - /// State client to interact with Dapr runtime. - /// A representing the result of the asynchronous operation. - /// "pubsub", the first parameter into the Topic attribute, is name of the default pub/sub configured by the Dapr CLI. - [Topic("pubsub", "multideposit", "amountDeadLetterTopic", false)] - [BulkSubscribe("multideposit", 500, 2000)] - [HttpPost("multideposit")] - public async Task> MultiDeposit([FromBody] - BulkSubscribeMessage> - bulkMessage, [FromServices] DaprClient daprClient) - { - logger.LogInformation("Enter bulk deposit"); - - List entries = new List(); - - foreach (var entry in bulkMessage.Entries) - { - try - { - var transaction = entry.Event.Data; - - var state = await daprClient.GetStateEntryAsync(StoreName, transaction.Id); - state.Value ??= new Account() { Id = transaction.Id, }; - logger.LogInformation("Id is {0}, the amount to be deposited is {1}", - transaction.Id, transaction.Amount); - - if (transaction.Amount < 0m) - { - return BadRequest(new { statusCode = 400, message = "bad request" }); - } - - state.Value.Balance += transaction.Amount; - logger.LogInformation("Balance is {0}", state.Value.Balance); - await state.SaveAsync(); - entries.Add( - new BulkSubscribeAppResponseEntry(entry.EntryId, BulkSubscribeAppResponseStatus.SUCCESS)); - } - catch (Exception e) - { - logger.LogError(e.Message); - entries.Add(new BulkSubscribeAppResponseEntry(entry.EntryId, BulkSubscribeAppResponseStatus.RETRY)); - } - } - - return new BulkSubscribeAppResponse(entries); - } - - /// - /// Method for viewing the error message when the deposit/withdrawal amounts - /// are negative. - /// - /// Transaction info. - [Topic("pubsub", "amountDeadLetterTopic")] - [HttpPost("deadLetterTopicRoute")] - public ActionResult ViewErrorMessage(Transaction transaction) - { - logger.LogInformation("The amount cannot be negative: {0}", transaction.Amount); - return Ok(); - } - - /// - /// Method for withdrawing from account as specified in transaction. - /// - /// Transaction info. - /// State client to interact with Dapr runtime. - /// A representing the result of the asynchronous operation. - /// "pubsub", the first parameter into the Topic attribute, is name of the default pub/sub configured by the Dapr CLI. - [Topic("pubsub", "withdraw", "amountDeadLetterTopic", false)] - [HttpPost("withdraw")] - public async Task> Withdraw(Transaction transaction, [FromServices] DaprClient daprClient) - { - logger.LogInformation("Enter withdraw method..."); - var state = await daprClient.GetStateEntryAsync(StoreName, transaction.Id); - logger.LogInformation("Id is {0}, the amount to be withdrawn is {1}", transaction.Id, transaction.Amount); - - if (state.Value == null) - { - return this.NotFound(); - } - - if (transaction.Amount < 0m) - { - return BadRequest(new { statusCode = 400, message = "bad request" }); - } - - state.Value.Balance -= transaction.Amount; - logger.LogInformation("Balance is {0}", state.Value.Balance); - await state.SaveAsync(); - return state.Value; - } - - /// - /// Method for withdrawing from account as specified in transaction. - /// - /// Transaction info. - /// State client to interact with Dapr runtime. - /// A representing the result of the asynchronous operation. - /// "pubsub", the first parameter into the Topic attribute, is name of the default pub/sub configured by the Dapr CLI. - [Topic("pubsub", "withdraw", "event.type ==\"withdraw.v2\"", 1)] - [HttpPost("withdraw.v2")] - public async Task> WithdrawV2(TransactionV2 transaction, - [FromServices] DaprClient daprClient) - { - logger.LogInformation("Enter withdraw.v2"); - if (transaction.Channel == "mobile" && transaction.Amount > 10000) - { - return this.Unauthorized("mobile transactions for large amounts are not permitted."); - } - - var state = await daprClient.GetStateEntryAsync(StoreName, transaction.Id); - - if (state.Value == null) - { - return this.NotFound(); - } - - state.Value.Balance -= transaction.Amount; - await state.SaveAsync(); - return state.Value; - } - - /// - /// Method for depositing to account as specified in transaction via a raw message. - /// - /// Transaction info. - /// State client to interact with Dapr runtime. - /// A representing the result of the asynchronous operation. - /// "pubsub", the first parameter into the Topic attribute, is name of the default pub/sub configured by the Dapr CLI. - [Topic("pubsub", "rawDeposit", true)] - [HttpPost("rawDeposit")] - public async Task> RawDeposit([FromBody] JsonDocument rawTransaction, - [FromServices] DaprClient daprClient) - { - var transactionString = rawTransaction.RootElement.GetProperty("data_base64").GetString(); - logger.LogInformation( - $"Enter deposit: {transactionString} - {Encoding.UTF8.GetString(Convert.FromBase64String(transactionString))}"); - var transactionJson = JsonSerializer.Deserialize(Convert.FromBase64String(transactionString)); - var transaction = - JsonSerializer.Deserialize(transactionJson.RootElement.GetProperty("data").GetRawText()); - var state = await daprClient.GetStateEntryAsync(StoreName, transaction.Id); - state.Value ??= new Account() { Id = transaction.Id, }; - logger.LogInformation("Id is {0}, the amount to be deposited is {1}", transaction.Id, transaction.Amount); - - if (transaction.Amount < 0m) - { - return BadRequest(new { statusCode = 400, message = "bad request" }); - } - - state.Value.Balance += transaction.Amount; - logger.LogInformation("Balance is {0}", state.Value.Balance); - await state.SaveAsync(); - return state.Value; - } - - /// - /// Method for returning a BadRequest result which will cause Dapr sidecar to throw an RpcException - /// - [HttpPost("throwException")] - public async Task> ThrowException(Transaction transaction, - [FromServices] DaprClient daprClient) - { - logger.LogInformation("Enter ThrowException"); - var task = Task.Delay(10); - await task; return BadRequest(new { statusCode = 400, message = "bad request" }); } - /// - /// - /// Method which uses for binding this endpoint to a subscription. - /// - /// - /// This endpoint will be bound to a subscription where the topic name is the value of the environment variable 'CUSTOM_TOPIC' - /// and the pubsub name is the value of the environment variable 'CUSTOM_PUBSUB'. - /// - /// - [CustomTopic("%CUSTOM_PUBSUB%", "%CUSTOM_TOPIC%")] - [HttpPost("exampleCustomTopic")] - public ActionResult ExampleCustomTopic(Transaction transaction) + state.Value.Balance += transaction.Amount; + logger.LogInformation("Balance for Id {0} is {1}", state.Value.Id, state.Value.Balance); + await state.SaveAsync(); + return state.Value; + } + + /// + /// Method for depositing multiple times to the account as specified in transaction. + /// + /// List of entries of type BulkMessageModel received from dapr. + /// State client to interact with Dapr runtime. + /// A representing the result of the asynchronous operation. + /// "pubsub", the first parameter into the Topic attribute, is name of the default pub/sub configured by the Dapr CLI. + [Topic("pubsub", "multideposit", "amountDeadLetterTopic", false)] + [BulkSubscribe("multideposit", 500, 2000)] + [HttpPost("multideposit")] + public async Task> MultiDeposit([FromBody] + BulkSubscribeMessage> + bulkMessage, [FromServices] DaprClient daprClient) + { + logger.LogInformation("Enter bulk deposit"); + + List entries = new List(); + + foreach (var entry in bulkMessage.Entries) { - return Ok(); + try + { + var transaction = entry.Event.Data; + + var state = await daprClient.GetStateEntryAsync(StoreName, transaction.Id); + state.Value ??= new Account() { Id = transaction.Id, }; + logger.LogInformation("Id is {0}, the amount to be deposited is {1}", + transaction.Id, transaction.Amount); + + if (transaction.Amount < 0m) + { + return BadRequest(new { statusCode = 400, message = "bad request" }); + } + + state.Value.Balance += transaction.Amount; + logger.LogInformation("Balance is {0}", state.Value.Balance); + await state.SaveAsync(); + entries.Add( + new BulkSubscribeAppResponseEntry(entry.EntryId, BulkSubscribeAppResponseStatus.SUCCESS)); + } + catch (Exception e) + { + logger.LogError(e.Message); + entries.Add(new BulkSubscribeAppResponseEntry(entry.EntryId, BulkSubscribeAppResponseStatus.RETRY)); + } } - /// - /// Method which uses for binding this endpoint to a subscription and adds routingkey metadata. - /// - /// - /// - [Topic("pubsub", "topicmetadata")] - [TopicMetadata("routingKey", "keyA")] - [HttpPost("examplecustomtopicmetadata")] - public ActionResult ExampleCustomTopicMetadata(Transaction transaction) - { - return Ok(); - } + return new BulkSubscribeAppResponse(entries); } -} + + /// + /// Method for viewing the error message when the deposit/withdrawal amounts + /// are negative. + /// + /// Transaction info. + [Topic("pubsub", "amountDeadLetterTopic")] + [HttpPost("deadLetterTopicRoute")] + public ActionResult ViewErrorMessage(Transaction transaction) + { + logger.LogInformation("The amount cannot be negative: {0}", transaction.Amount); + return Ok(); + } + + /// + /// Method for withdrawing from account as specified in transaction. + /// + /// Transaction info. + /// State client to interact with Dapr runtime. + /// A representing the result of the asynchronous operation. + /// "pubsub", the first parameter into the Topic attribute, is name of the default pub/sub configured by the Dapr CLI. + [Topic("pubsub", "withdraw", "amountDeadLetterTopic", false)] + [HttpPost("withdraw")] + public async Task> Withdraw(Transaction transaction, [FromServices] DaprClient daprClient) + { + logger.LogInformation("Enter withdraw method..."); + var state = await daprClient.GetStateEntryAsync(StoreName, transaction.Id); + logger.LogInformation("Id is {0}, the amount to be withdrawn is {1}", transaction.Id, transaction.Amount); + + if (state.Value == null) + { + return this.NotFound(); + } + + if (transaction.Amount < 0m) + { + return BadRequest(new { statusCode = 400, message = "bad request" }); + } + + state.Value.Balance -= transaction.Amount; + logger.LogInformation("Balance is {0}", state.Value.Balance); + await state.SaveAsync(); + return state.Value; + } + + /// + /// Method for withdrawing from account as specified in transaction. + /// + /// Transaction info. + /// State client to interact with Dapr runtime. + /// A representing the result of the asynchronous operation. + /// "pubsub", the first parameter into the Topic attribute, is name of the default pub/sub configured by the Dapr CLI. + [Topic("pubsub", "withdraw", "event.type ==\"withdraw.v2\"", 1)] + [HttpPost("withdraw.v2")] + public async Task> WithdrawV2(TransactionV2 transaction, + [FromServices] DaprClient daprClient) + { + logger.LogInformation("Enter withdraw.v2"); + if (transaction.Channel == "mobile" && transaction.Amount > 10000) + { + return this.Unauthorized("mobile transactions for large amounts are not permitted."); + } + + var state = await daprClient.GetStateEntryAsync(StoreName, transaction.Id); + + if (state.Value == null) + { + return this.NotFound(); + } + + state.Value.Balance -= transaction.Amount; + await state.SaveAsync(); + return state.Value; + } + + /// + /// Method for depositing to account as specified in transaction via a raw message. + /// + /// Transaction info. + /// State client to interact with Dapr runtime. + /// A representing the result of the asynchronous operation. + /// "pubsub", the first parameter into the Topic attribute, is name of the default pub/sub configured by the Dapr CLI. + [Topic("pubsub", "rawDeposit", true)] + [HttpPost("rawDeposit")] + public async Task> RawDeposit([FromBody] JsonDocument rawTransaction, + [FromServices] DaprClient daprClient) + { + var transactionString = rawTransaction.RootElement.GetProperty("data_base64").GetString(); + logger.LogInformation( + $"Enter deposit: {transactionString} - {Encoding.UTF8.GetString(Convert.FromBase64String(transactionString))}"); + var transactionJson = JsonSerializer.Deserialize(Convert.FromBase64String(transactionString)); + var transaction = + JsonSerializer.Deserialize(transactionJson.RootElement.GetProperty("data").GetRawText()); + var state = await daprClient.GetStateEntryAsync(StoreName, transaction.Id); + state.Value ??= new Account() { Id = transaction.Id, }; + logger.LogInformation("Id is {0}, the amount to be deposited is {1}", transaction.Id, transaction.Amount); + + if (transaction.Amount < 0m) + { + return BadRequest(new { statusCode = 400, message = "bad request" }); + } + + state.Value.Balance += transaction.Amount; + logger.LogInformation("Balance is {0}", state.Value.Balance); + await state.SaveAsync(); + return state.Value; + } + + /// + /// Method for returning a BadRequest result which will cause Dapr sidecar to throw an RpcException + /// + [HttpPost("throwException")] + public async Task> ThrowException(Transaction transaction, + [FromServices] DaprClient daprClient) + { + logger.LogInformation("Enter ThrowException"); + var task = Task.Delay(10); + await task; + return BadRequest(new { statusCode = 400, message = "bad request" }); + } + + /// + /// + /// Method which uses for binding this endpoint to a subscription. + /// + /// + /// This endpoint will be bound to a subscription where the topic name is the value of the environment variable 'CUSTOM_TOPIC' + /// and the pubsub name is the value of the environment variable 'CUSTOM_PUBSUB'. + /// + /// + [CustomTopic("%CUSTOM_PUBSUB%", "%CUSTOM_TOPIC%")] + [HttpPost("exampleCustomTopic")] + public ActionResult ExampleCustomTopic(Transaction transaction) + { + return Ok(); + } + + /// + /// Method which uses for binding this endpoint to a subscription and adds routingkey metadata. + /// + /// + /// + [Topic("pubsub", "topicmetadata")] + [TopicMetadata("routingKey", "keyA")] + [HttpPost("examplecustomtopicmetadata")] + public ActionResult ExampleCustomTopicMetadata(Transaction transaction) + { + return Ok(); + } +} \ No newline at end of file diff --git a/examples/AspNetCore/ControllerSample/CustomTopicAttribute.cs b/examples/AspNetCore/ControllerSample/CustomTopicAttribute.cs index 5c9996ae..eb96ba89 100644 --- a/examples/AspNetCore/ControllerSample/CustomTopicAttribute.cs +++ b/examples/AspNetCore/ControllerSample/CustomTopicAttribute.cs @@ -13,33 +13,32 @@ using Dapr.AspNetCore; -namespace ControllerSample +namespace ControllerSample; + +using System; +using Dapr; + +/// +/// Sample custom implementation that returns topic metadata from environment variables. +/// +[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] +public class CustomTopicAttribute : Attribute, ITopicMetadata { - using System; - using Dapr; - - /// - /// Sample custom implementation that returns topic metadata from environment variables. - /// - [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] - public class CustomTopicAttribute : Attribute, ITopicMetadata + public CustomTopicAttribute(string pubsubName, string name) { - public CustomTopicAttribute(string pubsubName, string name) - { - this.PubsubName = Environment.ExpandEnvironmentVariables(pubsubName); - this.Name = Environment.ExpandEnvironmentVariables(name); - } - - /// - public string PubsubName { get; } - - /// - public string Name { get; } - - /// - public new string Match { get; } - - /// - public int Priority { get; } + this.PubsubName = Environment.ExpandEnvironmentVariables(pubsubName); + this.Name = Environment.ExpandEnvironmentVariables(name); } -} + + /// + public string PubsubName { get; } + + /// + public string Name { get; } + + /// + public new string Match { get; } + + /// + public int Priority { get; } +} \ No newline at end of file diff --git a/examples/AspNetCore/ControllerSample/Program.cs b/examples/AspNetCore/ControllerSample/Program.cs index 251d11fc..68eb85cc 100644 --- a/examples/AspNetCore/ControllerSample/Program.cs +++ b/examples/AspNetCore/ControllerSample/Program.cs @@ -11,35 +11,34 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace ControllerSample +namespace ControllerSample; + +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +/// +/// Controller Sample. +/// +public class Program { - using Microsoft.AspNetCore.Hosting; - using Microsoft.Extensions.Hosting; + /// + /// Main for Controller Sample. + /// + /// Arguments. + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } /// - /// Controller Sample. + /// Creates WebHost Builder. /// - public class Program - { - /// - /// Main for Controller Sample. - /// - /// Arguments. - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } - - /// - /// Creates WebHost Builder. - /// - /// Arguments. - /// Returns IHostbuilder. - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }); - } -} + /// Arguments. + /// Returns IHostbuilder. + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); +} \ No newline at end of file diff --git a/examples/AspNetCore/ControllerSample/Startup.cs b/examples/AspNetCore/ControllerSample/Startup.cs index ddc6d1c5..17afa06b 100644 --- a/examples/AspNetCore/ControllerSample/Startup.cs +++ b/examples/AspNetCore/ControllerSample/Startup.cs @@ -16,68 +16,67 @@ using Dapr; using Dapr.AspNetCore; -namespace ControllerSample +namespace ControllerSample; + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +/// +/// Startup class. +/// +public class Startup { - using Microsoft.AspNetCore.Builder; - using Microsoft.AspNetCore.Hosting; - using Microsoft.Extensions.Configuration; - using Microsoft.Extensions.DependencyInjection; - using Microsoft.Extensions.Hosting; + /// + /// Initializes a new instance of the class. + /// + /// Configuration. + public Startup(IConfiguration configuration) + { + this.Configuration = configuration; + } /// - /// Startup class. + /// Gets the configuration. /// - public class Startup + public IConfiguration Configuration { get; } + + /// + /// Configures Services. + /// + /// Service Collection. + public void ConfigureServices(IServiceCollection services) { - /// - /// Initializes a new instance of the class. - /// - /// Configuration. - public Startup(IConfiguration configuration) - { - this.Configuration = configuration; - } - - /// - /// Gets the configuration. - /// - public IConfiguration Configuration { get; } - - /// - /// Configures Services. - /// - /// Service Collection. - public void ConfigureServices(IServiceCollection services) - { - services.AddControllers().AddDapr(); - } - - /// - /// Configures Application Builder and WebHost environment. - /// - /// Application builder. - /// Webhost environment. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.UseRouting(); - - app.UseCloudEvents(new CloudEventsMiddlewareOptions - { - ForwardCloudEventPropertiesAsHeaders = true - }); - - app.UseAuthorization(); - - app.UseEndpoints(endpoints => - { - endpoints.MapSubscribeHandler(); - endpoints.MapControllers(); - }); - } + services.AddControllers().AddDapr(); } -} + + /// + /// Configures Application Builder and WebHost environment. + /// + /// Application builder. + /// Webhost environment. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseRouting(); + + app.UseCloudEvents(new CloudEventsMiddlewareOptions + { + ForwardCloudEventPropertiesAsHeaders = true + }); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapSubscribeHandler(); + endpoints.MapControllers(); + }); + } +} \ No newline at end of file diff --git a/examples/AspNetCore/ControllerSample/Transaction.cs b/examples/AspNetCore/ControllerSample/Transaction.cs index 2d6f5a5a..5b7a1c86 100644 --- a/examples/AspNetCore/ControllerSample/Transaction.cs +++ b/examples/AspNetCore/ControllerSample/Transaction.cs @@ -11,24 +11,23 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace ControllerSample +namespace ControllerSample; + +using System.ComponentModel.DataAnnotations; + +/// +/// Represents a transaction used by sample code. +/// +public class Transaction { - using System.ComponentModel.DataAnnotations; + /// + /// Gets or sets account id for the transaction. + /// + [Required] + public string Id { get; set; } /// - /// Represents a transaction used by sample code. - /// - public class Transaction - { - /// - /// Gets or sets account id for the transaction. - /// - [Required] - public string Id { get; set; } - - /// - /// Gets or sets amount for the transaction. - /// +/// Represents a transaction used by sample code. +/// +public class TransactionV2 { - using System.ComponentModel.DataAnnotations; + /// + /// Gets or sets account id for the transaction. + /// + [Required] + public string Id { get; set; } /// - /// Represents a transaction used by sample code. + /// Gets or sets amount for the transaction. /// - public class TransactionV2 - { - /// - /// Gets or sets account id for the transaction. - /// - [Required] - public string Id { get; set; } + [Range(0, double.MaxValue)] + public decimal Amount { get; set; } - /// - /// Gets or sets amount for the transaction. - /// - [Range(0, double.MaxValue)] - public decimal Amount { get; set; } - - /// - /// Gets or sets channel from which this transaction was received. - /// - [Required] - public string Channel { get; set; } - } + /// + /// Gets or sets channel from which this transaction was received. + /// + [Required] + public string Channel { get; set; } } \ No newline at end of file diff --git a/examples/AspNetCore/GrpcServiceSample/Models/Account.cs b/examples/AspNetCore/GrpcServiceSample/Models/Account.cs index 0b6f810d..91746bef 100644 --- a/examples/AspNetCore/GrpcServiceSample/Models/Account.cs +++ b/examples/AspNetCore/GrpcServiceSample/Models/Account.cs @@ -11,21 +11,20 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace GrpcServiceSample.Models +namespace GrpcServiceSample.Models; + +/// +/// Class representing an Account for samples. +/// +public class Account { /// - /// Class representing an Account for samples. + /// Gets or sets account id. /// - public class Account - { - /// - /// Gets or sets account id. - /// - public string Id { get; set; } + public string Id { get; set; } - /// - /// Gets or sets account balance. - /// - public decimal Balance { get; set; } - } -} + /// + /// Gets or sets account balance. + /// + public decimal Balance { get; set; } +} \ No newline at end of file diff --git a/examples/AspNetCore/GrpcServiceSample/Models/GetAccountInput.cs b/examples/AspNetCore/GrpcServiceSample/Models/GetAccountInput.cs index bcb835bd..7d7f59fc 100644 --- a/examples/AspNetCore/GrpcServiceSample/Models/GetAccountInput.cs +++ b/examples/AspNetCore/GrpcServiceSample/Models/GetAccountInput.cs @@ -11,16 +11,15 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace GrpcServiceSample.Models +namespace GrpcServiceSample.Models; + +/// +/// BankService GetAccount input model +/// +public class GetAccountInput { /// - /// BankService GetAccount input model + /// Id of account /// - public class GetAccountInput - { - /// - /// Id of account - /// - public string Id { get; set; } - } -} + public string Id { get; set; } +} \ No newline at end of file diff --git a/examples/AspNetCore/GrpcServiceSample/Models/Transaction.cs b/examples/AspNetCore/GrpcServiceSample/Models/Transaction.cs index 7a1ef7cb..a13e5c5e 100644 --- a/examples/AspNetCore/GrpcServiceSample/Models/Transaction.cs +++ b/examples/AspNetCore/GrpcServiceSample/Models/Transaction.cs @@ -13,23 +13,22 @@ using System.ComponentModel.DataAnnotations; -namespace GrpcServiceSample.Models +namespace GrpcServiceSample.Models; + +/// +/// Represents a transaction used by sample code. +/// +public class Transaction { /// - /// Represents a transaction used by sample code. + /// Gets or sets account id for the transaction. /// - public class Transaction - { - /// - /// Gets or sets account id for the transaction. - /// - [Required] - public string Id { get; set; } + [Required] + public string Id { get; set; } - /// - /// Gets or sets amount for the transaction. - /// - [Range(0, double.MaxValue)] - public decimal Amount { get; set; } - } -} + /// + /// Gets or sets amount for the transaction. + /// + [Range(0, double.MaxValue)] + public decimal Amount { get; set; } +} \ No newline at end of file diff --git a/examples/AspNetCore/GrpcServiceSample/Program.cs b/examples/AspNetCore/GrpcServiceSample/Program.cs index dbec5ba2..d7af0170 100644 --- a/examples/AspNetCore/GrpcServiceSample/Program.cs +++ b/examples/AspNetCore/GrpcServiceSample/Program.cs @@ -15,39 +15,38 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.Hosting; -namespace GrpcServiceSample +namespace GrpcServiceSample; + +/// +/// GrpcService Sample +/// +public class Program { /// - /// GrpcService Sample + /// Entry point /// - public class Program + /// + public static void Main(string[] args) { - /// - /// Entry point - /// - /// - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } - - /// - /// Creates WebHost Builder. - /// - /// - /// - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.ConfigureKestrel(options => - { - // Setup a HTTP/2 endpoint without TLS. - options.ListenLocalhost(5050, o => o.Protocols = - HttpProtocols.Http2); - }); - - webBuilder.UseStartup(); - }); + CreateHostBuilder(args).Build().Run(); } -} + + /// + /// Creates WebHost Builder. + /// + /// + /// + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.ConfigureKestrel(options => + { + // Setup a HTTP/2 endpoint without TLS. + options.ListenLocalhost(5050, o => o.Protocols = + HttpProtocols.Http2); + }); + + webBuilder.UseStartup(); + }); +} \ No newline at end of file diff --git a/examples/AspNetCore/GrpcServiceSample/Services/BankingService.cs b/examples/AspNetCore/GrpcServiceSample/Services/BankingService.cs index 9518fd61..05b03784 100644 --- a/examples/AspNetCore/GrpcServiceSample/Services/BankingService.cs +++ b/examples/AspNetCore/GrpcServiceSample/Services/BankingService.cs @@ -22,158 +22,157 @@ using Grpc.Core; using GrpcServiceSample.Generated; using Microsoft.Extensions.Logging; -namespace GrpcServiceSample.Services +namespace GrpcServiceSample.Services; + +/// +/// BankAccount gRPC service +/// +public class BankingService : AppCallback.AppCallbackBase { /// - /// BankAccount gRPC service + /// State store name. /// - public class BankingService : AppCallback.AppCallbackBase + public const string StoreName = "statestore"; + + private readonly ILogger _logger; + private readonly DaprClient _daprClient; + + /// + /// Constructor + /// + /// + /// + public BankingService(DaprClient daprClient, ILogger logger) { - /// - /// State store name. - /// - public const string StoreName = "statestore"; - - private readonly ILogger _logger; - private readonly DaprClient _daprClient; - - /// - /// Constructor - /// - /// - /// - public BankingService(DaprClient daprClient, ILogger logger) - { - _daprClient = daprClient; - _logger = logger; - } - - readonly JsonSerializerOptions jsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; - - /// - /// implement OnIvoke to support getaccount, deposit and withdraw - /// - /// - /// - /// - public override async Task OnInvoke(InvokeRequest request, ServerCallContext context) - { - var response = new InvokeResponse(); - switch (request.Method) - { - case "getaccount": - var input = request.Data.Unpack(); - var output = await GetAccount(input, context); - response.Data = Any.Pack(output); - break; - case "deposit": - case "withdraw": - var transaction = request.Data.Unpack(); - var account = request.Method == "deposit" ? - await Deposit(transaction, context) : - await Withdraw(transaction, context); - response.Data = Any.Pack(account); - break; - default: - break; - } - return response; - } - - /// - /// implement ListTopicSubscriptions to register deposit and withdraw subscriber - /// - /// - /// - /// - public override Task ListTopicSubscriptions(Empty request, ServerCallContext context) - { - var result = new ListTopicSubscriptionsResponse(); - result.Subscriptions.Add(new TopicSubscription - { - PubsubName = "pubsub", - Topic = "deposit" - }); - result.Subscriptions.Add(new TopicSubscription - { - PubsubName = "pubsub", - Topic = "withdraw" - }); - return Task.FromResult(result); - } - - /// - /// implement OnTopicEvent to handle deposit and withdraw event - /// - /// - /// - /// - public override async Task OnTopicEvent(TopicEventRequest request, ServerCallContext context) - { - if (request.PubsubName == "pubsub") - { - var input = JsonSerializer.Deserialize(request.Data.ToStringUtf8(), this.jsonOptions); - var transaction = new GrpcServiceSample.Generated.Transaction() { Id = input.Id, Amount = (int)input.Amount, }; - if (request.Topic == "deposit") - { - await Deposit(transaction, context); - } - else - { - await Withdraw(transaction, context); - } - } - - return new TopicEventResponse(); - } - - /// - /// GetAccount - /// - /// - /// - /// - public async Task GetAccount(GetAccountRequest input, ServerCallContext context) - { - var state = await _daprClient.GetStateEntryAsync(StoreName, input.Id); - return new GrpcServiceSample.Generated.Account() { Id = state.Value.Id, Balance = (int)state.Value.Balance, }; - } - - /// - /// Deposit - /// - /// - /// - /// - public async Task Deposit(GrpcServiceSample.Generated.Transaction transaction, ServerCallContext context) - { - _logger.LogDebug("Enter deposit"); - var state = await _daprClient.GetStateEntryAsync(StoreName, transaction.Id); - state.Value ??= new Models.Account() { Id = transaction.Id, }; - state.Value.Balance += transaction.Amount; - await state.SaveAsync(); - return new GrpcServiceSample.Generated.Account() { Id = state.Value.Id, Balance = (int)state.Value.Balance, }; - } - - /// - /// Withdraw - /// - /// - /// - /// - public async Task Withdraw(GrpcServiceSample.Generated.Transaction transaction, ServerCallContext context) - { - _logger.LogDebug("Enter withdraw"); - var state = await _daprClient.GetStateEntryAsync(StoreName, transaction.Id); - - if (state.Value == null) - { - throw new Exception($"NotFound: {transaction.Id}"); - } - - state.Value.Balance -= transaction.Amount; - await state.SaveAsync(); - return new GrpcServiceSample.Generated.Account() { Id = state.Value.Id, Balance = (int)state.Value.Balance, }; - } + _daprClient = daprClient; + _logger = logger; } -} + + readonly JsonSerializerOptions jsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + + /// + /// implement OnIvoke to support getaccount, deposit and withdraw + /// + /// + /// + /// + public override async Task OnInvoke(InvokeRequest request, ServerCallContext context) + { + var response = new InvokeResponse(); + switch (request.Method) + { + case "getaccount": + var input = request.Data.Unpack(); + var output = await GetAccount(input, context); + response.Data = Any.Pack(output); + break; + case "deposit": + case "withdraw": + var transaction = request.Data.Unpack(); + var account = request.Method == "deposit" ? + await Deposit(transaction, context) : + await Withdraw(transaction, context); + response.Data = Any.Pack(account); + break; + default: + break; + } + return response; + } + + /// + /// implement ListTopicSubscriptions to register deposit and withdraw subscriber + /// + /// + /// + /// + public override Task ListTopicSubscriptions(Empty request, ServerCallContext context) + { + var result = new ListTopicSubscriptionsResponse(); + result.Subscriptions.Add(new TopicSubscription + { + PubsubName = "pubsub", + Topic = "deposit" + }); + result.Subscriptions.Add(new TopicSubscription + { + PubsubName = "pubsub", + Topic = "withdraw" + }); + return Task.FromResult(result); + } + + /// + /// implement OnTopicEvent to handle deposit and withdraw event + /// + /// + /// + /// + public override async Task OnTopicEvent(TopicEventRequest request, ServerCallContext context) + { + if (request.PubsubName == "pubsub") + { + var input = JsonSerializer.Deserialize(request.Data.ToStringUtf8(), this.jsonOptions); + var transaction = new GrpcServiceSample.Generated.Transaction() { Id = input.Id, Amount = (int)input.Amount, }; + if (request.Topic == "deposit") + { + await Deposit(transaction, context); + } + else + { + await Withdraw(transaction, context); + } + } + + return new TopicEventResponse(); + } + + /// + /// GetAccount + /// + /// + /// + /// + public async Task GetAccount(GetAccountRequest input, ServerCallContext context) + { + var state = await _daprClient.GetStateEntryAsync(StoreName, input.Id); + return new GrpcServiceSample.Generated.Account() { Id = state.Value.Id, Balance = (int)state.Value.Balance, }; + } + + /// + /// Deposit + /// + /// + /// + /// + public async Task Deposit(GrpcServiceSample.Generated.Transaction transaction, ServerCallContext context) + { + _logger.LogDebug("Enter deposit"); + var state = await _daprClient.GetStateEntryAsync(StoreName, transaction.Id); + state.Value ??= new Models.Account() { Id = transaction.Id, }; + state.Value.Balance += transaction.Amount; + await state.SaveAsync(); + return new GrpcServiceSample.Generated.Account() { Id = state.Value.Id, Balance = (int)state.Value.Balance, }; + } + + /// + /// Withdraw + /// + /// + /// + /// + public async Task Withdraw(GrpcServiceSample.Generated.Transaction transaction, ServerCallContext context) + { + _logger.LogDebug("Enter withdraw"); + var state = await _daprClient.GetStateEntryAsync(StoreName, transaction.Id); + + if (state.Value == null) + { + throw new Exception($"NotFound: {transaction.Id}"); + } + + state.Value.Balance -= transaction.Amount; + await state.SaveAsync(); + return new GrpcServiceSample.Generated.Account() { Id = state.Value.Id, Balance = (int)state.Value.Balance, }; + } +} \ No newline at end of file diff --git a/examples/AspNetCore/GrpcServiceSample/Startup.cs b/examples/AspNetCore/GrpcServiceSample/Startup.cs index 4aa5ac7d..f2c60649 100644 --- a/examples/AspNetCore/GrpcServiceSample/Startup.cs +++ b/examples/AspNetCore/GrpcServiceSample/Startup.cs @@ -19,47 +19,46 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -namespace GrpcServiceSample +namespace GrpcServiceSample; + +/// +/// Startup class. +/// +public class Startup { /// - /// Startup class. + /// Configure Services /// - public class Startup + /// + public void ConfigureServices(IServiceCollection services) { - /// - /// Configure Services - /// - /// - public void ConfigureServices(IServiceCollection services) - { - services.AddGrpc(); + services.AddGrpc(); - services.AddDaprClient(); - } - - /// - /// Configure app - /// - /// - /// - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.UseRouting(); - - app.UseEndpoints(endpoints => - { - endpoints.MapGrpcService(); - - endpoints.MapGet("/", async context => - { - await context.Response.WriteAsync("Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909"); - }); - }); - } + services.AddDaprClient(); } -} + + /// + /// Configure app + /// + /// + /// + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseRouting(); + + app.UseEndpoints(endpoints => + { + endpoints.MapGrpcService(); + + endpoints.MapGet("/", async context => + { + await context.Response.WriteAsync("Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909"); + }); + }); + } +} \ No newline at end of file diff --git a/examples/AspNetCore/RoutingSample/Account.cs b/examples/AspNetCore/RoutingSample/Account.cs index ede662b1..5aac78ce 100644 --- a/examples/AspNetCore/RoutingSample/Account.cs +++ b/examples/AspNetCore/RoutingSample/Account.cs @@ -11,21 +11,20 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace RoutingSample +namespace RoutingSample; + +/// +/// Class representing an Account for samples. +/// +public class Account { /// - /// Class representing an Account for samples. + /// Gets or sets account id. /// - public class Account - { - /// - /// Gets or sets account id. - /// - public string Id { get; set; } + public string Id { get; set; } - /// - /// Gets or sets account balance. - /// - public decimal Balance { get; set; } - } + /// + /// Gets or sets account balance. + /// + public decimal Balance { get; set; } } \ No newline at end of file diff --git a/examples/AspNetCore/RoutingSample/Program.cs b/examples/AspNetCore/RoutingSample/Program.cs index 505f58d1..6aa86d12 100644 --- a/examples/AspNetCore/RoutingSample/Program.cs +++ b/examples/AspNetCore/RoutingSample/Program.cs @@ -11,35 +11,34 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace RoutingSample +namespace RoutingSample; + +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +/// +/// Controller Sample. +/// +public static class Program { - using Microsoft.AspNetCore.Hosting; - using Microsoft.Extensions.Hosting; + /// + /// Main for Controller Sample. + /// + /// Arguments. + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } /// - /// Controller Sample. + /// Creates WebHost Builder. /// - public static class Program - { - /// - /// Main for Controller Sample. - /// - /// Arguments. - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } - - /// - /// Creates WebHost Builder. - /// - /// Arguments. - /// Returns IHostbuilder. - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }); - } -} + /// Arguments. + /// Returns IHostbuilder. + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); +} \ No newline at end of file diff --git a/examples/AspNetCore/RoutingSample/Startup.cs b/examples/AspNetCore/RoutingSample/Startup.cs index 3d71e9e1..3e7d7b0c 100644 --- a/examples/AspNetCore/RoutingSample/Startup.cs +++ b/examples/AspNetCore/RoutingSample/Startup.cs @@ -11,254 +11,253 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace RoutingSample +namespace RoutingSample; + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Threading.Tasks; +using Dapr; +using Dapr.AspNetCore; +using Dapr.Client; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +/// +/// Startup class. +/// +public class Startup { - using System; - using System.Collections.Generic; - using System.Text.Json; - using System.Threading.Tasks; - using Dapr; - using Dapr.AspNetCore; - using Dapr.Client; - using Microsoft.AspNetCore.Builder; - using Microsoft.AspNetCore.Hosting; - using Microsoft.AspNetCore.Http; - using Microsoft.Extensions.Configuration; - using Microsoft.Extensions.DependencyInjection; - using Microsoft.Extensions.Hosting; - using Microsoft.Extensions.Logging; + /// + /// State store name. + /// + public const string StoreName = "statestore"; /// - /// Startup class. + /// Pubsub component name. "pubsub" is name of the default pub/sub configured by the Dapr CLI. /// - public class Startup + public const string PubsubName = "pubsub"; + + /// + /// Initializes a new instance of the class. + /// + /// Configuration. + public Startup(IConfiguration configuration) { - /// - /// State store name. - /// - public const string StoreName = "statestore"; + this.Configuration = configuration; + } - /// - /// Pubsub component name. "pubsub" is name of the default pub/sub configured by the Dapr CLI. - /// - public const string PubsubName = "pubsub"; + /// + /// Gets the configuration. + /// + public IConfiguration Configuration { get; } - /// - /// Initializes a new instance of the class. - /// - /// Configuration. - public Startup(IConfiguration configuration) + /// + /// Configures Services. + /// + /// Service Collection. + public void ConfigureServices(IServiceCollection services) + { + services.AddDaprClient(); + + services.AddSingleton(new JsonSerializerOptions() { - this.Configuration = configuration; + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = true, + }); + } + + /// + /// Configures Application Builder and WebHost environment. + /// + /// Application builder. + /// Webhost environment. + /// Options for JSON serialization. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env, JsonSerializerOptions serializerOptions, + ILogger logger) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); } - /// - /// Gets the configuration. - /// - public IConfiguration Configuration { get; } + app.UseRouting(); - /// - /// Configures Services. - /// - /// Service Collection. - public void ConfigureServices(IServiceCollection services) + app.UseCloudEvents(); + + app.UseEndpoints(endpoints => { - services.AddDaprClient(); + endpoints.MapSubscribeHandler(); - services.AddSingleton(new JsonSerializerOptions() + var depositTopicOptions = new TopicOptions(); + depositTopicOptions.PubsubName = PubsubName; + depositTopicOptions.Name = "deposit"; + depositTopicOptions.DeadLetterTopic = "amountDeadLetterTopic"; + + var withdrawTopicOptions = new TopicOptions(); + withdrawTopicOptions.PubsubName = PubsubName; + withdrawTopicOptions.Name = "withdraw"; + withdrawTopicOptions.DeadLetterTopic = "amountDeadLetterTopic"; + + var multiDepositTopicOptions = new TopicOptions { PubsubName = PubsubName, Name = "multideposit" }; + + var bulkSubscribeTopicOptions = new BulkSubscribeTopicOptions { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - PropertyNameCaseInsensitive = true, - }); - } + TopicName = "multideposit", MaxMessagesCount = 250, MaxAwaitDurationMs = 1000 + }; - /// - /// Configures Application Builder and WebHost environment. - /// - /// Application builder. - /// Webhost environment. - /// Options for JSON serialization. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env, JsonSerializerOptions serializerOptions, - ILogger logger) + endpoints.MapGet("{id}", Balance); + endpoints.MapPost("deposit", Deposit).WithTopic(depositTopicOptions); + endpoints.MapPost("multideposit", MultiDeposit).WithTopic(multiDepositTopicOptions).WithBulkSubscribe(bulkSubscribeTopicOptions); + endpoints.MapPost("deadLetterTopicRoute", ViewErrorMessage).WithTopic(PubsubName, "amountDeadLetterTopic"); + endpoints.MapPost("withdraw", Withdraw).WithTopic(withdrawTopicOptions); + }); + + async Task Balance(HttpContext context) { - if (env.IsDevelopment()) + logger.LogInformation("Enter Balance"); + var client = context.RequestServices.GetRequiredService(); + + var id = (string)context.Request.RouteValues["id"]; + logger.LogInformation("id is {0}", id); + var account = await client.GetStateAsync(StoreName, id); + if (account == null) { - app.UseDeveloperExceptionPage(); - } - - app.UseRouting(); - - app.UseCloudEvents(); - - app.UseEndpoints(endpoints => - { - endpoints.MapSubscribeHandler(); - - var depositTopicOptions = new TopicOptions(); - depositTopicOptions.PubsubName = PubsubName; - depositTopicOptions.Name = "deposit"; - depositTopicOptions.DeadLetterTopic = "amountDeadLetterTopic"; - - var withdrawTopicOptions = new TopicOptions(); - withdrawTopicOptions.PubsubName = PubsubName; - withdrawTopicOptions.Name = "withdraw"; - withdrawTopicOptions.DeadLetterTopic = "amountDeadLetterTopic"; - - var multiDepositTopicOptions = new TopicOptions { PubsubName = PubsubName, Name = "multideposit" }; - - var bulkSubscribeTopicOptions = new BulkSubscribeTopicOptions - { - TopicName = "multideposit", MaxMessagesCount = 250, MaxAwaitDurationMs = 1000 - }; - - endpoints.MapGet("{id}", Balance); - endpoints.MapPost("deposit", Deposit).WithTopic(depositTopicOptions); - endpoints.MapPost("multideposit", MultiDeposit).WithTopic(multiDepositTopicOptions).WithBulkSubscribe(bulkSubscribeTopicOptions); - endpoints.MapPost("deadLetterTopicRoute", ViewErrorMessage).WithTopic(PubsubName, "amountDeadLetterTopic"); - endpoints.MapPost("withdraw", Withdraw).WithTopic(withdrawTopicOptions); - }); - - async Task Balance(HttpContext context) - { - logger.LogInformation("Enter Balance"); - var client = context.RequestServices.GetRequiredService(); - - var id = (string)context.Request.RouteValues["id"]; - logger.LogInformation("id is {0}", id); - var account = await client.GetStateAsync(StoreName, id); - if (account == null) - { - logger.LogInformation("Account not found"); - context.Response.StatusCode = 404; - return; - } - - logger.LogInformation("Account balance is {0}", account.Balance); - - context.Response.ContentType = "application/json"; - await JsonSerializer.SerializeAsync(context.Response.Body, account, serializerOptions); - } - - async Task Deposit(HttpContext context) - { - logger.LogInformation("Enter Deposit"); - - var client = context.RequestServices.GetRequiredService(); - var transaction = await JsonSerializer.DeserializeAsync(context.Request.Body, serializerOptions); - - logger.LogInformation("Id is {0}, Amount is {1}", transaction.Id, transaction.Amount); - - var account = await client.GetStateAsync(StoreName, transaction.Id); - if (account == null) - { - account = new Account() { Id = transaction.Id, }; - } - - if (transaction.Amount < 0m) - { - logger.LogInformation("Invalid amount"); - context.Response.StatusCode = 400; - return; - } - - account.Balance += transaction.Amount; - await client.SaveStateAsync(StoreName, transaction.Id, account); - logger.LogInformation("Balance is {0}", account.Balance); - - context.Response.ContentType = "application/json"; - await JsonSerializer.SerializeAsync(context.Response.Body, account, serializerOptions); - } - - async Task MultiDeposit(HttpContext context) - { - logger.LogInformation("Enter bulk deposit"); - - var client = context.RequestServices.GetRequiredService(); - - var bulkMessage = await JsonSerializer.DeserializeAsync>>( - context.Request.Body, serializerOptions); - - List entries = new List(); - - if (bulkMessage != null) - { - foreach (var entry in bulkMessage.Entries) - { - try - { - var transaction = entry.Event.Data; - - var state = await client.GetStateEntryAsync(StoreName, transaction.Id); - state.Value ??= new Account() { Id = transaction.Id, }; - logger.LogInformation("Id is {0}, the amount to be deposited is {1}", - transaction.Id, transaction.Amount); - - if (transaction.Amount < 0m) - { - logger.LogInformation("Invalid amount"); - context.Response.StatusCode = 400; - return; - } - - state.Value.Balance += transaction.Amount; - logger.LogInformation("Balance is {0}", state.Value.Balance); - await state.SaveAsync(); - entries.Add(new BulkSubscribeAppResponseEntry(entry.EntryId, - BulkSubscribeAppResponseStatus.SUCCESS)); - } - catch (Exception e) - { - logger.LogError(e.Message); - entries.Add(new BulkSubscribeAppResponseEntry(entry.EntryId, - BulkSubscribeAppResponseStatus.RETRY)); - } - } - } - - await JsonSerializer.SerializeAsync(context.Response.Body, - new BulkSubscribeAppResponse(entries), serializerOptions); - } - - async Task ViewErrorMessage(HttpContext context) - { - var transaction = await JsonSerializer.DeserializeAsync(context.Request.Body, serializerOptions); - - logger.LogInformation("The amount cannot be negative: {0}", transaction.Amount); - + logger.LogInformation("Account not found"); + context.Response.StatusCode = 404; return; } - async Task Withdraw(HttpContext context) + logger.LogInformation("Account balance is {0}", account.Balance); + + context.Response.ContentType = "application/json"; + await JsonSerializer.SerializeAsync(context.Response.Body, account, serializerOptions); + } + + async Task Deposit(HttpContext context) + { + logger.LogInformation("Enter Deposit"); + + var client = context.RequestServices.GetRequiredService(); + var transaction = await JsonSerializer.DeserializeAsync(context.Request.Body, serializerOptions); + + logger.LogInformation("Id is {0}, Amount is {1}", transaction.Id, transaction.Amount); + + var account = await client.GetStateAsync(StoreName, transaction.Id); + if (account == null) { - logger.LogInformation("Enter Withdraw"); - - var client = context.RequestServices.GetRequiredService(); - var transaction = await JsonSerializer.DeserializeAsync(context.Request.Body, serializerOptions); - - logger.LogInformation("Id is {0}, Amount is {1}", transaction.Id, transaction.Amount); - - var account = await client.GetStateAsync(StoreName, transaction.Id); - if (account == null) - { - logger.LogInformation("Account not found"); - context.Response.StatusCode = 404; - return; - } - - if (transaction.Amount < 0m) - { - logger.LogInformation("Invalid amount"); - context.Response.StatusCode = 400; - return; - } - - account.Balance -= transaction.Amount; - await client.SaveStateAsync(StoreName, transaction.Id, account); - logger.LogInformation("Balance is {0}", account.Balance); - - context.Response.ContentType = "application/json"; - await JsonSerializer.SerializeAsync(context.Response.Body, account, serializerOptions); + account = new Account() { Id = transaction.Id, }; } + + if (transaction.Amount < 0m) + { + logger.LogInformation("Invalid amount"); + context.Response.StatusCode = 400; + return; + } + + account.Balance += transaction.Amount; + await client.SaveStateAsync(StoreName, transaction.Id, account); + logger.LogInformation("Balance is {0}", account.Balance); + + context.Response.ContentType = "application/json"; + await JsonSerializer.SerializeAsync(context.Response.Body, account, serializerOptions); + } + + async Task MultiDeposit(HttpContext context) + { + logger.LogInformation("Enter bulk deposit"); + + var client = context.RequestServices.GetRequiredService(); + + var bulkMessage = await JsonSerializer.DeserializeAsync>>( + context.Request.Body, serializerOptions); + + List entries = new List(); + + if (bulkMessage != null) + { + foreach (var entry in bulkMessage.Entries) + { + try + { + var transaction = entry.Event.Data; + + var state = await client.GetStateEntryAsync(StoreName, transaction.Id); + state.Value ??= new Account() { Id = transaction.Id, }; + logger.LogInformation("Id is {0}, the amount to be deposited is {1}", + transaction.Id, transaction.Amount); + + if (transaction.Amount < 0m) + { + logger.LogInformation("Invalid amount"); + context.Response.StatusCode = 400; + return; + } + + state.Value.Balance += transaction.Amount; + logger.LogInformation("Balance is {0}", state.Value.Balance); + await state.SaveAsync(); + entries.Add(new BulkSubscribeAppResponseEntry(entry.EntryId, + BulkSubscribeAppResponseStatus.SUCCESS)); + } + catch (Exception e) + { + logger.LogError(e.Message); + entries.Add(new BulkSubscribeAppResponseEntry(entry.EntryId, + BulkSubscribeAppResponseStatus.RETRY)); + } + } + } + + await JsonSerializer.SerializeAsync(context.Response.Body, + new BulkSubscribeAppResponse(entries), serializerOptions); + } + + async Task ViewErrorMessage(HttpContext context) + { + var transaction = await JsonSerializer.DeserializeAsync(context.Request.Body, serializerOptions); + + logger.LogInformation("The amount cannot be negative: {0}", transaction.Amount); + + return; + } + + async Task Withdraw(HttpContext context) + { + logger.LogInformation("Enter Withdraw"); + + var client = context.RequestServices.GetRequiredService(); + var transaction = await JsonSerializer.DeserializeAsync(context.Request.Body, serializerOptions); + + logger.LogInformation("Id is {0}, Amount is {1}", transaction.Id, transaction.Amount); + + var account = await client.GetStateAsync(StoreName, transaction.Id); + if (account == null) + { + logger.LogInformation("Account not found"); + context.Response.StatusCode = 404; + return; + } + + if (transaction.Amount < 0m) + { + logger.LogInformation("Invalid amount"); + context.Response.StatusCode = 400; + return; + } + + account.Balance -= transaction.Amount; + await client.SaveStateAsync(StoreName, transaction.Id, account); + logger.LogInformation("Balance is {0}", account.Balance); + + context.Response.ContentType = "application/json"; + await JsonSerializer.SerializeAsync(context.Response.Body, account, serializerOptions); } } -} +} \ No newline at end of file diff --git a/examples/AspNetCore/RoutingSample/Transaction.cs b/examples/AspNetCore/RoutingSample/Transaction.cs index f37340f4..b6042e43 100644 --- a/examples/AspNetCore/RoutingSample/Transaction.cs +++ b/examples/AspNetCore/RoutingSample/Transaction.cs @@ -11,21 +11,20 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace RoutingSample +namespace RoutingSample; + +/// +/// Represents a transaction used by sample code. +/// +public class Transaction { /// - /// Represents a transaction used by sample code. + /// Gets or sets account id for the transaction. /// - public class Transaction - { - /// - /// Gets or sets account id for the transaction. - /// - public string Id { get; set; } + public string Id { get; set; } - /// - /// Gets or sets amount for the transaction. - /// - public decimal Amount { get; set; } - } + /// + /// Gets or sets amount for the transaction. + /// + public decimal Amount { get; set; } } \ No newline at end of file diff --git a/examples/AspNetCore/SecretStoreConfigurationProviderSample/Program.cs b/examples/AspNetCore/SecretStoreConfigurationProviderSample/Program.cs index 658d6d16..fd4b31c8 100644 --- a/examples/AspNetCore/SecretStoreConfigurationProviderSample/Program.cs +++ b/examples/AspNetCore/SecretStoreConfigurationProviderSample/Program.cs @@ -11,64 +11,63 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace SecretStoreConfigurationProviderSample +namespace SecretStoreConfigurationProviderSample; + +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; +using Dapr.Client; +using Dapr.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using System; + +/// +/// Secret Store Configuration Provider Sample. +/// +public class Program { - using Microsoft.AspNetCore.Hosting; - using Microsoft.Extensions.Hosting; - using Dapr.Client; - using Dapr.Extensions.Configuration; - using Microsoft.Extensions.DependencyInjection; - using System; + /// + /// Main for Secret Store Configuration Provider Sample. + /// + /// Arguments. + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } /// - /// Secret Store Configuration Provider Sample. + /// Creates WebHost Builder. /// - public class Program + /// Arguments. + /// Returns IHostbuilder. + public static IHostBuilder CreateHostBuilder(string[] args) { - /// - /// Main for Secret Store Configuration Provider Sample. - /// - /// Arguments. - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } + // Create Dapr Client + var client = new DaprClientBuilder() + .Build(); - /// - /// Creates WebHost Builder. - /// - /// Arguments. - /// Returns IHostbuilder. - public static IHostBuilder CreateHostBuilder(string[] args) - { - // Create Dapr Client - var client = new DaprClientBuilder() - .Build(); + return Host.CreateDefaultBuilder(args) + .ConfigureServices((services) => + { + // Add the DaprClient to DI. + services.AddSingleton(client); + }) + .ConfigureAppConfiguration((configBuilder) => + { + // To retrive specific secrets use secretDescriptors + // Create descriptors for the secrets you want to rerieve from the Dapr Secret Store. + // var secretDescriptors = new DaprSecretDescriptor[] + // { + // new DaprSecretDescriptor("super-secret") + // }; + // configBuilder.AddDaprSecretStore("demosecrets", secretDescriptors, client); - return Host.CreateDefaultBuilder(args) - .ConfigureServices((services) => - { - // Add the DaprClient to DI. - services.AddSingleton(client); - }) - .ConfigureAppConfiguration((configBuilder) => - { - // To retrive specific secrets use secretDescriptors - // Create descriptors for the secrets you want to rerieve from the Dapr Secret Store. - // var secretDescriptors = new DaprSecretDescriptor[] - // { - // new DaprSecretDescriptor("super-secret") - // }; - // configBuilder.AddDaprSecretStore("demosecrets", secretDescriptors, client); - - // Add the secret store Configuration Provider to the configuration builder. - // Including a TimeSpan allows us to dictate how long we should wait for the Sidecar to start. - configBuilder.AddDaprSecretStore("demosecrets", client, TimeSpan.FromSeconds(10)); - }) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }); - } + // Add the secret store Configuration Provider to the configuration builder. + // Including a TimeSpan allows us to dictate how long we should wait for the Sidecar to start. + configBuilder.AddDaprSecretStore("demosecrets", client, TimeSpan.FromSeconds(10)); + }) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); } } \ No newline at end of file diff --git a/examples/AspNetCore/SecretStoreConfigurationProviderSample/Startup.cs b/examples/AspNetCore/SecretStoreConfigurationProviderSample/Startup.cs index 6d2268d6..c89dcff8 100644 --- a/examples/AspNetCore/SecretStoreConfigurationProviderSample/Startup.cs +++ b/examples/AspNetCore/SecretStoreConfigurationProviderSample/Startup.cs @@ -11,72 +11,71 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace SecretStoreConfigurationProviderSample +namespace SecretStoreConfigurationProviderSample; + +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.DependencyInjection; + +/// +/// Startup class. +/// +public class Startup { - using System.Text.Json; - using System.Threading.Tasks; - using Microsoft.AspNetCore.Builder; - using Microsoft.AspNetCore.Hosting; - using Microsoft.AspNetCore.Http; - using Microsoft.Extensions.Configuration; - using Microsoft.Extensions.Hosting; - using Microsoft.Extensions.DependencyInjection; + /// + /// Initializes a new instance of the class. + /// + /// Configuration. + public Startup(IConfiguration configuration) + { + this.Configuration = configuration; + } /// - /// Startup class. + /// Gets the configuration. /// - public class Startup + public IConfiguration Configuration { get; } + + /// + /// Configures Services. + /// + /// Service Collection. + public void ConfigureServices(IServiceCollection services) { - /// - /// Initializes a new instance of the class. - /// - /// Configuration. - public Startup(IConfiguration configuration) + + } + + /// + /// Configures Application Builder and WebHost environment. + /// + /// Application builder. + /// Webhost environment. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) { - this.Configuration = configuration; + app.UseDeveloperExceptionPage(); } - /// - /// Gets the configuration. - /// - public IConfiguration Configuration { get; } + app.UseRouting(); - /// - /// Configures Services. - /// - /// Service Collection. - public void ConfigureServices(IServiceCollection services) + app.UseEndpoints(endpoints => { + endpoints.MapGet("secret", Secret); + }); - } - - /// - /// Configures Application Builder and WebHost environment. - /// - /// Application builder. - /// Webhost environment. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + async Task Secret(HttpContext context) { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } + // Get secret from configuration!!! + var secretValue = Configuration["super-secret"]; - app.UseRouting(); - - app.UseEndpoints(endpoints => - { - endpoints.MapGet("secret", Secret); - }); - - async Task Secret(HttpContext context) - { - // Get secret from configuration!!! - var secretValue = Configuration["super-secret"]; - - context.Response.ContentType = "application/json"; - await JsonSerializer.SerializeAsync(context.Response.Body, secretValue); - } + context.Response.ContentType = "application/json"; + await JsonSerializer.SerializeAsync(context.Response.Body, secretValue); } } } \ No newline at end of file diff --git a/examples/Client/ConfigurationApi/Controllers/ConfigurationController.cs b/examples/Client/ConfigurationApi/Controllers/ConfigurationController.cs index 9ceb60c0..faad2407 100644 --- a/examples/Client/ConfigurationApi/Controllers/ConfigurationController.cs +++ b/examples/Client/ConfigurationApi/Controllers/ConfigurationController.cs @@ -7,81 +7,80 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; -namespace ConfigurationApi.Controllers +namespace ConfigurationApi.Controllers; + +[ApiController] +[Route("configuration")] +public class ConfigurationController : ControllerBase { - [ApiController] - [Route("configuration")] - public class ConfigurationController : ControllerBase + private ILogger logger; + private IConfiguration configuration; + private DaprClient client; + + public ConfigurationController(ILogger logger, IConfiguration configuration, [FromServices] DaprClient client) { - private ILogger logger; - private IConfiguration configuration; - private DaprClient client; + this.logger = logger; + this.configuration = configuration; + this.client = client; + } - public ConfigurationController(ILogger logger, IConfiguration configuration, [FromServices] DaprClient client) + [HttpGet("get/{configStore}/{queryKey}")] + public async Task GetConfiguration([FromRoute] string configStore, [FromRoute] string queryKey) + { + logger.LogInformation($"Querying Configuration with key: {queryKey}"); + var configItems = await client.GetConfiguration(configStore, new List() { queryKey }); + + if (configItems.Items.Count == 0) { - this.logger = logger; - this.configuration = configuration; - this.client = client; + logger.LogInformation($"No configuration item found for key: {queryKey}"); } - [HttpGet("get/{configStore}/{queryKey}")] - public async Task GetConfiguration([FromRoute] string configStore, [FromRoute] string queryKey) + foreach (var item in configItems.Items) { - logger.LogInformation($"Querying Configuration with key: {queryKey}"); - var configItems = await client.GetConfiguration(configStore, new List() { queryKey }); - - if (configItems.Items.Count == 0) - { - logger.LogInformation($"No configuration item found for key: {queryKey}"); - } - - foreach (var item in configItems.Items) - { - logger.LogInformation($"Got configuration item:\nKey: {item.Key}\nValue: {item.Value.Value}\nVersion: {item.Value.Version}"); - } + logger.LogInformation($"Got configuration item:\nKey: {item.Key}\nValue: {item.Value.Value}\nVersion: {item.Value.Version}"); } + } - [HttpGet("extension")] - public Task SubscribeAndWatchConfiguration() - { - logger.LogInformation($"Getting values from Configuration Extension, watched values ['withdrawVersion', 'source']."); + [HttpGet("extension")] + public Task SubscribeAndWatchConfiguration() + { + logger.LogInformation($"Getting values from Configuration Extension, watched values ['withdrawVersion', 'source']."); - logger.LogInformation($"'withdrawVersion' from extension: {configuration["withdrawVersion"]}"); - logger.LogInformation($"'source' from extension: {configuration["source"]}"); + logger.LogInformation($"'withdrawVersion' from extension: {configuration["withdrawVersion"]}"); + logger.LogInformation($"'source' from extension: {configuration["source"]}"); - return Task.CompletedTask; - } + return Task.CompletedTask; + } #nullable enable - [HttpPost("withdraw")] - public async Task> CreateAccountHandler(Transaction transaction) + [HttpPost("withdraw")] + public async Task> CreateAccountHandler(Transaction transaction) + { + // Check if the V2 method is enabled. + if (configuration["withdrawVersion"] == "v2") { - // Check if the V2 method is enabled. - if (configuration["withdrawVersion"] == "v2") + var source = !string.IsNullOrEmpty(configuration["source"]) ? configuration["source"] : "local"; + var transactionV2 = new TransactionV2 { - var source = !string.IsNullOrEmpty(configuration["source"]) ? configuration["source"] : "local"; - var transactionV2 = new TransactionV2 - { - Id = transaction.Id, - Amount = transaction.Amount, - Channel = source - }; - logger.LogInformation($"Calling V2 Withdraw API - Id: {transactionV2.Id} Amount: {transactionV2.Amount} Channel: {transactionV2.Channel}"); - try - { - return await this.client.InvokeMethodAsync("controller", "withdraw.v2", transactionV2); - } - catch (DaprException ex) - { - logger.LogError($"Error executing withdrawal: {ex.Message}"); - return BadRequest(); - } + Id = transaction.Id, + Amount = transaction.Amount, + Channel = source + }; + logger.LogInformation($"Calling V2 Withdraw API - Id: {transactionV2.Id} Amount: {transactionV2.Amount} Channel: {transactionV2.Channel}"); + try + { + return await this.client.InvokeMethodAsync("controller", "withdraw.v2", transactionV2); + } + catch (DaprException ex) + { + logger.LogError($"Error executing withdrawal: {ex.Message}"); + return BadRequest(); } - - // Default to the original method. - logger.LogInformation($"Calling V1 Withdraw API: {transaction}"); - return await this.client.InvokeMethodAsync("controller", "withdraw", transaction); } -#nullable disable + + // Default to the original method. + logger.LogInformation($"Calling V1 Withdraw API: {transaction}"); + return await this.client.InvokeMethodAsync("controller", "withdraw", transaction); } -} +#nullable disable +} \ No newline at end of file diff --git a/examples/Client/ConfigurationApi/Program.cs b/examples/Client/ConfigurationApi/Program.cs index 22f12cfa..832b0624 100644 --- a/examples/Client/ConfigurationApi/Program.cs +++ b/examples/Client/ConfigurationApi/Program.cs @@ -5,37 +5,36 @@ using Dapr.Client; using Dapr.Extensions.Configuration; using System.Collections.Generic; -namespace ConfigurationApi -{ - public class Program - { - public static void Main(string[] args) - { - Console.WriteLine("Starting application."); - CreateHostBuilder(args).Build().Run(); - Console.WriteLine("Closing application."); - } +namespace ConfigurationApi; - /// - /// Creates WebHost Builder. - /// - /// Arguments. - /// Returns IHostbuilder. - public static IHostBuilder CreateHostBuilder(string[] args) - { - var client = new DaprClientBuilder().Build(); - return Host.CreateDefaultBuilder(args) - .ConfigureAppConfiguration(config => - { - // Get the initial value and continue to watch it for changes. - config.AddDaprConfigurationStore("redisconfig", new List() { "withdrawVersion" }, client, TimeSpan.FromSeconds(20)); - config.AddStreamingDaprConfigurationStore("redisconfig", new List() { "withdrawVersion", "source" }, client, TimeSpan.FromSeconds(20)); - - }) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }); - } +public class Program +{ + public static void Main(string[] args) + { + Console.WriteLine("Starting application."); + CreateHostBuilder(args).Build().Run(); + Console.WriteLine("Closing application."); } -} + + /// + /// Creates WebHost Builder. + /// + /// Arguments. + /// Returns IHostbuilder. + public static IHostBuilder CreateHostBuilder(string[] args) + { + var client = new DaprClientBuilder().Build(); + return Host.CreateDefaultBuilder(args) + .ConfigureAppConfiguration(config => + { + // Get the initial value and continue to watch it for changes. + config.AddDaprConfigurationStore("redisconfig", new List() { "withdrawVersion" }, client, TimeSpan.FromSeconds(20)); + config.AddStreamingDaprConfigurationStore("redisconfig", new List() { "withdrawVersion", "source" }, client, TimeSpan.FromSeconds(20)); + + }) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} \ No newline at end of file diff --git a/examples/Client/ConfigurationApi/Startup.cs b/examples/Client/ConfigurationApi/Startup.cs index b858b810..6ab1f8cf 100644 --- a/examples/Client/ConfigurationApi/Startup.cs +++ b/examples/Client/ConfigurationApi/Startup.cs @@ -4,51 +4,50 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -namespace ConfigurationApi +namespace ConfigurationApi; + +public class Startup { - public class Startup + /// + /// Initializes a new instance of the class. + /// + /// Configuration. + public Startup(IConfiguration configuration) { - /// - /// Initializes a new instance of the class. - /// - /// Configuration. - public Startup(IConfiguration configuration) - { - this.Configuration = configuration; - } - - /// - /// Gets the configuration. - /// - public IConfiguration Configuration { get; } - - /// - /// Configures Services. - /// - /// Service Collection. - public void ConfigureServices(IServiceCollection services) - { - services.AddControllers().AddDapr(); - } - - /// - /// Configures Application Builder and WebHost environment. - /// - /// Application builder. - /// Webhost environment. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.UseRouting(); - - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); - } + this.Configuration = configuration; } -} + + /// + /// Gets the configuration. + /// + public IConfiguration Configuration { get; } + + /// + /// Configures Services. + /// + /// Service Collection. + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers().AddDapr(); + } + + /// + /// Configures Application Builder and WebHost environment. + /// + /// Application builder. + /// Webhost environment. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseRouting(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + } +} \ No newline at end of file diff --git a/examples/Client/Cryptography/Example.cs b/examples/Client/Cryptography/Example.cs index 2c2d4162..ac3d525b 100644 --- a/examples/Client/Cryptography/Example.cs +++ b/examples/Client/Cryptography/Example.cs @@ -11,12 +11,11 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Cryptography -{ - internal abstract class Example - { - public abstract string DisplayName { get; } +namespace Cryptography; - public abstract Task RunAsync(CancellationToken cancellationToken); - } -} +internal abstract class Example +{ + public abstract string DisplayName { get; } + + public abstract Task RunAsync(CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/examples/Client/Cryptography/Examples/EncryptDecryptFileStreamExample.cs b/examples/Client/Cryptography/Examples/EncryptDecryptFileStreamExample.cs index 19df0634..43cf2dc9 100644 --- a/examples/Client/Cryptography/Examples/EncryptDecryptFileStreamExample.cs +++ b/examples/Client/Cryptography/Examples/EncryptDecryptFileStreamExample.cs @@ -15,59 +15,58 @@ using System.Buffers; using Dapr.Client; #pragma warning disable CS0618 // Type or member is obsolete -namespace Cryptography.Examples +namespace Cryptography.Examples; + +internal class EncryptDecryptFileStreamExample(string componentName, string keyName) : Example { - internal class EncryptDecryptFileStreamExample(string componentName, string keyName) : Example + public override string DisplayName => "Use Cryptography to encrypt and decrypt a file"; + public override async Task RunAsync(CancellationToken cancellationToken) { - public override string DisplayName => "Use Cryptography to encrypt and decrypt a file"; - public override async Task RunAsync(CancellationToken cancellationToken) + using var client = new DaprClientBuilder().Build(); + + // The name of the file we're using as an example + const string fileName = "file.txt"; + + Console.WriteLine("Original file contents:"); + foreach (var line in await File.ReadAllLinesAsync(fileName, cancellationToken)) { - using var client = new DaprClientBuilder().Build(); - - // The name of the file we're using as an example - const string fileName = "file.txt"; - - Console.WriteLine("Original file contents:"); - foreach (var line in await File.ReadAllLinesAsync(fileName, cancellationToken)) - { - Console.WriteLine(line); - } - - //Encrypt from a file stream and buffer the resulting bytes to an in-memory buffer - await using var encryptFs = new FileStream(fileName, FileMode.Open); - - var bufferedEncryptedBytes = new ArrayBufferWriter(); - await foreach (var bytes in (await client.EncryptAsync(componentName, encryptFs, keyName, - new EncryptionOptions(KeyWrapAlgorithm.Rsa), cancellationToken)) - .WithCancellation(cancellationToken)) - { - bufferedEncryptedBytes.Write(bytes.Span); - } - - Console.WriteLine("Encrypted bytes:"); - Console.WriteLine(Convert.ToBase64String(bufferedEncryptedBytes.WrittenMemory.ToArray())); - - //We'll write to a temporary file via a FileStream - var tempDecryptedFile = Path.GetTempFileName(); - await using var decryptFs = new FileStream(tempDecryptedFile, FileMode.Create); - - //We'll stream the decrypted bytes from a MemoryStream into the above temporary file - await using var encryptedMs = new MemoryStream(bufferedEncryptedBytes.WrittenMemory.ToArray()); - await foreach (var result in (await client.DecryptAsync(componentName, encryptedMs, keyName, - cancellationToken)).WithCancellation(cancellationToken)) - { - decryptFs.Write(result.Span); - } - - decryptFs.Close(); - - //Let's confirm the value as written to the file - var decryptedValue = await File.ReadAllTextAsync(tempDecryptedFile, cancellationToken); - Console.WriteLine("Decrypted value: "); - Console.WriteLine(decryptedValue); - - //And some cleanup to delete our temp file - File.Delete(tempDecryptedFile); + Console.WriteLine(line); } + + //Encrypt from a file stream and buffer the resulting bytes to an in-memory buffer + await using var encryptFs = new FileStream(fileName, FileMode.Open); + + var bufferedEncryptedBytes = new ArrayBufferWriter(); + await foreach (var bytes in (await client.EncryptAsync(componentName, encryptFs, keyName, + new EncryptionOptions(KeyWrapAlgorithm.Rsa), cancellationToken)) + .WithCancellation(cancellationToken)) + { + bufferedEncryptedBytes.Write(bytes.Span); + } + + Console.WriteLine("Encrypted bytes:"); + Console.WriteLine(Convert.ToBase64String(bufferedEncryptedBytes.WrittenMemory.ToArray())); + + //We'll write to a temporary file via a FileStream + var tempDecryptedFile = Path.GetTempFileName(); + await using var decryptFs = new FileStream(tempDecryptedFile, FileMode.Create); + + //We'll stream the decrypted bytes from a MemoryStream into the above temporary file + await using var encryptedMs = new MemoryStream(bufferedEncryptedBytes.WrittenMemory.ToArray()); + await foreach (var result in (await client.DecryptAsync(componentName, encryptedMs, keyName, + cancellationToken)).WithCancellation(cancellationToken)) + { + decryptFs.Write(result.Span); + } + + decryptFs.Close(); + + //Let's confirm the value as written to the file + var decryptedValue = await File.ReadAllTextAsync(tempDecryptedFile, cancellationToken); + Console.WriteLine("Decrypted value: "); + Console.WriteLine(decryptedValue); + + //And some cleanup to delete our temp file + File.Delete(tempDecryptedFile); } -} +} \ No newline at end of file diff --git a/examples/Client/Cryptography/Examples/EncryptDecryptStringExample.cs b/examples/Client/Cryptography/Examples/EncryptDecryptStringExample.cs index d29b24a6..1a31e915 100644 --- a/examples/Client/Cryptography/Examples/EncryptDecryptStringExample.cs +++ b/examples/Client/Cryptography/Examples/EncryptDecryptStringExample.cs @@ -15,29 +15,28 @@ using System.Text; using Dapr.Client; #pragma warning disable CS0618 // Type or member is obsolete -namespace Cryptography.Examples +namespace Cryptography.Examples; + +internal class EncryptDecryptStringExample(string componentName, string keyName) : Example { - internal class EncryptDecryptStringExample(string componentName, string keyName) : Example + public override string DisplayName => "Using Cryptography to encrypt and decrypt a string"; + + public override async Task RunAsync(CancellationToken cancellationToken) { - public override string DisplayName => "Using Cryptography to encrypt and decrypt a string"; - - public override async Task RunAsync(CancellationToken cancellationToken) - { - using var client = new DaprClientBuilder().Build(); + using var client = new DaprClientBuilder().Build(); - const string plaintextStr = "This is the value we're going to encrypt today"; - Console.WriteLine($"Original string value: '{plaintextStr}'"); + const string plaintextStr = "This is the value we're going to encrypt today"; + Console.WriteLine($"Original string value: '{plaintextStr}'"); - //Encrypt the string - var plaintextBytes = Encoding.UTF8.GetBytes(plaintextStr); - var encryptedBytesResult = await client.EncryptAsync(componentName, plaintextBytes, keyName, new EncryptionOptions(KeyWrapAlgorithm.Rsa), - cancellationToken); + //Encrypt the string + var plaintextBytes = Encoding.UTF8.GetBytes(plaintextStr); + var encryptedBytesResult = await client.EncryptAsync(componentName, plaintextBytes, keyName, new EncryptionOptions(KeyWrapAlgorithm.Rsa), + cancellationToken); - Console.WriteLine($"Encrypted bytes: '{Convert.ToBase64String(encryptedBytesResult.Span)}'"); + Console.WriteLine($"Encrypted bytes: '{Convert.ToBase64String(encryptedBytesResult.Span)}'"); - //Decrypt the string - var decryptedBytes = await client.DecryptAsync(componentName, encryptedBytesResult, keyName, cancellationToken); - Console.WriteLine($"Decrypted string: '{Encoding.UTF8.GetString(decryptedBytes.ToArray())}'"); - } + //Decrypt the string + var decryptedBytes = await client.DecryptAsync(componentName, encryptedBytesResult, keyName, cancellationToken); + Console.WriteLine($"Decrypted string: '{Encoding.UTF8.GetString(decryptedBytes.ToArray())}'"); } -} +} \ No newline at end of file diff --git a/examples/Client/Cryptography/Program.cs b/examples/Client/Cryptography/Program.cs index 5c63d736..9fe3ec97 100644 --- a/examples/Client/Cryptography/Program.cs +++ b/examples/Client/Cryptography/Program.cs @@ -13,37 +13,36 @@ using Cryptography.Examples; -namespace Cryptography +namespace Cryptography; + +class Program { - class Program - { - private const string ComponentName = "localstorage"; - private const string KeyName = "rsa-private-key.pem"; //This should match the name of your generated key - this sample expects an RSA symmetrical key. + private const string ComponentName = "localstorage"; + private const string KeyName = "rsa-private-key.pem"; //This should match the name of your generated key - this sample expects an RSA symmetrical key. - private static readonly Example[] Examples = new Example[] + private static readonly Example[] Examples = new Example[] + { + new EncryptDecryptStringExample(ComponentName, KeyName), + new EncryptDecryptFileStreamExample(ComponentName, KeyName) + }; + + static async Task Main(string[] args) + { + if (args.Length > 0 && int.TryParse(args[0], out var index) && index >= 0 && index < Examples.Length) { - new EncryptDecryptStringExample(ComponentName, KeyName), - new EncryptDecryptFileStreamExample(ComponentName, KeyName) - }; + var cts = new CancellationTokenSource(); + Console.CancelKeyPress += (object? sender, ConsoleCancelEventArgs e) => cts.Cancel(); - static async Task Main(string[] args) - { - if (args.Length > 0 && int.TryParse(args[0], out var index) && index >= 0 && index < Examples.Length) - { - var cts = new CancellationTokenSource(); - Console.CancelKeyPress += (object? sender, ConsoleCancelEventArgs e) => cts.Cancel(); - - await Examples[index].RunAsync(cts.Token); - return 0; - } - - Console.WriteLine("Hello, please choose a sample to run by passing your selection's number into the arguments, e.g. 'dotnet run 0':"); - for (var i = 0; i < Examples.Length; i++) - { - Console.WriteLine($"{i}: {Examples[i].DisplayName}"); - } - Console.WriteLine(); - return 1; + await Examples[index].RunAsync(cts.Token); + return 0; } + + Console.WriteLine("Hello, please choose a sample to run by passing your selection's number into the arguments, e.g. 'dotnet run 0':"); + for (var i = 0; i < Examples.Length; i++) + { + Console.WriteLine($"{i}: {Examples[i].DisplayName}"); + } + Console.WriteLine(); + return 1; } -} +} \ No newline at end of file diff --git a/examples/Client/DistributedLock/Controllers/BindingController.cs b/examples/Client/DistributedLock/Controllers/BindingController.cs index aa4dd1f5..37fbe637 100644 --- a/examples/Client/DistributedLock/Controllers/BindingController.cs +++ b/examples/Client/DistributedLock/Controllers/BindingController.cs @@ -8,100 +8,99 @@ using DistributedLock.Model; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -namespace DistributedLock.Controllers +namespace DistributedLock.Controllers; + +[ApiController] +public class BindingController : ControllerBase { - [ApiController] - public class BindingController : ControllerBase + private DaprClient client; + private ILogger logger; + private string appId; + + public BindingController(DaprClient client, ILogger logger) { - private DaprClient client; - private ILogger logger; - private string appId; + this.client = client; + this.logger = logger; + this.appId = Environment.GetEnvironmentVariable("APP_ID"); + } - public BindingController(DaprClient client, ILogger logger) + [HttpPost("cronbinding")] + [Obsolete] + public async Task HandleBindingEvent() + { + logger.LogInformation($"Received binding event on {appId}, scanning for work."); + + var request = new BindingRequest("localstorage", "list"); + var result = client.InvokeBindingAsync(request); + + var rawData = result.Result.Data.ToArray(); + var files = JsonSerializer.Deserialize(rawData); + + if (files != null) { - this.client = client; - this.logger = logger; - this.appId = Environment.GetEnvironmentVariable("APP_ID"); - } - - [HttpPost("cronbinding")] - [Obsolete] - public async Task HandleBindingEvent() - { - logger.LogInformation($"Received binding event on {appId}, scanning for work."); - - var request = new BindingRequest("localstorage", "list"); - var result = client.InvokeBindingAsync(request); - - var rawData = result.Result.Data.ToArray(); - var files = JsonSerializer.Deserialize(rawData); - - if (files != null) + foreach (var file in files.Select(file => file.Split("/").Last()).OrderBy(file => file)) { - foreach (var file in files.Select(file => file.Split("/").Last()).OrderBy(file => file)) - { - await AttemptToProcessFile(file); - } + await AttemptToProcessFile(file); } + } - return Ok(); - } + return Ok(); + } - [Obsolete] - private async Task AttemptToProcessFile(string fileName) + [Obsolete] + private async Task AttemptToProcessFile(string fileName) + { + // Locks are Disposable and will automatically unlock at the end of a 'using' statement. + logger.LogInformation($"Attempting to lock: {fileName}"); + await using (var fileLock = await client.Lock("redislock", fileName, appId, 60)) { - // Locks are Disposable and will automatically unlock at the end of a 'using' statement. - logger.LogInformation($"Attempting to lock: {fileName}"); - await using (var fileLock = await client.Lock("redislock", fileName, appId, 60)) + if (fileLock.Success) { - if (fileLock.Success) + logger.LogInformation($"Successfully locked file: {fileName}"); + + // Get the file after we've locked it, we're safe here because of the lock. + var fileState = await GetFile(fileName); + + if (fileState == null) { - logger.LogInformation($"Successfully locked file: {fileName}"); - - // Get the file after we've locked it, we're safe here because of the lock. - var fileState = await GetFile(fileName); - - if (fileState == null) - { - logger.LogWarning($"File {fileName} has already been processed!"); - return; - } - - // "Analyze" the file before committing it to our remote storage. - fileState.Analysis = fileState.Number > 50 ? "High" : "Low"; - - // Save it to remote storage. - await client.SaveStateAsync("redisstore", fileName, fileState); - - // Remove it from local storage. - var bindingDeleteRequest = new BindingRequest("localstorage", "delete"); - bindingDeleteRequest.Metadata["fileName"] = fileName; - await client.InvokeBindingAsync(bindingDeleteRequest); - - logger.LogInformation($"Done processing {fileName}"); + logger.LogWarning($"File {fileName} has already been processed!"); + return; } - else - { - logger.LogWarning($"Failed to lock {fileName}."); - } - } - } - private async Task GetFile(string fileName) - { - try - { - var bindingGetRequest = new BindingRequest("localstorage", "get"); - bindingGetRequest.Metadata["fileName"] = fileName; + // "Analyze" the file before committing it to our remote storage. + fileState.Analysis = fileState.Number > 50 ? "High" : "Low"; - var bindingResponse = await client.InvokeBindingAsync(bindingGetRequest); - return JsonSerializer.Deserialize(bindingResponse.Data.ToArray()); + // Save it to remote storage. + await client.SaveStateAsync("redisstore", fileName, fileState); + + // Remove it from local storage. + var bindingDeleteRequest = new BindingRequest("localstorage", "delete"); + bindingDeleteRequest.Metadata["fileName"] = fileName; + await client.InvokeBindingAsync(bindingDeleteRequest); + + logger.LogInformation($"Done processing {fileName}"); } - catch (DaprException) + else { - return null; - } + logger.LogWarning($"Failed to lock {fileName}."); + } } } -} + + private async Task GetFile(string fileName) + { + try + { + var bindingGetRequest = new BindingRequest("localstorage", "get"); + bindingGetRequest.Metadata["fileName"] = fileName; + + var bindingResponse = await client.InvokeBindingAsync(bindingGetRequest); + return JsonSerializer.Deserialize(bindingResponse.Data.ToArray()); + } + catch (DaprException) + { + return null; + } + } +} \ No newline at end of file diff --git a/examples/Client/DistributedLock/Model/StateData.cs b/examples/Client/DistributedLock/Model/StateData.cs index 0ad5d2fd..f5eeeb29 100644 --- a/examples/Client/DistributedLock/Model/StateData.cs +++ b/examples/Client/DistributedLock/Model/StateData.cs @@ -1,15 +1,13 @@ -namespace DistributedLock.Model -{ +namespace DistributedLock.Model; #nullable enable - public class StateData - { - public int Number { get; } - public string? Analysis { get; set; } +public class StateData +{ + public int Number { get; } + public string? Analysis { get; set; } - public StateData(int number, string? analysis = null) - { - Number = number; - Analysis = analysis; - } + public StateData(int number, string? analysis = null) + { + Number = number; + Analysis = analysis; } -} +} \ No newline at end of file diff --git a/examples/Client/DistributedLock/Program.cs b/examples/Client/DistributedLock/Program.cs index 3080b5b1..830fb5dc 100644 --- a/examples/Client/DistributedLock/Program.cs +++ b/examples/Client/DistributedLock/Program.cs @@ -2,22 +2,21 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; -namespace DistributedLock -{ - public class Program - { - static string DEFAULT_APP_PORT = "22222"; - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } +namespace DistributedLock; - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup() - .UseUrls($"http://localhost:{Environment.GetEnvironmentVariable("APP_PORT") ?? DEFAULT_APP_PORT}"); - }); +public class Program +{ + static string DEFAULT_APP_PORT = "22222"; + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); } -} + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup() + .UseUrls($"http://localhost:{Environment.GetEnvironmentVariable("APP_PORT") ?? DEFAULT_APP_PORT}"); + }); +} \ No newline at end of file diff --git a/examples/Client/DistributedLock/Services/GeneratorService.cs b/examples/Client/DistributedLock/Services/GeneratorService.cs index aa59d9b7..4d5c0a3f 100644 --- a/examples/Client/DistributedLock/Services/GeneratorService.cs +++ b/examples/Client/DistributedLock/Services/GeneratorService.cs @@ -3,30 +3,29 @@ using System.Threading; using Dapr.Client; using DistributedLock.Model; -namespace DistributedLock.Services +namespace DistributedLock.Services; + +public class GeneratorService { - public class GeneratorService + Timer generateDataTimer; + + public GeneratorService() { - Timer generateDataTimer; - - public GeneratorService() + // Generate some data every second. + if (Environment.GetEnvironmentVariable("APP_ID") == "generator") { - // Generate some data every second. - if (Environment.GetEnvironmentVariable("APP_ID") == "generator") - { - generateDataTimer = new Timer(GenerateData, null, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(10)); - } - } - - public async void GenerateData(Object stateInfo) - { - using (var client = new DaprClientBuilder().Build()) - { - var rand = new Random(); - var state = new StateData(rand.Next(100)); - - await client.InvokeBindingAsync("localstorage", "create", state); - } + generateDataTimer = new Timer(GenerateData, null, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(10)); } } -} + + public async void GenerateData(Object stateInfo) + { + using (var client = new DaprClientBuilder().Build()) + { + var rand = new Random(); + var state = new StateData(rand.Next(100)); + + await client.InvokeBindingAsync("localstorage", "create", state); + } + } +} \ No newline at end of file diff --git a/examples/Client/DistributedLock/Startup.cs b/examples/Client/DistributedLock/Startup.cs index 9f40e475..0e2e0df3 100644 --- a/examples/Client/DistributedLock/Startup.cs +++ b/examples/Client/DistributedLock/Startup.cs @@ -1,46 +1,45 @@ using Dapr.AspNetCore; -using DistributedLock.Services; +using DistributedLock.Services; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -namespace DistributedLock +namespace DistributedLock; + +public class Startup { - public class Startup + public Startup(IConfiguration configuration) { - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } - - public IConfiguration Configuration { get; } - - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) - { - services.AddControllers().AddDapr(); - services.AddLogging(); - services.AddSingleton(new GeneratorService()); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.UseRouting(); - - app.UseAuthorization(); - - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); - } + Configuration = configuration; } -} + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers().AddDapr(); + services.AddLogging(); + services.AddSingleton(new GeneratorService()); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + } +} \ No newline at end of file diff --git a/examples/Client/PublishSubscribe/BulkPublishEventExample/BulkPublishEventExample.cs b/examples/Client/PublishSubscribe/BulkPublishEventExample/BulkPublishEventExample.cs index 34361845..af1fc639 100644 --- a/examples/Client/PublishSubscribe/BulkPublishEventExample/BulkPublishEventExample.cs +++ b/examples/Client/PublishSubscribe/BulkPublishEventExample/BulkPublishEventExample.cs @@ -17,46 +17,45 @@ using System.Threading; using System.Threading.Tasks; using Dapr.Client; -namespace Samples.Client +namespace Samples.Client; + +public sealed class BulkPublishEventExample : Example { - public class BulkPublishEventExample : Example - { - private const string PubsubName = "pubsub"; - private const string TopicName = "deposit"; + private const string PubsubName = "pubsub"; + private const string TopicName = "deposit"; - IReadOnlyList BulkPublishData = new List() { - new { Id = "17", Amount = 10m }, - new { Id = "18", Amount = 20m }, - new { Id = "19", Amount = 30m } - }; + IReadOnlyList BulkPublishData = new List() { + new { Id = "17", Amount = 10m }, + new { Id = "18", Amount = 20m }, + new { Id = "19", Amount = 30m } + }; - public override string DisplayName => "Bulk Publishing Events"; + public override string DisplayName => "Bulk Publishing Events"; - public override async Task RunAsync(CancellationToken cancellationToken) - { - using var client = new DaprClientBuilder().Build(); + public override async Task RunAsync(CancellationToken cancellationToken) + { + using var client = new DaprClientBuilder().Build(); - var res = await client.BulkPublishEventAsync(PubsubName, TopicName, - BulkPublishData); + var res = await client.BulkPublishEventAsync(PubsubName, TopicName, + BulkPublishData, cancellationToken: cancellationToken); - if (res != null) { - if (res.FailedEntries.Count > 0) - { - Console.WriteLine("Some events failed to be published!"); + if (res != null) { + if (res.FailedEntries.Count > 0) + { + Console.WriteLine("Some events failed to be published!"); - foreach (var failedEntry in res.FailedEntries) - { - Console.WriteLine("EntryId : " + failedEntry.Entry.EntryId + " Error message : " + - failedEntry.ErrorMessage); - } - } - else + foreach (var failedEntry in res.FailedEntries) { - Console.WriteLine("Published multiple deposit events!"); + Console.WriteLine("EntryId : " + failedEntry.Entry.EntryId + " Error message : " + + failedEntry.ErrorMessage); } - } else { - throw new Exception("null response from dapr"); } + else + { + Console.WriteLine("Published multiple deposit events!"); + } + } else { + throw new Exception("null response from dapr"); } } } diff --git a/examples/Client/PublishSubscribe/BulkPublishEventExample/Example.cs b/examples/Client/PublishSubscribe/BulkPublishEventExample/Example.cs index e12e38d1..eb0b8c3d 100644 --- a/examples/Client/PublishSubscribe/BulkPublishEventExample/Example.cs +++ b/examples/Client/PublishSubscribe/BulkPublishEventExample/Example.cs @@ -14,12 +14,11 @@ using System.Threading; using System.Threading.Tasks; -namespace Samples.Client -{ - public abstract class Example - { - public abstract string DisplayName { get; } +namespace Samples.Client; - public abstract Task RunAsync(CancellationToken cancellationToken); - } -} +public abstract class Example +{ + public abstract string DisplayName { get; } + + public abstract Task RunAsync(CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/examples/Client/PublishSubscribe/BulkPublishEventExample/Program.cs b/examples/Client/PublishSubscribe/BulkPublishEventExample/Program.cs index 47698235..768f5ad4 100644 --- a/examples/Client/PublishSubscribe/BulkPublishEventExample/Program.cs +++ b/examples/Client/PublishSubscribe/BulkPublishEventExample/Program.cs @@ -13,35 +13,26 @@ using System; using System.Threading; -using System.Threading.Tasks; +using Samples.Client; -namespace Samples.Client +Example[] Examples = +[ + new BulkPublishEventExample() +]; + +if (args.Length > 0 && int.TryParse(args[0], out var index) && index >= 0 && index < Examples.Length) { - class Program - { - private static readonly Example[] Examples = new Example[] - { - new BulkPublishEventExample(), - }; + var cts = new CancellationTokenSource(); + Console.CancelKeyPress += (_, _) => cts.Cancel(); - static async Task Main(string[] args) - { - if (args.Length > 0 && int.TryParse(args[0], out var index) && index >= 0 && index < Examples.Length) - { - var cts = new CancellationTokenSource(); - Console.CancelKeyPress += (object? sender, ConsoleCancelEventArgs e) => cts.Cancel(); - - await Examples[index].RunAsync(cts.Token); - return 0; - } - - Console.WriteLine("Hello, please choose a sample to run:"); - for (var i = 0; i < Examples.Length; i++) - { - Console.WriteLine($"{i}: {Examples[i].DisplayName}"); - } - Console.WriteLine(); - return 0; - } - } + await Examples[index].RunAsync(cts.Token); + return 0; } + +Console.WriteLine("Hello, please choose a sample to run:"); +for (var i = 0; i < Examples.Length; i++) +{ + Console.WriteLine($"{i}: {Examples[i].DisplayName}"); +} +Console.WriteLine(); +return 0; diff --git a/examples/Client/PublishSubscribe/PublishEventExample/Example.cs b/examples/Client/PublishSubscribe/PublishEventExample/Example.cs index fbe78124..fe51b56f 100644 --- a/examples/Client/PublishSubscribe/PublishEventExample/Example.cs +++ b/examples/Client/PublishSubscribe/PublishEventExample/Example.cs @@ -14,14 +14,13 @@ using System.Threading; using System.Threading.Tasks; -namespace Samples.Client +namespace Samples.Client; + +public abstract class Example { - public abstract class Example - { - protected static readonly string pubsubName = "pubsub"; + protected static readonly string pubsubName = "pubsub"; - public abstract string DisplayName { get; } + public abstract string DisplayName { get; } - public abstract Task RunAsync(CancellationToken cancellationToken); - } -} + public abstract Task RunAsync(CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/examples/Client/PublishSubscribe/PublishEventExample/Program.cs b/examples/Client/PublishSubscribe/PublishEventExample/Program.cs index af74ad90..e1f5c95e 100644 --- a/examples/Client/PublishSubscribe/PublishEventExample/Program.cs +++ b/examples/Client/PublishSubscribe/PublishEventExample/Program.cs @@ -15,34 +15,33 @@ using System; using System.Threading; using System.Threading.Tasks; -namespace Samples.Client +namespace Samples.Client; + +class Program { - class Program + private static readonly Example[] Examples = new Example[] { - private static readonly Example[] Examples = new Example[] + new PublishEventExample(), + new PublishBytesExample(), + }; + + static async Task Main(string[] args) + { + if (args.Length > 0 && int.TryParse(args[0], out var index) && index >= 0 && index < Examples.Length) { - new PublishEventExample(), - new PublishBytesExample(), - }; + var cts = new CancellationTokenSource(); + Console.CancelKeyPress += (object? sender, ConsoleCancelEventArgs e) => cts.Cancel(); - static async Task Main(string[] args) - { - if (args.Length > 0 && int.TryParse(args[0], out var index) && index >= 0 && index < Examples.Length) - { - var cts = new CancellationTokenSource(); - Console.CancelKeyPress += (object? sender, ConsoleCancelEventArgs e) => cts.Cancel(); - - await Examples[index].RunAsync(cts.Token); - return 0; - } - - Console.WriteLine("Hello, please choose a sample to run:"); - for (var i = 0; i < Examples.Length; i++) - { - Console.WriteLine($"{i}: {Examples[i].DisplayName}"); - } - Console.WriteLine(); + await Examples[index].RunAsync(cts.Token); return 0; } + + Console.WriteLine("Hello, please choose a sample to run:"); + for (var i = 0; i < Examples.Length; i++) + { + Console.WriteLine($"{i}: {Examples[i].DisplayName}"); + } + Console.WriteLine(); + return 0; } -} +} \ No newline at end of file diff --git a/examples/Client/PublishSubscribe/PublishEventExample/PublishBytesExample.cs b/examples/Client/PublishSubscribe/PublishEventExample/PublishBytesExample.cs index 3334421c..4834e80a 100644 --- a/examples/Client/PublishSubscribe/PublishEventExample/PublishBytesExample.cs +++ b/examples/Client/PublishSubscribe/PublishEventExample/PublishBytesExample.cs @@ -19,21 +19,20 @@ using System.Threading; using System.Threading.Tasks; using Dapr.Client; -namespace Samples.Client +namespace Samples.Client; + +public class PublishBytesExample : Example { - public class PublishBytesExample : Example + public override string DisplayName => "Publish Bytes"; + + public async override Task RunAsync(CancellationToken cancellationToken) { - public override string DisplayName => "Publish Bytes"; + using var client = new DaprClientBuilder().Build(); - public async override Task RunAsync(CancellationToken cancellationToken) - { - using var client = new DaprClientBuilder().Build(); + var transaction = new { Id = "17", Amount = 30m }; + var content = JsonSerializer.SerializeToUtf8Bytes(transaction); - var transaction = new { Id = "17", Amount = 30m }; - var content = JsonSerializer.SerializeToUtf8Bytes(transaction); - - await client.PublishByteEventAsync(pubsubName, "deposit", content.AsMemory(), MediaTypeNames.Application.Json, new Dictionary { }, cancellationToken); - Console.WriteLine("Published deposit event!"); - } + await client.PublishByteEventAsync(pubsubName, "deposit", content.AsMemory(), MediaTypeNames.Application.Json, new Dictionary { }, cancellationToken); + Console.WriteLine("Published deposit event!"); } -} +} \ No newline at end of file diff --git a/examples/Client/PublishSubscribe/PublishEventExample/PublishEventExample.cs b/examples/Client/PublishSubscribe/PublishEventExample/PublishEventExample.cs index 9d34ae50..e8b1e80e 100644 --- a/examples/Client/PublishSubscribe/PublishEventExample/PublishEventExample.cs +++ b/examples/Client/PublishSubscribe/PublishEventExample/PublishEventExample.cs @@ -16,19 +16,18 @@ using System.Threading; using System.Threading.Tasks; using Dapr.Client; -namespace Samples.Client +namespace Samples.Client; + +public class PublishEventExample : Example { - public class PublishEventExample : Example + public override string DisplayName => "Publishing Events"; + + public override async Task RunAsync(CancellationToken cancellationToken) { - public override string DisplayName => "Publishing Events"; + using var client = new DaprClientBuilder().Build(); - public override async Task RunAsync(CancellationToken cancellationToken) - { - using var client = new DaprClientBuilder().Build(); - - var eventData = new { Id = "17", Amount = 10m, }; - await client.PublishEventAsync(pubsubName, "deposit", eventData, cancellationToken); - Console.WriteLine("Published deposit event!"); - } + var eventData = new { Id = "17", Amount = 10m, }; + await client.PublishEventAsync(pubsubName, "deposit", eventData, cancellationToken); + Console.WriteLine("Published deposit event!"); } -} +} \ No newline at end of file diff --git a/examples/Client/ServiceInvocation/Example.cs b/examples/Client/ServiceInvocation/Example.cs index d2c07bfe..507bdcfd 100644 --- a/examples/Client/ServiceInvocation/Example.cs +++ b/examples/Client/ServiceInvocation/Example.cs @@ -14,12 +14,11 @@ using System.Threading; using System.Threading.Tasks; -namespace Samples.Client -{ - public abstract class Example - { - public abstract string DisplayName { get; } +namespace Samples.Client; - public abstract Task RunAsync(CancellationToken cancellationToken); - } -} +public abstract class Example +{ + public abstract string DisplayName { get; } + + public abstract Task RunAsync(CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/examples/Client/ServiceInvocation/InvokeServiceGrpcExample.cs b/examples/Client/ServiceInvocation/InvokeServiceGrpcExample.cs index 564cf038..127d9003 100644 --- a/examples/Client/ServiceInvocation/InvokeServiceGrpcExample.cs +++ b/examples/Client/ServiceInvocation/InvokeServiceGrpcExample.cs @@ -17,32 +17,31 @@ using System.Threading.Tasks; using Dapr.Client; using GrpcServiceSample.Generated; -namespace Samples.Client +namespace Samples.Client; + +public class InvokeServiceGrpcExample : Example { - public class InvokeServiceGrpcExample : Example + public override string DisplayName => "Invoking a gRPC service with gRPC semantics and Protobuf with DaprClient"; + + // Note: the data types used in this sample are generated from data.proto in GrpcServiceSample + public override async Task RunAsync(CancellationToken cancellationToken) { - public override string DisplayName => "Invoking a gRPC service with gRPC semantics and Protobuf with DaprClient"; + using var client = new DaprClientBuilder().Build(); - // Note: the data types used in this sample are generated from data.proto in GrpcServiceSample - public override async Task RunAsync(CancellationToken cancellationToken) - { - using var client = new DaprClientBuilder().Build(); + Console.WriteLine("Invoking grpc deposit"); + var deposit = new GrpcServiceSample.Generated.Transaction() { Id = "17", Amount = 99 }; + var account = await client.InvokeMethodGrpcAsync("grpcsample", "deposit", deposit, cancellationToken); + Console.WriteLine("Returned: id:{0} | Balance:{1}", account.Id, account.Balance); + Console.WriteLine("Completed grpc deposit"); - Console.WriteLine("Invoking grpc deposit"); - var deposit = new GrpcServiceSample.Generated.Transaction() { Id = "17", Amount = 99 }; - var account = await client.InvokeMethodGrpcAsync("grpcsample", "deposit", deposit, cancellationToken); - Console.WriteLine("Returned: id:{0} | Balance:{1}", account.Id, account.Balance); - Console.WriteLine("Completed grpc deposit"); + Console.WriteLine("Invoking grpc withdraw"); + var withdraw = new GrpcServiceSample.Generated.Transaction() { Id = "17", Amount = 10, }; + await client.InvokeMethodGrpcAsync("grpcsample", "withdraw", withdraw, cancellationToken); + Console.WriteLine("Completed grpc withdraw"); - Console.WriteLine("Invoking grpc withdraw"); - var withdraw = new GrpcServiceSample.Generated.Transaction() { Id = "17", Amount = 10, }; - await client.InvokeMethodGrpcAsync("grpcsample", "withdraw", withdraw, cancellationToken); - Console.WriteLine("Completed grpc withdraw"); - - Console.WriteLine("Invoking grpc balance"); - var request = new GetAccountRequest() { Id = "17", }; - account = await client.InvokeMethodGrpcAsync("grpcsample", "getaccount", request, cancellationToken); - Console.WriteLine($"Received grpc balance {account.Balance}"); - } + Console.WriteLine("Invoking grpc balance"); + var request = new GetAccountRequest() { Id = "17", }; + account = await client.InvokeMethodGrpcAsync("grpcsample", "getaccount", request, cancellationToken); + Console.WriteLine($"Received grpc balance {account.Balance}"); } -} +} \ No newline at end of file diff --git a/examples/Client/ServiceInvocation/InvokeServiceHttpClientExample.cs b/examples/Client/ServiceInvocation/InvokeServiceHttpClientExample.cs index 72d68096..415e984e 100644 --- a/examples/Client/ServiceInvocation/InvokeServiceHttpClientExample.cs +++ b/examples/Client/ServiceInvocation/InvokeServiceHttpClientExample.cs @@ -17,41 +17,40 @@ using System.Threading; using System.Threading.Tasks; using Dapr.Client; -namespace Samples.Client +namespace Samples.Client; + +public class InvokeServiceHttpClientExample : Example { - public class InvokeServiceHttpClientExample : Example + public override string DisplayName => "Invoking an HTTP service with HttpClient"; + + public override async Task RunAsync(CancellationToken cancellationToken) { - public override string DisplayName => "Invoking an HTTP service with HttpClient"; + var client = DaprClient.CreateInvokeHttpClient(appId: "routing"); - public override async Task RunAsync(CancellationToken cancellationToken) - { - var client = DaprClient.CreateInvokeHttpClient(appId: "routing"); + var deposit = new Transaction { Id = "17", Amount = 99m }; + var response = await client.PostAsJsonAsync("/deposit", deposit, cancellationToken); + var account = await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken); + Console.WriteLine("Returned: id:{0} | Balance:{1}", account?.Id, account?.Balance); - var deposit = new Transaction { Id = "17", Amount = 99m }; - var response = await client.PostAsJsonAsync("/deposit", deposit, cancellationToken); - var account = await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken); - Console.WriteLine("Returned: id:{0} | Balance:{1}", account?.Id, account?.Balance); + var withdraw = new Transaction { Id = "17", Amount = 10m, }; + response = await client.PostAsJsonAsync("/withdraw", withdraw, cancellationToken); + response.EnsureSuccessStatusCode(); - var withdraw = new Transaction { Id = "17", Amount = 10m, }; - response = await client.PostAsJsonAsync("/withdraw", withdraw, cancellationToken); - response.EnsureSuccessStatusCode(); - - account = await client.GetFromJsonAsync("/17", cancellationToken); - Console.WriteLine($"Received balance {account?.Balance}"); - } - - internal class Transaction - { - public string? Id { get; set; } - - public decimal? Amount { get; set; } - } - - internal class Account - { - public string? Id { get; set; } - - public decimal? Balance { get; set; } - } + account = await client.GetFromJsonAsync("/17", cancellationToken); + Console.WriteLine($"Received balance {account?.Balance}"); } -} + + internal class Transaction + { + public string? Id { get; set; } + + public decimal? Amount { get; set; } + } + + internal class Account + { + public string? Id { get; set; } + + public decimal? Balance { get; set; } + } +} \ No newline at end of file diff --git a/examples/Client/ServiceInvocation/InvokeServiceHttpExample.cs b/examples/Client/ServiceInvocation/InvokeServiceHttpExample.cs index 74235c13..5595ae26 100644 --- a/examples/Client/ServiceInvocation/InvokeServiceHttpExample.cs +++ b/examples/Client/ServiceInvocation/InvokeServiceHttpExample.cs @@ -17,46 +17,45 @@ using System.Threading; using System.Threading.Tasks; using Dapr.Client; -namespace Samples.Client +namespace Samples.Client; + +public class InvokeServiceHttpExample : Example { - public class InvokeServiceHttpExample : Example + public override string DisplayName => "Invoking an HTTP service with DaprClient"; + + public override async Task RunAsync(CancellationToken cancellationToken) { - public override string DisplayName => "Invoking an HTTP service with DaprClient"; + using var client = new DaprClientBuilder().Build(); - public override async Task RunAsync(CancellationToken cancellationToken) - { - using var client = new DaprClientBuilder().Build(); + // Invokes a POST method named "deposit" that takes input of type "Transaction" as define in the RoutingSample. + Console.WriteLine("Invoking deposit"); + var data = new { id = "17", amount = 99m }; + var account = await client.InvokeMethodAsync("routing", "deposit", data, cancellationToken); + Console.WriteLine("Returned: id:{0} | Balance:{1}", account.Id, account.Balance); - // Invokes a POST method named "deposit" that takes input of type "Transaction" as define in the RoutingSample. - Console.WriteLine("Invoking deposit"); - var data = new { id = "17", amount = 99m }; - var account = await client.InvokeMethodAsync("routing", "deposit", data, cancellationToken); - Console.WriteLine("Returned: id:{0} | Balance:{1}", account.Id, account.Balance); + // Invokes a POST method named "Withdraw" that takes input of type "Transaction" as define in the RoutingSample. + Console.WriteLine("Invoking withdraw"); + data = new { id = "17", amount = 10m, }; + await client.InvokeMethodAsync("routing", "Withdraw", data, cancellationToken); + Console.WriteLine("Completed"); - // Invokes a POST method named "Withdraw" that takes input of type "Transaction" as define in the RoutingSample. - Console.WriteLine("Invoking withdraw"); - data = new { id = "17", amount = 10m, }; - await client.InvokeMethodAsync("routing", "Withdraw", data, cancellationToken); - Console.WriteLine("Completed"); - - // Invokes a GET method named "hello" that takes input of type "MyData" and returns a string. - Console.WriteLine("Invoking balance"); - account = await client.InvokeMethodAsync(HttpMethod.Get, "routing", "17", cancellationToken); - Console.WriteLine($"Received balance {account.Balance}"); - } - - internal class Transaction - { - public string? Id { get; set; } - - public decimal? Amount { get; set; } - } - - internal class Account - { - public string? Id { get; set; } - - public decimal? Balance { get; set; } - } + // Invokes a GET method named "hello" that takes input of type "MyData" and returns a string. + Console.WriteLine("Invoking balance"); + account = await client.InvokeMethodAsync(HttpMethod.Get, "routing", "17", cancellationToken); + Console.WriteLine($"Received balance {account.Balance}"); } -} + + internal class Transaction + { + public string? Id { get; set; } + + public decimal? Amount { get; set; } + } + + internal class Account + { + public string? Id { get; set; } + + public decimal? Balance { get; set; } + } +} \ No newline at end of file diff --git a/examples/Client/ServiceInvocation/Program.cs b/examples/Client/ServiceInvocation/Program.cs index b0c38ebc..6d371337 100644 --- a/examples/Client/ServiceInvocation/Program.cs +++ b/examples/Client/ServiceInvocation/Program.cs @@ -15,35 +15,34 @@ using System; using System.Threading; using System.Threading.Tasks; -namespace Samples.Client +namespace Samples.Client; + +class Program { - class Program + private static readonly Example[] Examples = new Example[] { - private static readonly Example[] Examples = new Example[] + new InvokeServiceGrpcExample(), + new InvokeServiceHttpExample(), + new InvokeServiceHttpClientExample(), + }; + + static async Task Main(string[] args) + { + if (args.Length > 0 && int.TryParse(args[0], out var index) && index >= 0 && index < Examples.Length) { - new InvokeServiceGrpcExample(), - new InvokeServiceHttpExample(), - new InvokeServiceHttpClientExample(), - }; + var cts = new CancellationTokenSource(); + Console.CancelKeyPress += (object? sender, ConsoleCancelEventArgs e) => cts.Cancel(); - static async Task Main(string[] args) - { - if (args.Length > 0 && int.TryParse(args[0], out var index) && index >= 0 && index < Examples.Length) - { - var cts = new CancellationTokenSource(); - Console.CancelKeyPress += (object? sender, ConsoleCancelEventArgs e) => cts.Cancel(); - - await Examples[index].RunAsync(cts.Token); - return 0; - } - - Console.WriteLine("Hello, please choose a sample to run:"); - for (var i = 0; i < Examples.Length; i++) - { - Console.WriteLine($"{i}: {Examples[i].DisplayName}"); - } - Console.WriteLine(); - return 1; + await Examples[index].RunAsync(cts.Token); + return 0; } + + Console.WriteLine("Hello, please choose a sample to run:"); + for (var i = 0; i < Examples.Length; i++) + { + Console.WriteLine($"{i}: {Examples[i].DisplayName}"); + } + Console.WriteLine(); + return 1; } -} +} \ No newline at end of file diff --git a/examples/Client/StateManagement/BulkStateExample.cs b/examples/Client/StateManagement/BulkStateExample.cs index 250c69ce..da25b157 100644 --- a/examples/Client/StateManagement/BulkStateExample.cs +++ b/examples/Client/StateManagement/BulkStateExample.cs @@ -4,51 +4,50 @@ using System.Threading; using System.Threading.Tasks; using Dapr.Client; -namespace Samples.Client +namespace Samples.Client; + +public class BulkStateExample : Example { - public class BulkStateExample : Example + private static readonly string firstKey = "testKey1"; + private static readonly string secondKey = "testKey2"; + private static readonly string firstEtag = "123"; + private static readonly string secondEtag = "456"; + private static readonly string storeName = "statestore"; + + public override string DisplayName => "Using the State Store"; + + public override async Task RunAsync(CancellationToken cancellationToken) { - private static readonly string firstKey = "testKey1"; - private static readonly string secondKey = "testKey2"; - private static readonly string firstEtag = "123"; - private static readonly string secondEtag = "456"; - private static readonly string storeName = "statestore"; + using var client = new DaprClientBuilder().Build(); - public override string DisplayName => "Using the State Store"; - - public override async Task RunAsync(CancellationToken cancellationToken) - { - using var client = new DaprClientBuilder().Build(); - - var state1 = new Widget() { Size = "small", Color = "yellow", }; - var state2 = new Widget() { Size = "big", Color = "green", }; + var state1 = new Widget() { Size = "small", Color = "yellow", }; + var state2 = new Widget() { Size = "big", Color = "green", }; - var stateItem1 = new SaveStateItem(firstKey, state1, firstEtag); - var stateItem2 = new SaveStateItem(secondKey, state2, secondEtag); + var stateItem1 = new SaveStateItem(firstKey, state1, firstEtag); + var stateItem2 = new SaveStateItem(secondKey, state2, secondEtag); - await client.SaveBulkStateAsync(storeName, new List>() { stateItem1, stateItem2}); + await client.SaveBulkStateAsync(storeName, new List>() { stateItem1, stateItem2}); - Console.WriteLine("Saved 2 States!"); + Console.WriteLine("Saved 2 States!"); - await Task.Delay(2000); + await Task.Delay(2000); - IReadOnlyList states = await client.GetBulkStateAsync(storeName, - new List(){firstKey, secondKey}, null); + IReadOnlyList states = await client.GetBulkStateAsync(storeName, + new List(){firstKey, secondKey}, null); - Console.WriteLine($"Got {states.Count} States: "); + Console.WriteLine($"Got {states.Count} States: "); - var deleteBulkStateItem1 = new BulkDeleteStateItem(states[0].Key, states[0].ETag); - var deleteBulkStateItem2 = new BulkDeleteStateItem(states[1].Key, states[1].ETag); + var deleteBulkStateItem1 = new BulkDeleteStateItem(states[0].Key, states[0].ETag); + var deleteBulkStateItem2 = new BulkDeleteStateItem(states[1].Key, states[1].ETag); - await client.DeleteBulkStateAsync(storeName, new List() { deleteBulkStateItem1, deleteBulkStateItem2 }); + await client.DeleteBulkStateAsync(storeName, new List() { deleteBulkStateItem1, deleteBulkStateItem2 }); - Console.WriteLine("Deleted States!"); - } - - private class Widget - { - public string? Size { get; set; } - public string? Color { get; set; } - } + Console.WriteLine("Deleted States!"); } -} + + private class Widget + { + public string? Size { get; set; } + public string? Color { get; set; } + } +} \ No newline at end of file diff --git a/examples/Client/StateManagement/Example.cs b/examples/Client/StateManagement/Example.cs index d2c07bfe..507bdcfd 100644 --- a/examples/Client/StateManagement/Example.cs +++ b/examples/Client/StateManagement/Example.cs @@ -14,12 +14,11 @@ using System.Threading; using System.Threading.Tasks; -namespace Samples.Client -{ - public abstract class Example - { - public abstract string DisplayName { get; } +namespace Samples.Client; - public abstract Task RunAsync(CancellationToken cancellationToken); - } -} +public abstract class Example +{ + public abstract string DisplayName { get; } + + public abstract Task RunAsync(CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/examples/Client/StateManagement/Program.cs b/examples/Client/StateManagement/Program.cs index e9ef3697..006a0224 100644 --- a/examples/Client/StateManagement/Program.cs +++ b/examples/Client/StateManagement/Program.cs @@ -15,37 +15,36 @@ using System; using System.Threading; using System.Threading.Tasks; -namespace Samples.Client +namespace Samples.Client; + +class Program { - class Program + private static readonly Example[] Examples = new Example[] { - private static readonly Example[] Examples = new Example[] + new StateStoreExample(), + new StateStoreTransactionsExample(), + new StateStoreETagsExample(), + new BulkStateExample(), + new StateStoreBinaryExample() + }; + + static async Task Main(string[] args) + { + if (args.Length > 0 && int.TryParse(args[0], out var index) && index >= 0 && index < Examples.Length) { - new StateStoreExample(), - new StateStoreTransactionsExample(), - new StateStoreETagsExample(), - new BulkStateExample(), - new StateStoreBinaryExample() - }; + var cts = new CancellationTokenSource(); + Console.CancelKeyPress += (object? sender, ConsoleCancelEventArgs e) => cts.Cancel(); - static async Task Main(string[] args) - { - if (args.Length > 0 && int.TryParse(args[0], out var index) && index >= 0 && index < Examples.Length) - { - var cts = new CancellationTokenSource(); - Console.CancelKeyPress += (object? sender, ConsoleCancelEventArgs e) => cts.Cancel(); - - await Examples[index].RunAsync(cts.Token); - return 0; - } - - Console.WriteLine("Hello, please choose a sample to run:"); - for (var i = 0; i < Examples.Length; i++) - { - Console.WriteLine($"{i}: {Examples[i].DisplayName}"); - } - Console.WriteLine(); - return 1; + await Examples[index].RunAsync(cts.Token); + return 0; } + + Console.WriteLine("Hello, please choose a sample to run:"); + for (var i = 0; i < Examples.Length; i++) + { + Console.WriteLine($"{i}: {Examples[i].DisplayName}"); + } + Console.WriteLine(); + return 1; } -} +} \ No newline at end of file diff --git a/examples/Client/StateManagement/StateStoreBinaryExample.cs b/examples/Client/StateManagement/StateStoreBinaryExample.cs index edf23704..d29797c8 100644 --- a/examples/Client/StateManagement/StateStoreBinaryExample.cs +++ b/examples/Client/StateManagement/StateStoreBinaryExample.cs @@ -6,42 +6,41 @@ using System.Threading.Tasks; using System.Threading; using Google.Protobuf; -namespace Samples.Client +namespace Samples.Client; + +public class StateStoreBinaryExample : Example { - 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(); - private static readonly string stateKeyName = "binarydata"; - private static readonly string storeName = "statestore"; + 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!"); - 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()); + 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!"); + 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!"); } -} + + +} \ No newline at end of file diff --git a/examples/Client/StateManagement/StateStoreETagsExample.cs b/examples/Client/StateManagement/StateStoreETagsExample.cs index d8302253..3f4e610a 100644 --- a/examples/Client/StateManagement/StateStoreETagsExample.cs +++ b/examples/Client/StateManagement/StateStoreETagsExample.cs @@ -16,62 +16,61 @@ using System.Threading; using System.Threading.Tasks; using Dapr.Client; -namespace Samples.Client +namespace Samples.Client; + +public class StateStoreETagsExample : Example { - public class StateStoreETagsExample : Example + private static readonly string stateKeyName = "widget"; + private static readonly string storeName = "statestore"; + + public override string DisplayName => "Using the State Store with ETags"; + + public override async Task RunAsync(CancellationToken cancellationToken) { - private static readonly string stateKeyName = "widget"; - private static readonly string storeName = "statestore"; + using var client = new DaprClientBuilder().Build(); - public override string DisplayName => "Using the State Store with ETags"; + // Save state which will create a new etag + await client.SaveStateAsync(storeName, stateKeyName, new Widget() { Size = "small", Color = "yellow", }, cancellationToken: cancellationToken); + Console.WriteLine($"Saved state which created a new entry with an initial etag"); - public override async Task RunAsync(CancellationToken cancellationToken) - { - using var client = new DaprClientBuilder().Build(); + // Read the state and etag + var (original, originalETag) = await client.GetStateAndETagAsync(storeName, stateKeyName, cancellationToken: cancellationToken); + Console.WriteLine($"Retrieved state: {original.Size} {original.Color} with etag: {originalETag}"); - // Save state which will create a new etag - await client.SaveStateAsync(storeName, stateKeyName, new Widget() { Size = "small", Color = "yellow", }, cancellationToken: cancellationToken); - Console.WriteLine($"Saved state which created a new entry with an initial etag"); + // Save state which will update the etag + await client.SaveStateAsync(storeName, stateKeyName, new Widget() { Size = "small", Color = "yellow", }, cancellationToken: cancellationToken); + Console.WriteLine($"Saved state with new etag"); - // Read the state and etag - var (original, originalETag) = await client.GetStateAndETagAsync(storeName, stateKeyName, cancellationToken: cancellationToken); - Console.WriteLine($"Retrieved state: {original.Size} {original.Color} with etag: {originalETag}"); + // Modify the state and try saving it with the old etag. This will fail + original.Color = "purple"; + var isSaveStateSuccess = await client.TrySaveStateAsync(storeName, stateKeyName, original, originalETag, cancellationToken: cancellationToken); + Console.WriteLine($"Saved state with old etag: {originalETag} successfully? {isSaveStateSuccess}"); - // Save state which will update the etag - await client.SaveStateAsync(storeName, stateKeyName, new Widget() { Size = "small", Color = "yellow", }, cancellationToken: cancellationToken); - Console.WriteLine($"Saved state with new etag"); + // Read the updated state and etag + var (updated, updatedETag) = await client.GetStateAndETagAsync(storeName, stateKeyName, cancellationToken: cancellationToken); + Console.WriteLine($"Retrieved state: {updated.Size} {updated.Color} with etag: {updatedETag}"); - // Modify the state and try saving it with the old etag. This will fail - original.Color = "purple"; - var isSaveStateSuccess = await client.TrySaveStateAsync(storeName, stateKeyName, original, originalETag, cancellationToken: cancellationToken); - Console.WriteLine($"Saved state with old etag: {originalETag} successfully? {isSaveStateSuccess}"); + // Modify the state and try saving it with the updated etag. This will succeed + updated.Color = "green"; + isSaveStateSuccess = await client.TrySaveStateAsync(storeName, stateKeyName, updated, updatedETag, cancellationToken: cancellationToken); + Console.WriteLine($"Saved state with latest etag: {updatedETag} successfully? {isSaveStateSuccess}"); - // Read the updated state and etag - var (updated, updatedETag) = await client.GetStateAndETagAsync(storeName, stateKeyName, cancellationToken: cancellationToken); - Console.WriteLine($"Retrieved state: {updated.Size} {updated.Color} with etag: {updatedETag}"); + // Delete with an old etag. This will fail + var isDeleteStateSuccess = await client.TryDeleteStateAsync(storeName, stateKeyName, originalETag, cancellationToken: cancellationToken); + Console.WriteLine($"Deleted state with old etag: {originalETag} successfully? {isDeleteStateSuccess}"); - // Modify the state and try saving it with the updated etag. This will succeed - updated.Color = "green"; - isSaveStateSuccess = await client.TrySaveStateAsync(storeName, stateKeyName, updated, updatedETag, cancellationToken: cancellationToken); - Console.WriteLine($"Saved state with latest etag: {updatedETag} successfully? {isSaveStateSuccess}"); + // Read the updated state and etag + (updated, updatedETag) = await client.GetStateAndETagAsync(storeName, stateKeyName, cancellationToken: cancellationToken); + Console.WriteLine($"Retrieved state: {updated.Size} {updated.Color} with etag: {updatedETag}"); - // Delete with an old etag. This will fail - var isDeleteStateSuccess = await client.TryDeleteStateAsync(storeName, stateKeyName, originalETag, cancellationToken: cancellationToken); - Console.WriteLine($"Deleted state with old etag: {originalETag} successfully? {isDeleteStateSuccess}"); - - // Read the updated state and etag - (updated, updatedETag) = await client.GetStateAndETagAsync(storeName, stateKeyName, cancellationToken: cancellationToken); - Console.WriteLine($"Retrieved state: {updated.Size} {updated.Color} with etag: {updatedETag}"); - - // Delete with updated etag. This will succeed - isDeleteStateSuccess = await client.TryDeleteStateAsync(storeName, stateKeyName, updatedETag, cancellationToken: cancellationToken); - Console.WriteLine($"Deleted state with updated etag: {updatedETag} successfully? {isDeleteStateSuccess}"); - } - - private class Widget - { - public string? Size { get; set; } - public string? Color { get; set; } - } + // Delete with updated etag. This will succeed + isDeleteStateSuccess = await client.TryDeleteStateAsync(storeName, stateKeyName, updatedETag, cancellationToken: cancellationToken); + Console.WriteLine($"Deleted state with updated etag: {updatedETag} successfully? {isDeleteStateSuccess}"); } -} + + private class Widget + { + public string? Size { get; set; } + public string? Color { get; set; } + } +} \ No newline at end of file diff --git a/examples/Client/StateManagement/StateStoreExample.cs b/examples/Client/StateManagement/StateStoreExample.cs index 7dd53b1e..f6eb43f0 100644 --- a/examples/Client/StateManagement/StateStoreExample.cs +++ b/examples/Client/StateManagement/StateStoreExample.cs @@ -16,41 +16,40 @@ using System.Threading; using System.Threading.Tasks; using Dapr.Client; -namespace Samples.Client +namespace Samples.Client; + +public class StateStoreExample : Example { - public class StateStoreExample : Example + private static readonly string stateKeyName = "widget"; + private static readonly string storeName = "statestore"; + + public override string DisplayName => "Using the State Store"; + + public override async Task RunAsync(CancellationToken cancellationToken) { - private static readonly string stateKeyName = "widget"; - private static readonly string storeName = "statestore"; + using var client = new DaprClientBuilder().Build(); - public override string DisplayName => "Using the State Store"; + var state = new Widget() { Size = "small", Color = "yellow", }; + await client.SaveStateAsync(storeName, stateKeyName, state, cancellationToken: cancellationToken); + Console.WriteLine("Saved State!"); - public override async Task RunAsync(CancellationToken cancellationToken) + state = await client.GetStateAsync(storeName, stateKeyName, cancellationToken: cancellationToken); + if (state == null) { - using var client = new DaprClientBuilder().Build(); - - var state = new Widget() { Size = "small", Color = "yellow", }; - await client.SaveStateAsync(storeName, stateKeyName, state, cancellationToken: cancellationToken); - Console.WriteLine("Saved State!"); - - state = await client.GetStateAsync(storeName, stateKeyName, cancellationToken: cancellationToken); - if (state == null) - { - Console.WriteLine("State not found in store"); - } - else - { - Console.WriteLine($"Got State: {state.Size} {state.Color}"); - } - - await client.DeleteStateAsync(storeName, stateKeyName, cancellationToken: cancellationToken); - Console.WriteLine("Deleted State!"); + Console.WriteLine("State not found in store"); + } + else + { + Console.WriteLine($"Got State: {state.Size} {state.Color}"); } - private class Widget - { - public string? Size { get; set; } - public string? Color { get; set; } - } + await client.DeleteStateAsync(storeName, stateKeyName, cancellationToken: cancellationToken); + Console.WriteLine("Deleted State!"); } -} + + private class Widget + { + public string? Size { get; set; } + public string? Color { get; set; } + } +} \ No newline at end of file diff --git a/examples/Client/StateManagement/StateStoreTransactionsExample.cs b/examples/Client/StateManagement/StateStoreTransactionsExample.cs index d3595876..1989daf9 100644 --- a/examples/Client/StateManagement/StateStoreTransactionsExample.cs +++ b/examples/Client/StateManagement/StateStoreTransactionsExample.cs @@ -18,48 +18,47 @@ using System.Threading; using System.Threading.Tasks; using Dapr.Client; -namespace Samples.Client +namespace Samples.Client; + +public class StateStoreTransactionsExample : Example { - public class StateStoreTransactionsExample : Example + private static readonly string storeName = "statestore"; + + public override string DisplayName => "Using the State Store with Transactions"; + + public override async Task RunAsync(CancellationToken cancellationToken) { - private static readonly string storeName = "statestore"; + using var client = new DaprClientBuilder().Build(); - public override string DisplayName => "Using the State Store with Transactions"; + var value = new Widget() { Size = "small", Color = "yellow", }; - public override async Task RunAsync(CancellationToken cancellationToken) + // State transactions operate on raw bytes + var bytes = JsonSerializer.SerializeToUtf8Bytes(value); + + var requests = new List() { - using var client = new DaprClientBuilder().Build(); - - var value = new Widget() { Size = "small", Color = "yellow", }; - - // State transactions operate on raw bytes - var bytes = JsonSerializer.SerializeToUtf8Bytes(value); - - var requests = new List() - { - new StateTransactionRequest("widget", bytes, StateOperationType.Upsert), - new StateTransactionRequest("oldwidget", null, StateOperationType.Delete) - }; + new StateTransactionRequest("widget", bytes, StateOperationType.Upsert), + new StateTransactionRequest("oldwidget", null, StateOperationType.Delete) + }; - Console.WriteLine("Executing transaction - save state and delete state"); - await client.ExecuteStateTransactionAsync(storeName, requests, cancellationToken: cancellationToken); - Console.WriteLine("Executed State Transaction!"); + Console.WriteLine("Executing transaction - save state and delete state"); + await client.ExecuteStateTransactionAsync(storeName, requests, cancellationToken: cancellationToken); + Console.WriteLine("Executed State Transaction!"); - var state = await client.GetStateAsync(storeName, "widget", cancellationToken: cancellationToken); - if (state == null) - { - Console.WriteLine("State not found in store"); - } - else - { - Console.WriteLine($"Got State: {state.Size} {state.Color}"); - } - } - - private class Widget + var state = await client.GetStateAsync(storeName, "widget", cancellationToken: cancellationToken); + if (state == null) { - public string? Size { get; set; } - public string? Color { get; set; } + Console.WriteLine("State not found in store"); + } + else + { + Console.WriteLine($"Got State: {state.Size} {state.Color}"); } } -} + + private class Widget + { + public string? Size { get; set; } + public string? Color { get; set; } + } +} \ No newline at end of file diff --git a/examples/GeneratedActor/ActorClient/IGenericClientActor.cs b/examples/GeneratedActor/ActorClient/IGenericClientActor.cs index 166f4a9e..33f500f8 100644 --- a/examples/GeneratedActor/ActorClient/IGenericClientActor.cs +++ b/examples/GeneratedActor/ActorClient/IGenericClientActor.cs @@ -13,15 +13,14 @@ using Dapr.Actors.Generators; -namespace GeneratedActor -{ - [GenerateActorClient] - internal interface IGenericClientActor - { - [ActorMethod(Name = "GetState")] - Task GetStateAsync(CancellationToken cancellationToken = default); +namespace GeneratedActor; - [ActorMethod(Name = "SetState")] - Task SetStateAsync(TGenericType2 state, CancellationToken cancellationToken = default); - } -} +[GenerateActorClient] +internal interface IGenericClientActor +{ + [ActorMethod(Name = "GetState")] + Task GetStateAsync(CancellationToken cancellationToken = default); + + [ActorMethod(Name = "SetState")] + Task SetStateAsync(TGenericType2 state, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/examples/Workflow/WorkflowConsoleApp/Activities/NotifyActivity.cs b/examples/Workflow/WorkflowConsoleApp/Activities/NotifyActivity.cs index 7c4616bd..c6a5a418 100644 --- a/examples/Workflow/WorkflowConsoleApp/Activities/NotifyActivity.cs +++ b/examples/Workflow/WorkflowConsoleApp/Activities/NotifyActivity.cs @@ -1,24 +1,23 @@ using Dapr.Workflow; using Microsoft.Extensions.Logging; -namespace WorkflowConsoleApp.Activities +namespace WorkflowConsoleApp.Activities; + +public record Notification(string Message); + +public class NotifyActivity : WorkflowActivity { - public record Notification(string Message); + readonly ILogger logger; - public class NotifyActivity : WorkflowActivity + public NotifyActivity(ILoggerFactory loggerFactory) { - readonly ILogger logger; - - public NotifyActivity(ILoggerFactory loggerFactory) - { - this.logger = loggerFactory.CreateLogger(); - } - - public override Task RunAsync(WorkflowActivityContext context, Notification notification) - { - this.logger.LogInformation(notification.Message); - - return Task.FromResult(null); - } + this.logger = loggerFactory.CreateLogger(); } -} + + public override Task RunAsync(WorkflowActivityContext context, Notification notification) + { + this.logger.LogInformation(notification.Message); + + return Task.FromResult(null); + } +} \ No newline at end of file diff --git a/examples/Workflow/WorkflowConsoleApp/Activities/ProcessPaymentActivity.cs b/examples/Workflow/WorkflowConsoleApp/Activities/ProcessPaymentActivity.cs index 4ca39f0a..7138797d 100644 --- a/examples/Workflow/WorkflowConsoleApp/Activities/ProcessPaymentActivity.cs +++ b/examples/Workflow/WorkflowConsoleApp/Activities/ProcessPaymentActivity.cs @@ -1,34 +1,33 @@ using Dapr.Workflow; using Microsoft.Extensions.Logging; -namespace WorkflowConsoleApp.Activities +namespace WorkflowConsoleApp.Activities; + +public class ProcessPaymentActivity : WorkflowActivity { - public class ProcessPaymentActivity : WorkflowActivity + readonly ILogger logger; + + public ProcessPaymentActivity(ILoggerFactory loggerFactory) { - readonly ILogger logger; - - public ProcessPaymentActivity(ILoggerFactory loggerFactory) - { - this.logger = loggerFactory.CreateLogger(); - } - - public override async Task RunAsync(WorkflowActivityContext context, PaymentRequest req) - { - this.logger.LogInformation( - "Processing payment: {requestId} for {amount} {item} at ${currency}", - req.RequestId, - req.Amount, - req.ItemName, - req.Currency); - - // Simulate slow processing - await Task.Delay(TimeSpan.FromSeconds(7)); - - this.logger.LogInformation( - "Payment for request ID '{requestId}' processed successfully", - req.RequestId); - - return null; - } + this.logger = loggerFactory.CreateLogger(); } -} + + public override async Task RunAsync(WorkflowActivityContext context, PaymentRequest req) + { + this.logger.LogInformation( + "Processing payment: {requestId} for {amount} {item} at ${currency}", + req.RequestId, + req.Amount, + req.ItemName, + req.Currency); + + // Simulate slow processing + await Task.Delay(TimeSpan.FromSeconds(7)); + + this.logger.LogInformation( + "Payment for request ID '{requestId}' processed successfully", + req.RequestId); + + return null; + } +} \ No newline at end of file diff --git a/examples/Workflow/WorkflowConsoleApp/Activities/RequestApprovalActivity.cs b/examples/Workflow/WorkflowConsoleApp/Activities/RequestApprovalActivity.cs index d40078fc..44e153c3 100644 --- a/examples/Workflow/WorkflowConsoleApp/Activities/RequestApprovalActivity.cs +++ b/examples/Workflow/WorkflowConsoleApp/Activities/RequestApprovalActivity.cs @@ -1,23 +1,22 @@ using Dapr.Workflow; using Microsoft.Extensions.Logging; -namespace WorkflowConsoleApp.Activities +namespace WorkflowConsoleApp.Activities; + +public class RequestApprovalActivity : WorkflowActivity { - public class RequestApprovalActivity : WorkflowActivity + readonly ILogger logger; + + public RequestApprovalActivity(ILoggerFactory loggerFactory) { - readonly ILogger logger; - - public RequestApprovalActivity(ILoggerFactory loggerFactory) - { - this.logger = loggerFactory.CreateLogger(); - } - - public override Task RunAsync(WorkflowActivityContext context, OrderPayload input) - { - string orderId = context.InstanceId.ToString(); - this.logger.LogInformation("Requesting approval for order {orderId}", orderId); - - return Task.FromResult(null); - } + this.logger = loggerFactory.CreateLogger(); } -} + + public override Task RunAsync(WorkflowActivityContext context, OrderPayload input) + { + string orderId = context.InstanceId.ToString(); + this.logger.LogInformation("Requesting approval for order {orderId}", orderId); + + return Task.FromResult(null); + } +} \ No newline at end of file diff --git a/examples/Workflow/WorkflowConsoleApp/Activities/ReserveInventoryActivity.cs b/examples/Workflow/WorkflowConsoleApp/Activities/ReserveInventoryActivity.cs index cdae1c6e..55dc1aaa 100644 --- a/examples/Workflow/WorkflowConsoleApp/Activities/ReserveInventoryActivity.cs +++ b/examples/Workflow/WorkflowConsoleApp/Activities/ReserveInventoryActivity.cs @@ -2,57 +2,56 @@ using Dapr.Workflow; using Microsoft.Extensions.Logging; -namespace WorkflowConsoleApp.Activities +namespace WorkflowConsoleApp.Activities; + +public class ReserveInventoryActivity : WorkflowActivity { - public class ReserveInventoryActivity : WorkflowActivity + readonly ILogger logger; + readonly DaprClient client; + static readonly string storeName = "statestore"; + + public ReserveInventoryActivity(ILoggerFactory loggerFactory, DaprClient client) { - readonly ILogger logger; - readonly DaprClient client; - static readonly string storeName = "statestore"; + this.logger = loggerFactory.CreateLogger(); + this.client = client; + } - public ReserveInventoryActivity(ILoggerFactory loggerFactory, DaprClient client) + public override async Task RunAsync(WorkflowActivityContext context, InventoryRequest req) + { + this.logger.LogInformation( + "Reserving inventory for order '{requestId}' of {quantity} {name}", + req.RequestId, + req.Quantity, + req.ItemName); + + // Ensure that the store has items + InventoryItem item = await this.client.GetStateAsync( + storeName, + req.ItemName.ToLowerInvariant()); + + // Catch for the case where the statestore isn't setup + if (item == null) { - this.logger = loggerFactory.CreateLogger(); - this.client = client; - } - - public override async Task RunAsync(WorkflowActivityContext context, InventoryRequest req) - { - this.logger.LogInformation( - "Reserving inventory for order '{requestId}' of {quantity} {name}", - req.RequestId, - req.Quantity, - req.ItemName); - - // Ensure that the store has items - InventoryItem item = await this.client.GetStateAsync( - storeName, - req.ItemName.ToLowerInvariant()); - - // Catch for the case where the statestore isn't setup - if (item == null) - { - // Not enough items. - return new InventoryResult(false, item); - } - - this.logger.LogInformation( - "There are {quantity} {name} available for purchase", - item.Quantity, - item.Name); - - // See if there're enough items to purchase - if (item.Quantity >= req.Quantity) - { - // Simulate slow processing - await Task.Delay(TimeSpan.FromSeconds(2)); - - return new InventoryResult(true, item); - } - // Not enough items. return new InventoryResult(false, item); - } + + this.logger.LogInformation( + "There are {quantity} {name} available for purchase", + item.Quantity, + item.Name); + + // See if there're enough items to purchase + if (item.Quantity >= req.Quantity) + { + // Simulate slow processing + await Task.Delay(TimeSpan.FromSeconds(2)); + + return new InventoryResult(true, item); + } + + // Not enough items. + return new InventoryResult(false, item); + } -} +} \ No newline at end of file diff --git a/examples/Workflow/WorkflowConsoleApp/Activities/UpdateInventoryActivity.cs b/examples/Workflow/WorkflowConsoleApp/Activities/UpdateInventoryActivity.cs index c035aadd..8e278010 100644 --- a/examples/Workflow/WorkflowConsoleApp/Activities/UpdateInventoryActivity.cs +++ b/examples/Workflow/WorkflowConsoleApp/Activities/UpdateInventoryActivity.cs @@ -2,56 +2,55 @@ using Dapr.Workflow; using Microsoft.Extensions.Logging; -namespace WorkflowConsoleApp.Activities +namespace WorkflowConsoleApp.Activities; + +class UpdateInventoryActivity : WorkflowActivity { - class UpdateInventoryActivity : WorkflowActivity + static readonly string storeName = "statestore"; + readonly ILogger logger; + readonly DaprClient client; + + public UpdateInventoryActivity(ILoggerFactory loggerFactory, DaprClient client) { - static readonly string storeName = "statestore"; - readonly ILogger logger; - readonly DaprClient client; - - public UpdateInventoryActivity(ILoggerFactory loggerFactory, DaprClient client) - { - this.logger = loggerFactory.CreateLogger(); - this.client = client; - } - - public override async Task RunAsync(WorkflowActivityContext context, PaymentRequest req) - { - this.logger.LogInformation( - "Checking inventory for order '{requestId}' for {amount} {name}", - req.RequestId, - req.Amount, - req.ItemName); - - // Simulate slow processing - await Task.Delay(TimeSpan.FromSeconds(5)); - - // Determine if there are enough Items for purchase - InventoryItem item = await client.GetStateAsync( - storeName, - req.ItemName.ToLowerInvariant()); - int newQuantity = item.Quantity - req.Amount; - if (newQuantity < 0) - { - this.logger.LogInformation( - "Payment for request ID '{requestId}' could not be processed. Insufficient inventory.", - req.RequestId); - throw new InvalidOperationException($"Not enough '{req.ItemName}' inventory! Requested {req.Amount} but only {item.Quantity} available."); - } - - // Update the statestore with the new amount of the item - await client.SaveStateAsync( - storeName, - req.ItemName.ToLowerInvariant(), - new InventoryItem(Name: req.ItemName, PerItemCost: item.PerItemCost, Quantity: newQuantity)); - - this.logger.LogInformation( - "There are now {quantity} {name} left in stock", - newQuantity, - item.Name); - - return null; - } + this.logger = loggerFactory.CreateLogger(); + this.client = client; } -} + + public override async Task RunAsync(WorkflowActivityContext context, PaymentRequest req) + { + this.logger.LogInformation( + "Checking inventory for order '{requestId}' for {amount} {name}", + req.RequestId, + req.Amount, + req.ItemName); + + // Simulate slow processing + await Task.Delay(TimeSpan.FromSeconds(5)); + + // Determine if there are enough Items for purchase + InventoryItem item = await client.GetStateAsync( + storeName, + req.ItemName.ToLowerInvariant()); + int newQuantity = item.Quantity - req.Amount; + if (newQuantity < 0) + { + this.logger.LogInformation( + "Payment for request ID '{requestId}' could not be processed. Insufficient inventory.", + req.RequestId); + throw new InvalidOperationException($"Not enough '{req.ItemName}' inventory! Requested {req.Amount} but only {item.Quantity} available."); + } + + // Update the statestore with the new amount of the item + await client.SaveStateAsync( + storeName, + req.ItemName.ToLowerInvariant(), + new InventoryItem(Name: req.ItemName, PerItemCost: item.PerItemCost, Quantity: newQuantity)); + + this.logger.LogInformation( + "There are now {quantity} {name} left in stock", + newQuantity, + item.Name); + + return null; + } +} \ No newline at end of file diff --git a/examples/Workflow/WorkflowConsoleApp/Models.cs b/examples/Workflow/WorkflowConsoleApp/Models.cs index 7892c752..dfa132dd 100644 --- a/examples/Workflow/WorkflowConsoleApp/Models.cs +++ b/examples/Workflow/WorkflowConsoleApp/Models.cs @@ -1,15 +1,14 @@ -namespace WorkflowConsoleApp +namespace WorkflowConsoleApp; + +public record OrderPayload(string Name, double TotalCost, int Quantity = 1); +public record InventoryRequest(string RequestId, string ItemName, int Quantity); +public record InventoryResult(bool Success, InventoryItem orderPayload); +public record PaymentRequest(string RequestId, string ItemName, int Amount, double Currency); +public record OrderResult(bool Processed); +public record InventoryItem(string Name, double PerItemCost, int Quantity); +public enum ApprovalResult { - public record OrderPayload(string Name, double TotalCost, int Quantity = 1); - public record InventoryRequest(string RequestId, string ItemName, int Quantity); - public record InventoryResult(bool Success, InventoryItem orderPayload); - public record PaymentRequest(string RequestId, string ItemName, int Amount, double Currency); - public record OrderResult(bool Processed); - public record InventoryItem(string Name, double PerItemCost, int Quantity); - public enum ApprovalResult - { - Unspecified = 0, - Approved = 1, - Rejected = 2, - } -} + Unspecified = 0, + Approved = 1, + Rejected = 2, +} \ No newline at end of file diff --git a/examples/Workflow/WorkflowConsoleApp/Workflows/OrderProcessingWorkflow.cs b/examples/Workflow/WorkflowConsoleApp/Workflows/OrderProcessingWorkflow.cs index 8c5e9d13..988c9693 100644 --- a/examples/Workflow/WorkflowConsoleApp/Workflows/OrderProcessingWorkflow.cs +++ b/examples/Workflow/WorkflowConsoleApp/Workflows/OrderProcessingWorkflow.cs @@ -2,122 +2,121 @@ using Microsoft.Extensions.Logging; using WorkflowConsoleApp.Activities; -namespace WorkflowConsoleApp.Workflows +namespace WorkflowConsoleApp.Workflows; + +public class OrderProcessingWorkflow : Workflow { - public class OrderProcessingWorkflow : Workflow + readonly WorkflowTaskOptions defaultActivityRetryOptions = new WorkflowTaskOptions { - readonly WorkflowTaskOptions defaultActivityRetryOptions = new WorkflowTaskOptions - { - // NOTE: Beware that changing the number of retries is a breaking change for existing workflows. - RetryPolicy = new WorkflowRetryPolicy( - maxNumberOfAttempts: 3, - firstRetryInterval: TimeSpan.FromSeconds(5)), - }; + // NOTE: Beware that changing the number of retries is a breaking change for existing workflows. + RetryPolicy = new WorkflowRetryPolicy( + maxNumberOfAttempts: 3, + firstRetryInterval: TimeSpan.FromSeconds(5)), + }; - public override async Task RunAsync(WorkflowContext context, OrderPayload order) - { - string orderId = context.InstanceId; - var logger = context.CreateReplaySafeLogger(); + 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); + 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 + // Notify the user that an order has come through + await context.CallActivityAsync( + nameof(NotifyActivity), + new Notification($"Received order {orderId} for {order.Quantity} {order.Name} at ${order.TotalCost}")); + + // Determine if there is enough of the item available for purchase by checking the inventory + InventoryResult result = await context.CallActivityAsync( + nameof(ReserveInventoryActivity), + new InventoryRequest(RequestId: orderId, order.Name, order.Quantity), + this.defaultActivityRetryOptions); + + // 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), - new Notification($"Received order {orderId} for {order.Quantity} {order.Name} at ${order.TotalCost}")); + new Notification($"Insufficient inventory for {order.Name}")); + return new OrderResult(Processed: false); + } - // Determine if there is enough of the item available for purchase by checking the inventory - InventoryResult result = await context.CallActivityAsync( - nameof(ReserveInventoryActivity), - new InventoryRequest(RequestId: orderId, order.Name, order.Quantity), - this.defaultActivityRetryOptions); - - // 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), - new Notification($"Insufficient inventory for {order.Name}")); - return new OrderResult(Processed: false); - } - - // Require orders over a certain threshold to be approved - 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); - - try - { - // Pause and wait for a manager to approve the order - context.SetCustomStatus("Waiting for approval"); - 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), - new Notification($"Order was rejected by approver")); - return new OrderResult(Processed: false); - } - } - 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), - new Notification($"Cancelling order because it didn't receive an approval")); - return new OrderResult(Processed: false); - } - } - - // 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), - this.defaultActivityRetryOptions); + // Require orders over a certain threshold to be approved + 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); try { - // There is enough inventory available so the user can purchase the item(s). Process their payment - await context.CallActivityAsync( - nameof(UpdateInventoryActivity), - new PaymentRequest(RequestId: orderId, order.Name, order.Quantity, order.TotalCost), - this.defaultActivityRetryOptions); + // Pause and wait for a manager to approve the order + context.SetCustomStatus("Waiting for approval"); + 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), + new Notification($"Order was rejected by approver")); + return new OrderResult(Processed: false); + } } - catch (WorkflowTaskFailedException e) + catch (TaskCanceledException) { - // Let them know their payment processing failed - logger.LogError("Order {orderId} failed! Details: {errorMessage}", orderId, e.FailureDetails.ErrorMessage); + logger.LogError("Cancelling order because it didn't receive an approval"); + + // An approval timeout results in automatic order cancellation await context.CallActivityAsync( nameof(NotifyActivity), - new Notification($"Order {orderId} Failed! Details: {e.FailureDetails.ErrorMessage}")); + new Notification($"Cancelling order because it didn't receive an approval")); return new OrderResult(Processed: false); } + } - // Let them know their payment was processed - logger.LogError("Order {orderId} has completed!", orderId); + // 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), + this.defaultActivityRetryOptions); + + try + { + // There is enough inventory available so the user can purchase the item(s). Process their payment + await context.CallActivityAsync( + nameof(UpdateInventoryActivity), + new PaymentRequest(RequestId: orderId, order.Name, order.Quantity, order.TotalCost), + this.defaultActivityRetryOptions); + } + 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} has completed!")); - - // End the workflow with a success result - return new OrderResult(Processed: true); + new Notification($"Order {orderId} Failed! Details: {e.FailureDetails.ErrorMessage}")); + return new OrderResult(Processed: false); } + + // 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!")); + + // End the workflow with a success result + return new OrderResult(Processed: true); } -} +} \ No newline at end of file diff --git a/examples/Workflow/WorkflowUnitTest/OrderProcessingTests.cs b/examples/Workflow/WorkflowUnitTest/OrderProcessingTests.cs index 3093e5c7..de511ba7 100644 --- a/examples/Workflow/WorkflowUnitTest/OrderProcessingTests.cs +++ b/examples/Workflow/WorkflowUnitTest/OrderProcessingTests.cs @@ -6,80 +6,79 @@ using WorkflowConsoleApp.Activities; using WorkflowConsoleApp.Workflows; using Xunit; -namespace WorkflowUnitTest +namespace WorkflowUnitTest; + +[Trait("Example", "true")] +public class OrderProcessingTests { - [Trait("Example", "true")] - public class OrderProcessingTests + [Fact] + public async Task TestSuccessfulOrder() { - [Fact] - public async Task TestSuccessfulOrder() - { - // Test payloads - OrderPayload order = new(Name: "Paperclips", TotalCost: 99.95, Quantity: 10); - PaymentRequest expectedPaymentRequest = new(It.IsAny(), order.Name, order.Quantity, order.TotalCost); - InventoryRequest expectedInventoryRequest = new(It.IsAny(), order.Name, order.Quantity); - InventoryResult inventoryResult = new(Success: true, new InventoryItem(order.Name, 9.99, order.Quantity)); + // Test payloads + OrderPayload order = new(Name: "Paperclips", TotalCost: 99.95, Quantity: 10); + PaymentRequest expectedPaymentRequest = new(It.IsAny(), order.Name, order.Quantity, order.TotalCost); + InventoryRequest expectedInventoryRequest = new(It.IsAny(), order.Name, order.Quantity); + InventoryResult inventoryResult = new(Success: true, new InventoryItem(order.Name, 9.99, order.Quantity)); - // Mock the call to ReserveInventoryActivity - Mock mockContext = new(); - mockContext - .Setup(ctx => ctx.CallActivityAsync(nameof(ReserveInventoryActivity), It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(inventoryResult)); + // Mock the call to ReserveInventoryActivity + Mock mockContext = new(); + mockContext + .Setup(ctx => ctx.CallActivityAsync(nameof(ReserveInventoryActivity), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(inventoryResult)); - // Run the workflow directly - OrderResult result = await new OrderProcessingWorkflow().RunAsync(mockContext.Object, order); + // Run the workflow directly + OrderResult result = await new OrderProcessingWorkflow().RunAsync(mockContext.Object, order); - // Verify that workflow result matches what we expect - Assert.NotNull(result); - Assert.True(result.Processed); + // Verify that workflow result matches what we expect + Assert.NotNull(result); + Assert.True(result.Processed); - // Verify that ReserveInventoryActivity was called with a specific input - mockContext.Verify( - ctx => ctx.CallActivityAsync(nameof(ReserveInventoryActivity), expectedInventoryRequest, It.IsAny()), - Times.Once()); + // Verify that ReserveInventoryActivity was called with a specific input + mockContext.Verify( + ctx => ctx.CallActivityAsync(nameof(ReserveInventoryActivity), expectedInventoryRequest, It.IsAny()), + Times.Once()); - // Verify that ProcessPaymentActivity was called with a specific input - mockContext.Verify( - ctx => ctx.CallActivityAsync(nameof(ProcessPaymentActivity), expectedPaymentRequest, It.IsAny()), - Times.Once()); + // Verify that ProcessPaymentActivity was called with a specific input + mockContext.Verify( + ctx => ctx.CallActivityAsync(nameof(ProcessPaymentActivity), expectedPaymentRequest, It.IsAny()), + Times.Once()); - // Verify that there were two calls to NotifyActivity - mockContext.Verify( - ctx => ctx.CallActivityAsync(nameof(NotifyActivity), It.IsAny(), It.IsAny()), - Times.Exactly(2)); - } - - [Fact] - public async Task TestInsufficientInventory() - { - // Test payloads - OrderPayload order = new(Name: "Paperclips", TotalCost: 99.95, Quantity: int.MaxValue); - InventoryRequest expectedInventoryRequest = new(It.IsAny(), order.Name, order.Quantity); - InventoryResult inventoryResult = new(Success: false, null); - - // Mock the call to ReserveInventoryActivity - Mock mockContext = new(); - mockContext - .Setup(ctx => ctx.CallActivityAsync(nameof(ReserveInventoryActivity), It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(inventoryResult)); - - // Run the workflow directly - await new OrderProcessingWorkflow().RunAsync(mockContext.Object, order); - - // Verify that ReserveInventoryActivity was called with a specific input - mockContext.Verify( - ctx => ctx.CallActivityAsync(nameof(ReserveInventoryActivity), expectedInventoryRequest, It.IsAny()), - Times.Once()); - - // Verify that ProcessPaymentActivity was never called - mockContext.Verify( - ctx => ctx.CallActivityAsync(nameof(ProcessPaymentActivity), It.IsAny(), It.IsAny()), - Times.Never()); - - // Verify that there were two calls to NotifyActivity - mockContext.Verify( - ctx => ctx.CallActivityAsync(nameof(NotifyActivity), It.IsAny(), It.IsAny()), - Times.Exactly(2)); - } + // Verify that there were two calls to NotifyActivity + mockContext.Verify( + ctx => ctx.CallActivityAsync(nameof(NotifyActivity), It.IsAny(), It.IsAny()), + Times.Exactly(2)); } -} + + [Fact] + public async Task TestInsufficientInventory() + { + // Test payloads + OrderPayload order = new(Name: "Paperclips", TotalCost: 99.95, Quantity: int.MaxValue); + InventoryRequest expectedInventoryRequest = new(It.IsAny(), order.Name, order.Quantity); + InventoryResult inventoryResult = new(Success: false, null); + + // Mock the call to ReserveInventoryActivity + Mock mockContext = new(); + mockContext + .Setup(ctx => ctx.CallActivityAsync(nameof(ReserveInventoryActivity), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(inventoryResult)); + + // Run the workflow directly + await new OrderProcessingWorkflow().RunAsync(mockContext.Object, order); + + // Verify that ReserveInventoryActivity was called with a specific input + mockContext.Verify( + ctx => ctx.CallActivityAsync(nameof(ReserveInventoryActivity), expectedInventoryRequest, It.IsAny()), + Times.Once()); + + // Verify that ProcessPaymentActivity was never called + mockContext.Verify( + ctx => ctx.CallActivityAsync(nameof(ProcessPaymentActivity), It.IsAny(), It.IsAny()), + Times.Never()); + + // Verify that there were two calls to NotifyActivity + mockContext.Verify( + ctx => ctx.CallActivityAsync(nameof(NotifyActivity), It.IsAny(), It.IsAny()), + Times.Exactly(2)); + } +} \ No newline at end of file diff --git a/src/Dapr.AI/Conversation/DaprConversationClientBuilder.cs b/src/Dapr.AI/Conversation/DaprConversationClientBuilder.cs index 88cf3b2a..ae897fcd 100644 --- a/src/Dapr.AI/Conversation/DaprConversationClientBuilder.cs +++ b/src/Dapr.AI/Conversation/DaprConversationClientBuilder.cs @@ -20,16 +20,9 @@ namespace Dapr.AI.Conversation; /// /// Used to create a new instance of a . /// -public sealed class DaprConversationClientBuilder : DaprGenericClientBuilder +/// An optional to configure the client with. +public sealed class DaprConversationClientBuilder(IConfiguration? configuration = null) : DaprGenericClientBuilder(configuration) { - /// - /// Used to initialize a new instance of the . - /// - /// An optional to configure the client with. - public DaprConversationClientBuilder(IConfiguration? configuration = null) : base(configuration) - { - } - /// /// Builds the client instance from the properties of the builder. /// diff --git a/src/Dapr.Actors.AspNetCore/ActorsEndpointRouteBuilderExtensions.cs b/src/Dapr.Actors.AspNetCore/ActorsEndpointRouteBuilderExtensions.cs index 574a172a..132ca548 100644 --- a/src/Dapr.Actors.AspNetCore/ActorsEndpointRouteBuilderExtensions.cs +++ b/src/Dapr.Actors.AspNetCore/ActorsEndpointRouteBuilderExtensions.cs @@ -21,170 +21,170 @@ using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using System.Diagnostics; -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.Builder; + +/// +/// Contains extension methods for using Dapr Actors with endpoint routing. +/// +public static class ActorsEndpointRouteBuilderExtensions { /// - /// Contains extension methods for using Dapr Actors with endpoint routing. + /// Maps endpoints for Dapr Actors into the . /// - public static class ActorsEndpointRouteBuilderExtensions + /// The . + /// + /// An that can be used to configure the endpoints. + /// + public static IEndpointConventionBuilder MapActorsHandlers(this IEndpointRouteBuilder endpoints) { - /// - /// Maps endpoints for Dapr Actors into the . - /// - /// The . - /// - /// An that can be used to configure the endpoints. - /// - public static IEndpointConventionBuilder MapActorsHandlers(this IEndpointRouteBuilder endpoints) + if (endpoints.ServiceProvider.GetService() is null) { - if (endpoints.ServiceProvider.GetService() is null) + throw new InvalidOperationException( + "The ActorRuntime service is not registered with the dependency injection container. " + + "Call AddActors() inside ConfigureServices() to register the actor runtime and actor types."); + } + + var builders = new[] + { + MapDaprConfigEndpoint(endpoints), + MapActorDeactivationEndpoint(endpoints), + MapActorMethodEndpoint(endpoints), + MapReminderEndpoint(endpoints), + MapTimerEndpoint(endpoints), + MapActorHealthChecks(endpoints) + }; + + return new CompositeEndpointConventionBuilder(builders); + } + + private static IEndpointConventionBuilder MapDaprConfigEndpoint(this IEndpointRouteBuilder endpoints) + { + var runtime = endpoints.ServiceProvider.GetRequiredService(); + return endpoints.MapGet("dapr/config", async context => + { + context.Response.ContentType = "application/json"; + await runtime.SerializeSettingsAndRegisteredTypes(context.Response.BodyWriter); + await context.Response.BodyWriter.FlushAsync(); + }).WithDisplayName(b => "Dapr Actors Config"); + } + + private static IEndpointConventionBuilder MapActorDeactivationEndpoint(this IEndpointRouteBuilder endpoints) + { + var runtime = endpoints.ServiceProvider.GetRequiredService(); + return endpoints.MapDelete("actors/{actorTypeName}/{actorId}", async context => + { + var routeValues = context.Request.RouteValues; + var actorTypeName = (string)routeValues["actorTypeName"]; + var actorId = (string)routeValues["actorId"]; + await runtime.DeactivateAsync(actorTypeName, actorId); + }).WithDisplayName(b => "Dapr Actors Deactivation"); + } + + private static IEndpointConventionBuilder MapActorMethodEndpoint(this IEndpointRouteBuilder endpoints) + { + var runtime = endpoints.ServiceProvider.GetRequiredService(); + return endpoints.MapPut("actors/{actorTypeName}/{actorId}/method/{methodName}", async context => + { + var routeValues = context.Request.RouteValues; + var actorTypeName = (string)routeValues["actorTypeName"]; + var actorId = (string)routeValues["actorId"]; + var methodName = (string)routeValues["methodName"]; + + if (context.Request.Headers.ContainsKey(Constants.ReentrancyRequestHeaderName)) { - throw new InvalidOperationException( - "The ActorRuntime service is not registered with the dependency injection container. " + - "Call AddActors() inside ConfigureServices() to register the actor runtime and actor types."); + var daprReentrancyHeader = context.Request.Headers[Constants.ReentrancyRequestHeaderName]; + ActorReentrancyContextAccessor.ReentrancyContext = daprReentrancyHeader; } - var builders = new[] + // If Header is present, call is made using Remoting, use Remoting dispatcher. + if (context.Request.Headers.ContainsKey(Constants.RequestHeaderName)) { - MapDaprConfigEndpoint(endpoints), - MapActorDeactivationEndpoint(endpoints), - MapActorMethodEndpoint(endpoints), - MapReminderEndpoint(endpoints), - MapTimerEndpoint(endpoints), - MapActorHealthChecks(endpoints) - }; + var daprActorheader = context.Request.Headers[Constants.RequestHeaderName]; - return new CompositeEndpointConventionBuilder(builders); - } - - private static IEndpointConventionBuilder MapDaprConfigEndpoint(this IEndpointRouteBuilder endpoints) - { - var runtime = endpoints.ServiceProvider.GetRequiredService(); - return endpoints.MapGet("dapr/config", async context => - { - context.Response.ContentType = "application/json"; - await runtime.SerializeSettingsAndRegisteredTypes(context.Response.BodyWriter); - await context.Response.BodyWriter.FlushAsync(); - }).WithDisplayName(b => "Dapr Actors Config"); - } - - private static IEndpointConventionBuilder MapActorDeactivationEndpoint(this IEndpointRouteBuilder endpoints) - { - var runtime = endpoints.ServiceProvider.GetRequiredService(); - return endpoints.MapDelete("actors/{actorTypeName}/{actorId}", async context => - { - var routeValues = context.Request.RouteValues; - var actorTypeName = (string)routeValues["actorTypeName"]; - var actorId = (string)routeValues["actorId"]; - await runtime.DeactivateAsync(actorTypeName, actorId); - }).WithDisplayName(b => "Dapr Actors Deactivation"); - } - - private static IEndpointConventionBuilder MapActorMethodEndpoint(this IEndpointRouteBuilder endpoints) - { - var runtime = endpoints.ServiceProvider.GetRequiredService(); - return endpoints.MapPut("actors/{actorTypeName}/{actorId}/method/{methodName}", async context => - { - var routeValues = context.Request.RouteValues; - var actorTypeName = (string)routeValues["actorTypeName"]; - var actorId = (string)routeValues["actorId"]; - var methodName = (string)routeValues["methodName"]; - - if (context.Request.Headers.ContainsKey(Constants.ReentrancyRequestHeaderName)) + try { - var daprReentrancyHeader = context.Request.Headers[Constants.ReentrancyRequestHeaderName]; - ActorReentrancyContextAccessor.ReentrancyContext = daprReentrancyHeader; - } + var (header, body) = await runtime.DispatchWithRemotingAsync(actorTypeName, actorId, methodName, daprActorheader, context.Request.Body, context.RequestAborted); - // If Header is present, call is made using Remoting, use Remoting dispatcher. - if (context.Request.Headers.ContainsKey(Constants.RequestHeaderName)) + // Item 1 is header , Item 2 is body + if (header != string.Empty) + { + // exception case + context.Response.Headers[Constants.ErrorResponseHeaderName] = header; // add error header + } + + await context.Response.Body.WriteAsync(body, 0, body.Length, context.RequestAborted); // add response message body + } + catch (Exception ex) { - var daprActorheader = context.Request.Headers[Constants.RequestHeaderName]; + var (header, body) = CreateExceptionResponseMessage(ex); - try - { - var (header, body) = await runtime.DispatchWithRemotingAsync(actorTypeName, actorId, methodName, daprActorheader, context.Request.Body, context.RequestAborted); - - // Item 1 is header , Item 2 is body - if (header != string.Empty) - { - // exception case - context.Response.Headers[Constants.ErrorResponseHeaderName] = header; // add error header - } - - await context.Response.Body.WriteAsync(body, 0, body.Length, context.RequestAborted); // add response message body - } - catch (Exception ex) - { - var (header, body) = CreateExceptionResponseMessage(ex); - - context.Response.Headers[Constants.ErrorResponseHeaderName] = header; - await context.Response.Body.WriteAsync(body, 0, body.Length, context.RequestAborted); - } - finally - { - ActorReentrancyContextAccessor.ReentrancyContext = null; - } + context.Response.Headers[Constants.ErrorResponseHeaderName] = header; + await context.Response.Body.WriteAsync(body, 0, body.Length, context.RequestAborted); } - else + finally { - try - { - await runtime.DispatchWithoutRemotingAsync(actorTypeName, actorId, methodName, context.Request.Body, context.Response.Body, context.RequestAborted); - } - finally - { - ActorReentrancyContextAccessor.ReentrancyContext = null; - } + ActorReentrancyContextAccessor.ReentrancyContext = null; } - }).WithDisplayName(b => "Dapr Actors Invoke"); - } - - private static IEndpointConventionBuilder MapReminderEndpoint(this IEndpointRouteBuilder endpoints) - { - var runtime = endpoints.ServiceProvider.GetRequiredService(); - return endpoints.MapPut("actors/{actorTypeName}/{actorId}/method/remind/{reminderName}", async context => + } + else { - var routeValues = context.Request.RouteValues; - var actorTypeName = (string)routeValues["actorTypeName"]; - var actorId = (string)routeValues["actorId"]; - var reminderName = (string)routeValues["reminderName"]; + try + { + await runtime.DispatchWithoutRemotingAsync(actorTypeName, actorId, methodName, context.Request.Body, context.Response.Body, context.RequestAborted); + } + finally + { + ActorReentrancyContextAccessor.ReentrancyContext = null; + } + } + }).WithDisplayName(b => "Dapr Actors Invoke"); + } - // read dueTime, period and data from Request Body. - await runtime.FireReminderAsync(actorTypeName, actorId, reminderName, context.Request.Body); - }).WithDisplayName(b => "Dapr Actors Reminder"); - } - - private static IEndpointConventionBuilder MapTimerEndpoint(this IEndpointRouteBuilder endpoints) + private static IEndpointConventionBuilder MapReminderEndpoint(this IEndpointRouteBuilder endpoints) + { + var runtime = endpoints.ServiceProvider.GetRequiredService(); + return endpoints.MapPut("actors/{actorTypeName}/{actorId}/method/remind/{reminderName}", async context => { - var runtime = endpoints.ServiceProvider.GetRequiredService(); - return endpoints.MapPut("actors/{actorTypeName}/{actorId}/method/timer/{timerName}", async context => - { - var routeValues = context.Request.RouteValues; - var actorTypeName = (string)routeValues["actorTypeName"]; - var actorId = (string)routeValues["actorId"]; - var timerName = (string)routeValues["timerName"]; + var routeValues = context.Request.RouteValues; + var actorTypeName = (string)routeValues["actorTypeName"]; + var actorId = (string)routeValues["actorId"]; + var reminderName = (string)routeValues["reminderName"]; - // read dueTime, period and data from Request Body. - await runtime.FireTimerAsync(actorTypeName, actorId, timerName, context.Request.Body); - }).WithDisplayName(b => "Dapr Actors Timer"); - } + // read dueTime, period and data from Request Body. + await runtime.FireReminderAsync(actorTypeName, actorId, reminderName, context.Request.Body); + }).WithDisplayName(b => "Dapr Actors Reminder"); + } - private static IEndpointConventionBuilder MapActorHealthChecks(this IEndpointRouteBuilder endpoints) + private static IEndpointConventionBuilder MapTimerEndpoint(this IEndpointRouteBuilder endpoints) + { + var runtime = endpoints.ServiceProvider.GetRequiredService(); + return endpoints.MapPut("actors/{actorTypeName}/{actorId}/method/timer/{timerName}", async context => { - var builder = endpoints.MapHealthChecks("/healthz").WithMetadata(new AllowAnonymousAttribute()); - builder.Add(b => - { - // Sets the route order so that this is matched with lower priority than an endpoint - // configured by default. - // - // This is necessary because it allows a user defined `/healthz` endpoint to win in the - // most common cases, but still fulfills Dapr's contract when the user doesn't have - // a health check at `/healthz`. - ((RouteEndpointBuilder)b).Order = 100; - }); - return builder.WithDisplayName(b => "Dapr Actors Health Check"); - } + var routeValues = context.Request.RouteValues; + var actorTypeName = (string)routeValues["actorTypeName"]; + var actorId = (string)routeValues["actorId"]; + var timerName = (string)routeValues["timerName"]; + + // read dueTime, period and data from Request Body. + await runtime.FireTimerAsync(actorTypeName, actorId, timerName, context.Request.Body); + }).WithDisplayName(b => "Dapr Actors Timer"); + } + + private static IEndpointConventionBuilder MapActorHealthChecks(this IEndpointRouteBuilder endpoints) + { + var builder = endpoints.MapHealthChecks("/healthz").WithMetadata(new AllowAnonymousAttribute()); + builder.Add(b => + { + // Sets the route order so that this is matched with lower priority than an endpoint + // configured by default. + // + // This is necessary because it allows a user defined `/healthz` endpoint to win in the + // most common cases, but still fulfills Dapr's contract when the user doesn't have + // a health check at `/healthz`. + ((RouteEndpointBuilder)b).Order = 100; + }); + return builder.WithDisplayName(b => "Dapr Actors Health Check"); + } private static Tuple CreateExceptionResponseMessage(Exception ex) { @@ -210,20 +210,19 @@ namespace Microsoft.AspNetCore.Builder return $"Exception: {ex.GetType().Name}, Method Name: {frame.GetMethod().Name}, Line Number: {frame.GetFileLineNumber()}, Exception uuid: {Guid.NewGuid().ToString()}"; } private class CompositeEndpointConventionBuilder : IEndpointConventionBuilder + { + private readonly IEndpointConventionBuilder[] inner; + + public CompositeEndpointConventionBuilder(IEndpointConventionBuilder[] inner) { - private readonly IEndpointConventionBuilder[] inner; + this.inner = inner; + } - public CompositeEndpointConventionBuilder(IEndpointConventionBuilder[] inner) + public void Add(Action convention) + { + for (var i = 0; i < inner.Length; i++) { - this.inner = inner; - } - - public void Add(Action convention) - { - for (var i = 0; i < inner.Length; i++) - { - inner[i].Add(convention); - } + inner[i].Add(convention); } } } diff --git a/src/Dapr.Actors.AspNetCore/ActorsServiceCollectionExtensions.cs b/src/Dapr.Actors.AspNetCore/ActorsServiceCollectionExtensions.cs index 9b80975d..278df4bb 100644 --- a/src/Dapr.Actors.AspNetCore/ActorsServiceCollectionExtensions.cs +++ b/src/Dapr.Actors.AspNetCore/ActorsServiceCollectionExtensions.cs @@ -22,91 +22,90 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -namespace Microsoft.Extensions.DependencyInjection +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Contains extension methods for using Dapr Actors with dependency injection. +/// +public static class ActorsServiceCollectionExtensions { /// - /// Contains extension methods for using Dapr Actors with dependency injection. + /// Adds Dapr Actors support to the service collection. /// - public static class ActorsServiceCollectionExtensions + /// The . + /// A delegate used to configure actor options and register actor types. + /// The lifetime of the registered services. + public static void AddActors(this IServiceCollection? services, Action? configure, ServiceLifetime lifetime = ServiceLifetime.Singleton) { - /// - /// Adds Dapr Actors support to the service collection. - /// - /// The . - /// A delegate used to configure actor options and register actor types. - /// The lifetime of the registered services. - public static void AddActors(this IServiceCollection? services, Action? configure, ServiceLifetime lifetime = ServiceLifetime.Singleton) + ArgumentNullException.ThrowIfNull(services, nameof(services)); + + // Routing, health checks and logging are required dependencies. + services.AddRouting(); + services.AddHealthChecks(); + services.AddLogging(); + + var actorRuntimeRegistration = new Func(s => { - ArgumentNullException.ThrowIfNull(services, nameof(services)); - - // Routing, health checks and logging are required dependencies. - services.AddRouting(); - services.AddHealthChecks(); - services.AddLogging(); - - var actorRuntimeRegistration = new Func(s => - { - var options = s.GetRequiredService>().Value; - ConfigureActorOptions(s, options); + var options = s.GetRequiredService>().Value; + ConfigureActorOptions(s, options); - var loggerFactory = s.GetRequiredService(); - var activatorFactory = s.GetRequiredService(); - var proxyFactory = s.GetRequiredService(); - return new ActorRuntime(options, loggerFactory, activatorFactory, proxyFactory); - }); - var proxyFactoryRegistration = new Func(serviceProvider => - { - var options = serviceProvider.GetRequiredService>().Value; - ConfigureActorOptions(serviceProvider, options); - - var factory = new ActorProxyFactory() - { - DefaultOptions = - { - JsonSerializerOptions = options.JsonSerializerOptions, - DaprApiToken = options.DaprApiToken, - HttpEndpoint = options.HttpEndpoint, - } - }; - - return factory; - }); - - switch (lifetime) - { - case ServiceLifetime.Scoped: - services.TryAddScoped(); - services.TryAddScoped(actorRuntimeRegistration); - services.TryAddScoped(proxyFactoryRegistration); - break; - case ServiceLifetime.Transient: - services.TryAddTransient(); - services.TryAddTransient(actorRuntimeRegistration); - services.TryAddTransient(proxyFactoryRegistration); - break; - default: - case ServiceLifetime.Singleton: - services.TryAddSingleton(); - services.TryAddSingleton(actorRuntimeRegistration); - services.TryAddSingleton(proxyFactoryRegistration); - break; - } - - if (configure != null) - { - services.Configure(configure); - } - } - - private static void ConfigureActorOptions(IServiceProvider serviceProvider, ActorRuntimeOptions options) + var loggerFactory = s.GetRequiredService(); + var activatorFactory = s.GetRequiredService(); + var proxyFactory = s.GetRequiredService(); + return new ActorRuntime(options, loggerFactory, activatorFactory, proxyFactory); + }); + var proxyFactoryRegistration = new Func(serviceProvider => { - var configuration = serviceProvider.GetService(); - options.DaprApiToken = !string.IsNullOrWhiteSpace(options.DaprApiToken) - ? options.DaprApiToken - : DaprDefaults.GetDefaultDaprApiToken(configuration); - options.HttpEndpoint = !string.IsNullOrWhiteSpace(options.HttpEndpoint) - ? options.HttpEndpoint - : DaprDefaults.GetDefaultHttpEndpoint(); + var options = serviceProvider.GetRequiredService>().Value; + ConfigureActorOptions(serviceProvider, options); + + var factory = new ActorProxyFactory() + { + DefaultOptions = + { + JsonSerializerOptions = options.JsonSerializerOptions, + DaprApiToken = options.DaprApiToken, + HttpEndpoint = options.HttpEndpoint, + } + }; + + return factory; + }); + + switch (lifetime) + { + case ServiceLifetime.Scoped: + services.TryAddScoped(); + services.TryAddScoped(actorRuntimeRegistration); + services.TryAddScoped(proxyFactoryRegistration); + break; + case ServiceLifetime.Transient: + services.TryAddTransient(); + services.TryAddTransient(actorRuntimeRegistration); + services.TryAddTransient(proxyFactoryRegistration); + break; + default: + case ServiceLifetime.Singleton: + services.TryAddSingleton(); + services.TryAddSingleton(actorRuntimeRegistration); + services.TryAddSingleton(proxyFactoryRegistration); + break; + } + + if (configure != null) + { + services.Configure(configure); } } -} + + private static void ConfigureActorOptions(IServiceProvider serviceProvider, ActorRuntimeOptions options) + { + var configuration = serviceProvider.GetService(); + options.DaprApiToken = !string.IsNullOrWhiteSpace(options.DaprApiToken) + ? options.DaprApiToken + : DaprDefaults.GetDefaultDaprApiToken(configuration); + options.HttpEndpoint = !string.IsNullOrWhiteSpace(options.HttpEndpoint) + ? options.HttpEndpoint + : DaprDefaults.GetDefaultHttpEndpoint(); + } +} \ No newline at end of file diff --git a/src/Dapr.Actors.AspNetCore/Runtime/DependencyInjectionActorActivator.cs b/src/Dapr.Actors.AspNetCore/Runtime/DependencyInjectionActorActivator.cs index 46a458f6..497a279c 100644 --- a/src/Dapr.Actors.AspNetCore/Runtime/DependencyInjectionActorActivator.cs +++ b/src/Dapr.Actors.AspNetCore/Runtime/DependencyInjectionActorActivator.cs @@ -16,85 +16,84 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; -namespace Dapr.Actors.Runtime +namespace Dapr.Actors.Runtime; + +// Implementation of ActorActivator that uses Microsoft.Extensions.DependencyInjection. +internal class DependencyInjectionActorActivator : ActorActivator { - // Implementation of ActorActivator that uses Microsoft.Extensions.DependencyInjection. - internal class DependencyInjectionActorActivator : ActorActivator + private readonly IServiceProvider services; + private readonly ActorTypeInformation type; + private readonly Func initializer; + + // factory is used to create the actor instance - initialization of the factory is protected + // by the initialized and @lock fields. + // + // This serves as a cache for the generated code that constructs the object from DI. + private ObjectFactory factory; + private bool initialized; + private object @lock; + + public DependencyInjectionActorActivator(IServiceProvider services, ActorTypeInformation type) { - private readonly IServiceProvider services; - private readonly ActorTypeInformation type; - private readonly Func initializer; + this.services = services; + this.type = type; - // factory is used to create the actor instance - initialization of the factory is protected - // by the initialized and @lock fields. - // - // This serves as a cache for the generated code that constructs the object from DI. - private ObjectFactory factory; - private bool initialized; - private object @lock; - - public DependencyInjectionActorActivator(IServiceProvider services, ActorTypeInformation type) + // Will be invoked to initialize the factory. + initializer = () => { - this.services = services; - this.type = type; + return ActivatorUtilities.CreateFactory(this.type.ImplementationType, new Type[]{ typeof(ActorHost), }); + }; + } - // Will be invoked to initialize the factory. - initializer = () => - { - return ActivatorUtilities.CreateFactory(this.type.ImplementationType, new Type[]{ typeof(ActorHost), }); - }; + public override async Task CreateAsync(ActorHost host) + { + var scope = services.CreateScope(); + try + { + var factory = LazyInitializer.EnsureInitialized( + ref this.factory, + ref this.initialized, + ref this.@lock, + this.initializer); + + var actor = (Actor)factory(scope.ServiceProvider, new object[] { host }); + return new State(actor, scope); } - - public override async Task CreateAsync(ActorHost host) + catch { - var scope = services.CreateScope(); - try - { - var factory = LazyInitializer.EnsureInitialized( - ref this.factory, - ref this.initialized, - ref this.@lock, - this.initializer); - - var actor = (Actor)factory(scope.ServiceProvider, new object[] { host }); - return new State(actor, scope); - } - catch - { - // Make sure to clean up the scope if we fail to create the actor; - await DisposeCore(scope); - throw; - } - } - - public override async Task DeleteAsync(ActorActivatorState obj) - { - var state = (State)obj; - await DisposeCore(state.Actor); - await DisposeCore(state.Scope); - } - - private async ValueTask DisposeCore(object obj) - { - if (obj is IAsyncDisposable asyncDisposable) - { - await asyncDisposable.DisposeAsync(); - } - else if (obj is IDisposable disposable) - { - disposable.Dispose(); - } - } - - private class State : ActorActivatorState - { - public State(Actor actor, IServiceScope scope) - : base(actor) - { - Scope = scope; - } - - public IServiceScope Scope { get; } + // Make sure to clean up the scope if we fail to create the actor; + await DisposeCore(scope); + throw; } } -} + + public override async Task DeleteAsync(ActorActivatorState obj) + { + var state = (State)obj; + await DisposeCore(state.Actor); + await DisposeCore(state.Scope); + } + + private async ValueTask DisposeCore(object obj) + { + if (obj is IAsyncDisposable asyncDisposable) + { + await asyncDisposable.DisposeAsync(); + } + else if (obj is IDisposable disposable) + { + disposable.Dispose(); + } + } + + private class State : ActorActivatorState + { + public State(Actor actor, IServiceScope scope) + : base(actor) + { + Scope = scope; + } + + public IServiceScope Scope { get; } + } +} \ No newline at end of file diff --git a/src/Dapr.Actors.AspNetCore/Runtime/DependencyInjectionActorActivatorFactory.cs b/src/Dapr.Actors.AspNetCore/Runtime/DependencyInjectionActorActivatorFactory.cs index f14d54a4..9f424206 100644 --- a/src/Dapr.Actors.AspNetCore/Runtime/DependencyInjectionActorActivatorFactory.cs +++ b/src/Dapr.Actors.AspNetCore/Runtime/DependencyInjectionActorActivatorFactory.cs @@ -13,21 +13,20 @@ using System; -namespace Dapr.Actors.Runtime +namespace Dapr.Actors.Runtime; + +// Implementation of ActorActivatorFactory that uses Microsoft.Extensions.DependencyInjection. +internal class DependencyInjectionActorActivatorFactory : ActorActivatorFactory { - // Implementation of ActorActivatorFactory that uses Microsoft.Extensions.DependencyInjection. - internal class DependencyInjectionActorActivatorFactory : ActorActivatorFactory + private readonly IServiceProvider services; + + public DependencyInjectionActorActivatorFactory(IServiceProvider services) { - private readonly IServiceProvider services; - - public DependencyInjectionActorActivatorFactory(IServiceProvider services) - { - this.services = services; - } - - public override ActorActivator CreateActivator(ActorTypeInformation type) - { - return new DependencyInjectionActorActivator(services, type); - } + this.services = services; } -} + + public override ActorActivator CreateActivator(ActorTypeInformation type) + { + return new DependencyInjectionActorActivator(services, type); + } +} \ No newline at end of file diff --git a/src/Dapr.Actors.Generators/Constants.cs b/src/Dapr.Actors.Generators/Constants.cs index 392def4e..70e6c8ba 100644 --- a/src/Dapr.Actors.Generators/Constants.cs +++ b/src/Dapr.Actors.Generators/Constants.cs @@ -1,38 +1,37 @@ -namespace Dapr.Actors.Generators +namespace Dapr.Actors.Generators; + +/// +/// Constants used by the code generator. +/// +internal static class Constants { /// - /// Constants used by the code generator. + /// The namespace used by the generated code. /// - internal static class Constants - { - /// - /// The namespace used by the generated code. - /// - public const string GeneratorsNamespace = "Dapr.Actors.Generators"; + public const string GeneratorsNamespace = "Dapr.Actors.Generators"; - /// - /// The name of the attribute used to mark actor interfaces. - /// - public const string ActorMethodAttributeTypeName = "ActorMethodAttribute"; + /// + /// The name of the attribute used to mark actor interfaces. + /// + public const string ActorMethodAttributeTypeName = "ActorMethodAttribute"; - /// - /// The full type name of the attribute used to mark actor interfaces. - /// - public const string ActorMethodAttributeFullTypeName = GeneratorsNamespace + "." + ActorMethodAttributeTypeName; + /// + /// The full type name of the attribute used to mark actor interfaces. + /// + public const string ActorMethodAttributeFullTypeName = GeneratorsNamespace + "." + ActorMethodAttributeTypeName; - /// - /// The name of the attribute used to mark actor interfaces. - /// - public const string GenerateActorClientAttributeTypeName = "GenerateActorClientAttribute"; + /// + /// The name of the attribute used to mark actor interfaces. + /// + public const string GenerateActorClientAttributeTypeName = "GenerateActorClientAttribute"; - /// - /// The full type name of the attribute used to mark actor interfaces. - /// - public const string GenerateActorClientAttributeFullTypeName = GeneratorsNamespace + "." + GenerateActorClientAttributeTypeName; + /// + /// The full type name of the attribute used to mark actor interfaces. + /// + public const string GenerateActorClientAttributeFullTypeName = GeneratorsNamespace + "." + GenerateActorClientAttributeTypeName; - /// - /// Actor proxy type name. - /// - public const string ActorProxyTypeName = "Dapr.Actors.Client.ActorProxy"; - } -} + /// + /// Actor proxy type name. + /// + public const string ActorProxyTypeName = "Dapr.Actors.Client.ActorProxy"; +} \ No newline at end of file diff --git a/src/Dapr.Actors.Generators/Diagnostics/CancellationTokensMustBeTheLastArgument.cs b/src/Dapr.Actors.Generators/Diagnostics/CancellationTokensMustBeTheLastArgument.cs index 376bb360..fc21dfa5 100644 --- a/src/Dapr.Actors.Generators/Diagnostics/CancellationTokensMustBeTheLastArgument.cs +++ b/src/Dapr.Actors.Generators/Diagnostics/CancellationTokensMustBeTheLastArgument.cs @@ -1,25 +1,24 @@ using Microsoft.CodeAnalysis; -namespace Dapr.Actors.Generators.Diagnostics +namespace Dapr.Actors.Generators.Diagnostics; + +internal static class CancellationTokensMustBeTheLastArgument { - internal static class CancellationTokensMustBeTheLastArgument - { - public const string DiagnosticId = "DAPR0001"; - public const string Title = "Invalid method signature"; - public const string MessageFormat = "Cancellation tokens must be the last argument"; - public const string Category = "Usage"; + public const string DiagnosticId = "DAPR0001"; + public const string Title = "Invalid method signature"; + public const string MessageFormat = "Cancellation tokens must be the last argument"; + public const string Category = "Usage"; - private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor( - DiagnosticId, - Title, - MessageFormat, - Category, - DiagnosticSeverity.Error, - isEnabledByDefault: true); + private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor( + DiagnosticId, + Title, + MessageFormat, + Category, + DiagnosticSeverity.Error, + isEnabledByDefault: true); - internal static Diagnostic CreateDiagnostic(ISymbol symbol) => Diagnostic.Create( - Rule, - symbol.Locations.First(), - symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); - } -} + internal static Diagnostic CreateDiagnostic(ISymbol symbol) => Diagnostic.Create( + Rule, + symbol.Locations.First(), + symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); +} \ No newline at end of file diff --git a/src/Dapr.Actors.Generators/Diagnostics/MethodMustOnlyHaveASingleArgumentOptionallyFollowedByACancellationToken.cs b/src/Dapr.Actors.Generators/Diagnostics/MethodMustOnlyHaveASingleArgumentOptionallyFollowedByACancellationToken.cs index c82b2063..76c8f7ab 100644 --- a/src/Dapr.Actors.Generators/Diagnostics/MethodMustOnlyHaveASingleArgumentOptionallyFollowedByACancellationToken.cs +++ b/src/Dapr.Actors.Generators/Diagnostics/MethodMustOnlyHaveASingleArgumentOptionallyFollowedByACancellationToken.cs @@ -1,25 +1,24 @@ using Microsoft.CodeAnalysis; -namespace Dapr.Actors.Generators.Diagnostics +namespace Dapr.Actors.Generators.Diagnostics; + +internal static class MethodMustOnlyHaveASingleArgumentOptionallyFollowedByACancellationToken { - internal static class MethodMustOnlyHaveASingleArgumentOptionallyFollowedByACancellationToken - { - public const string DiagnosticId = "DAPR0002"; - public const string Title = "Invalid method signature"; - public const string MessageFormat = "Only methods with a single argument or a single argument followed by a cancellation token are supported"; - public const string Category = "Usage"; + public const string DiagnosticId = "DAPR0002"; + public const string Title = "Invalid method signature"; + public const string MessageFormat = "Only methods with a single argument or a single argument followed by a cancellation token are supported"; + public const string Category = "Usage"; - private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor( - DiagnosticId, - Title, - MessageFormat, - Category, - DiagnosticSeverity.Error, - isEnabledByDefault: true); + private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor( + DiagnosticId, + Title, + MessageFormat, + Category, + DiagnosticSeverity.Error, + isEnabledByDefault: true); - internal static Diagnostic CreateDiagnostic(ISymbol symbol) => Diagnostic.Create( - Rule, - symbol.Locations.First(), - symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); - } -} + internal static Diagnostic CreateDiagnostic(ISymbol symbol) => Diagnostic.Create( + Rule, + symbol.Locations.First(), + symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); +} \ No newline at end of file diff --git a/src/Dapr.Actors.Generators/DiagnosticsException.cs b/src/Dapr.Actors.Generators/DiagnosticsException.cs index d196f848..ddbb3b45 100644 --- a/src/Dapr.Actors.Generators/DiagnosticsException.cs +++ b/src/Dapr.Actors.Generators/DiagnosticsException.cs @@ -1,25 +1,24 @@ using Microsoft.CodeAnalysis; -namespace Dapr.Actors.Generators +namespace Dapr.Actors.Generators; + +/// +/// Exception thrown when diagnostics are encountered during code generation. +/// +internal sealed class DiagnosticsException : Exception { /// - /// Exception thrown when diagnostics are encountered during code generation. + /// Initializes a new instance of the class. /// - internal sealed class DiagnosticsException : Exception + /// List of diagnostics generated. + public DiagnosticsException(IEnumerable diagnostics) + : base(string.Join("\n", diagnostics.Select(d => d.ToString()))) { - /// - /// Initializes a new instance of the class. - /// - /// List of diagnostics generated. - public DiagnosticsException(IEnumerable diagnostics) - : base(string.Join("\n", diagnostics.Select(d => d.ToString()))) - { - this.Diagnostics = diagnostics.ToArray(); - } - - /// - /// Diagnostics encountered during code generation. - /// - public ICollection Diagnostics { get; } + this.Diagnostics = diagnostics.ToArray(); } -} + + /// + /// Diagnostics encountered during code generation. + /// + public ICollection Diagnostics { get; } +} \ No newline at end of file diff --git a/src/Dapr.Actors.Generators/Extensions/IEnumerableExtensions.cs b/src/Dapr.Actors.Generators/Extensions/IEnumerableExtensions.cs index 6b45e86f..77b0bfd7 100644 --- a/src/Dapr.Actors.Generators/Extensions/IEnumerableExtensions.cs +++ b/src/Dapr.Actors.Generators/Extensions/IEnumerableExtensions.cs @@ -1,34 +1,33 @@ -namespace Dapr.Actors.Generators.Extensions +namespace Dapr.Actors.Generators.Extensions; + +internal static class IEnumerableExtensions { - internal static class IEnumerableExtensions + /// + /// Returns the index of the first item in the sequence that satisfies the predicate. If no item satisfies the predicate, -1 is returned. + /// + /// The type of objects in the . + /// in which to search. + /// Function performed to check whether an item satisfies the condition. + /// Return the zero-based index of the first occurrence of an element that satisfies the condition, if found; otherwise, -1. + internal static int IndexOf(this IEnumerable source, Func predicate) { - /// - /// Returns the index of the first item in the sequence that satisfies the predicate. If no item satisfies the predicate, -1 is returned. - /// - /// The type of objects in the . - /// in which to search. - /// Function performed to check whether an item satisfies the condition. - /// Return the zero-based index of the first occurrence of an element that satisfies the condition, if found; otherwise, -1. - internal static int IndexOf(this IEnumerable source, Func predicate) + if (predicate is null) { - if (predicate is null) - { - throw new ArgumentNullException(nameof(predicate)); - } - - int index = 0; - - foreach (var item in source) - { - if (predicate(item)) - { - return index; - } - - index++; - } - - return -1; + throw new ArgumentNullException(nameof(predicate)); } + + int index = 0; + + foreach (var item in source) + { + if (predicate(item)) + { + return index; + } + + index++; + } + + return -1; } -} +} \ No newline at end of file diff --git a/src/Dapr.Actors.Generators/Helpers/SyntaxFactoryHelpers.cs b/src/Dapr.Actors.Generators/Helpers/SyntaxFactoryHelpers.cs index 36df7b28..cf4b6ea3 100644 --- a/src/Dapr.Actors.Generators/Helpers/SyntaxFactoryHelpers.cs +++ b/src/Dapr.Actors.Generators/Helpers/SyntaxFactoryHelpers.cs @@ -2,158 +2,157 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; -namespace Dapr.Actors.Generators.Helpers +namespace Dapr.Actors.Generators.Helpers; + +/// +/// Syntax factory helpers for generating syntax. +/// +internal static partial class SyntaxFactoryHelpers { /// - /// Syntax factory helpers for generating syntax. + /// Generates a syntax for an based on the given argument name. /// - internal static partial class SyntaxFactoryHelpers + /// Name of the argument that generated the exception. + /// Returns used to throw an . + public static ThrowExpressionSyntax ThrowArgumentNullException(string argumentName) { - /// - /// Generates a syntax for an based on the given argument name. - /// - /// Name of the argument that generated the exception. - /// Returns used to throw an . - public static ThrowExpressionSyntax ThrowArgumentNullException(string argumentName) - { - return SyntaxFactory.ThrowExpression( - SyntaxFactory.Token(SyntaxKind.ThrowKeyword), - SyntaxFactory.ObjectCreationExpression( - SyntaxFactory.Token(SyntaxKind.NewKeyword), - SyntaxFactory.ParseTypeName("System.ArgumentNullException"), - SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList(new[] - { - SyntaxFactory.Argument(NameOfExpression(argumentName)) - })), - default - ) - ); - } - - /// - /// Generates a syntax for null check for the given argument name. - /// - /// Name of the argument whose null check is to be generated. - /// Returns representing an argument null check. - public static IfStatementSyntax ThrowIfArgumentNull(string argumentName) - { - return SyntaxFactory.IfStatement( - SyntaxFactory.BinaryExpression( - SyntaxKind.IsExpression, - SyntaxFactory.IdentifierName(argumentName), - SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression) - ), - SyntaxFactory.Block(SyntaxFactory.List(new StatementSyntax[] - { - SyntaxFactory.ExpressionStatement(ThrowArgumentNullException(argumentName)) - })) - ); - } - - /// - /// Generates a syntax for nameof expression for the given argument name. - /// - /// Name of the argument from which the syntax is to be generated. - /// Return a representing a NameOf expression. - public static ExpressionSyntax NameOfExpression(string argumentName) - { - var nameofIdentifier = SyntaxFactory.Identifier( - SyntaxFactory.TriviaList(), - SyntaxKind.NameOfKeyword, - "nameof", - "nameof", - SyntaxFactory.TriviaList()); - - return SyntaxFactory.InvocationExpression( - SyntaxFactory.IdentifierName(nameofIdentifier), + return SyntaxFactory.ThrowExpression( + SyntaxFactory.Token(SyntaxKind.ThrowKeyword), + SyntaxFactory.ObjectCreationExpression( + SyntaxFactory.Token(SyntaxKind.NewKeyword), + SyntaxFactory.ParseTypeName("System.ArgumentNullException"), SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList(new[] { - SyntaxFactory.Argument(SyntaxFactory.IdentifierName(argumentName)) - })) - ); - } + SyntaxFactory.Argument(NameOfExpression(argumentName)) + })), + default + ) + ); + } - /// - /// Generates the invocation syntax to call a remote method with the actor proxy. - /// - /// Member syntax to access actorProxy member. - /// Name of remote method to invoke. - /// Remote method parameters. - /// Return types of remote method invocation. - /// Returns the representing a call to the actor proxy. - public static InvocationExpressionSyntax ActorProxyInvokeMethodAsync( - MemberAccessExpressionSyntax actorProxyMemberSyntax, - string remoteMethodName, - IEnumerable remoteMethodParameters, - IEnumerable remoteMethodReturnTypes) - { - // Define the type arguments to pass to the actor proxy method invocation. - var proxyInvocationTypeArguments = new List() - .Concat(remoteMethodParameters - .Where(p => p.Type is not { Name: "CancellationToken" }) - .Select(p => SyntaxFactory.ParseTypeName(p.Type.ToString()))) - .Concat(remoteMethodReturnTypes - .Select(a => SyntaxFactory.ParseTypeName(a.OriginalDefinition.ToString()))); + /// + /// Generates a syntax for null check for the given argument name. + /// + /// Name of the argument whose null check is to be generated. + /// Returns representing an argument null check. + public static IfStatementSyntax ThrowIfArgumentNull(string argumentName) + { + return SyntaxFactory.IfStatement( + SyntaxFactory.BinaryExpression( + SyntaxKind.IsExpression, + SyntaxFactory.IdentifierName(argumentName), + SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression) + ), + SyntaxFactory.Block(SyntaxFactory.List(new StatementSyntax[] + { + SyntaxFactory.ExpressionStatement(ThrowArgumentNullException(argumentName)) + })) + ); + } - // Define the arguments to pass to the actor proxy method invocation. - var proxyInvocationArguments = new List() - // Name of remote method to invoke. - .Append(SyntaxFactory.Argument(SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(remoteMethodName)))) - // Actor method arguments, including the CancellationToken if it exists. - .Concat(remoteMethodParameters.Select(p => SyntaxFactory.Argument(SyntaxFactory.IdentifierName(p.Name)))); + /// + /// Generates a syntax for nameof expression for the given argument name. + /// + /// Name of the argument from which the syntax is to be generated. + /// Return a representing a NameOf expression. + public static ExpressionSyntax NameOfExpression(string argumentName) + { + var nameofIdentifier = SyntaxFactory.Identifier( + SyntaxFactory.TriviaList(), + SyntaxKind.NameOfKeyword, + "nameof", + "nameof", + SyntaxFactory.TriviaList()); - // If the invocation has return types or input parameters, we need to use the generic version of the method. - SimpleNameSyntax invokeAsyncSyntax = proxyInvocationTypeArguments.Any() - ? SyntaxFactory.GenericName( - SyntaxFactory.Identifier("InvokeMethodAsync"), - SyntaxFactory.TypeArgumentList(SyntaxFactory.SeparatedList(proxyInvocationTypeArguments))) - : SyntaxFactory.IdentifierName("InvokeMethodAsync"); + return SyntaxFactory.InvocationExpression( + SyntaxFactory.IdentifierName(nameofIdentifier), + SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList(new[] + { + SyntaxFactory.Argument(SyntaxFactory.IdentifierName(argumentName)) + })) + ); + } - // Generate the invocation syntax. - var generatedInvocationSyntax = SyntaxFactory.InvocationExpression( + /// + /// Generates the invocation syntax to call a remote method with the actor proxy. + /// + /// Member syntax to access actorProxy member. + /// Name of remote method to invoke. + /// Remote method parameters. + /// Return types of remote method invocation. + /// Returns the representing a call to the actor proxy. + public static InvocationExpressionSyntax ActorProxyInvokeMethodAsync( + MemberAccessExpressionSyntax actorProxyMemberSyntax, + string remoteMethodName, + IEnumerable remoteMethodParameters, + IEnumerable remoteMethodReturnTypes) + { + // Define the type arguments to pass to the actor proxy method invocation. + var proxyInvocationTypeArguments = new List() + .Concat(remoteMethodParameters + .Where(p => p.Type is not { Name: "CancellationToken" }) + .Select(p => SyntaxFactory.ParseTypeName(p.Type.ToString()))) + .Concat(remoteMethodReturnTypes + .Select(a => SyntaxFactory.ParseTypeName(a.OriginalDefinition.ToString()))); + + // Define the arguments to pass to the actor proxy method invocation. + var proxyInvocationArguments = new List() + // Name of remote method to invoke. + .Append(SyntaxFactory.Argument(SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(remoteMethodName)))) + // Actor method arguments, including the CancellationToken if it exists. + .Concat(remoteMethodParameters.Select(p => SyntaxFactory.Argument(SyntaxFactory.IdentifierName(p.Name)))); + + // If the invocation has return types or input parameters, we need to use the generic version of the method. + SimpleNameSyntax invokeAsyncSyntax = proxyInvocationTypeArguments.Any() + ? SyntaxFactory.GenericName( + SyntaxFactory.Identifier("InvokeMethodAsync"), + SyntaxFactory.TypeArgumentList(SyntaxFactory.SeparatedList(proxyInvocationTypeArguments))) + : SyntaxFactory.IdentifierName("InvokeMethodAsync"); + + // Generate the invocation syntax. + var generatedInvocationSyntax = SyntaxFactory.InvocationExpression( SyntaxFactory.MemberAccessExpression( SyntaxKind.SimpleMemberAccessExpression, actorProxyMemberSyntax, invokeAsyncSyntax )) - .WithArgumentList(SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList(proxyInvocationArguments))); + .WithArgumentList(SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList(proxyInvocationArguments))); - return generatedInvocationSyntax; - } - - /// - /// Returns the for the specified accessibility. - /// - /// Accessibility to convert into a . - /// Returns the collection of representing the given accessibility. - /// Throws when un unexpected accessibility is passed. - public static ICollection GetSyntaxKinds(Accessibility accessibility) - { - var syntaxKinds = new List(); - - switch (accessibility) - { - case Accessibility.Public: - syntaxKinds.Add(SyntaxKind.PublicKeyword); - break; - case Accessibility.Internal: - syntaxKinds.Add(SyntaxKind.InternalKeyword); - break; - case Accessibility.Private: - syntaxKinds.Add(SyntaxKind.PrivateKeyword); - break; - case Accessibility.Protected: - syntaxKinds.Add(SyntaxKind.ProtectedKeyword); - break; - case Accessibility.ProtectedAndInternal: - syntaxKinds.Add(SyntaxKind.ProtectedKeyword); - syntaxKinds.Add(SyntaxKind.InternalKeyword); - break; - default: - throw new InvalidOperationException("Unexpected accessibility"); - } - - return syntaxKinds; - } + return generatedInvocationSyntax; } -} + + /// + /// Returns the for the specified accessibility. + /// + /// Accessibility to convert into a . + /// Returns the collection of representing the given accessibility. + /// Throws when un unexpected accessibility is passed. + public static ICollection GetSyntaxKinds(Accessibility accessibility) + { + var syntaxKinds = new List(); + + switch (accessibility) + { + case Accessibility.Public: + syntaxKinds.Add(SyntaxKind.PublicKeyword); + break; + case Accessibility.Internal: + syntaxKinds.Add(SyntaxKind.InternalKeyword); + break; + case Accessibility.Private: + syntaxKinds.Add(SyntaxKind.PrivateKeyword); + break; + case Accessibility.Protected: + syntaxKinds.Add(SyntaxKind.ProtectedKeyword); + break; + case Accessibility.ProtectedAndInternal: + syntaxKinds.Add(SyntaxKind.ProtectedKeyword); + syntaxKinds.Add(SyntaxKind.InternalKeyword); + break; + default: + throw new InvalidOperationException("Unexpected accessibility"); + } + + return syntaxKinds; + } +} \ No newline at end of file diff --git a/src/Dapr.Actors.Generators/Models/ActorClientDescriptor.cs b/src/Dapr.Actors.Generators/Models/ActorClientDescriptor.cs index e1f54fac..c939cce1 100644 --- a/src/Dapr.Actors.Generators/Models/ActorClientDescriptor.cs +++ b/src/Dapr.Actors.Generators/Models/ActorClientDescriptor.cs @@ -1,46 +1,45 @@ using System.Collections.Immutable; using Microsoft.CodeAnalysis; -namespace Dapr.Actors.Generators.Models +namespace Dapr.Actors.Generators.Models; + +/// +/// Describes an actor client to generate. +/// +internal record class ActorClientDescriptor : IEquatable { /// - /// Describes an actor client to generate. + /// Gets or sets the symbol representing the actor interface. /// - internal record class ActorClientDescriptor : IEquatable - { - /// - /// Gets or sets the symbol representing the actor interface. - /// - public INamedTypeSymbol InterfaceType { get; set; } = null!; + public INamedTypeSymbol InterfaceType { get; set; } = null!; - /// - /// Accessibility of the generated client. - /// - public Accessibility Accessibility { get; set; } + /// + /// Accessibility of the generated client. + /// + public Accessibility Accessibility { get; set; } - /// - /// Namespace of the generated client. - /// - public string NamespaceName { get; set; } = string.Empty; + /// + /// Namespace of the generated client. + /// + public string NamespaceName { get; set; } = string.Empty; - /// - /// Name of the generated client. - /// - public string ClientTypeName { get; set; } = string.Empty; + /// + /// Name of the generated client. + /// + public string ClientTypeName { get; set; } = string.Empty; - /// - /// Fully qualified type name of the generated client. - /// - public string FullyQualifiedTypeName => $"{NamespaceName}.{ClientTypeName}"; + /// + /// Fully qualified type name of the generated client. + /// + public string FullyQualifiedTypeName => $"{NamespaceName}.{ClientTypeName}"; - /// - /// Methods to generate in the client. - /// - public ImmutableArray Methods { get; set; } = Array.Empty().ToImmutableArray(); + /// + /// Methods to generate in the client. + /// + public ImmutableArray Methods { get; set; } = Array.Empty().ToImmutableArray(); - /// - /// Compilation to use for generating the client. - /// - public Compilation Compilation { get; set; } = null!; - } -} + /// + /// Compilation to use for generating the client. + /// + public Compilation Compilation { get; set; } = null!; +} \ No newline at end of file diff --git a/src/Dapr.Actors.Generators/Templates.cs b/src/Dapr.Actors.Generators/Templates.cs index 6cc4c9f8..a9dd6836 100644 --- a/src/Dapr.Actors.Generators/Templates.cs +++ b/src/Dapr.Actors.Generators/Templates.cs @@ -3,27 +3,27 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Text; -namespace Dapr.Actors.Generators +namespace Dapr.Actors.Generators; + +/// +/// Templates for generating source code. +/// +internal static partial class Templates { /// - /// Templates for generating source code. + /// Returns the for the ActorMethodAttribute. /// - internal static partial class Templates + /// Namespace where to generate attribute. + /// The representing the ActorMethodAttribute. + /// Throws when destinationNamespace is null. + public static SourceText ActorMethodAttributeSourceText(string destinationNamespace) { - /// - /// Returns the for the ActorMethodAttribute. - /// - /// Namespace where to generate attribute. - /// The representing the ActorMethodAttribute. - /// Throws when destinationNamespace is null. - public static SourceText ActorMethodAttributeSourceText(string destinationNamespace) + if (destinationNamespace is null) { - if (destinationNamespace is null) - { - throw new ArgumentNullException(nameof(destinationNamespace)); - } + throw new ArgumentNullException(nameof(destinationNamespace)); + } - var source = $@" + var source = $@" // #nullable enable @@ -39,27 +39,27 @@ namespace {destinationNamespace} }} }}"; - return SourceText.From( - SyntaxFactory.ParseCompilationUnit(source) - .NormalizeWhitespace() - .ToFullString(), - Encoding.UTF8); + return SourceText.From( + SyntaxFactory.ParseCompilationUnit(source) + .NormalizeWhitespace() + .ToFullString(), + Encoding.UTF8); + } + + /// + /// Returns the for the GenerateActorClientAttribute. + /// + /// Namespace where to generate attribute. + /// The representing the ActorMethodAttribute. + /// Throws when destinationNamespace is null. + public static SourceText GenerateActorClientAttributeSourceText(string destinationNamespace) + { + if (destinationNamespace is null) + { + throw new ArgumentNullException(nameof(destinationNamespace)); } - /// - /// Returns the for the GenerateActorClientAttribute. - /// - /// Namespace where to generate attribute. - /// The representing the ActorMethodAttribute. - /// Throws when destinationNamespace is null. - public static SourceText GenerateActorClientAttributeSourceText(string destinationNamespace) - { - if (destinationNamespace is null) - { - throw new ArgumentNullException(nameof(destinationNamespace)); - } - - string source = $@" + string source = $@" // #nullable enable @@ -77,11 +77,10 @@ namespace {destinationNamespace} }} }}"; - return SourceText.From( - SyntaxFactory.ParseCompilationUnit(source) - .NormalizeWhitespace() - .ToFullString(), - Encoding.UTF8); - } + return SourceText.From( + SyntaxFactory.ParseCompilationUnit(source) + .NormalizeWhitespace() + .ToFullString(), + Encoding.UTF8); } -} +} \ No newline at end of file diff --git a/src/Dapr.Actors/ActorId.cs b/src/Dapr.Actors/ActorId.cs index 88b10bb9..0b3b558d 100644 --- a/src/Dapr.Actors/ActorId.cs +++ b/src/Dapr.Actors/ActorId.cs @@ -11,164 +11,163 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors +namespace Dapr.Actors; + +using System; +using System.Runtime.Serialization; +using System.Text.Json.Serialization; +using Dapr.Actors.Seralization; + +/// +/// The ActorId represents the identity of an actor within an actor service. +/// +[JsonConverter(typeof(ActorIdJsonConverter))] +[DataContract(Name = "ActorId")] +public class ActorId { - using System; - using System.Runtime.Serialization; - using System.Text.Json.Serialization; - using Dapr.Actors.Seralization; + [DataMember(Name = "ActorId", Order = 0, IsRequired = true)] + private readonly string stringId; /// - /// The ActorId represents the identity of an actor within an actor service. + /// Initializes a new instance of the class with id value of type . /// - [JsonConverter(typeof(ActorIdJsonConverter))] - [DataContract(Name = "ActorId")] - public class ActorId + /// Value for actor id. + public ActorId(string id) { - [DataMember(Name = "ActorId", Order = 0, IsRequired = true)] - private readonly string stringId; - - /// - /// Initializes a new instance of the class with id value of type . - /// - /// Value for actor id. - public ActorId(string id) + if (string.IsNullOrWhiteSpace(id)) { - if (string.IsNullOrWhiteSpace(id)) - { - throw new ArgumentException("The value cannot be null, empty or white spaces.", nameof(id)); - } - this.stringId = id; + throw new ArgumentException("The value cannot be null, empty or white spaces.", nameof(id)); } + this.stringId = id; + } - /// - /// Determines whether two specified actorIds have the same id. - /// - /// The first actorId to compare, or null. - /// The second actorId to compare, or null. - /// true if the id is same for both objects; otherwise, false. - public static bool operator ==(ActorId id1, ActorId id2) + /// + /// Determines whether two specified actorIds have the same id. + /// + /// The first actorId to compare, or null. + /// The second actorId to compare, or null. + /// true if the id is same for both objects; otherwise, false. + public static bool operator ==(ActorId id1, ActorId id2) + { + if (id1 is null && id2 is null) { - if (id1 is null && id2 is null) - { - return true; - } - else if (id1 is null || id2 is null) - { - return false; - } - else - { - return EqualsContents(id1, id2); - } + return true; } - - /// - /// Determines whether two specified actorIds have different values for id./>. - /// - /// The first actorId to compare, or null. - /// The second actorId to compare, or null. - /// true if the id is different for both objects; otherwise, true. - public static bool operator !=(ActorId id1, ActorId id2) + else if (id1 is null || id2 is null) { - return !(id1 == id2); + return false; } - - /// - /// Create a new instance of the with a random id value. - /// - /// A new ActorId object. - /// This method is thread-safe and generates a new random every time it is called. - public static ActorId CreateRandom() + else { - return new ActorId(Guid.NewGuid().ToString()); - } - - /// - /// Gets id. - /// - /// The id value for ActorId. - public string GetId() - { - return this.stringId; - } - - /// - /// Overrides . - /// - /// Returns a string that represents the current object. - public override string ToString() - { - return this.stringId; - } - - /// - /// Overrides . - /// - /// Hash code for the current object. - public override int GetHashCode() - { - return this.stringId.GetHashCode(); - } - - /// - /// Determines whether this instance and a specified object, which must also be a object, - /// have the same value. Overrides . - /// - /// The actorId to compare to this instance. - /// true if obj is a and its value is the same as this instance; - /// otherwise, false. If obj is null, the method returns false. - public override bool Equals(object obj) - { - if (obj is null || obj.GetType() != typeof(ActorId)) - { - return false; - } - else - { - return EqualsContents(this, (ActorId)obj); - } - } - - /// - /// Determines whether this instance and another specified object have the same value. - /// - /// The actorId to compare to this instance. - /// true if the id of the other parameter is the same as the id of this instance; otherwise, false. - /// If other is null, the method returns false. - public bool Equals(ActorId other) - { - if (other is null) - { - return false; - } - else - { - return EqualsContents(this, other); - } - } - - /// - /// Compares this instance with a specified object and indicates whether this - /// instance precedes, follows, or appears in the same position in the sort order as the specified actorId. - /// - /// The actorId to compare with this instance. - /// A 32-bit signed integer that indicates whether this instance precedes, follows, or appears - /// in the same position in the sort order as the other parameter. - /// The comparison is done based on the id if both the instances. - public int CompareTo(ActorId other) - { - return other is null ? 1 : CompareContents(this, other); - } - - private static bool EqualsContents(ActorId id1, ActorId id2) - { - return string.Equals(id1.stringId, id2.stringId, StringComparison.OrdinalIgnoreCase); - } - - private static int CompareContents(ActorId id1, ActorId id2) - { - return string.Compare(id1.stringId, id2.stringId, StringComparison.OrdinalIgnoreCase); + return EqualsContents(id1, id2); } } -} + + /// + /// Determines whether two specified actorIds have different values for id./>. + /// + /// The first actorId to compare, or null. + /// The second actorId to compare, or null. + /// true if the id is different for both objects; otherwise, true. + public static bool operator !=(ActorId id1, ActorId id2) + { + return !(id1 == id2); + } + + /// + /// Create a new instance of the with a random id value. + /// + /// A new ActorId object. + /// This method is thread-safe and generates a new random every time it is called. + public static ActorId CreateRandom() + { + return new ActorId(Guid.NewGuid().ToString()); + } + + /// + /// Gets id. + /// + /// The id value for ActorId. + public string GetId() + { + return this.stringId; + } + + /// + /// Overrides . + /// + /// Returns a string that represents the current object. + public override string ToString() + { + return this.stringId; + } + + /// + /// Overrides . + /// + /// Hash code for the current object. + public override int GetHashCode() + { + return this.stringId.GetHashCode(); + } + + /// + /// Determines whether this instance and a specified object, which must also be a object, + /// have the same value. Overrides . + /// + /// The actorId to compare to this instance. + /// true if obj is a and its value is the same as this instance; + /// otherwise, false. If obj is null, the method returns false. + public override bool Equals(object obj) + { + if (obj is null || obj.GetType() != typeof(ActorId)) + { + return false; + } + else + { + return EqualsContents(this, (ActorId)obj); + } + } + + /// + /// Determines whether this instance and another specified object have the same value. + /// + /// The actorId to compare to this instance. + /// true if the id of the other parameter is the same as the id of this instance; otherwise, false. + /// If other is null, the method returns false. + public bool Equals(ActorId other) + { + if (other is null) + { + return false; + } + else + { + return EqualsContents(this, other); + } + } + + /// + /// Compares this instance with a specified object and indicates whether this + /// instance precedes, follows, or appears in the same position in the sort order as the specified actorId. + /// + /// The actorId to compare with this instance. + /// A 32-bit signed integer that indicates whether this instance precedes, follows, or appears + /// in the same position in the sort order as the other parameter. + /// The comparison is done based on the id if both the instances. + public int CompareTo(ActorId other) + { + return other is null ? 1 : CompareContents(this, other); + } + + private static bool EqualsContents(ActorId id1, ActorId id2) + { + return string.Equals(id1.stringId, id2.stringId, StringComparison.OrdinalIgnoreCase); + } + + private static int CompareContents(ActorId id1, ActorId id2) + { + return string.Compare(id1.stringId, id2.stringId, StringComparison.OrdinalIgnoreCase); + } +} \ No newline at end of file diff --git a/src/Dapr.Actors/ActorMethodInvocationException.cs b/src/Dapr.Actors/ActorMethodInvocationException.cs index bbc3b5b7..3f0ba972 100644 --- a/src/Dapr.Actors/ActorMethodInvocationException.cs +++ b/src/Dapr.Actors/ActorMethodInvocationException.cs @@ -11,44 +11,43 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors +namespace Dapr.Actors; + +using System; + +/// +/// Exception for Remote Actor Method Invocation. +/// +[Serializable] +public class ActorMethodInvocationException : DaprApiException { - using System; + /// + /// Initializes a new instance of the class. + /// + public ActorMethodInvocationException() + : base(Constants.ErrorActorInvokeMethod, false) + { + } /// - /// Exception for Remote Actor Method Invocation. + /// Initializes a new instance of the class. /// - [Serializable] - public class ActorMethodInvocationException : DaprApiException + /// The error message that explains the reason for the exception. + /// True, if the exception is to be treated as an transient exception. + public ActorMethodInvocationException(string message, bool isTransient) + : base(message, Constants.ErrorActorInvokeMethod, isTransient) { - /// - /// Initializes a new instance of the class. - /// - public ActorMethodInvocationException() - : base(Constants.ErrorActorInvokeMethod, false) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The error message that explains the reason for the exception. - /// True, if the exception is to be treated as an transient exception. - public ActorMethodInvocationException(string message, bool isTransient) - : base(message, Constants.ErrorActorInvokeMethod, isTransient) - { - } - - /// - /// Initializes a new instance of the class with a specified error - /// message and a reference to the inner exception that is the cause of this exception. - /// - /// The error message that explains the reason for the exception. - /// The exception that is the cause of the current exception. - /// True, if the exception is to be treated as an transient exception. - public ActorMethodInvocationException(string message, Exception innerException, bool isTransient) - : base(message, innerException, Constants.ErrorActorInvokeMethod, isTransient) - { - } } -} + + /// + /// Initializes a new instance of the class with a specified error + /// message and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception. + /// True, if the exception is to be treated as an transient exception. + public ActorMethodInvocationException(string message, Exception innerException, bool isTransient) + : base(message, innerException, Constants.ErrorActorInvokeMethod, isTransient) + { + } +} \ No newline at end of file diff --git a/src/Dapr.Actors/ActorReentrancyConfig.cs b/src/Dapr.Actors/ActorReentrancyConfig.cs index 2e65d1be..317f13e6 100644 --- a/src/Dapr.Actors/ActorReentrancyConfig.cs +++ b/src/Dapr.Actors/ActorReentrancyConfig.cs @@ -11,49 +11,48 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors +namespace Dapr.Actors; + +/// +/// Represents the configuration required for Actor Reentrancy. +/// +/// See: https://docs.dapr.io/developing-applications/building-blocks/actors/actor-reentrancy/ +/// +public sealed class ActorReentrancyConfig { + private bool enabled; + private int? maxStackDepth; + /// - /// Represents the configuration required for Actor Reentrancy. - /// - /// See: https://docs.dapr.io/developing-applications/building-blocks/actors/actor-reentrancy/ + /// Determines if Actor Reentrancy is enabled or disabled. /// - public sealed class ActorReentrancyConfig + public bool Enabled { - private bool enabled; - private int? maxStackDepth; - - /// - /// Determines if Actor Reentrancy is enabled or disabled. - /// - public bool Enabled + get { - get - { - return this.enabled; - } - - set - { - this.enabled = value; - } + return this.enabled; } - /// - /// Optional parameter that will stop a reentrant call from progressing past the defined - /// limit. This is a safety measure against infinite reentrant calls. - /// - public int? MaxStackDepth + set { - get - { - return this.maxStackDepth; - } + this.enabled = value; + } + } - set - { - this.maxStackDepth = value; - } + /// + /// Optional parameter that will stop a reentrant call from progressing past the defined + /// limit. This is a safety measure against infinite reentrant calls. + /// + public int? MaxStackDepth + { + get + { + return this.maxStackDepth; + } + + set + { + this.maxStackDepth = value; } } } \ No newline at end of file diff --git a/src/Dapr.Actors/ActorReentrancyContextAccessor.cs b/src/Dapr.Actors/ActorReentrancyContextAccessor.cs index 9b92d697..c4a776b3 100644 --- a/src/Dapr.Actors/ActorReentrancyContextAccessor.cs +++ b/src/Dapr.Actors/ActorReentrancyContextAccessor.cs @@ -13,44 +13,43 @@ using System.Threading; -namespace Dapr.Actors +namespace Dapr.Actors; + +/// +/// Accessor for the reentrancy context. This provides the necessary ID to continue a reentrant request +/// across actor invocations. +/// +internal static class ActorReentrancyContextAccessor { + private static readonly AsyncLocal state = new AsyncLocal(); + /// - /// Accessor for the reentrancy context. This provides the necessary ID to continue a reentrant request - /// across actor invocations. + /// The reentrancy context for a given request, if one is present. /// - internal static class ActorReentrancyContextAccessor + public static string ReentrancyContext { - private static readonly AsyncLocal state = new AsyncLocal(); - - /// - /// The reentrancy context for a given request, if one is present. - /// - public static string ReentrancyContext + get { - get - { - return state.Value?.Context; - } - set - { - var holder = state.Value; - // Reset the current state if it exists. - if (holder != null) - { - holder.Context = null; - } - - if (value != null) - { - state.Value = new ActorReentrancyContextHolder { Context = value }; - } - } + return state.Value?.Context; } - - private class ActorReentrancyContextHolder + set { - public string Context; + var holder = state.Value; + // Reset the current state if it exists. + if (holder != null) + { + holder.Context = null; + } + + if (value != null) + { + state.Value = new ActorReentrancyContextHolder { Context = value }; + } } } -} + + private class ActorReentrancyContextHolder + { + public string Context; + } +} \ No newline at end of file diff --git a/src/Dapr.Actors/ActorReference.cs b/src/Dapr.Actors/ActorReference.cs index d72b6676..96c1ca93 100644 --- a/src/Dapr.Actors/ActorReference.cs +++ b/src/Dapr.Actors/ActorReference.cs @@ -11,87 +11,86 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors +namespace Dapr.Actors; + +using System; +using System.Runtime.Serialization; +using Dapr.Actors.Client; +using Dapr.Actors.Runtime; + +/// +/// Encapsulation of a reference to an actor for serialization. +/// +[DataContract(Name = "ActorReference", Namespace = Constants.Namespace)] +[Serializable] +public sealed class ActorReference : IActorReference { - using System; - using System.Runtime.Serialization; - using Dapr.Actors.Client; - using Dapr.Actors.Runtime; + /// + /// Initializes a new instance of the class. + /// + public ActorReference() + { + } /// - /// Encapsulation of a reference to an actor for serialization. + /// Gets or sets the of the actor. /// - [DataContract(Name = "ActorReference", Namespace = Constants.Namespace)] - [Serializable] - public sealed class ActorReference : IActorReference + /// of the actor. + [DataMember(Name = "ActorId", Order = 0, IsRequired = true)] + public ActorId ActorId { get; set; } + + /// + /// Gets or sets the implementation type of the actor. + /// + /// Implementation type name of the actor. + [DataMember(Name = "ActorType", Order = 0, IsRequired = true)] + public string ActorType { get; set; } + + /// + /// Gets for the actor. + /// + /// Actor object to get for. + /// object for the actor. + /// A null value is returned if actor is passed as null. + public static ActorReference Get(object actor) { - /// - /// Initializes a new instance of the class. - /// - public ActorReference() + if (actor != null) { + return GetActorReference(actor); } - /// - /// Gets or sets the of the actor. - /// - /// of the actor. - [DataMember(Name = "ActorId", Order = 0, IsRequired = true)] - public ActorId ActorId { get; set; } - - /// - /// Gets or sets the implementation type of the actor. - /// - /// Implementation type name of the actor. - [DataMember(Name = "ActorType", Order = 0, IsRequired = true)] - public string ActorType { get; set; } - - /// - /// Gets for the actor. - /// - /// Actor object to get for. - /// object for the actor. - /// A null value is returned if actor is passed as null. - public static ActorReference Get(object actor) - { - if (actor != null) - { - return GetActorReference(actor); - } - - return null; - } - - /// - public object Bind(Type actorInterfaceType) - { - return ActorProxy.DefaultProxyFactory.CreateActorProxy(this.ActorId, actorInterfaceType, this.ActorType); - } - - private static ActorReference GetActorReference(object actor) - { - ArgumentNullException.ThrowIfNull(actor, nameof(actor)); - - var actorReference = actor switch - { - // try as IActorProxy for backward compatibility as customers's mock framework may rely on it before V2 remoting stack. - IActorProxy actorProxy => new ActorReference() - { - ActorId = actorProxy.ActorId, - ActorType = actorProxy.ActorType, - }, - // Handle case when we want to get ActorReference inside the Actor implementation, - // we gather actor id and actor type from Actor base class. - Actor actorBase => new ActorReference() - { - ActorId = actorBase.Id, - ActorType = actorBase.Host.ActorTypeInfo.ActorTypeName, - }, - // Handle case when we can't cast to IActorProxy or Actor. - _ => throw new ArgumentOutOfRangeException("actor", "Invalid actor object type."), - }; - - return actorReference; - } + return null; } -} + + /// + public object Bind(Type actorInterfaceType) + { + return ActorProxy.DefaultProxyFactory.CreateActorProxy(this.ActorId, actorInterfaceType, this.ActorType); + } + + private static ActorReference GetActorReference(object actor) + { + ArgumentNullException.ThrowIfNull(actor, nameof(actor)); + + var actorReference = actor switch + { + // try as IActorProxy for backward compatibility as customers's mock framework may rely on it before V2 remoting stack. + IActorProxy actorProxy => new ActorReference() + { + ActorId = actorProxy.ActorId, + ActorType = actorProxy.ActorType, + }, + // Handle case when we want to get ActorReference inside the Actor implementation, + // we gather actor id and actor type from Actor base class. + Actor actorBase => new ActorReference() + { + ActorId = actorBase.Id, + ActorType = actorBase.Host.ActorTypeInfo.ActorTypeName, + }, + // Handle case when we can't cast to IActorProxy or Actor. + _ => throw new ArgumentOutOfRangeException("actor", "Invalid actor object type."), + }; + + return actorReference; + } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Builder/ActorCodeBuilder.cs b/src/Dapr.Actors/Builder/ActorCodeBuilder.cs index a025af7c..2a08d3bf 100644 --- a/src/Dapr.Actors/Builder/ActorCodeBuilder.cs +++ b/src/Dapr.Actors/Builder/ActorCodeBuilder.cs @@ -11,179 +11,178 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Builder +namespace Dapr.Actors.Builder; + +using System; +using System.Collections.Generic; +using System.Linq; +using Dapr.Actors.Description; +using Dapr.Actors.Runtime; + +internal class ActorCodeBuilder : ICodeBuilder { - using System; - using System.Collections.Generic; - using System.Linq; - using Dapr.Actors.Description; - using Dapr.Actors.Runtime; + internal static readonly InterfaceDetailsStore InterfaceDetailsStore = new InterfaceDetailsStore(); + private static readonly ICodeBuilder Instance = new ActorCodeBuilder(new ActorCodeBuilderNames("V1")); + private static readonly object BuildLock = new object(); + private readonly MethodBodyTypesBuilder methodBodyTypesBuilder; + private readonly MethodDispatcherBuilder methodDispatcherBuilder; + private readonly ActorProxyGeneratorBuilder proxyGeneratorBuilder; - internal class ActorCodeBuilder : ICodeBuilder + private readonly Dictionary methodBodyTypesBuildResultMap; + private readonly Dictionary methodDispatcherBuildResultMap; + private readonly Dictionary proxyGeneratorBuildResultMap; + + private readonly ICodeBuilderNames codeBuilderNames; + + public ActorCodeBuilder(ICodeBuilderNames codeBuilderNames) { - internal static readonly InterfaceDetailsStore InterfaceDetailsStore = new InterfaceDetailsStore(); - private static readonly ICodeBuilder Instance = new ActorCodeBuilder(new ActorCodeBuilderNames("V1")); - private static readonly object BuildLock = new object(); - private readonly MethodBodyTypesBuilder methodBodyTypesBuilder; - private readonly MethodDispatcherBuilder methodDispatcherBuilder; - private readonly ActorProxyGeneratorBuilder proxyGeneratorBuilder; + this.codeBuilderNames = codeBuilderNames; - private readonly Dictionary methodBodyTypesBuildResultMap; - private readonly Dictionary methodDispatcherBuildResultMap; - private readonly Dictionary proxyGeneratorBuildResultMap; + this.methodBodyTypesBuildResultMap = new Dictionary(); + this.methodDispatcherBuildResultMap = new Dictionary(); + this.proxyGeneratorBuildResultMap = new Dictionary(); - private readonly ICodeBuilderNames codeBuilderNames; + this.methodBodyTypesBuilder = new MethodBodyTypesBuilder(this); + this.methodDispatcherBuilder = new MethodDispatcherBuilder(this); + this.proxyGeneratorBuilder = new ActorProxyGeneratorBuilder(this); + } - public ActorCodeBuilder(ICodeBuilderNames codeBuilderNames) + ICodeBuilderNames ICodeBuilder.Names + { + get { return this.codeBuilderNames; } + } + + public static ActorProxyGenerator GetOrCreateProxyGenerator(Type actorInterfaceType) + { + lock (BuildLock) { - this.codeBuilderNames = codeBuilderNames; - - this.methodBodyTypesBuildResultMap = new Dictionary(); - this.methodDispatcherBuildResultMap = new Dictionary(); - this.proxyGeneratorBuildResultMap = new Dictionary(); - - this.methodBodyTypesBuilder = new MethodBodyTypesBuilder(this); - this.methodDispatcherBuilder = new MethodDispatcherBuilder(this); - this.proxyGeneratorBuilder = new ActorProxyGeneratorBuilder(this); - } - - ICodeBuilderNames ICodeBuilder.Names - { - get { return this.codeBuilderNames; } - } - - public static ActorProxyGenerator GetOrCreateProxyGenerator(Type actorInterfaceType) - { - lock (BuildLock) - { - return (ActorProxyGenerator)Instance.GetOrBuildProxyGenerator(actorInterfaceType).ProxyGenerator; - } - } - - public static ActorMethodDispatcherBase GetOrCreateMethodDispatcher(Type actorInterfaceType) - { - lock (BuildLock) - { - return (ActorMethodDispatcherBase)Instance.GetOrBuilderMethodDispatcher(actorInterfaceType).MethodDispatcher; - } - } - - MethodDispatcherBuildResult ICodeBuilder.GetOrBuilderMethodDispatcher(Type interfaceType) - { - if (this.TryGetMethodDispatcher(interfaceType, out var result)) - { - return result; - } - - result = this.BuildMethodDispatcher(interfaceType); - this.UpdateMethodDispatcherBuildMap(interfaceType, result); - - return result; - } - - MethodBodyTypesBuildResult ICodeBuilder.GetOrBuildMethodBodyTypes(Type interfaceType) - { - if (this.methodBodyTypesBuildResultMap.TryGetValue(interfaceType, out var result)) - { - return result; - } - - result = this.BuildMethodBodyTypes(interfaceType); - this.methodBodyTypesBuildResultMap.Add(interfaceType, result); - - return result; - } - - ActorProxyGeneratorBuildResult ICodeBuilder.GetOrBuildProxyGenerator(Type interfaceType) - { - if (this.TryGetProxyGenerator(interfaceType, out var result)) - { - return result; - } - - result = this.BuildProxyGenerator(interfaceType); - this.UpdateProxyGeneratorMap(interfaceType, result); - - return result; - } - - internal static bool TryGetKnownTypes(int interfaceId, out InterfaceDetails interfaceDetails) - { - return InterfaceDetailsStore.TryGetKnownTypes(interfaceId, out interfaceDetails); - } - - internal static bool TryGetKnownTypes(string interfaceName, out InterfaceDetails interfaceDetails) - { - return InterfaceDetailsStore.TryGetKnownTypes(interfaceName, out interfaceDetails); - } - - protected MethodDispatcherBuildResult BuildMethodDispatcher(Type interfaceType) - { - var actorInterfaceDescription = ActorInterfaceDescription.CreateUsingCRCId(interfaceType); - var res = this.methodDispatcherBuilder.Build(actorInterfaceDescription); - return res; - } - - protected MethodBodyTypesBuildResult BuildMethodBodyTypes(Type interfaceType) - { - var actorInterfaceDescriptions = ActorInterfaceDescription.CreateUsingCRCId(interfaceType); - var result = this.methodBodyTypesBuilder.Build(actorInterfaceDescriptions); - InterfaceDetailsStore.UpdateKnownTypeDetail(actorInterfaceDescriptions, result); - return result; - } - - protected ActorProxyGeneratorBuildResult BuildProxyGenerator(Type interfaceType) - { - // create all actor interfaces that this interface derives from - var actorInterfaces = new List() { interfaceType }; - actorInterfaces.AddRange(interfaceType.GetActorInterfaces()); - - // create interface descriptions for all interfaces - var actorInterfaceDescriptions = actorInterfaces.Select( - t => ActorInterfaceDescription.CreateUsingCRCId(t)); - - var res = this.proxyGeneratorBuilder.Build(interfaceType, actorInterfaceDescriptions); - return res; - } - - protected void UpdateMethodDispatcherBuildMap(Type interfaceType, MethodDispatcherBuildResult result) - { - this.methodDispatcherBuildResultMap.Add(interfaceType, result); - } - - protected bool TryGetMethodDispatcher( - Type interfaceType, - out MethodDispatcherBuildResult builderMethodDispatcher) - { - if (this.methodDispatcherBuildResultMap.TryGetValue(interfaceType, out var result)) - { - { - builderMethodDispatcher = result; - return true; - } - } - - builderMethodDispatcher = null; - return false; - } - - protected void UpdateProxyGeneratorMap(Type interfaceType, ActorProxyGeneratorBuildResult result) - { - this.proxyGeneratorBuildResultMap.Add(interfaceType, result); - } - - protected bool TryGetProxyGenerator(Type interfaceType, out ActorProxyGeneratorBuildResult orBuildProxyGenerator) - { - if (this.proxyGeneratorBuildResultMap.TryGetValue(interfaceType, out var result)) - { - { - orBuildProxyGenerator = result; - return true; - } - } - - orBuildProxyGenerator = null; - return false; + return (ActorProxyGenerator)Instance.GetOrBuildProxyGenerator(actorInterfaceType).ProxyGenerator; } } -} + + public static ActorMethodDispatcherBase GetOrCreateMethodDispatcher(Type actorInterfaceType) + { + lock (BuildLock) + { + return (ActorMethodDispatcherBase)Instance.GetOrBuilderMethodDispatcher(actorInterfaceType).MethodDispatcher; + } + } + + MethodDispatcherBuildResult ICodeBuilder.GetOrBuilderMethodDispatcher(Type interfaceType) + { + if (this.TryGetMethodDispatcher(interfaceType, out var result)) + { + return result; + } + + result = this.BuildMethodDispatcher(interfaceType); + this.UpdateMethodDispatcherBuildMap(interfaceType, result); + + return result; + } + + MethodBodyTypesBuildResult ICodeBuilder.GetOrBuildMethodBodyTypes(Type interfaceType) + { + if (this.methodBodyTypesBuildResultMap.TryGetValue(interfaceType, out var result)) + { + return result; + } + + result = this.BuildMethodBodyTypes(interfaceType); + this.methodBodyTypesBuildResultMap.Add(interfaceType, result); + + return result; + } + + ActorProxyGeneratorBuildResult ICodeBuilder.GetOrBuildProxyGenerator(Type interfaceType) + { + if (this.TryGetProxyGenerator(interfaceType, out var result)) + { + return result; + } + + result = this.BuildProxyGenerator(interfaceType); + this.UpdateProxyGeneratorMap(interfaceType, result); + + return result; + } + + internal static bool TryGetKnownTypes(int interfaceId, out InterfaceDetails interfaceDetails) + { + return InterfaceDetailsStore.TryGetKnownTypes(interfaceId, out interfaceDetails); + } + + internal static bool TryGetKnownTypes(string interfaceName, out InterfaceDetails interfaceDetails) + { + return InterfaceDetailsStore.TryGetKnownTypes(interfaceName, out interfaceDetails); + } + + protected MethodDispatcherBuildResult BuildMethodDispatcher(Type interfaceType) + { + var actorInterfaceDescription = ActorInterfaceDescription.CreateUsingCRCId(interfaceType); + var res = this.methodDispatcherBuilder.Build(actorInterfaceDescription); + return res; + } + + protected MethodBodyTypesBuildResult BuildMethodBodyTypes(Type interfaceType) + { + var actorInterfaceDescriptions = ActorInterfaceDescription.CreateUsingCRCId(interfaceType); + var result = this.methodBodyTypesBuilder.Build(actorInterfaceDescriptions); + InterfaceDetailsStore.UpdateKnownTypeDetail(actorInterfaceDescriptions, result); + return result; + } + + protected ActorProxyGeneratorBuildResult BuildProxyGenerator(Type interfaceType) + { + // create all actor interfaces that this interface derives from + var actorInterfaces = new List() { interfaceType }; + actorInterfaces.AddRange(interfaceType.GetActorInterfaces()); + + // create interface descriptions for all interfaces + var actorInterfaceDescriptions = actorInterfaces.Select( + t => ActorInterfaceDescription.CreateUsingCRCId(t)); + + var res = this.proxyGeneratorBuilder.Build(interfaceType, actorInterfaceDescriptions); + return res; + } + + protected void UpdateMethodDispatcherBuildMap(Type interfaceType, MethodDispatcherBuildResult result) + { + this.methodDispatcherBuildResultMap.Add(interfaceType, result); + } + + protected bool TryGetMethodDispatcher( + Type interfaceType, + out MethodDispatcherBuildResult builderMethodDispatcher) + { + if (this.methodDispatcherBuildResultMap.TryGetValue(interfaceType, out var result)) + { + { + builderMethodDispatcher = result; + return true; + } + } + + builderMethodDispatcher = null; + return false; + } + + protected void UpdateProxyGeneratorMap(Type interfaceType, ActorProxyGeneratorBuildResult result) + { + this.proxyGeneratorBuildResultMap.Add(interfaceType, result); + } + + protected bool TryGetProxyGenerator(Type interfaceType, out ActorProxyGeneratorBuildResult orBuildProxyGenerator) + { + if (this.proxyGeneratorBuildResultMap.TryGetValue(interfaceType, out var result)) + { + { + orBuildProxyGenerator = result; + return true; + } + } + + orBuildProxyGenerator = null; + return false; + } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Builder/ActorCodeBuilderNames.cs b/src/Dapr.Actors/Builder/ActorCodeBuilderNames.cs index 5bf2ffd2..cbc38e76 100644 --- a/src/Dapr.Actors/Builder/ActorCodeBuilderNames.cs +++ b/src/Dapr.Actors/Builder/ActorCodeBuilderNames.cs @@ -11,103 +11,102 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Builder +namespace Dapr.Actors.Builder; + +using System; +using System.Globalization; + +internal class ActorCodeBuilderNames : ICodeBuilderNames { - using System; - using System.Globalization; + private readonly string namePrefix; - internal class ActorCodeBuilderNames : ICodeBuilderNames + public ActorCodeBuilderNames() + : this("actor") { - private readonly string namePrefix; - - public ActorCodeBuilderNames() - : this("actor") - { - } - - public ActorCodeBuilderNames(string namePrefix) - { - this.namePrefix = "actor" + namePrefix; - } - - public string InterfaceId - { - get { return "interfaceId"; } - } - - public string MethodId - { - get { return "methodId"; } - } - - public string RetVal - { - get { return "retVal"; } - } - - public string RequestBody - { - get { return "requestBody"; } - } - - public string GetMethodBodyTypesAssemblyName(Type interfaceType) - { - return string.Format(CultureInfo.InvariantCulture, "{0}_.{1}.mt", interfaceType.FullName, this.namePrefix); - } - - public string GetMethodBodyTypesAssemblyNamespace(Type interfaceType) - { - return string.Format(CultureInfo.InvariantCulture, "{0}_.{1}.mt", interfaceType.FullName, this.namePrefix); - } - - public string GetRequestBodyTypeName(string methodName) - { - return string.Format(CultureInfo.InvariantCulture, "{0}ReqBody", methodName); - } - - public string GetResponseBodyTypeName(string methodName) - { - return string.Format(CultureInfo.InvariantCulture, "{0}RespBody", methodName); - } - - public string GetMethodDispatcherAssemblyName(Type interfaceType) - { - return string.Format(CultureInfo.InvariantCulture, "{0}_.{1}.disp", interfaceType.FullName, this.namePrefix); - } - - public string GetMethodDispatcherAssemblyNamespace(Type interfaceType) - { - return string.Format(CultureInfo.InvariantCulture, "{0}_.{1}.disp", interfaceType.FullName, this.namePrefix); - } - - public string GetMethodDispatcherClassName(Type interfaceType) - { - return string.Format(CultureInfo.InvariantCulture, "{0}MethodDispatcher", interfaceType.Name); - } - - public string GetProxyAssemblyName(Type interfaceType) - { - return string.Format(CultureInfo.InvariantCulture, "{0}_.{1}.proxy", interfaceType.FullName, this.namePrefix); - } - - public string GetProxyAssemblyNamespace(Type interfaceType) - { - return string.Format(CultureInfo.InvariantCulture, "{0}_.{1}.proxy", interfaceType.FullName, this.namePrefix); - } - - public string GetProxyClassName(Type interfaceType) - { - return string.Format(CultureInfo.InvariantCulture, "{0}{1}Proxy", interfaceType.Name, this.namePrefix); - } - - public string GetProxyActivatorClassName(Type interfaceType) - { - return string.Format(CultureInfo.InvariantCulture, "{0}{1}ProxyActivator", interfaceType.Name, this.namePrefix); - } - - public string GetDataContractNamespace() - { - return Constants.Namespace; - } } -} + + public ActorCodeBuilderNames(string namePrefix) + { + this.namePrefix = "actor" + namePrefix; + } + + public string InterfaceId + { + get { return "interfaceId"; } + } + + public string MethodId + { + get { return "methodId"; } + } + + public string RetVal + { + get { return "retVal"; } + } + + public string RequestBody + { + get { return "requestBody"; } + } + + public string GetMethodBodyTypesAssemblyName(Type interfaceType) + { + return string.Format(CultureInfo.InvariantCulture, "{0}_.{1}.mt", interfaceType.FullName, this.namePrefix); + } + + public string GetMethodBodyTypesAssemblyNamespace(Type interfaceType) + { + return string.Format(CultureInfo.InvariantCulture, "{0}_.{1}.mt", interfaceType.FullName, this.namePrefix); + } + + public string GetRequestBodyTypeName(string methodName) + { + return string.Format(CultureInfo.InvariantCulture, "{0}ReqBody", methodName); + } + + public string GetResponseBodyTypeName(string methodName) + { + return string.Format(CultureInfo.InvariantCulture, "{0}RespBody", methodName); + } + + public string GetMethodDispatcherAssemblyName(Type interfaceType) + { + return string.Format(CultureInfo.InvariantCulture, "{0}_.{1}.disp", interfaceType.FullName, this.namePrefix); + } + + public string GetMethodDispatcherAssemblyNamespace(Type interfaceType) + { + return string.Format(CultureInfo.InvariantCulture, "{0}_.{1}.disp", interfaceType.FullName, this.namePrefix); + } + + public string GetMethodDispatcherClassName(Type interfaceType) + { + return string.Format(CultureInfo.InvariantCulture, "{0}MethodDispatcher", interfaceType.Name); + } + + public string GetProxyAssemblyName(Type interfaceType) + { + return string.Format(CultureInfo.InvariantCulture, "{0}_.{1}.proxy", interfaceType.FullName, this.namePrefix); + } + + public string GetProxyAssemblyNamespace(Type interfaceType) + { + return string.Format(CultureInfo.InvariantCulture, "{0}_.{1}.proxy", interfaceType.FullName, this.namePrefix); + } + + public string GetProxyClassName(Type interfaceType) + { + return string.Format(CultureInfo.InvariantCulture, "{0}{1}Proxy", interfaceType.Name, this.namePrefix); + } + + public string GetProxyActivatorClassName(Type interfaceType) + { + return string.Format(CultureInfo.InvariantCulture, "{0}{1}ProxyActivator", interfaceType.Name, this.namePrefix); + } + + public string GetDataContractNamespace() + { + return Constants.Namespace; + } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Builder/ActorMethodDispatcherBase.cs b/src/Dapr.Actors/Builder/ActorMethodDispatcherBase.cs index 6a8c1956..8eb5237c 100644 --- a/src/Dapr.Actors/Builder/ActorMethodDispatcherBase.cs +++ b/src/Dapr.Actors/Builder/ActorMethodDispatcherBase.cs @@ -11,236 +11,235 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Builder +namespace Dapr.Actors.Builder; + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Threading; +using System.Threading.Tasks; +using Dapr.Actors.Communication; +using Dapr.Actors.Description; +using Dapr.Actors.Resources; + +/// +/// The class is used by actor remoting code generator to generate a type that dispatches requests to actor +/// object by invoking right method on it. +/// +public abstract class ActorMethodDispatcherBase { - using System; - using System.Collections.Generic; - using System.Globalization; - using System.Threading; - using System.Threading.Tasks; - using Dapr.Actors.Communication; - using Dapr.Actors.Description; - using Dapr.Actors.Resources; + private IReadOnlyDictionary methodNameMap; /// - /// The class is used by actor remoting code generator to generate a type that dispatches requests to actor - /// object by invoking right method on it. + /// Gets the id of the interface supported by this method dispatcher. /// - public abstract class ActorMethodDispatcherBase + public int InterfaceId { get; private set; } + + /// + /// Why we pass IMessageBodyFactory to this function instead of + /// setting at class level?. Since we cache MethodDispatcher for each interface, + /// we can't set IMessageBodyFactory at class level. + /// These can be cases where multiple IMessageBodyFactory implmenetation but single dispatcher class. + /// This method is used to dispatch request to the specified methodId of the + /// interface implemented by the remoted object. + /// + /// The object impplemented the remoted interface. + /// Id of the method to which to dispatch the request to. + /// The body of the request object that needs to be dispatched to the object. + /// IMessageBodyFactory implementaion. + /// The cancellation token that will be signaled if this operation is cancelled. + /// A task that represents the outstanding asynchronous call to the implementation object. + /// The return value of the task contains the returned value from the invoked method. + public Task DispatchAsync( + object objectImplementation, + int methodId, + IActorRequestMessageBody requestBody, + IActorMessageBodyFactory remotingMessageBodyFactory, + CancellationToken cancellationToken) { - private IReadOnlyDictionary methodNameMap; + cancellationToken.ThrowIfCancellationRequested(); - /// - /// Gets the id of the interface supported by this method dispatcher. - /// - public int InterfaceId { get; private set; } + var dispatchTask = this.OnDispatchAsync( + methodId, + objectImplementation, + requestBody, + remotingMessageBodyFactory, + cancellationToken); - /// - /// Why we pass IMessageBodyFactory to this function instead of - /// setting at class level?. Since we cache MethodDispatcher for each interface, - /// we can't set IMessageBodyFactory at class level. - /// These can be cases where multiple IMessageBodyFactory implmenetation but single dispatcher class. - /// This method is used to dispatch request to the specified methodId of the - /// interface implemented by the remoted object. - /// - /// The object impplemented the remoted interface. - /// Id of the method to which to dispatch the request to. - /// The body of the request object that needs to be dispatched to the object. - /// IMessageBodyFactory implementaion. - /// The cancellation token that will be signaled if this operation is cancelled. - /// A task that represents the outstanding asynchronous call to the implementation object. - /// The return value of the task contains the returned value from the invoked method. - public Task DispatchAsync( - object objectImplementation, - int methodId, - IActorRequestMessageBody requestBody, - IActorMessageBodyFactory remotingMessageBodyFactory, - CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - var dispatchTask = this.OnDispatchAsync( - methodId, - objectImplementation, - requestBody, - remotingMessageBodyFactory, - cancellationToken); - - return dispatchTask; - } - - /// - /// This method is used to dispatch one way messages to the specified methodId of the - /// interface implemented by the remoted object. - /// - /// The object implemented the remoted interface. - /// Id of the method to which to dispatch the request to. - /// The body of the request object that needs to be dispatched to the remoting implementation. - public void Dispatch(object objectImplementation, int methodId, IActorRequestMessageBody requestMessageBody) - { - this.OnDispatch(methodId, objectImplementation, requestMessageBody); - } - - /// - /// Gets the name of the method that has the specified methodId. - /// - /// The id of the method. - /// The name of the method corresponding to the specified method id. - public string GetMethodName(int methodId) - { - if (!this.methodNameMap.TryGetValue(methodId, out var methodName)) - { - throw new MissingMethodException(string.Format( - CultureInfo.CurrentCulture, - SR.ErrorMissingMethod, - methodId, - this.InterfaceId)); - } - - return methodName; - } - - internal void Initialize(InterfaceDescription description, IReadOnlyDictionary methodMap) - { - this.SetInterfaceId(description.Id); - this.SetMethodNameMap(methodMap); - } - - internal void SetInterfaceId(int interfaceId) - { - this.InterfaceId = interfaceId; - } - - internal void SetMethodNameMap(IReadOnlyDictionary methodNameMap) - { - this.methodNameMap = methodNameMap; - } - - /// - /// This method is used to create the remoting response from the specified return value. - /// - /// Interface Name of the remoting Interface. - /// Method Name of the remoting method. - /// MethodId of the remoting method. - /// MessageFactory for the remoting Interface. - /// Response returned by remoting method. - /// Actor Response Message Body. - protected IActorResponseMessageBody CreateResponseMessageBody( - string interfaceName, - string methodName, - int methodId, - IActorMessageBodyFactory remotingMessageBodyFactory, - object response) - { - var msg = remotingMessageBodyFactory.CreateResponseMessageBody( - interfaceName, - methodName, - this.CreateWrappedResponseBody(methodId, response)); - - if (!(msg is WrappedMessage)) - { - msg.Set(response); - } - - return msg; - } - - /// - /// This method is implemented by the generated method dispatcher to dispatch request to the specified methodId of the - /// interface implemented by the remoted object. - /// - /// Id of the method. - /// The remoted object instance. - /// Request body. - /// Remoting Message Body Factory implementation needed for creating response object. - /// Cancellation token. - /// - /// A Task that represents outstanding operation. - /// The result of the task is the return value from the method. - /// - protected abstract Task OnDispatchAsync( - int methodId, - object remotedObject, - IActorRequestMessageBody requestBody, - IActorMessageBodyFactory remotingMessageBodyFactory, - CancellationToken cancellationToken); - - /// - /// This method is implemented by the generated method dispatcher to dispatch one way messages to the specified methodId of the - /// interface implemented by the remoted object. - /// - /// Id of the method. - /// The remoted object instance. - /// Request body. - protected abstract void OnDispatch(int methodId, object remotedObject, IActorRequestMessageBody requestBody); - - /// - /// Internal - used by Service remoting. - /// - /// Interface Name of the remoting Interface. - /// Method Name of the remoting method. - /// MethodId of the remoting method. - /// MessageFactory for the remoting Interface. - /// continuation task. - /// - /// A Task that represents outstanding operation. - /// - /// The response type for the remoting method. - protected Task ContinueWithResult( - string interfaceName, - string methodName, - int methodId, - IActorMessageBodyFactory remotingMessageBodyFactory, - Task task) - { - return task.ContinueWith( - t => this.CreateResponseMessageBody(interfaceName, methodName, methodId, remotingMessageBodyFactory, t.GetAwaiter().GetResult()), - TaskContinuationOptions.ExecuteSynchronously); - } - - /// - /// Internal - used by remoting. - /// - /// continuation task. - /// - /// A Task that represents outstanding operation. - /// - protected Task ContinueWith(Task task) - { - return task.ContinueWith( - t => - { - t.GetAwaiter().GetResult(); - return null; - }, - TaskContinuationOptions.ExecuteSynchronously); - } - - /// Internal - used by remoting - /// - /// This checks if we are wrapping actor message body or not. - /// - /// Actor Request Message Body. - /// true or false. - protected bool CheckIfItsWrappedRequest(IActorRequestMessageBody requestMessageBody) - { - if (requestMessageBody is WrappedMessage) - { - return true; - } - - return false; - } - - /// - /// Creates Wrapped Response Object for a method. - /// - /// MethodId of the remoting method. - /// Response for a method. - /// Wrapped Ressponse object. - // Generated By Code-gen - protected abstract object CreateWrappedResponseBody( - int methodId, - object retVal); + return dispatchTask; } -} + + /// + /// This method is used to dispatch one way messages to the specified methodId of the + /// interface implemented by the remoted object. + /// + /// The object implemented the remoted interface. + /// Id of the method to which to dispatch the request to. + /// The body of the request object that needs to be dispatched to the remoting implementation. + public void Dispatch(object objectImplementation, int methodId, IActorRequestMessageBody requestMessageBody) + { + this.OnDispatch(methodId, objectImplementation, requestMessageBody); + } + + /// + /// Gets the name of the method that has the specified methodId. + /// + /// The id of the method. + /// The name of the method corresponding to the specified method id. + public string GetMethodName(int methodId) + { + if (!this.methodNameMap.TryGetValue(methodId, out var methodName)) + { + throw new MissingMethodException(string.Format( + CultureInfo.CurrentCulture, + SR.ErrorMissingMethod, + methodId, + this.InterfaceId)); + } + + return methodName; + } + + internal void Initialize(InterfaceDescription description, IReadOnlyDictionary methodMap) + { + this.SetInterfaceId(description.Id); + this.SetMethodNameMap(methodMap); + } + + internal void SetInterfaceId(int interfaceId) + { + this.InterfaceId = interfaceId; + } + + internal void SetMethodNameMap(IReadOnlyDictionary methodNameMap) + { + this.methodNameMap = methodNameMap; + } + + /// + /// This method is used to create the remoting response from the specified return value. + /// + /// Interface Name of the remoting Interface. + /// Method Name of the remoting method. + /// MethodId of the remoting method. + /// MessageFactory for the remoting Interface. + /// Response returned by remoting method. + /// Actor Response Message Body. + protected IActorResponseMessageBody CreateResponseMessageBody( + string interfaceName, + string methodName, + int methodId, + IActorMessageBodyFactory remotingMessageBodyFactory, + object response) + { + var msg = remotingMessageBodyFactory.CreateResponseMessageBody( + interfaceName, + methodName, + this.CreateWrappedResponseBody(methodId, response)); + + if (!(msg is WrappedMessage)) + { + msg.Set(response); + } + + return msg; + } + + /// + /// This method is implemented by the generated method dispatcher to dispatch request to the specified methodId of the + /// interface implemented by the remoted object. + /// + /// Id of the method. + /// The remoted object instance. + /// Request body. + /// Remoting Message Body Factory implementation needed for creating response object. + /// Cancellation token. + /// + /// A Task that represents outstanding operation. + /// The result of the task is the return value from the method. + /// + protected abstract Task OnDispatchAsync( + int methodId, + object remotedObject, + IActorRequestMessageBody requestBody, + IActorMessageBodyFactory remotingMessageBodyFactory, + CancellationToken cancellationToken); + + /// + /// This method is implemented by the generated method dispatcher to dispatch one way messages to the specified methodId of the + /// interface implemented by the remoted object. + /// + /// Id of the method. + /// The remoted object instance. + /// Request body. + protected abstract void OnDispatch(int methodId, object remotedObject, IActorRequestMessageBody requestBody); + + /// + /// Internal - used by Service remoting. + /// + /// Interface Name of the remoting Interface. + /// Method Name of the remoting method. + /// MethodId of the remoting method. + /// MessageFactory for the remoting Interface. + /// continuation task. + /// + /// A Task that represents outstanding operation. + /// + /// The response type for the remoting method. + protected Task ContinueWithResult( + string interfaceName, + string methodName, + int methodId, + IActorMessageBodyFactory remotingMessageBodyFactory, + Task task) + { + return task.ContinueWith( + t => this.CreateResponseMessageBody(interfaceName, methodName, methodId, remotingMessageBodyFactory, t.GetAwaiter().GetResult()), + TaskContinuationOptions.ExecuteSynchronously); + } + + /// + /// Internal - used by remoting. + /// + /// continuation task. + /// + /// A Task that represents outstanding operation. + /// + protected Task ContinueWith(Task task) + { + return task.ContinueWith( + t => + { + t.GetAwaiter().GetResult(); + return null; + }, + TaskContinuationOptions.ExecuteSynchronously); + } + + /// Internal - used by remoting + /// + /// This checks if we are wrapping actor message body or not. + /// + /// Actor Request Message Body. + /// true or false. + protected bool CheckIfItsWrappedRequest(IActorRequestMessageBody requestMessageBody) + { + if (requestMessageBody is WrappedMessage) + { + return true; + } + + return false; + } + + /// + /// Creates Wrapped Response Object for a method. + /// + /// MethodId of the remoting method. + /// Response for a method. + /// Wrapped Ressponse object. + // Generated By Code-gen + protected abstract object CreateWrappedResponseBody( + int methodId, + object retVal); +} \ No newline at end of file diff --git a/src/Dapr.Actors/Builder/ActorProxyGenerator.cs b/src/Dapr.Actors/Builder/ActorProxyGenerator.cs index 6b4ad8e2..8829ff95 100644 --- a/src/Dapr.Actors/Builder/ActorProxyGenerator.cs +++ b/src/Dapr.Actors/Builder/ActorProxyGenerator.cs @@ -11,28 +11,27 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Builder +namespace Dapr.Actors.Builder; + +using System; +using Dapr.Actors.Client; + +internal class ActorProxyGenerator { - using System; - using Dapr.Actors.Client; + private readonly IProxyActivator proxyActivator; - internal class ActorProxyGenerator + public ActorProxyGenerator( + Type proxyInterfaceType, + IProxyActivator proxyActivator) { - private readonly IProxyActivator proxyActivator; - - public ActorProxyGenerator( - Type proxyInterfaceType, - IProxyActivator proxyActivator) - { - this.proxyActivator = proxyActivator; - this.ProxyInterfaceType = proxyInterfaceType; - } - - public Type ProxyInterfaceType { get; } - - public ActorProxy CreateActorProxy() - { - return (ActorProxy)this.proxyActivator.CreateInstance(); - } + this.proxyActivator = proxyActivator; + this.ProxyInterfaceType = proxyInterfaceType; } -} + + public Type ProxyInterfaceType { get; } + + public ActorProxy CreateActorProxy() + { + return (ActorProxy)this.proxyActivator.CreateInstance(); + } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Builder/ActorProxyGeneratorBuildResult.cs b/src/Dapr.Actors/Builder/ActorProxyGeneratorBuildResult.cs index 965a08e6..3bdf54cc 100644 --- a/src/Dapr.Actors/Builder/ActorProxyGeneratorBuildResult.cs +++ b/src/Dapr.Actors/Builder/ActorProxyGeneratorBuildResult.cs @@ -11,21 +11,20 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Builder +namespace Dapr.Actors.Builder; + +using System; + +internal class ActorProxyGeneratorBuildResult : BuildResult { - using System; - - internal class ActorProxyGeneratorBuildResult : BuildResult + public ActorProxyGeneratorBuildResult(CodeBuilderContext buildContext) + : base(buildContext) { - public ActorProxyGeneratorBuildResult(CodeBuilderContext buildContext) - : base(buildContext) - { - } - - public Type ProxyType { get; set; } - - public Type ProxyActivatorType { get; set; } - - public ActorProxyGenerator ProxyGenerator { get; set; } } -} + + public Type ProxyType { get; set; } + + public Type ProxyActivatorType { get; set; } + + public ActorProxyGenerator ProxyGenerator { get; set; } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Builder/ActorProxyGeneratorBuilder.cs b/src/Dapr.Actors/Builder/ActorProxyGeneratorBuilder.cs index a62607fc..fc911c58 100644 --- a/src/Dapr.Actors/Builder/ActorProxyGeneratorBuilder.cs +++ b/src/Dapr.Actors/Builder/ActorProxyGeneratorBuilder.cs @@ -11,559 +11,558 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Builder +namespace Dapr.Actors.Builder; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using System.Threading; +using System.Threading.Tasks; +using Dapr.Actors.Client; +using Dapr.Actors.Communication; +using Dapr.Actors.Description; + +internal class ActorProxyGeneratorBuilder : CodeBuilderModule { - using System; - using System.Collections.Generic; - using System.Linq; - using System.Reflection; - using System.Reflection.Emit; - using System.Threading; - using System.Threading.Tasks; - using Dapr.Actors.Client; - using Dapr.Actors.Communication; - using Dapr.Actors.Description; + private readonly Type proxyBaseType; + private readonly MethodInfo createMessage; + private readonly MethodInfo invokeAsyncMethodInfo; + private readonly MethodInfo invokeMethodInfo; + private readonly MethodInfo continueWithResultMethodInfo; + private readonly MethodInfo continueWithMethodInfo; + private readonly MethodInfo checkIfitsWrapped; - internal class ActorProxyGeneratorBuilder : CodeBuilderModule + public ActorProxyGeneratorBuilder(ICodeBuilder codeBuilder) + : base(codeBuilder) { - private readonly Type proxyBaseType; - private readonly MethodInfo createMessage; - private readonly MethodInfo invokeAsyncMethodInfo; - private readonly MethodInfo invokeMethodInfo; - private readonly MethodInfo continueWithResultMethodInfo; - private readonly MethodInfo continueWithMethodInfo; - private readonly MethodInfo checkIfitsWrapped; + this.proxyBaseType = typeof(ActorProxy); - public ActorProxyGeneratorBuilder(ICodeBuilder codeBuilder) - : base(codeBuilder) - { - this.proxyBaseType = typeof(ActorProxy); + // TODO Should this search change to BindingFlags.NonPublic + this.invokeAsyncMethodInfo = this.proxyBaseType.GetMethod( + "InvokeMethodAsync", + BindingFlags.Instance | BindingFlags.NonPublic, + null, + CallingConventions.Any, + new[] { typeof(int), typeof(int), typeof(string), typeof(IActorRequestMessageBody), typeof(CancellationToken) }, + null); - // TODO Should this search change to BindingFlags.NonPublic - this.invokeAsyncMethodInfo = this.proxyBaseType.GetMethod( - "InvokeMethodAsync", - BindingFlags.Instance | BindingFlags.NonPublic, - null, - CallingConventions.Any, - new[] { typeof(int), typeof(int), typeof(string), typeof(IActorRequestMessageBody), typeof(CancellationToken) }, - null); + this.checkIfitsWrapped = this.proxyBaseType.GetMethod( + "CheckIfItsWrappedRequest", + BindingFlags.Instance | BindingFlags.NonPublic, + null, + CallingConventions.Any, + new[] { typeof(IActorRequestMessageBody) }, + null); - this.checkIfitsWrapped = this.proxyBaseType.GetMethod( - "CheckIfItsWrappedRequest", - BindingFlags.Instance | BindingFlags.NonPublic, - null, - CallingConventions.Any, - new[] { typeof(IActorRequestMessageBody) }, - null); + this.createMessage = this.proxyBaseType.GetMethod( + "CreateRequestMessageBody", + BindingFlags.Instance | BindingFlags.NonPublic, + null, + CallingConventions.Any, + new[] { typeof(string), typeof(string), typeof(int), typeof(object) }, + null); - this.createMessage = this.proxyBaseType.GetMethod( - "CreateRequestMessageBody", - BindingFlags.Instance | BindingFlags.NonPublic, - null, - CallingConventions.Any, - new[] { typeof(string), typeof(string), typeof(int), typeof(object) }, - null); + this.invokeMethodInfo = this.proxyBaseType.GetMethod( + "Invoke", + BindingFlags.Instance | BindingFlags.NonPublic, + null, + CallingConventions.Any, + new[] { typeof(int), typeof(int), typeof(IActorRequestMessageBody) }, + null); - this.invokeMethodInfo = this.proxyBaseType.GetMethod( - "Invoke", - BindingFlags.Instance | BindingFlags.NonPublic, - null, - CallingConventions.Any, - new[] { typeof(int), typeof(int), typeof(IActorRequestMessageBody) }, - null); + this.continueWithResultMethodInfo = this.proxyBaseType.GetMethod( + "ContinueWithResult", + BindingFlags.Instance | BindingFlags.NonPublic, + null, + CallingConventions.Any, + new[] { typeof(int), typeof(int), typeof(Task) }, + null); - this.continueWithResultMethodInfo = this.proxyBaseType.GetMethod( - "ContinueWithResult", - BindingFlags.Instance | BindingFlags.NonPublic, - null, - CallingConventions.Any, - new[] { typeof(int), typeof(int), typeof(Task) }, - null); + this.continueWithMethodInfo = this.proxyBaseType.GetMethod( + "ContinueWith", + BindingFlags.Instance | BindingFlags.NonPublic); + } - this.continueWithMethodInfo = this.proxyBaseType.GetMethod( - "ContinueWith", - BindingFlags.Instance | BindingFlags.NonPublic); - } + protected Type ProxyBaseType => this.proxyBaseType; - protected Type ProxyBaseType => this.proxyBaseType; + public ActorProxyGeneratorBuildResult Build( + Type proxyInterfaceType, + IEnumerable interfaceDescriptions) + { + // create the context to build the proxy + var context = new CodeBuilderContext( + assemblyName: this.CodeBuilder.Names.GetProxyAssemblyName(proxyInterfaceType), + assemblyNamespace: this.CodeBuilder.Names.GetProxyAssemblyNamespace(proxyInterfaceType), + enableDebugging: CodeBuilderAttribute.IsDebuggingEnabled(proxyInterfaceType)); + var result = new ActorProxyGeneratorBuildResult(context); - public ActorProxyGeneratorBuildResult Build( - Type proxyInterfaceType, - IEnumerable interfaceDescriptions) - { - // create the context to build the proxy - var context = new CodeBuilderContext( - assemblyName: this.CodeBuilder.Names.GetProxyAssemblyName(proxyInterfaceType), - assemblyNamespace: this.CodeBuilder.Names.GetProxyAssemblyNamespace(proxyInterfaceType), - enableDebugging: CodeBuilderAttribute.IsDebuggingEnabled(proxyInterfaceType)); - var result = new ActorProxyGeneratorBuildResult(context); + // ensure that method data types are built for each of the remote interfaces + var methodBodyTypesResultsMap = interfaceDescriptions.ToDictionary( + d => d, + d => this.CodeBuilder.GetOrBuildMethodBodyTypes(d.InterfaceType)); - // ensure that method data types are built for each of the remote interfaces - var methodBodyTypesResultsMap = interfaceDescriptions.ToDictionary( - d => d, - d => this.CodeBuilder.GetOrBuildMethodBodyTypes(d.InterfaceType)); + // build the proxy class that implements all of the interfaces explicitly + result.ProxyType = this.BuildProxyType(context, proxyInterfaceType, methodBodyTypesResultsMap); - // build the proxy class that implements all of the interfaces explicitly - result.ProxyType = this.BuildProxyType(context, proxyInterfaceType, methodBodyTypesResultsMap); + // build the activator type to create instances of the proxy + result.ProxyActivatorType = this.BuildProxyActivatorType(context, proxyInterfaceType, result.ProxyType); - // build the activator type to create instances of the proxy - result.ProxyActivatorType = this.BuildProxyActivatorType(context, proxyInterfaceType, result.ProxyType); + // build the proxy generator + result.ProxyGenerator = this.CreateProxyGenerator( + proxyInterfaceType, + result.ProxyActivatorType); - // build the proxy generator - result.ProxyGenerator = this.CreateProxyGenerator( - proxyInterfaceType, - result.ProxyActivatorType); + context.Complete(); + return result; + } - context.Complete(); - return result; - } - - internal static LocalBuilder CreateWrappedRequestBody( + internal static LocalBuilder CreateWrappedRequestBody( MethodDescription methodDescription, MethodBodyTypes methodBodyTypes, ILGenerator ilGen, ParameterInfo[] parameters) + { + var parameterLength = parameters.Length; + if (methodDescription.HasCancellationToken) { - var parameterLength = parameters.Length; + // Cancellation token is tracked locally and should not be serialized and sent + // as a part of the request body. + parameterLength -= 1; + } + + if (parameterLength == 0) + { + return null; + } + + LocalBuilder wrappedRequestBody = ilGen.DeclareLocal(methodBodyTypes.RequestBodyType); + var requestBodyCtor = methodBodyTypes.RequestBodyType.GetConstructor(Type.EmptyTypes); + + if (requestBodyCtor != null) + { + ilGen.Emit(OpCodes.Newobj, requestBodyCtor); + ilGen.Emit(OpCodes.Stloc, wrappedRequestBody); + + var argsLength = parameters.Length; if (methodDescription.HasCancellationToken) { // Cancellation token is tracked locally and should not be serialized and sent // as a part of the request body. - parameterLength -= 1; + argsLength -= 1; } - if (parameterLength == 0) + for (var i = 0; i < argsLength; i++) { - return null; + ilGen.Emit(OpCodes.Ldloc, wrappedRequestBody); + ilGen.Emit(OpCodes.Ldarg, i + 1); + ilGen.Emit(OpCodes.Stfld, methodBodyTypes.RequestBodyType.GetField(parameters[i].Name)); } - - LocalBuilder wrappedRequestBody = ilGen.DeclareLocal(methodBodyTypes.RequestBodyType); - var requestBodyCtor = methodBodyTypes.RequestBodyType.GetConstructor(Type.EmptyTypes); - - if (requestBodyCtor != null) - { - ilGen.Emit(OpCodes.Newobj, requestBodyCtor); - ilGen.Emit(OpCodes.Stloc, wrappedRequestBody); - - var argsLength = parameters.Length; - if (methodDescription.HasCancellationToken) - { - // Cancellation token is tracked locally and should not be serialized and sent - // as a part of the request body. - argsLength -= 1; - } - - for (var i = 0; i < argsLength; i++) - { - ilGen.Emit(OpCodes.Ldloc, wrappedRequestBody); - ilGen.Emit(OpCodes.Ldarg, i + 1); - ilGen.Emit(OpCodes.Stfld, methodBodyTypes.RequestBodyType.GetField(parameters[i].Name)); - } - } - - return wrappedRequestBody; } - internal void AddVoidMethodImplementation( - ILGenerator ilGen, - int interfaceDescriptionId, - MethodDescription methodDescription, - LocalBuilder wrappedRequestBody, - string interfaceName) + return wrappedRequestBody; + } + + internal void AddVoidMethodImplementation( + ILGenerator ilGen, + int interfaceDescriptionId, + MethodDescription methodDescription, + LocalBuilder wrappedRequestBody, + string interfaceName) + { + var interfaceMethod = methodDescription.MethodInfo; + + var parameters = interfaceMethod.GetParameters(); + + LocalBuilder requestBody = null; + + if (parameters.Length > 0) { - var interfaceMethod = methodDescription.MethodInfo; + // create IServiceRemotingRequestMessageBody message + requestBody = this.CreateRequestRemotingMessageBody( + methodDescription, + interfaceName, + ilGen, + parameters.Length, + wrappedRequestBody); - var parameters = interfaceMethod.GetParameters(); - - LocalBuilder requestBody = null; - - if (parameters.Length > 0) - { - // create IServiceRemotingRequestMessageBody message - requestBody = this.CreateRequestRemotingMessageBody( - methodDescription, - interfaceName, - ilGen, - parameters.Length, - wrappedRequestBody); - - // Check if requestMessage is not implementing WrappedMessage , then call SetParam - this.SetParameterIfNeeded(ilGen, requestBody, parameters.Length, parameters); - } - - // call the base Invoke method - ilGen.Emit(OpCodes.Ldarg_0); // base - ilGen.Emit(OpCodes.Ldc_I4, interfaceDescriptionId); // interfaceId - ilGen.Emit(OpCodes.Ldc_I4, methodDescription.Id); // methodId - - if (parameters.Length > 0) - { - ilGen.Emit(OpCodes.Ldloc, requestBody); - } - else - { - ilGen.Emit(OpCodes.Ldnull); - } - - ilGen.EmitCall(OpCodes.Call, this.invokeMethodInfo, null); + // Check if requestMessage is not implementing WrappedMessage , then call SetParam + this.SetParameterIfNeeded(ilGen, requestBody, parameters.Length, parameters); } - protected void AddInterfaceImplementations( - TypeBuilder classBuilder, - IDictionary methodBodyTypesResultsMap) + // call the base Invoke method + ilGen.Emit(OpCodes.Ldarg_0); // base + ilGen.Emit(OpCodes.Ldc_I4, interfaceDescriptionId); // interfaceId + ilGen.Emit(OpCodes.Ldc_I4, methodDescription.Id); // methodId + + if (parameters.Length > 0) { - foreach (var item in methodBodyTypesResultsMap) + ilGen.Emit(OpCodes.Ldloc, requestBody); + } + else + { + ilGen.Emit(OpCodes.Ldnull); + } + + ilGen.EmitCall(OpCodes.Call, this.invokeMethodInfo, null); + } + + protected void AddInterfaceImplementations( + TypeBuilder classBuilder, + IDictionary methodBodyTypesResultsMap) + { + foreach (var item in methodBodyTypesResultsMap) + { + var interfaceDescription = item.Key; + var methodBodyTypesMap = item.Value.MethodBodyTypesMap; + + foreach (var methodDescription in interfaceDescription.Methods) { - var interfaceDescription = item.Key; - var methodBodyTypesMap = item.Value.MethodBodyTypesMap; + var methodBodyTypes = methodBodyTypesMap[methodDescription.Name]; - foreach (var methodDescription in interfaceDescription.Methods) + if (TypeUtility.IsTaskType(methodDescription.ReturnType)) { - var methodBodyTypes = methodBodyTypesMap[methodDescription.Name]; - - if (TypeUtility.IsTaskType(methodDescription.ReturnType)) - { - this.AddAsyncMethodImplementation( - classBuilder, - interfaceDescription.Id, - methodDescription, - methodBodyTypes, - interfaceDescription.InterfaceType.FullName); - } - else if (TypeUtility.IsVoidType(methodDescription.ReturnType)) - { - this.AddVoidMethodImplementation( - classBuilder, - interfaceDescription.Id, - methodDescription, - methodBodyTypes, - interfaceDescription.InterfaceType.FullName); - } + this.AddAsyncMethodImplementation( + classBuilder, + interfaceDescription.Id, + methodDescription, + methodBodyTypes, + interfaceDescription.InterfaceType.FullName); + } + else if (TypeUtility.IsVoidType(methodDescription.ReturnType)) + { + this.AddVoidMethodImplementation( + classBuilder, + interfaceDescription.Id, + methodDescription, + methodBodyTypes, + interfaceDescription.InterfaceType.FullName); } } } + } - protected ActorProxyGenerator CreateProxyGenerator( - Type proxyInterfaceType, - Type proxyActivatorType) + protected ActorProxyGenerator CreateProxyGenerator( + Type proxyInterfaceType, + Type proxyActivatorType) + { + return new ActorProxyGenerator( + proxyInterfaceType, + (IProxyActivator)Activator.CreateInstance(proxyActivatorType)); + } + + private static void AddCreateInstanceMethod( + TypeBuilder classBuilder, + Type proxyType) + { + var methodBuilder = CodeBuilderUtils.CreatePublicMethodBuilder( + classBuilder, + "CreateInstance", + typeof(IActorProxy)); + + var ilGen = methodBuilder.GetILGenerator(); + var proxyCtor = proxyType.GetConstructor(Type.EmptyTypes); + if (proxyCtor != null) { - return new ActorProxyGenerator( - proxyInterfaceType, - (IProxyActivator)Activator.CreateInstance(proxyActivatorType)); + ilGen.Emit(OpCodes.Newobj, proxyCtor); + ilGen.Emit(OpCodes.Ret); } - - private static void AddCreateInstanceMethod( - TypeBuilder classBuilder, - Type proxyType) + else { - var methodBuilder = CodeBuilderUtils.CreatePublicMethodBuilder( - classBuilder, - "CreateInstance", - typeof(IActorProxy)); - - var ilGen = methodBuilder.GetILGenerator(); - var proxyCtor = proxyType.GetConstructor(Type.EmptyTypes); - if (proxyCtor != null) - { - ilGen.Emit(OpCodes.Newobj, proxyCtor); - ilGen.Emit(OpCodes.Ret); - } - else - { - ilGen.Emit(OpCodes.Ldnull); - ilGen.Emit(OpCodes.Ret); - } + ilGen.Emit(OpCodes.Ldnull); + ilGen.Emit(OpCodes.Ret); } + } - private Type BuildProxyActivatorType( - CodeBuilderContext context, - Type proxyInterfaceType, - Type proxyType) - { - var classBuilder = CodeBuilderUtils.CreateClassBuilder( - context.ModuleBuilder, - ns: context.AssemblyNamespace, - className: this.CodeBuilder.Names.GetProxyActivatorClassName(proxyInterfaceType), - interfaces: new[] { typeof(IProxyActivator) }); + private Type BuildProxyActivatorType( + CodeBuilderContext context, + Type proxyInterfaceType, + Type proxyType) + { + var classBuilder = CodeBuilderUtils.CreateClassBuilder( + context.ModuleBuilder, + ns: context.AssemblyNamespace, + className: this.CodeBuilder.Names.GetProxyActivatorClassName(proxyInterfaceType), + interfaces: new[] { typeof(IProxyActivator) }); - AddCreateInstanceMethod(classBuilder, proxyType); - return classBuilder.CreateTypeInfo().AsType(); - } + AddCreateInstanceMethod(classBuilder, proxyType); + return classBuilder.CreateTypeInfo().AsType(); + } - private void AddAsyncMethodImplementation( + private void AddAsyncMethodImplementation( TypeBuilder classBuilder, int interfaceId, MethodDescription methodDescription, MethodBodyTypes methodBodyTypes, string interfaceName) + { + var interfaceMethod = methodDescription.MethodInfo; + var parameters = interfaceMethod.GetParameters(); + var methodBuilder = CodeBuilderUtils.CreateExplitInterfaceMethodBuilder( + classBuilder, + interfaceMethod); + var ilGen = methodBuilder.GetILGenerator(); + var parameterLength = parameters.Length; + if (methodDescription.HasCancellationToken) { - var interfaceMethod = methodDescription.MethodInfo; - var parameters = interfaceMethod.GetParameters(); - var methodBuilder = CodeBuilderUtils.CreateExplitInterfaceMethodBuilder( - classBuilder, - interfaceMethod); - var ilGen = methodBuilder.GetILGenerator(); - var parameterLength = parameters.Length; - if (methodDescription.HasCancellationToken) - { - // Cancellation token is tracked locally and should not be serialized and sent - // as a part of the request body. - parameterLength -= 1; - } + // Cancellation token is tracked locally and should not be serialized and sent + // as a part of the request body. + parameterLength -= 1; + } - LocalBuilder requestMessage = null; - if (parameterLength > 0) - { - // Create Wrapped Message - // create requestBody and assign the values to its field from the arguments - var wrappedRequestBody = CreateWrappedRequestBody(methodDescription, methodBodyTypes, ilGen, parameters); + LocalBuilder requestMessage = null; + if (parameterLength > 0) + { + // Create Wrapped Message + // create requestBody and assign the values to its field from the arguments + var wrappedRequestBody = CreateWrappedRequestBody(methodDescription, methodBodyTypes, ilGen, parameters); - // create IServiceRemotingRequestMessageBody message - requestMessage = this.CreateRequestRemotingMessageBody(methodDescription, interfaceName, ilGen, parameterLength, wrappedRequestBody); + // create IServiceRemotingRequestMessageBody message + requestMessage = this.CreateRequestRemotingMessageBody(methodDescription, interfaceName, ilGen, parameterLength, wrappedRequestBody); - // Check if requestMessage is not implementing WrappedMessage , then call SetParam - this.SetParameterIfNeeded(ilGen, requestMessage, parameterLength, parameters); - } + // Check if requestMessage is not implementing WrappedMessage , then call SetParam + this.SetParameterIfNeeded(ilGen, requestMessage, parameterLength, parameters); + } - var objectTask = ilGen.DeclareLocal(typeof(Task)); + var objectTask = ilGen.DeclareLocal(typeof(Task)); - // call the base InvokeMethodAsync method - ilGen.Emit(OpCodes.Ldarg_0); // base + // call the base InvokeMethodAsync method + ilGen.Emit(OpCodes.Ldarg_0); // base + ilGen.Emit(OpCodes.Ldc_I4, interfaceId); // interfaceId + ilGen.Emit(OpCodes.Ldc_I4, methodDescription.Id); // methodId + ilGen.Emit(OpCodes.Ldstr, methodDescription.Name); // method name + + if (requestMessage != null) + { + ilGen.Emit(OpCodes.Ldloc, requestMessage); + } + else + { + ilGen.Emit(OpCodes.Ldnull); + } + + // Cancellation token argument + if (methodDescription.HasCancellationToken) + { + // Last argument should be the cancellation token + var cancellationTokenArgIndex = parameters.Length; + ilGen.Emit(OpCodes.Ldarg, cancellationTokenArgIndex); + } + else + { + var cancellationTokenNone = typeof(CancellationToken).GetMethod("get_None"); + ilGen.EmitCall(OpCodes.Call, cancellationTokenNone, null); + } + + ilGen.EmitCall(OpCodes.Call, this.invokeAsyncMethodInfo, null); + ilGen.Emit(OpCodes.Stloc, objectTask); + + // call the base method to get the continuation task and + // convert the response body to return value when the task is finished + if (TypeUtility.IsTaskType(methodDescription.ReturnType) && + methodDescription.ReturnType.GetTypeInfo().IsGenericType) + { + var retvalType = methodDescription.ReturnType.GetGenericArguments()[0]; + + ilGen.Emit(OpCodes.Ldarg_0); // base pointer ilGen.Emit(OpCodes.Ldc_I4, interfaceId); // interfaceId ilGen.Emit(OpCodes.Ldc_I4, methodDescription.Id); // methodId - ilGen.Emit(OpCodes.Ldstr, methodDescription.Name); // method name - - if (requestMessage != null) - { - ilGen.Emit(OpCodes.Ldloc, requestMessage); - } - else - { - ilGen.Emit(OpCodes.Ldnull); - } - - // Cancellation token argument - if (methodDescription.HasCancellationToken) - { - // Last argument should be the cancellation token - var cancellationTokenArgIndex = parameters.Length; - ilGen.Emit(OpCodes.Ldarg, cancellationTokenArgIndex); - } - else - { - var cancellationTokenNone = typeof(CancellationToken).GetMethod("get_None"); - ilGen.EmitCall(OpCodes.Call, cancellationTokenNone, null); - } - - ilGen.EmitCall(OpCodes.Call, this.invokeAsyncMethodInfo, null); - ilGen.Emit(OpCodes.Stloc, objectTask); - - // call the base method to get the continuation task and - // convert the response body to return value when the task is finished - if (TypeUtility.IsTaskType(methodDescription.ReturnType) && - methodDescription.ReturnType.GetTypeInfo().IsGenericType) - { - var retvalType = methodDescription.ReturnType.GetGenericArguments()[0]; - - ilGen.Emit(OpCodes.Ldarg_0); // base pointer - ilGen.Emit(OpCodes.Ldc_I4, interfaceId); // interfaceId - ilGen.Emit(OpCodes.Ldc_I4, methodDescription.Id); // methodId - ilGen.Emit(OpCodes.Ldloc, objectTask); // task - ilGen.Emit(OpCodes.Call, this.continueWithResultMethodInfo.MakeGenericMethod(retvalType)); - ilGen.Emit(OpCodes.Ret); // return base.ContinueWithResult(task); - } - else - { - ilGen.Emit(OpCodes.Ldarg_0); // base pointer - ilGen.Emit(OpCodes.Ldloc, objectTask); // task - ilGen.Emit(OpCodes.Call, this.continueWithMethodInfo); - ilGen.Emit(OpCodes.Ret); // return base.ContinueWith(task); - } + ilGen.Emit(OpCodes.Ldloc, objectTask); // task + ilGen.Emit(OpCodes.Call, this.continueWithResultMethodInfo.MakeGenericMethod(retvalType)); + ilGen.Emit(OpCodes.Ret); // return base.ContinueWithResult(task); } - - private Type BuildProxyType( - CodeBuilderContext context, - Type proxyInterfaceType, - IDictionary methodBodyTypesResultsMap) + else { - var classBuilder = CodeBuilderUtils.CreateClassBuilder( - context.ModuleBuilder, - ns: context.AssemblyNamespace, - className: this.CodeBuilder.Names.GetProxyClassName(proxyInterfaceType), - baseType: this.proxyBaseType, - interfaces: methodBodyTypesResultsMap.Select(item => item.Key.InterfaceType).ToArray()); - - this.AddGetReturnValueMethod(classBuilder, methodBodyTypesResultsMap); - this.AddInterfaceImplementations(classBuilder, methodBodyTypesResultsMap); - - return classBuilder.CreateTypeInfo().AsType(); - } - - private void AddGetReturnValueMethod( - TypeBuilder classBuilder, - IDictionary methodBodyTypesResultsMap) - { - var methodBuilder = CodeBuilderUtils.CreateProtectedMethodBuilder( - classBuilder, - "GetReturnValue", - typeof(object), // return value from the reponseBody - typeof(int), // interfaceId - typeof(int), // methodId - typeof(object)); // responseBody - - var ilGen = methodBuilder.GetILGenerator(); - - foreach (var item in methodBodyTypesResultsMap) - { - var interfaceDescription = item.Key; - var methodBodyTypesMap = item.Value.MethodBodyTypesMap; - - foreach (var methodDescription in interfaceDescription.Methods) - { - var methodBodyTypes = methodBodyTypesMap[methodDescription.Name]; - if (methodBodyTypes.ResponseBodyType == null) - { - continue; - } - - var elseLabel = ilGen.DefineLabel(); - - this.AddIfInterfaceIdAndMethodIdReturnRetvalBlock( - ilGen, - elseLabel, - interfaceDescription.Id, - methodDescription.Id, - methodBodyTypes.ResponseBodyType); - - ilGen.MarkLabel(elseLabel); - } - } - - // return null; (if method id's and interfaceId do not mGetReturnValueatch) - ilGen.Emit(OpCodes.Ldnull); - ilGen.Emit(OpCodes.Ret); - } - - private void SetParameterIfNeeded( - ILGenerator ilGen, - LocalBuilder requestMessage, - int parameterLength, - ParameterInfo[] parameters) - { - var boolres = ilGen.DeclareLocal(typeof(bool)); - var boolres2 = ilGen.DeclareLocal(typeof(bool)); - ilGen.Emit(OpCodes.Ldarg_0); // base - ilGen.Emit(OpCodes.Ldloc_1, requestMessage); - ilGen.Emit(OpCodes.Call, this.checkIfitsWrapped); - ilGen.Emit(OpCodes.Stloc, boolres); - ilGen.Emit(OpCodes.Ldloc_2); - ilGen.Emit(OpCodes.Ldc_I4_0); - ilGen.Emit(OpCodes.Ceq); - ilGen.Emit(OpCodes.Stloc, boolres2); - ilGen.Emit(OpCodes.Ldloc_3, boolres2); - var elseLabel = ilGen.DefineLabel(); - ilGen.Emit(OpCodes.Brfalse, elseLabel); - - // if false ,Call SetParamater - var setMethod = typeof(IActorRequestMessageBody).GetMethod("SetParameter"); - - // Add to Dictionary - for (var i = 0; i < parameterLength; i++) - { - ilGen.Emit(OpCodes.Ldloc, requestMessage); - ilGen.Emit(OpCodes.Ldc_I4, i); - ilGen.Emit(OpCodes.Ldstr, parameters[i].Name); - ilGen.Emit(OpCodes.Ldarg, i + 1); - if (!parameters[i].ParameterType.IsClass) - { - ilGen.Emit(OpCodes.Box, parameters[i].ParameterType); - } - - ilGen.Emit(OpCodes.Callvirt, setMethod); - } - - ilGen.MarkLabel(elseLabel); - } - - private LocalBuilder CreateRequestRemotingMessageBody( - MethodDescription methodDescription, - string interfaceName, - ILGenerator ilGen, - int parameterLength, - LocalBuilder wrappedRequestBody) - { - LocalBuilder requestMessage; - ilGen.Emit(OpCodes.Ldarg_0); // base - requestMessage = ilGen.DeclareLocal(typeof(IActorRequestMessageBody)); - ilGen.Emit(OpCodes.Ldstr, interfaceName); - ilGen.Emit(OpCodes.Ldstr, methodDescription.Name); - ilGen.Emit(OpCodes.Ldc_I4, parameterLength); - ilGen.Emit(OpCodes.Ldloc, wrappedRequestBody); - ilGen.EmitCall(OpCodes.Call, this.createMessage, null); - ilGen.Emit(OpCodes.Stloc, requestMessage); - return requestMessage; - } - - private void AddIfInterfaceIdAndMethodIdReturnRetvalBlock( - ILGenerator ilGen, - Label elseLabel, - int interfaceId, - int methodId, - Type responseBodyType) - { - // if (interfaceId == ) - ilGen.Emit(OpCodes.Ldarg_1); - ilGen.Emit(OpCodes.Ldc_I4, interfaceId); - ilGen.Emit(OpCodes.Bne_Un, elseLabel); - - // if (methodId == ) - ilGen.Emit(OpCodes.Ldarg_2); - ilGen.Emit(OpCodes.Ldc_I4, methodId); - ilGen.Emit(OpCodes.Bne_Un, elseLabel); - - var castedResponseBody = ilGen.DeclareLocal(responseBodyType); - ilGen.Emit(OpCodes.Ldarg_3); // load responseBody object - ilGen.Emit(OpCodes.Castclass, responseBodyType); // cast it to responseBodyType - ilGen.Emit(OpCodes.Stloc, castedResponseBody); // store casted result to castedResponseBody local variable - - var fieldInfo = responseBodyType.GetField(this.CodeBuilder.Names.RetVal); - ilGen.Emit(OpCodes.Ldloc, castedResponseBody); - ilGen.Emit(OpCodes.Ldfld, fieldInfo); - if (!fieldInfo.FieldType.GetTypeInfo().IsClass) - { - ilGen.Emit(OpCodes.Box, fieldInfo.FieldType); - } - - ilGen.Emit(OpCodes.Ret); - } - - private void AddVoidMethodImplementation( - TypeBuilder classBuilder, - int interfaceDescriptionId, - MethodDescription methodDescription, - MethodBodyTypes methodBodyTypes, - string interfaceName) - { - var interfaceMethod = methodDescription.MethodInfo; - - var methodBuilder = CodeBuilderUtils.CreateExplitInterfaceMethodBuilder( - classBuilder, - interfaceMethod); - - var ilGen = methodBuilder.GetILGenerator(); - - // Create Wrapped Request - LocalBuilder wrappedRequestBody = - CreateWrappedRequestBody(methodDescription, methodBodyTypes, ilGen, methodDescription.MethodInfo.GetParameters()); - - this.AddVoidMethodImplementation( - ilGen, - interfaceDescriptionId, - methodDescription, - wrappedRequestBody, - interfaceName); - - ilGen.Emit(OpCodes.Ret); + ilGen.Emit(OpCodes.Ldarg_0); // base pointer + ilGen.Emit(OpCodes.Ldloc, objectTask); // task + ilGen.Emit(OpCodes.Call, this.continueWithMethodInfo); + ilGen.Emit(OpCodes.Ret); // return base.ContinueWith(task); } } -} + + private Type BuildProxyType( + CodeBuilderContext context, + Type proxyInterfaceType, + IDictionary methodBodyTypesResultsMap) + { + var classBuilder = CodeBuilderUtils.CreateClassBuilder( + context.ModuleBuilder, + ns: context.AssemblyNamespace, + className: this.CodeBuilder.Names.GetProxyClassName(proxyInterfaceType), + baseType: this.proxyBaseType, + interfaces: methodBodyTypesResultsMap.Select(item => item.Key.InterfaceType).ToArray()); + + this.AddGetReturnValueMethod(classBuilder, methodBodyTypesResultsMap); + this.AddInterfaceImplementations(classBuilder, methodBodyTypesResultsMap); + + return classBuilder.CreateTypeInfo().AsType(); + } + + private void AddGetReturnValueMethod( + TypeBuilder classBuilder, + IDictionary methodBodyTypesResultsMap) + { + var methodBuilder = CodeBuilderUtils.CreateProtectedMethodBuilder( + classBuilder, + "GetReturnValue", + typeof(object), // return value from the reponseBody + typeof(int), // interfaceId + typeof(int), // methodId + typeof(object)); // responseBody + + var ilGen = methodBuilder.GetILGenerator(); + + foreach (var item in methodBodyTypesResultsMap) + { + var interfaceDescription = item.Key; + var methodBodyTypesMap = item.Value.MethodBodyTypesMap; + + foreach (var methodDescription in interfaceDescription.Methods) + { + var methodBodyTypes = methodBodyTypesMap[methodDescription.Name]; + if (methodBodyTypes.ResponseBodyType == null) + { + continue; + } + + var elseLabel = ilGen.DefineLabel(); + + this.AddIfInterfaceIdAndMethodIdReturnRetvalBlock( + ilGen, + elseLabel, + interfaceDescription.Id, + methodDescription.Id, + methodBodyTypes.ResponseBodyType); + + ilGen.MarkLabel(elseLabel); + } + } + + // return null; (if method id's and interfaceId do not mGetReturnValueatch) + ilGen.Emit(OpCodes.Ldnull); + ilGen.Emit(OpCodes.Ret); + } + + private void SetParameterIfNeeded( + ILGenerator ilGen, + LocalBuilder requestMessage, + int parameterLength, + ParameterInfo[] parameters) + { + var boolres = ilGen.DeclareLocal(typeof(bool)); + var boolres2 = ilGen.DeclareLocal(typeof(bool)); + ilGen.Emit(OpCodes.Ldarg_0); // base + ilGen.Emit(OpCodes.Ldloc_1, requestMessage); + ilGen.Emit(OpCodes.Call, this.checkIfitsWrapped); + ilGen.Emit(OpCodes.Stloc, boolres); + ilGen.Emit(OpCodes.Ldloc_2); + ilGen.Emit(OpCodes.Ldc_I4_0); + ilGen.Emit(OpCodes.Ceq); + ilGen.Emit(OpCodes.Stloc, boolres2); + ilGen.Emit(OpCodes.Ldloc_3, boolres2); + var elseLabel = ilGen.DefineLabel(); + ilGen.Emit(OpCodes.Brfalse, elseLabel); + + // if false ,Call SetParamater + var setMethod = typeof(IActorRequestMessageBody).GetMethod("SetParameter"); + + // Add to Dictionary + for (var i = 0; i < parameterLength; i++) + { + ilGen.Emit(OpCodes.Ldloc, requestMessage); + ilGen.Emit(OpCodes.Ldc_I4, i); + ilGen.Emit(OpCodes.Ldstr, parameters[i].Name); + ilGen.Emit(OpCodes.Ldarg, i + 1); + if (!parameters[i].ParameterType.IsClass) + { + ilGen.Emit(OpCodes.Box, parameters[i].ParameterType); + } + + ilGen.Emit(OpCodes.Callvirt, setMethod); + } + + ilGen.MarkLabel(elseLabel); + } + + private LocalBuilder CreateRequestRemotingMessageBody( + MethodDescription methodDescription, + string interfaceName, + ILGenerator ilGen, + int parameterLength, + LocalBuilder wrappedRequestBody) + { + LocalBuilder requestMessage; + ilGen.Emit(OpCodes.Ldarg_0); // base + requestMessage = ilGen.DeclareLocal(typeof(IActorRequestMessageBody)); + ilGen.Emit(OpCodes.Ldstr, interfaceName); + ilGen.Emit(OpCodes.Ldstr, methodDescription.Name); + ilGen.Emit(OpCodes.Ldc_I4, parameterLength); + ilGen.Emit(OpCodes.Ldloc, wrappedRequestBody); + ilGen.EmitCall(OpCodes.Call, this.createMessage, null); + ilGen.Emit(OpCodes.Stloc, requestMessage); + return requestMessage; + } + + private void AddIfInterfaceIdAndMethodIdReturnRetvalBlock( + ILGenerator ilGen, + Label elseLabel, + int interfaceId, + int methodId, + Type responseBodyType) + { + // if (interfaceId == ) + ilGen.Emit(OpCodes.Ldarg_1); + ilGen.Emit(OpCodes.Ldc_I4, interfaceId); + ilGen.Emit(OpCodes.Bne_Un, elseLabel); + + // if (methodId == ) + ilGen.Emit(OpCodes.Ldarg_2); + ilGen.Emit(OpCodes.Ldc_I4, methodId); + ilGen.Emit(OpCodes.Bne_Un, elseLabel); + + var castedResponseBody = ilGen.DeclareLocal(responseBodyType); + ilGen.Emit(OpCodes.Ldarg_3); // load responseBody object + ilGen.Emit(OpCodes.Castclass, responseBodyType); // cast it to responseBodyType + ilGen.Emit(OpCodes.Stloc, castedResponseBody); // store casted result to castedResponseBody local variable + + var fieldInfo = responseBodyType.GetField(this.CodeBuilder.Names.RetVal); + ilGen.Emit(OpCodes.Ldloc, castedResponseBody); + ilGen.Emit(OpCodes.Ldfld, fieldInfo); + if (!fieldInfo.FieldType.GetTypeInfo().IsClass) + { + ilGen.Emit(OpCodes.Box, fieldInfo.FieldType); + } + + ilGen.Emit(OpCodes.Ret); + } + + private void AddVoidMethodImplementation( + TypeBuilder classBuilder, + int interfaceDescriptionId, + MethodDescription methodDescription, + MethodBodyTypes methodBodyTypes, + string interfaceName) + { + var interfaceMethod = methodDescription.MethodInfo; + + var methodBuilder = CodeBuilderUtils.CreateExplitInterfaceMethodBuilder( + classBuilder, + interfaceMethod); + + var ilGen = methodBuilder.GetILGenerator(); + + // Create Wrapped Request + LocalBuilder wrappedRequestBody = + CreateWrappedRequestBody(methodDescription, methodBodyTypes, ilGen, methodDescription.MethodInfo.GetParameters()); + + this.AddVoidMethodImplementation( + ilGen, + interfaceDescriptionId, + methodDescription, + wrappedRequestBody, + interfaceName); + + ilGen.Emit(OpCodes.Ret); + } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Builder/BuildResult.cs b/src/Dapr.Actors/Builder/BuildResult.cs index c3e32bab..e9f1b732 100644 --- a/src/Dapr.Actors/Builder/BuildResult.cs +++ b/src/Dapr.Actors/Builder/BuildResult.cs @@ -11,15 +11,14 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Builder -{ - internal class BuildResult - { - protected BuildResult(CodeBuilderContext buildContext) - { - this.BuildContext = buildContext; - } +namespace Dapr.Actors.Builder; - public CodeBuilderContext BuildContext { get; } +internal class BuildResult +{ + protected BuildResult(CodeBuilderContext buildContext) + { + this.BuildContext = buildContext; } -} + + public CodeBuilderContext BuildContext { get; } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Builder/CodeBuilderAttribute.cs b/src/Dapr.Actors/Builder/CodeBuilderAttribute.cs index 22b18392..e1887c4c 100644 --- a/src/Dapr.Actors/Builder/CodeBuilderAttribute.cs +++ b/src/Dapr.Actors/Builder/CodeBuilderAttribute.cs @@ -11,54 +11,53 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Builder +namespace Dapr.Actors.Builder; + +using System; +using System.Reflection; + +/// +/// The Attribute class to configure dyanamic code generation process for service remoting. +/// +[AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Interface)] +public class CodeBuilderAttribute : Attribute { - using System; - using System.Reflection; + /// + /// Initializes a new instance of the class. + /// + public CodeBuilderAttribute() + { + } /// - /// The Attribute class to configure dyanamic code generation process for service remoting. + /// Gets or sets a value indicating whether to enable debugging flag for the attribute to be used by auto code generation. /// - [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Interface)] - public class CodeBuilderAttribute : Attribute + /// to get or set enable debugging flag for the attribute to be used by auto code generation. + public bool EnableDebugging { get; set; } + + internal static bool IsDebuggingEnabled(Type type = null) { - /// - /// Initializes a new instance of the class. - /// - public CodeBuilderAttribute() + var enableDebugging = false; + var entryAssembly = Assembly.GetEntryAssembly(); + + if (entryAssembly != null) { + var attribute = entryAssembly.GetCustomAttribute(); + enableDebugging = ((attribute != null) && (attribute.EnableDebugging)); } - /// - /// Gets or sets a value indicating whether to enable debugging flag for the attribute to be used by auto code generation. - /// - /// to get or set enable debugging flag for the attribute to be used by auto code generation. - public bool EnableDebugging { get; set; } - - internal static bool IsDebuggingEnabled(Type type = null) + if (!enableDebugging && (type != null)) { - var enableDebugging = false; - var entryAssembly = Assembly.GetEntryAssembly(); + var attribute = type.GetTypeInfo().Assembly.GetCustomAttribute(); + enableDebugging = ((attribute != null) && (attribute.EnableDebugging)); - if (entryAssembly != null) + if (!enableDebugging) { - var attribute = entryAssembly.GetCustomAttribute(); + attribute = type.GetTypeInfo().GetCustomAttribute(true); enableDebugging = ((attribute != null) && (attribute.EnableDebugging)); } - - if (!enableDebugging && (type != null)) - { - var attribute = type.GetTypeInfo().Assembly.GetCustomAttribute(); - enableDebugging = ((attribute != null) && (attribute.EnableDebugging)); - - if (!enableDebugging) - { - attribute = type.GetTypeInfo().GetCustomAttribute(true); - enableDebugging = ((attribute != null) && (attribute.EnableDebugging)); - } - } - - return enableDebugging; } + + return enableDebugging; } -} +} \ No newline at end of file diff --git a/src/Dapr.Actors/Builder/CodeBuilderContext.cs b/src/Dapr.Actors/Builder/CodeBuilderContext.cs index e8e0ec5f..0cba2276 100644 --- a/src/Dapr.Actors/Builder/CodeBuilderContext.cs +++ b/src/Dapr.Actors/Builder/CodeBuilderContext.cs @@ -11,37 +11,36 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Builder +namespace Dapr.Actors.Builder; + +using System.Reflection.Emit; + +internal class CodeBuilderContext { - using System.Reflection.Emit; + private readonly bool enableDebugging; - internal class CodeBuilderContext + public CodeBuilderContext(string assemblyName, string assemblyNamespace, bool enableDebugging = false) { - private readonly bool enableDebugging; + this.AssemblyNamespace = assemblyNamespace; + this.enableDebugging = enableDebugging; - public CodeBuilderContext(string assemblyName, string assemblyNamespace, bool enableDebugging = false) + this.AssemblyBuilder = CodeBuilderUtils.CreateAssemblyBuilder(assemblyName); + this.ModuleBuilder = CodeBuilderUtils.CreateModuleBuilder(this.AssemblyBuilder, assemblyName); + } + + public AssemblyBuilder AssemblyBuilder { get; } + + public ModuleBuilder ModuleBuilder { get; } + + public string AssemblyNamespace { get; } + + public void Complete() + { + if (this.enableDebugging) { - this.AssemblyNamespace = assemblyNamespace; - this.enableDebugging = enableDebugging; - - this.AssemblyBuilder = CodeBuilderUtils.CreateAssemblyBuilder(assemblyName); - this.ModuleBuilder = CodeBuilderUtils.CreateModuleBuilder(this.AssemblyBuilder, assemblyName); - } - - public AssemblyBuilder AssemblyBuilder { get; } - - public ModuleBuilder ModuleBuilder { get; } - - public string AssemblyNamespace { get; } - - public void Complete() - { - if (this.enableDebugging) - { #if !DotNetCoreClr this.assemblyBuilder.Save(this.assemblyBuilder.GetName().Name + ".dll"); #endif - } } } -} +} \ No newline at end of file diff --git a/src/Dapr.Actors/Builder/CodeBuilderModule.cs b/src/Dapr.Actors/Builder/CodeBuilderModule.cs index e2d1738f..021e9a7b 100644 --- a/src/Dapr.Actors/Builder/CodeBuilderModule.cs +++ b/src/Dapr.Actors/Builder/CodeBuilderModule.cs @@ -11,29 +11,28 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Builder +namespace Dapr.Actors.Builder; + +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using Dapr.Actors.Description; + +internal abstract class CodeBuilderModule { - using System.Collections.Generic; - using System.Collections.ObjectModel; - using System.Linq; - using Dapr.Actors.Description; - - internal abstract class CodeBuilderModule + protected CodeBuilderModule(ICodeBuilder codeBuilder) { - protected CodeBuilderModule(ICodeBuilder codeBuilder) - { - this.CodeBuilder = codeBuilder; - } - - protected ICodeBuilder CodeBuilder { get; } - - protected static IReadOnlyDictionary GetMethodNameMap(InterfaceDescription interfaceDescription) - { - var methodNameMap = interfaceDescription.Methods.ToDictionary( - methodDescription => methodDescription.Id, - methodDescription => methodDescription.Name); - - return new ReadOnlyDictionary(methodNameMap); - } + this.CodeBuilder = codeBuilder; } -} + + protected ICodeBuilder CodeBuilder { get; } + + protected static IReadOnlyDictionary GetMethodNameMap(InterfaceDescription interfaceDescription) + { + var methodNameMap = interfaceDescription.Methods.ToDictionary( + methodDescription => methodDescription.Id, + methodDescription => methodDescription.Name); + + return new ReadOnlyDictionary(methodNameMap); + } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Builder/CodeBuilderUtils.cs b/src/Dapr.Actors/Builder/CodeBuilderUtils.cs index f4904880..6640dfed 100644 --- a/src/Dapr.Actors/Builder/CodeBuilderUtils.cs +++ b/src/Dapr.Actors/Builder/CodeBuilderUtils.cs @@ -11,234 +11,233 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Builder +namespace Dapr.Actors.Builder; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using System.Runtime.Serialization; + +internal static class CodeBuilderUtils { - using System; - using System.Collections.Generic; - using System.Linq; - using System.Reflection; - using System.Reflection.Emit; - using System.Runtime.Serialization; + private static readonly ConstructorInfo DcAttrCtorInfo; + private static readonly PropertyInfo DcAttrNamePropInfo; + private static readonly PropertyInfo DcAttrNamespacePropInfo; + private static readonly ConstructorInfo DmAttrCtorInfo; + private static readonly PropertyInfo DmAttrIsRequiredPropInfo; + private static readonly object[] EmptyObjectArray = { }; - internal static class CodeBuilderUtils + static CodeBuilderUtils() { - private static readonly ConstructorInfo DcAttrCtorInfo; - private static readonly PropertyInfo DcAttrNamePropInfo; - private static readonly PropertyInfo DcAttrNamespacePropInfo; - private static readonly ConstructorInfo DmAttrCtorInfo; - private static readonly PropertyInfo DmAttrIsRequiredPropInfo; - private static readonly object[] EmptyObjectArray = { }; + var dcAttrType = typeof(DataContractAttribute); + DcAttrCtorInfo = dcAttrType.GetConstructor(Type.EmptyTypes); + DcAttrNamePropInfo = dcAttrType.GetProperty("Name"); + DcAttrNamespacePropInfo = dcAttrType.GetProperty("Namespace"); - static CodeBuilderUtils() + var dmAttrType = typeof(DataMemberAttribute); + DmAttrCtorInfo = dmAttrType.GetConstructor(Type.EmptyTypes); + DmAttrIsRequiredPropInfo = dmAttrType.GetProperty("IsRequired"); + } + + public static AssemblyBuilder CreateAssemblyBuilder(string assemblyName) + { + return AssemblyBuilder.DefineDynamicAssembly( + new AssemblyName(assemblyName), + AssemblyBuilderAccess.RunAndCollect); + } + + public static ModuleBuilder CreateModuleBuilder( + AssemblyBuilder assemblyBuilder, + string moduleName) + { + return assemblyBuilder.DefineDynamicModule(moduleName); + } + + public static TypeBuilder CreateClassBuilder( + ModuleBuilder moduleBuilder, + string ns, + string className, + Type baseType = null) + { + if (!string.IsNullOrEmpty(ns)) { - var dcAttrType = typeof(DataContractAttribute); - DcAttrCtorInfo = dcAttrType.GetConstructor(Type.EmptyTypes); - DcAttrNamePropInfo = dcAttrType.GetProperty("Name"); - DcAttrNamespacePropInfo = dcAttrType.GetProperty("Namespace"); - - var dmAttrType = typeof(DataMemberAttribute); - DmAttrCtorInfo = dmAttrType.GetConstructor(Type.EmptyTypes); - DmAttrIsRequiredPropInfo = dmAttrType.GetProperty("IsRequired"); + className = string.Concat(ns, ".", className); } - public static AssemblyBuilder CreateAssemblyBuilder(string assemblyName) + if (baseType != null) { - return AssemblyBuilder.DefineDynamicAssembly( - new AssemblyName(assemblyName), - AssemblyBuilderAccess.RunAndCollect); - } - - public static ModuleBuilder CreateModuleBuilder( - AssemblyBuilder assemblyBuilder, - string moduleName) - { - return assemblyBuilder.DefineDynamicModule(moduleName); - } - - public static TypeBuilder CreateClassBuilder( - ModuleBuilder moduleBuilder, - string ns, - string className, - Type baseType = null) - { - if (!string.IsNullOrEmpty(ns)) - { - className = string.Concat(ns, ".", className); - } - - if (baseType != null) - { - return moduleBuilder.DefineType( - className, - TypeAttributes.Public | TypeAttributes.Class, - baseType); - } - return moduleBuilder.DefineType( className, - TypeAttributes.Public | TypeAttributes.Class); + TypeAttributes.Public | TypeAttributes.Class, + baseType); } - public static TypeBuilder CreateClassBuilder( - ModuleBuilder moduleBuilder, - string ns, - string className, - IEnumerable interfaces = null) - { - return CreateClassBuilder(moduleBuilder, ns, className, null, interfaces); - } + return moduleBuilder.DefineType( + className, + TypeAttributes.Public | TypeAttributes.Class); + } - public static TypeBuilder CreateClassBuilder( - ModuleBuilder moduleBuilder, - string ns, - string className, - Type baseType, - IEnumerable interfaces) + public static TypeBuilder CreateClassBuilder( + ModuleBuilder moduleBuilder, + string ns, + string className, + IEnumerable interfaces = null) + { + return CreateClassBuilder(moduleBuilder, ns, className, null, interfaces); + } + + public static TypeBuilder CreateClassBuilder( + ModuleBuilder moduleBuilder, + string ns, + string className, + Type baseType, + IEnumerable interfaces) + { + var typeBuilder = CreateClassBuilder(moduleBuilder, ns, className, baseType); + if (interfaces != null) { - var typeBuilder = CreateClassBuilder(moduleBuilder, ns, className, baseType); - if (interfaces != null) + foreach (var interfaceType in interfaces) { - foreach (var interfaceType in interfaces) - { - typeBuilder.AddInterfaceImplementation(interfaceType); - } + typeBuilder.AddInterfaceImplementation(interfaceType); } - - return typeBuilder; } - public static FieldBuilder CreateFieldBuilder(TypeBuilder typeBuilder, Type fieldType, string fieldName) + return typeBuilder; + } + + public static FieldBuilder CreateFieldBuilder(TypeBuilder typeBuilder, Type fieldType, string fieldName) + { + return typeBuilder.DefineField( + fieldName, + fieldType, + FieldAttributes.Public); + } + + public static MethodBuilder CreatePublicMethodBuilder( + TypeBuilder typeBuilder, + string methodName) + { + return typeBuilder.DefineMethod( + methodName, + MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.Virtual); + } + + public static MethodBuilder CreatePublicMethodBuilder( + TypeBuilder typeBuilder, + string methodName, + Type returnType, + params Type[] parameterTypes) + { + return typeBuilder.DefineMethod( + methodName, + (MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.Virtual), + returnType, + parameterTypes); + } + + public static MethodBuilder CreateProtectedMethodBuilder( + TypeBuilder typeBuilder, + string methodName, + Type returnType, + params Type[] parameterTypes) + { + return typeBuilder.DefineMethod( + methodName, + (MethodAttributes.Family | MethodAttributes.HideBySig | MethodAttributes.Virtual), + returnType, + parameterTypes); + } + + public static MethodBuilder CreateExplitInterfaceMethodBuilder( + TypeBuilder typeBuilder, + MethodInfo interfaceMethod) + { + var parameters = interfaceMethod.GetParameters(); + var parameterTypes = parameters.Select(pi => pi.ParameterType).ToArray(); + + var methodAttrs = (MethodAttributes.Private | + MethodAttributes.HideBySig | + MethodAttributes.NewSlot | + MethodAttributes.Virtual | + MethodAttributes.Final); + + var methodBuilder = typeBuilder.DefineMethod( + string.Concat(interfaceMethod.DeclaringType.Name, ".", interfaceMethod.Name), + methodAttrs, + interfaceMethod.ReturnType, + parameterTypes); + + typeBuilder.DefineMethodOverride(methodBuilder, interfaceMethod); + return methodBuilder; + } + + #region Data Contract Type Generation Utilities + + public static TypeBuilder CreateDataContractTypeBuilder( + ModuleBuilder moduleBuilder, + string ns, + string typeName, + string dcNamespace = null, + string dcName = null) + { + var typeBuilder = CreateClassBuilder(moduleBuilder, ns, typeName, Type.EmptyTypes); + AddDataContractAttribute(typeBuilder, dcNamespace, dcName); + return typeBuilder; + } + + public static void AddDataMemberField(TypeBuilder dcTypeBuilder, Type fieldType, string fieldName) + { + var fieldBuilder = CreateFieldBuilder(dcTypeBuilder, fieldType, fieldName); + fieldBuilder.SetCustomAttribute(CreateCustomDataMemberAttributeBuilder()); + } + + private static void AddDataContractAttribute(TypeBuilder typeBuilder, string dcNamespace, string dcName) + { + typeBuilder.SetCustomAttribute(CreateCustomDataContractAttributeBuilder(dcNamespace, dcName)); + } + + private static CustomAttributeBuilder CreateCustomDataContractAttributeBuilder(string dcNamespace, string dcName) + { + if (string.IsNullOrEmpty(dcName) && string.IsNullOrEmpty(dcNamespace)) { - return typeBuilder.DefineField( - fieldName, - fieldType, - FieldAttributes.Public); + return new CustomAttributeBuilder(DcAttrCtorInfo, EmptyObjectArray); } - public static MethodBuilder CreatePublicMethodBuilder( - TypeBuilder typeBuilder, - string methodName) + if (string.IsNullOrEmpty(dcName)) { - return typeBuilder.DefineMethod( - methodName, - MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.Virtual); - } - - public static MethodBuilder CreatePublicMethodBuilder( - TypeBuilder typeBuilder, - string methodName, - Type returnType, - params Type[] parameterTypes) - { - return typeBuilder.DefineMethod( - methodName, - (MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.Virtual), - returnType, - parameterTypes); - } - - public static MethodBuilder CreateProtectedMethodBuilder( - TypeBuilder typeBuilder, - string methodName, - Type returnType, - params Type[] parameterTypes) - { - return typeBuilder.DefineMethod( - methodName, - (MethodAttributes.Family | MethodAttributes.HideBySig | MethodAttributes.Virtual), - returnType, - parameterTypes); - } - - public static MethodBuilder CreateExplitInterfaceMethodBuilder( - TypeBuilder typeBuilder, - MethodInfo interfaceMethod) - { - var parameters = interfaceMethod.GetParameters(); - var parameterTypes = parameters.Select(pi => pi.ParameterType).ToArray(); - - var methodAttrs = (MethodAttributes.Private | - MethodAttributes.HideBySig | - MethodAttributes.NewSlot | - MethodAttributes.Virtual | - MethodAttributes.Final); - - var methodBuilder = typeBuilder.DefineMethod( - string.Concat(interfaceMethod.DeclaringType.Name, ".", interfaceMethod.Name), - methodAttrs, - interfaceMethod.ReturnType, - parameterTypes); - - typeBuilder.DefineMethodOverride(methodBuilder, interfaceMethod); - return methodBuilder; - } - - #region Data Contract Type Generation Utilities - - public static TypeBuilder CreateDataContractTypeBuilder( - ModuleBuilder moduleBuilder, - string ns, - string typeName, - string dcNamespace = null, - string dcName = null) - { - var typeBuilder = CreateClassBuilder(moduleBuilder, ns, typeName, Type.EmptyTypes); - AddDataContractAttribute(typeBuilder, dcNamespace, dcName); - return typeBuilder; - } - - public static void AddDataMemberField(TypeBuilder dcTypeBuilder, Type fieldType, string fieldName) - { - var fieldBuilder = CreateFieldBuilder(dcTypeBuilder, fieldType, fieldName); - fieldBuilder.SetCustomAttribute(CreateCustomDataMemberAttributeBuilder()); - } - - private static void AddDataContractAttribute(TypeBuilder typeBuilder, string dcNamespace, string dcName) - { - typeBuilder.SetCustomAttribute(CreateCustomDataContractAttributeBuilder(dcNamespace, dcName)); - } - - private static CustomAttributeBuilder CreateCustomDataContractAttributeBuilder(string dcNamespace, string dcName) - { - if (string.IsNullOrEmpty(dcName) && string.IsNullOrEmpty(dcNamespace)) - { - return new CustomAttributeBuilder(DcAttrCtorInfo, EmptyObjectArray); - } - - if (string.IsNullOrEmpty(dcName)) - { - return new CustomAttributeBuilder( - DcAttrCtorInfo, - EmptyObjectArray, - new[] { DcAttrNamespacePropInfo }, - new object[] { dcNamespace }); - } - - if (string.IsNullOrEmpty(dcNamespace)) - { - return new CustomAttributeBuilder( - DcAttrCtorInfo, - EmptyObjectArray, - new[] { DcAttrNamePropInfo }, - new object[] { dcName }); - } - return new CustomAttributeBuilder( DcAttrCtorInfo, EmptyObjectArray, - new[] { DcAttrNamespacePropInfo, DcAttrNamePropInfo }, - new object[] { dcNamespace, dcName }); + new[] { DcAttrNamespacePropInfo }, + new object[] { dcNamespace }); } - private static CustomAttributeBuilder CreateCustomDataMemberAttributeBuilder() + if (string.IsNullOrEmpty(dcNamespace)) { return new CustomAttributeBuilder( - DmAttrCtorInfo, + DcAttrCtorInfo, EmptyObjectArray, - new[] { DmAttrIsRequiredPropInfo }, - new object[] { false }); + new[] { DcAttrNamePropInfo }, + new object[] { dcName }); } - #endregion + return new CustomAttributeBuilder( + DcAttrCtorInfo, + EmptyObjectArray, + new[] { DcAttrNamespacePropInfo, DcAttrNamePropInfo }, + new object[] { dcNamespace, dcName }); } -} + + private static CustomAttributeBuilder CreateCustomDataMemberAttributeBuilder() + { + return new CustomAttributeBuilder( + DmAttrCtorInfo, + EmptyObjectArray, + new[] { DmAttrIsRequiredPropInfo }, + new object[] { false }); + } + + #endregion +} \ No newline at end of file diff --git a/src/Dapr.Actors/Builder/ICodeBuilder.cs b/src/Dapr.Actors/Builder/ICodeBuilder.cs index 4013f74a..dbd47b7b 100644 --- a/src/Dapr.Actors/Builder/ICodeBuilder.cs +++ b/src/Dapr.Actors/Builder/ICodeBuilder.cs @@ -11,39 +11,38 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Builder +namespace Dapr.Actors.Builder; + +using System; + +/// +/// Represents an interface for generating the code to support communication. +/// +internal interface ICodeBuilder { - using System; + /// + /// Gets the interface for getting the names of the generated code (types, interfaces, methods etc.) + /// + ICodeBuilderNames Names { get; } /// - /// Represents an interface for generating the code to support communication. + /// Gets or builds a type that can send the communication messages to the object implementing the specified interface. /// - internal interface ICodeBuilder - { - /// - /// Gets the interface for getting the names of the generated code (types, interfaces, methods etc.) - /// - ICodeBuilderNames Names { get; } + /// Interface for which to generate the method dispatcher. + /// A containing the dispatcher to dispatch the messages destined the specified interfaces. + MethodDispatcherBuildResult GetOrBuilderMethodDispatcher(Type interfaceType); - /// - /// Gets or builds a type that can send the communication messages to the object implementing the specified interface. - /// - /// Interface for which to generate the method dispatcher. - /// A containing the dispatcher to dispatch the messages destined the specified interfaces. - MethodDispatcherBuildResult GetOrBuilderMethodDispatcher(Type interfaceType); + /// + /// Gets or builds a communication message body types that can store the method arguments of the specified interface. + /// + /// Interface for which to generate the method body types. + /// A containing the method body types for each of the methods of the specified interface. + MethodBodyTypesBuildResult GetOrBuildMethodBodyTypes(Type interfaceType); - /// - /// Gets or builds a communication message body types that can store the method arguments of the specified interface. - /// - /// Interface for which to generate the method body types. - /// A containing the method body types for each of the methods of the specified interface. - MethodBodyTypesBuildResult GetOrBuildMethodBodyTypes(Type interfaceType); - - /// - /// Gets or builds a factory object that can generate communication proxy for the specified interface. - /// - /// Interface for which to generate the proxy factory object. - /// A containing the generator for communication proxy for the speficifed interface. - ActorProxyGeneratorBuildResult GetOrBuildProxyGenerator(Type interfaceType); - } -} + /// + /// Gets or builds a factory object that can generate communication proxy for the specified interface. + /// + /// Interface for which to generate the proxy factory object. + /// A containing the generator for communication proxy for the speficifed interface. + ActorProxyGeneratorBuildResult GetOrBuildProxyGenerator(Type interfaceType); +} \ No newline at end of file diff --git a/src/Dapr.Actors/Builder/ICodeBuilderNames.cs b/src/Dapr.Actors/Builder/ICodeBuilderNames.cs index 9b42c8d5..420013fb 100644 --- a/src/Dapr.Actors/Builder/ICodeBuilderNames.cs +++ b/src/Dapr.Actors/Builder/ICodeBuilderNames.cs @@ -11,116 +11,115 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Builder +namespace Dapr.Actors.Builder; + +using System; + +/// +/// Provides the names for the generated types, methods, arguments, members etc. +/// +internal interface ICodeBuilderNames { - using System; + /// + /// Gets the name for the interface Id field. + /// + string InterfaceId { get; } /// - /// Provides the names for the generated types, methods, arguments, members etc. + /// Gets the name for the method Id field. /// - internal interface ICodeBuilderNames - { - /// - /// Gets the name for the interface Id field. - /// - string InterfaceId { get; } + string MethodId { get; } - /// - /// Gets the name for the method Id field. - /// - string MethodId { get; } + /// + /// Gets the name for the retval field. + /// + string RetVal { get; } - /// - /// Gets the name for the retval field. - /// - string RetVal { get; } + /// + /// Gets the name for the request body field. + /// + string RequestBody { get; } - /// - /// Gets the name for the request body field. - /// - string RequestBody { get; } + /// + /// Gets the name of the assembly in which to generate the method body types. + /// + /// The name of the remoted interface. + /// The assembly name for the method body types. + string GetMethodBodyTypesAssemblyName(Type interfaceType); - /// - /// Gets the name of the assembly in which to generate the method body types. - /// - /// The name of the remoted interface. - /// The assembly name for the method body types. - string GetMethodBodyTypesAssemblyName(Type interfaceType); + /// + /// Gets the namespace of the assembly in which to generate the method body types. + /// + /// The name of the remoted interface. + /// The assembly namespace for the method body types. + string GetMethodBodyTypesAssemblyNamespace(Type interfaceType); - /// - /// Gets the namespace of the assembly in which to generate the method body types. - /// - /// The name of the remoted interface. - /// The assembly namespace for the method body types. - string GetMethodBodyTypesAssemblyNamespace(Type interfaceType); + /// + /// Gets the name of the request body type for the specified method. + /// + /// Name of the method whose parameters needs to be wraped in the body type. + /// The name of the request body type. + string GetRequestBodyTypeName(string methodName); - /// - /// Gets the name of the request body type for the specified method. - /// - /// Name of the method whose parameters needs to be wraped in the body type. - /// The name of the request body type. - string GetRequestBodyTypeName(string methodName); + /// + /// Gets the name of the response body type for the specified method. + /// + /// Name of the method whose return value needs to be wraped in the body type. + /// The name of the response body type. + string GetResponseBodyTypeName(string methodName); - /// - /// Gets the name of the response body type for the specified method. - /// - /// Name of the method whose return value needs to be wraped in the body type. - /// The name of the response body type. - string GetResponseBodyTypeName(string methodName); + /// + /// Gets the data contract namespace for the generated types. + /// + /// The data contract namespace. + string GetDataContractNamespace(); - /// - /// Gets the data contract namespace for the generated types. - /// - /// The data contract namespace. - string GetDataContractNamespace(); + /// + /// Gets the name of the assembly in which to generate the method dispatcher type. + /// + /// The remoted interface type. + /// The name of the assembly for method disptacher. + string GetMethodDispatcherAssemblyName(Type interfaceType); - /// - /// Gets the name of the assembly in which to generate the method dispatcher type. - /// - /// The remoted interface type. - /// The name of the assembly for method disptacher. - string GetMethodDispatcherAssemblyName(Type interfaceType); + /// + /// Gets the namespace of the assembly in which to generate the method dispatcher type. + /// + /// The remoted interface type. + /// The namespace of the assembly for method disptacher. + string GetMethodDispatcherAssemblyNamespace(Type interfaceType); - /// - /// Gets the namespace of the assembly in which to generate the method dispatcher type. - /// - /// The remoted interface type. - /// The namespace of the assembly for method disptacher. - string GetMethodDispatcherAssemblyNamespace(Type interfaceType); + /// + /// Gets the name of the method dispatcher class for dispatching methods to the implementation of the specified interface. + /// + /// The remoted interface type. + /// The name of the method dispatcher class. + string GetMethodDispatcherClassName(Type interfaceType); - /// - /// Gets the name of the method dispatcher class for dispatching methods to the implementation of the specified interface. - /// - /// The remoted interface type. - /// The name of the method dispatcher class. - string GetMethodDispatcherClassName(Type interfaceType); + /// + /// Gets the name of the assembly in which to generate the proxy of the specified remoted interface. + /// + /// The remoted interface type. + /// The name of the assembly for proxy. + string GetProxyAssemblyName(Type interfaceType); - /// - /// Gets the name of the assembly in which to generate the proxy of the specified remoted interface. - /// - /// The remoted interface type. - /// The name of the assembly for proxy. - string GetProxyAssemblyName(Type interfaceType); + /// + /// Gets the namespace of the assembly in which to generate the proxy of the specified remoted interface. + /// + /// The remoted interface type. + /// The namespace of the assembly for proxy. + string GetProxyAssemblyNamespace(Type interfaceType); - /// - /// Gets the namespace of the assembly in which to generate the proxy of the specified remoted interface. - /// - /// The remoted interface type. - /// The namespace of the assembly for proxy. - string GetProxyAssemblyNamespace(Type interfaceType); + /// + /// Gets the name of the proxy class for the specified interface. + /// + /// The remoted interface type. + /// The name of proxy class. + string GetProxyClassName(Type interfaceType); - /// - /// Gets the name of the proxy class for the specified interface. - /// - /// The remoted interface type. - /// The name of proxy class. - string GetProxyClassName(Type interfaceType); - - /// - /// Gets the name of the proxy factory (or activator) class for the specified interface. - /// - /// The remoted interface type. - /// The name of proxy activator class. - string GetProxyActivatorClassName(Type interfaceType); - } -} + /// + /// Gets the name of the proxy factory (or activator) class for the specified interface. + /// + /// The remoted interface type. + /// The name of proxy activator class. + string GetProxyActivatorClassName(Type interfaceType); +} \ No newline at end of file diff --git a/src/Dapr.Actors/Builder/IProxyActivator.cs b/src/Dapr.Actors/Builder/IProxyActivator.cs index d895961f..b62c04ba 100644 --- a/src/Dapr.Actors/Builder/IProxyActivator.cs +++ b/src/Dapr.Actors/Builder/IProxyActivator.cs @@ -11,19 +11,18 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Builder -{ - using Dapr.Actors.Client; +namespace Dapr.Actors.Builder; +using Dapr.Actors.Client; + +/// +/// Interface to create objects. +/// +public interface IProxyActivator +{ /// - /// Interface to create objects. + /// Create the instance of the generated proxy type. /// - public interface IProxyActivator - { - /// - /// Create the instance of the generated proxy type. - /// - /// An instance of the generated proxy as type. - IActorProxy CreateInstance(); - } -} + /// An instance of the generated proxy as type. + IActorProxy CreateInstance(); +} \ No newline at end of file diff --git a/src/Dapr.Actors/Builder/InterfaceDetails.cs b/src/Dapr.Actors/Builder/InterfaceDetails.cs index eaec6b89..79dbd965 100644 --- a/src/Dapr.Actors/Builder/InterfaceDetails.cs +++ b/src/Dapr.Actors/Builder/InterfaceDetails.cs @@ -11,36 +11,35 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Builder +namespace Dapr.Actors.Builder; + +using System; +using System.Collections.Generic; + +internal class InterfaceDetails { - using System; - using System.Collections.Generic; - - internal class InterfaceDetails + internal InterfaceDetails() { - internal InterfaceDetails() - { - this.Id = 0; - this.MethodNames = new Dictionary(); - this.RequestWrappedKnownTypes = new List(); - this.ResponseWrappedKnownTypes = new List(); - this.ResponseKnownTypes = new List(); - this.ServiceInterfaceType = null; - this.RequestKnownTypes = new List(); - } - - public Type ServiceInterfaceType { get; internal set; } - - public int Id { get; internal set; } - - public List RequestKnownTypes { get; internal set; } - - public List ResponseKnownTypes { get; internal set; } - - public IEnumerable RequestWrappedKnownTypes { get; internal set; } - - public IEnumerable ResponseWrappedKnownTypes { get; internal set; } - - public Dictionary MethodNames { get; internal set; } + this.Id = 0; + this.MethodNames = new Dictionary(); + this.RequestWrappedKnownTypes = new List(); + this.ResponseWrappedKnownTypes = new List(); + this.ResponseKnownTypes = new List(); + this.ServiceInterfaceType = null; + this.RequestKnownTypes = new List(); } -} + + public Type ServiceInterfaceType { get; internal set; } + + public int Id { get; internal set; } + + public List RequestKnownTypes { get; internal set; } + + public List ResponseKnownTypes { get; internal set; } + + public IEnumerable RequestWrappedKnownTypes { get; internal set; } + + public IEnumerable ResponseWrappedKnownTypes { get; internal set; } + + public Dictionary MethodNames { get; internal set; } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Builder/InterfaceDetailsStore.cs b/src/Dapr.Actors/Builder/InterfaceDetailsStore.cs index 08ab7ffd..c87602c6 100644 --- a/src/Dapr.Actors/Builder/InterfaceDetailsStore.cs +++ b/src/Dapr.Actors/Builder/InterfaceDetailsStore.cs @@ -11,89 +11,88 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Builder +namespace Dapr.Actors.Builder; + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Dapr.Actors.Description; + +internal class InterfaceDetailsStore { - using System; - using System.Collections.Concurrent; - using System.Collections.Generic; - using System.Linq; - using System.Reflection; - using Dapr.Actors.Description; + private readonly ConcurrentDictionary knownTypesMap = + new ConcurrentDictionary(); - internal class InterfaceDetailsStore + private readonly ConcurrentDictionary interfaceIdMapping = + new ConcurrentDictionary(); + + public bool TryGetKnownTypes(int interfaceId, out InterfaceDetails interfaceDetails) { - private readonly ConcurrentDictionary knownTypesMap = - new ConcurrentDictionary(); + return this.knownTypesMap.TryGetValue(interfaceId, out interfaceDetails); + } - private readonly ConcurrentDictionary interfaceIdMapping = - new ConcurrentDictionary(); - - public bool TryGetKnownTypes(int interfaceId, out InterfaceDetails interfaceDetails) + public bool TryGetKnownTypes(string interfaceName, out InterfaceDetails interfaceDetails) + { + if (!this.interfaceIdMapping.TryGetValue(interfaceName, out var interfaceId)) { - return this.knownTypesMap.TryGetValue(interfaceId, out interfaceDetails); + // TODO : Add EventSource diagnostics + interfaceDetails = null; + return false; } - public bool TryGetKnownTypes(string interfaceName, out InterfaceDetails interfaceDetails) - { - if (!this.interfaceIdMapping.TryGetValue(interfaceName, out var interfaceId)) - { - // TODO : Add EventSource diagnostics - interfaceDetails = null; - return false; - } + return this.knownTypesMap.TryGetValue(interfaceId, out interfaceDetails); + } - return this.knownTypesMap.TryGetValue(interfaceId, out interfaceDetails); - } - - public void UpdateKnownTypeDetail(InterfaceDescription interfaceDescription, MethodBodyTypesBuildResult methodBodyTypesBuildResult) + public void UpdateKnownTypeDetail(InterfaceDescription interfaceDescription, MethodBodyTypesBuildResult methodBodyTypesBuildResult) + { + var responseKnownTypes = new List(); + var requestKnownType = new List(); + foreach (var entry in interfaceDescription.Methods) { - var responseKnownTypes = new List(); - var requestKnownType = new List(); - foreach (var entry in interfaceDescription.Methods) + if (TypeUtility.IsTaskType(entry.ReturnType) && entry.ReturnType.GetTypeInfo().IsGenericType) { - if (TypeUtility.IsTaskType(entry.ReturnType) && entry.ReturnType.GetTypeInfo().IsGenericType) + var returnType = entry.MethodInfo.ReturnType.GetGenericArguments()[0]; + if (!responseKnownTypes.Contains(returnType)) { - var returnType = entry.MethodInfo.ReturnType.GetGenericArguments()[0]; - if (!responseKnownTypes.Contains(returnType)) - { - responseKnownTypes.Add(returnType); - } + responseKnownTypes.Add(returnType); } - - requestKnownType.AddRange(entry.MethodInfo.GetParameters() - .ToList() - .Select(p => p.ParameterType) - .Except(requestKnownType)); } - var knownType = new InterfaceDetails - { - Id = interfaceDescription.Id, - ServiceInterfaceType = interfaceDescription.InterfaceType, - RequestKnownTypes = requestKnownType, - ResponseKnownTypes = responseKnownTypes, - MethodNames = interfaceDescription.Methods.ToDictionary(item => item.Name, item => item.Id), - RequestWrappedKnownTypes = methodBodyTypesBuildResult.GetRequestBodyTypes(), - ResponseWrappedKnownTypes = methodBodyTypesBuildResult.GetResponseBodyTypes(), - }; - this.UpdateKnownTypes(interfaceDescription.Id, interfaceDescription.InterfaceType.FullName, knownType); + requestKnownType.AddRange(entry.MethodInfo.GetParameters() + .ToList() + .Select(p => p.ParameterType) + .Except(requestKnownType)); } - private void UpdateKnownTypes( - int interfaceId, - string interfaceName, - InterfaceDetails knownTypes) + var knownType = new InterfaceDetails { - if (this.knownTypesMap.ContainsKey(interfaceId)) - { - // TODO : Add EventSource diagnostics - return; - } + Id = interfaceDescription.Id, + ServiceInterfaceType = interfaceDescription.InterfaceType, + RequestKnownTypes = requestKnownType, + ResponseKnownTypes = responseKnownTypes, + MethodNames = interfaceDescription.Methods.ToDictionary(item => item.Name, item => item.Id), + RequestWrappedKnownTypes = methodBodyTypesBuildResult.GetRequestBodyTypes(), + ResponseWrappedKnownTypes = methodBodyTypesBuildResult.GetResponseBodyTypes(), + }; + this.UpdateKnownTypes(interfaceDescription.Id, interfaceDescription.InterfaceType.FullName, knownType); + } - if (this.knownTypesMap.TryAdd(interfaceId, knownTypes)) - { - this.interfaceIdMapping.TryAdd(interfaceName, interfaceId); - } + private void UpdateKnownTypes( + int interfaceId, + string interfaceName, + InterfaceDetails knownTypes) + { + if (this.knownTypesMap.ContainsKey(interfaceId)) + { + // TODO : Add EventSource diagnostics + return; + } + + if (this.knownTypesMap.TryAdd(interfaceId, knownTypes)) + { + this.interfaceIdMapping.TryAdd(interfaceName, interfaceId); } } -} +} \ No newline at end of file diff --git a/src/Dapr.Actors/Builder/MethodBodyTypes.cs b/src/Dapr.Actors/Builder/MethodBodyTypes.cs index 0ee0aa89..7bed0db5 100644 --- a/src/Dapr.Actors/Builder/MethodBodyTypes.cs +++ b/src/Dapr.Actors/Builder/MethodBodyTypes.cs @@ -11,16 +11,15 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Builder +namespace Dapr.Actors.Builder; + +using System; + +internal class MethodBodyTypes { - using System; + public Type RequestBodyType { get; set; } - internal class MethodBodyTypes - { - public Type RequestBodyType { get; set; } + public Type ResponseBodyType { get; set; } - public Type ResponseBodyType { get; set; } - - public bool HasCancellationTokenArgument { get; set; } - } -} + public bool HasCancellationTokenArgument { get; set; } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Builder/MethodBodyTypesBuildResult.cs b/src/Dapr.Actors/Builder/MethodBodyTypesBuildResult.cs index 1cfb711d..88728f18 100644 --- a/src/Dapr.Actors/Builder/MethodBodyTypesBuildResult.cs +++ b/src/Dapr.Actors/Builder/MethodBodyTypesBuildResult.cs @@ -11,53 +11,52 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Builder +namespace Dapr.Actors.Builder; + +using System; +using System.Collections.Generic; + +internal class MethodBodyTypesBuildResult : BuildResult { - using System; - using System.Collections.Generic; - - internal class MethodBodyTypesBuildResult : BuildResult + public MethodBodyTypesBuildResult(CodeBuilderContext buildContext) + : base(buildContext) { - public MethodBodyTypesBuildResult(CodeBuilderContext buildContext) - : base(buildContext) - { - } - - // methodName, methodBodyTypes (RequestType, ResponseType) map - public IDictionary MethodBodyTypesMap { get; set; } - - public IEnumerable GetRequestBodyTypes() - { - var result = new List(); - if (this.MethodBodyTypesMap != null) - { - foreach (var item in this.MethodBodyTypesMap) - { - if (item.Value.RequestBodyType != null) - { - result.Add(item.Value.RequestBodyType); - } - } - } - - return result; - } - - public IEnumerable GetResponseBodyTypes() - { - var result = new List(); - if (this.MethodBodyTypesMap != null) - { - foreach (var item in this.MethodBodyTypesMap) - { - if (item.Value.ResponseBodyType != null) - { - result.Add(item.Value.ResponseBodyType); - } - } - } - - return result; - } } -} + + // methodName, methodBodyTypes (RequestType, ResponseType) map + public IDictionary MethodBodyTypesMap { get; set; } + + public IEnumerable GetRequestBodyTypes() + { + var result = new List(); + if (this.MethodBodyTypesMap != null) + { + foreach (var item in this.MethodBodyTypesMap) + { + if (item.Value.RequestBodyType != null) + { + result.Add(item.Value.RequestBodyType); + } + } + } + + return result; + } + + public IEnumerable GetResponseBodyTypes() + { + var result = new List(); + if (this.MethodBodyTypesMap != null) + { + foreach (var item in this.MethodBodyTypesMap) + { + if (item.Value.ResponseBodyType != null) + { + result.Add(item.Value.ResponseBodyType); + } + } + } + + return result; + } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Builder/MethodBodyTypesBuilder.cs b/src/Dapr.Actors/Builder/MethodBodyTypesBuilder.cs index f564296d..24f81248 100644 --- a/src/Dapr.Actors/Builder/MethodBodyTypesBuilder.cs +++ b/src/Dapr.Actors/Builder/MethodBodyTypesBuilder.cs @@ -11,107 +11,106 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Builder +namespace Dapr.Actors.Builder; + +using System; +using System.Collections.Generic; +using System.Reflection; +using Dapr.Actors.Description; + +internal class MethodBodyTypesBuilder : CodeBuilderModule { - using System; - using System.Collections.Generic; - using System.Reflection; - using Dapr.Actors.Description; - - internal class MethodBodyTypesBuilder : CodeBuilderModule + public MethodBodyTypesBuilder(ICodeBuilder codeBuilder) + : base(codeBuilder) { - public MethodBodyTypesBuilder(ICodeBuilder codeBuilder) - : base(codeBuilder) - { - } - - public MethodBodyTypesBuildResult Build(InterfaceDescription interfaceDescription) - { - var context = new CodeBuilderContext( - assemblyName: this.CodeBuilder.Names.GetMethodBodyTypesAssemblyName(interfaceDescription.InterfaceType), - assemblyNamespace: this.CodeBuilder.Names.GetMethodBodyTypesAssemblyNamespace(interfaceDescription.InterfaceType), - enableDebugging: CodeBuilderAttribute.IsDebuggingEnabled(interfaceDescription.InterfaceType)); - - var result = new MethodBodyTypesBuildResult(context) - { - MethodBodyTypesMap = new Dictionary(), - }; - foreach (var method in interfaceDescription.Methods) - { - result.MethodBodyTypesMap.Add( - method.Name, - Build(this.CodeBuilder.Names, context, method)); - } - - context.Complete(); - return result; - } - - private static MethodBodyTypes Build( - ICodeBuilderNames codeBuilderNames, - CodeBuilderContext context, - MethodDescription methodDescription) - { - var methodDataTypes = new MethodBodyTypes() - { - RequestBodyType = null, - ResponseBodyType = null, - HasCancellationTokenArgument = methodDescription.HasCancellationToken, - }; - - if ((methodDescription.Arguments != null) && (methodDescription.Arguments.Length != 0)) - { - methodDataTypes.RequestBodyType = BuildRequestBodyType(codeBuilderNames, context, methodDescription); - } - - if (TypeUtility.IsTaskType(methodDescription.ReturnType) && methodDescription.ReturnType.GetTypeInfo().IsGenericType) - { - methodDataTypes.ResponseBodyType = BuildResponseBodyType(codeBuilderNames, context, methodDescription); - } - - return methodDataTypes; - } - - private static Type BuildRequestBodyType( - ICodeBuilderNames codeBuilderNames, - CodeBuilderContext context, - MethodDescription methodDescription) - { - var requestBodyTypeBuilder = CodeBuilderUtils.CreateDataContractTypeBuilder( - moduleBuilder: context.ModuleBuilder, - ns: context.AssemblyNamespace, - typeName: codeBuilderNames.GetRequestBodyTypeName(methodDescription.Name), - dcNamespace: codeBuilderNames.GetDataContractNamespace()); - - foreach (var argument in methodDescription.Arguments) - { - CodeBuilderUtils.AddDataMemberField( - dcTypeBuilder: requestBodyTypeBuilder, - fieldType: argument.ArgumentType, - fieldName: argument.Name); - } - - return requestBodyTypeBuilder.CreateTypeInfo().AsType(); - } - - private static Type BuildResponseBodyType( - ICodeBuilderNames codeBuilderNames, - CodeBuilderContext context, - MethodDescription methodDescription) - { - var responseBodyTypeBuilder = CodeBuilderUtils.CreateDataContractTypeBuilder( - moduleBuilder: context.ModuleBuilder, - ns: context.AssemblyNamespace, - typeName: codeBuilderNames.GetResponseBodyTypeName(methodDescription.Name), - dcNamespace: codeBuilderNames.GetDataContractNamespace()); - - var returnDataType = methodDescription.ReturnType.GetGenericArguments()[0]; - CodeBuilderUtils.AddDataMemberField( - dcTypeBuilder: responseBodyTypeBuilder, - fieldType: returnDataType, - fieldName: codeBuilderNames.RetVal); - - return responseBodyTypeBuilder.CreateTypeInfo().AsType(); - } } -} + + public MethodBodyTypesBuildResult Build(InterfaceDescription interfaceDescription) + { + var context = new CodeBuilderContext( + assemblyName: this.CodeBuilder.Names.GetMethodBodyTypesAssemblyName(interfaceDescription.InterfaceType), + assemblyNamespace: this.CodeBuilder.Names.GetMethodBodyTypesAssemblyNamespace(interfaceDescription.InterfaceType), + enableDebugging: CodeBuilderAttribute.IsDebuggingEnabled(interfaceDescription.InterfaceType)); + + var result = new MethodBodyTypesBuildResult(context) + { + MethodBodyTypesMap = new Dictionary(), + }; + foreach (var method in interfaceDescription.Methods) + { + result.MethodBodyTypesMap.Add( + method.Name, + Build(this.CodeBuilder.Names, context, method)); + } + + context.Complete(); + return result; + } + + private static MethodBodyTypes Build( + ICodeBuilderNames codeBuilderNames, + CodeBuilderContext context, + MethodDescription methodDescription) + { + var methodDataTypes = new MethodBodyTypes() + { + RequestBodyType = null, + ResponseBodyType = null, + HasCancellationTokenArgument = methodDescription.HasCancellationToken, + }; + + if ((methodDescription.Arguments != null) && (methodDescription.Arguments.Length != 0)) + { + methodDataTypes.RequestBodyType = BuildRequestBodyType(codeBuilderNames, context, methodDescription); + } + + if (TypeUtility.IsTaskType(methodDescription.ReturnType) && methodDescription.ReturnType.GetTypeInfo().IsGenericType) + { + methodDataTypes.ResponseBodyType = BuildResponseBodyType(codeBuilderNames, context, methodDescription); + } + + return methodDataTypes; + } + + private static Type BuildRequestBodyType( + ICodeBuilderNames codeBuilderNames, + CodeBuilderContext context, + MethodDescription methodDescription) + { + var requestBodyTypeBuilder = CodeBuilderUtils.CreateDataContractTypeBuilder( + moduleBuilder: context.ModuleBuilder, + ns: context.AssemblyNamespace, + typeName: codeBuilderNames.GetRequestBodyTypeName(methodDescription.Name), + dcNamespace: codeBuilderNames.GetDataContractNamespace()); + + foreach (var argument in methodDescription.Arguments) + { + CodeBuilderUtils.AddDataMemberField( + dcTypeBuilder: requestBodyTypeBuilder, + fieldType: argument.ArgumentType, + fieldName: argument.Name); + } + + return requestBodyTypeBuilder.CreateTypeInfo().AsType(); + } + + private static Type BuildResponseBodyType( + ICodeBuilderNames codeBuilderNames, + CodeBuilderContext context, + MethodDescription methodDescription) + { + var responseBodyTypeBuilder = CodeBuilderUtils.CreateDataContractTypeBuilder( + moduleBuilder: context.ModuleBuilder, + ns: context.AssemblyNamespace, + typeName: codeBuilderNames.GetResponseBodyTypeName(methodDescription.Name), + dcNamespace: codeBuilderNames.GetDataContractNamespace()); + + var returnDataType = methodDescription.ReturnType.GetGenericArguments()[0]; + CodeBuilderUtils.AddDataMemberField( + dcTypeBuilder: responseBodyTypeBuilder, + fieldType: returnDataType, + fieldName: codeBuilderNames.RetVal); + + return responseBodyTypeBuilder.CreateTypeInfo().AsType(); + } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Builder/MethodDispatcherBuildResult.cs b/src/Dapr.Actors/Builder/MethodDispatcherBuildResult.cs index 6964dd8e..fb29cdd6 100644 --- a/src/Dapr.Actors/Builder/MethodDispatcherBuildResult.cs +++ b/src/Dapr.Actors/Builder/MethodDispatcherBuildResult.cs @@ -11,19 +11,18 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Builder +namespace Dapr.Actors.Builder; + +using System; + +internal class MethodDispatcherBuildResult : BuildResult { - using System; - - internal class MethodDispatcherBuildResult : BuildResult + public MethodDispatcherBuildResult(CodeBuilderContext buildContext) + : base(buildContext) { - public MethodDispatcherBuildResult(CodeBuilderContext buildContext) - : base(buildContext) - { - } - - public Type MethodDispatcherType { get; set; } - - public ActorMethodDispatcherBase MethodDispatcher { get; set; } } -} + + public Type MethodDispatcherType { get; set; } + + public ActorMethodDispatcherBase MethodDispatcher { get; set; } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Builder/MethodDispatcherBuilder.cs b/src/Dapr.Actors/Builder/MethodDispatcherBuilder.cs index 7bdc9276..37ada87d 100644 --- a/src/Dapr.Actors/Builder/MethodDispatcherBuilder.cs +++ b/src/Dapr.Actors/Builder/MethodDispatcherBuilder.cs @@ -11,429 +11,428 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Builder +namespace Dapr.Actors.Builder; + +using System; +using System.Reflection; +using System.Reflection.Emit; +using System.Threading; +using System.Threading.Tasks; +using Dapr.Actors.Communication; +using Dapr.Actors.Description; + +internal class MethodDispatcherBuilder : CodeBuilderModule + where TMethodDispatcher : ActorMethodDispatcherBase { - using System; - using System.Reflection; - using System.Reflection.Emit; - using System.Threading; - using System.Threading.Tasks; - using Dapr.Actors.Communication; - using Dapr.Actors.Description; + private static readonly MethodInfo getTypeFromHandle = typeof(Type).GetMethod(nameof(Type.GetTypeFromHandle)); + private readonly Type methodDispatcherBaseType; + private readonly MethodInfo continueWithResultMethodInfo; + private readonly MethodInfo continueWithMethodInfo; + private readonly MethodInfo checkIfitsWrapped; - internal class MethodDispatcherBuilder : CodeBuilderModule - where TMethodDispatcher : ActorMethodDispatcherBase + public MethodDispatcherBuilder(ICodeBuilder codeBuilder) + : base(codeBuilder) { - private static readonly MethodInfo getTypeFromHandle = typeof(Type).GetMethod(nameof(Type.GetTypeFromHandle)); - private readonly Type methodDispatcherBaseType; - private readonly MethodInfo continueWithResultMethodInfo; - private readonly MethodInfo continueWithMethodInfo; - private readonly MethodInfo checkIfitsWrapped; + this.methodDispatcherBaseType = typeof(TMethodDispatcher); - public MethodDispatcherBuilder(ICodeBuilder codeBuilder) - : base(codeBuilder) + this.continueWithResultMethodInfo = this.methodDispatcherBaseType.GetMethod( + "ContinueWithResult", + BindingFlags.Instance | BindingFlags.NonPublic); + + this.continueWithMethodInfo = this.methodDispatcherBaseType.GetMethod( + "ContinueWith", + BindingFlags.Instance | BindingFlags.NonPublic); + this.checkIfitsWrapped = this.methodDispatcherBaseType.GetMethod( + "CheckIfItsWrappedRequest", + BindingFlags.Instance | BindingFlags.NonPublic, + null, + CallingConventions.Any, + new[] { typeof(IActorRequestMessageBody) }, + null); + } + + public MethodDispatcherBuildResult Build( + InterfaceDescription interfaceDescription) + { + var context = new CodeBuilderContext( + assemblyName: this.CodeBuilder.Names.GetMethodDispatcherAssemblyName(interfaceDescription.InterfaceType), + assemblyNamespace: this.CodeBuilder.Names.GetMethodDispatcherAssemblyNamespace(interfaceDescription + .InterfaceType), + enableDebugging: CodeBuilderAttribute.IsDebuggingEnabled(interfaceDescription.InterfaceType)); + + var result = new MethodDispatcherBuildResult(context); + + // ensure that the method body types are built + var methodBodyTypesBuildResult = + this.CodeBuilder.GetOrBuildMethodBodyTypes(interfaceDescription.InterfaceType); + + // build dispatcher class + var classBuilder = CodeBuilderUtils.CreateClassBuilder( + context.ModuleBuilder, + ns: context.AssemblyNamespace, + className: this.CodeBuilder.Names.GetMethodDispatcherClassName(interfaceDescription.InterfaceType), + baseType: this.methodDispatcherBaseType); + + this.AddCreateResponseBodyMethod(classBuilder, interfaceDescription, methodBodyTypesBuildResult); + this.AddOnDispatchAsyncMethod(classBuilder, interfaceDescription, methodBodyTypesBuildResult); + this.AddOnDispatchMethod(classBuilder, interfaceDescription, methodBodyTypesBuildResult); + + var methodNameMap = GetMethodNameMap(interfaceDescription); + + // create the dispatcher type, instantiate and initialize it + result.MethodDispatcherType = classBuilder.CreateTypeInfo().AsType(); + result.MethodDispatcher = (TMethodDispatcher)Activator.CreateInstance(result.MethodDispatcherType); + var v2MethodDispatcherBase = (ActorMethodDispatcherBase)result.MethodDispatcher; + v2MethodDispatcherBase.Initialize(interfaceDescription, methodNameMap); + + context.Complete(); + return result; + } + + private static void AddIfNotWrapMsgGetParameter( + ILGenerator ilGen, + LocalBuilder castedObject, + MethodDescription methodDescription, + Type requestBody) + { + // now invoke the method on the casted object + ilGen.Emit(OpCodes.Ldloc, castedObject); + + if ((methodDescription.Arguments != null) && (methodDescription.Arguments.Length != 0)) { - this.methodDispatcherBaseType = typeof(TMethodDispatcher); - - this.continueWithResultMethodInfo = this.methodDispatcherBaseType.GetMethod( - "ContinueWithResult", - BindingFlags.Instance | BindingFlags.NonPublic); - - this.continueWithMethodInfo = this.methodDispatcherBaseType.GetMethod( - "ContinueWith", - BindingFlags.Instance | BindingFlags.NonPublic); - this.checkIfitsWrapped = this.methodDispatcherBaseType.GetMethod( - "CheckIfItsWrappedRequest", - BindingFlags.Instance | BindingFlags.NonPublic, - null, - CallingConventions.Any, - new[] { typeof(IActorRequestMessageBody) }, - null); - } - - public MethodDispatcherBuildResult Build( - InterfaceDescription interfaceDescription) - { - var context = new CodeBuilderContext( - assemblyName: this.CodeBuilder.Names.GetMethodDispatcherAssemblyName(interfaceDescription.InterfaceType), - assemblyNamespace: this.CodeBuilder.Names.GetMethodDispatcherAssemblyNamespace(interfaceDescription - .InterfaceType), - enableDebugging: CodeBuilderAttribute.IsDebuggingEnabled(interfaceDescription.InterfaceType)); - - var result = new MethodDispatcherBuildResult(context); - - // ensure that the method body types are built - var methodBodyTypesBuildResult = - this.CodeBuilder.GetOrBuildMethodBodyTypes(interfaceDescription.InterfaceType); - - // build dispatcher class - var classBuilder = CodeBuilderUtils.CreateClassBuilder( - context.ModuleBuilder, - ns: context.AssemblyNamespace, - className: this.CodeBuilder.Names.GetMethodDispatcherClassName(interfaceDescription.InterfaceType), - baseType: this.methodDispatcherBaseType); - - this.AddCreateResponseBodyMethod(classBuilder, interfaceDescription, methodBodyTypesBuildResult); - this.AddOnDispatchAsyncMethod(classBuilder, interfaceDescription, methodBodyTypesBuildResult); - this.AddOnDispatchMethod(classBuilder, interfaceDescription, methodBodyTypesBuildResult); - - var methodNameMap = GetMethodNameMap(interfaceDescription); - - // create the dispatcher type, instantiate and initialize it - result.MethodDispatcherType = classBuilder.CreateTypeInfo().AsType(); - result.MethodDispatcher = (TMethodDispatcher)Activator.CreateInstance(result.MethodDispatcherType); - var v2MethodDispatcherBase = (ActorMethodDispatcherBase)result.MethodDispatcher; - v2MethodDispatcherBase.Initialize(interfaceDescription, methodNameMap); - - context.Complete(); - return result; - } - - private static void AddIfNotWrapMsgGetParameter( - ILGenerator ilGen, - LocalBuilder castedObject, - MethodDescription methodDescription, - Type requestBody) - { - // now invoke the method on the casted object - ilGen.Emit(OpCodes.Ldloc, castedObject); - - if ((methodDescription.Arguments != null) && (methodDescription.Arguments.Length != 0)) + var method = requestBody.GetMethod("GetParameter"); + for (var i = 0; i < methodDescription.Arguments.Length; i++) { - var method = requestBody.GetMethod("GetParameter"); - for (var i = 0; i < methodDescription.Arguments.Length; i++) - { - var argument = methodDescription.Arguments[i]; + var argument = methodDescription.Arguments[i]; - // ReSharper disable once AssignNullToNotNullAttribute - // castedRequestBody is set to non-null in the previous if check on the same condition - ilGen.Emit(OpCodes.Ldarg_3); - ilGen.Emit(OpCodes.Ldc_I4, i); - ilGen.Emit(OpCodes.Ldstr, argument.Name); - ilGen.Emit(OpCodes.Ldtoken, argument.ArgumentType); - ilGen.Emit(OpCodes.Call, getTypeFromHandle); - ilGen.Emit(OpCodes.Callvirt, method); - ilGen.Emit(OpCodes.Unbox_Any, argument.ArgumentType); - } - } - } - - private static void AddIfWrapMsgGetParameters( - ILGenerator ilGen, - LocalBuilder castedObject, - MethodBodyTypes methodBodyTypes) - { - var wrappedRequest = ilGen.DeclareLocal(typeof(object)); - - var getValueMethod = typeof(WrappedMessage).GetProperty("Value").GetGetMethod(); - ilGen.Emit(OpCodes.Ldarg_3); // request object - ilGen.Emit(OpCodes.Callvirt, getValueMethod); - ilGen.Emit(OpCodes.Stloc, wrappedRequest); - - // then cast and call GetField - LocalBuilder castedRequestBody = null; - - if (methodBodyTypes.RequestBodyType != null) - { - // cast the request body - // var castedRequestBody = ()requestBody; - castedRequestBody = ilGen.DeclareLocal(methodBodyTypes.RequestBodyType); - ilGen.Emit(OpCodes.Ldloc, wrappedRequest); // wrapped request - ilGen.Emit(OpCodes.Castclass, methodBodyTypes.RequestBodyType); - ilGen.Emit(OpCodes.Stloc, castedRequestBody); - } - - // now invoke the method on the casted object - ilGen.Emit(OpCodes.Ldloc, castedObject); - - if (methodBodyTypes.RequestBodyType != null) - { - foreach (var field in methodBodyTypes.RequestBodyType.GetFields()) - { - // ReSharper disable once AssignNullToNotNullAttribute - // castedRequestBody is set to non-null in the previous if check on the same condition - ilGen.Emit(OpCodes.Ldloc, castedRequestBody); - ilGen.Emit(OpCodes.Ldfld, field); - } - } - } - - private void AddOnDispatchMethod( - TypeBuilder classBuilder, - InterfaceDescription interfaceDescription, - MethodBodyTypesBuildResult methodBodyTypesBuildResult) - { - var dispatchMethodImpl = CodeBuilderUtils.CreateProtectedMethodBuilder( - classBuilder, - "OnDispatch", - typeof(void), - typeof(int), // methodid - typeof(object), // remoted object - typeof(IActorRequestMessageBody)); // requestBody - - var ilGen = dispatchMethodImpl.GetILGenerator(); - - var castedObject = ilGen.DeclareLocal(interfaceDescription.InterfaceType); - ilGen.Emit(OpCodes.Ldarg_2); // load remoted object - ilGen.Emit(OpCodes.Castclass, interfaceDescription.InterfaceType); - ilGen.Emit(OpCodes.Stloc, castedObject); // store casted result to local 0 - - foreach (var methodDescription in interfaceDescription.Methods) - { - if (!TypeUtility.IsVoidType(methodDescription.ReturnType)) - { - continue; - } - - var elseLable = ilGen.DefineLabel(); - - this.AddIfMethodIdInvokeBlock( - ilGen: ilGen, - elseLabel: elseLable, - castedObject: castedObject, - methodDescription: methodDescription, - methodBodyTypes: methodBodyTypesBuildResult.MethodBodyTypesMap[methodDescription.Name]); - - ilGen.MarkLabel(elseLable); - } - - ilGen.ThrowException(typeof(MissingMethodException)); - } - - private void AddIfMethodIdInvokeBlock( - ILGenerator ilGen, - Label elseLabel, - LocalBuilder castedObject, - MethodDescription methodDescription, - MethodBodyTypes methodBodyTypes) - { - ilGen.Emit(OpCodes.Ldarg_1); - ilGen.Emit(OpCodes.Ldc_I4, methodDescription.Id); - ilGen.Emit(OpCodes.Bne_Un, elseLabel); - - // Check If its Wrapped , then call getparam - var requestBody = typeof(IActorRequestMessageBody); - - // now invoke the method on the casted object - ilGen.Emit(OpCodes.Ldloc, castedObject); - - // Check if its WrappedMessage - var elseLabelforWrapped = ilGen.DefineLabel(); - this.AddACheckIfItsWrappedMessage(ilGen, elseLabelforWrapped); - var endlabel = ilGen.DefineLabel(); - - // 2 If true then call GetValue - AddIfWrapMsgGetParameters(ilGen, castedObject, methodBodyTypes); - ilGen.Emit(OpCodes.Br, endlabel); - ilGen.MarkLabel(elseLabelforWrapped); - - // else call GetParameter on IServiceRemotingMessageBody - AddIfNotWrapMsgGetParameter(ilGen, castedObject, methodDescription, requestBody); - - ilGen.MarkLabel(endlabel); - - ilGen.EmitCall(OpCodes.Callvirt, methodDescription.MethodInfo, null); - ilGen.Emit(OpCodes.Ret); - } - - private void AddOnDispatchAsyncMethod( - TypeBuilder classBuilder, - InterfaceDescription interfaceDescription, - MethodBodyTypesBuildResult methodBodyTypesBuildResult) - { - var dispatchMethodImpl = CodeBuilderUtils.CreateProtectedMethodBuilder( - classBuilder, - "OnDispatchAsync", - typeof(Task), - typeof(int), // methodid - typeof(object), // remoted object - typeof(IActorRequestMessageBody), // requestBody - typeof(IActorMessageBodyFactory), // remotingmessageBodyFactory - typeof(CancellationToken)); // CancellationToken - - var ilGen = dispatchMethodImpl.GetILGenerator(); - - var castedObject = ilGen.DeclareLocal(interfaceDescription.InterfaceType); - ilGen.Emit(OpCodes.Ldarg_2); // load remoted object - ilGen.Emit(OpCodes.Castclass, interfaceDescription.InterfaceType); - ilGen.Emit(OpCodes.Stloc, castedObject); // store casted result to local 0 - - foreach (var methodDescription in interfaceDescription.Methods) - { - if (!TypeUtility.IsTaskType(methodDescription.ReturnType)) - { - continue; - } - - var elseLable = ilGen.DefineLabel(); - - this.AddIfMethodIdInvokeAsyncBlock( - ilGen: ilGen, - elseLabel: elseLable, - castedObject: castedObject, - methodDescription: methodDescription, - interfaceName: interfaceDescription.InterfaceType.FullName, - methodBodyTypes: methodBodyTypesBuildResult.MethodBodyTypesMap[methodDescription.Name]); - - ilGen.MarkLabel(elseLable); - } - - ilGen.ThrowException(typeof(MissingMethodException)); - } - - private void AddIfMethodIdInvokeAsyncBlock( - ILGenerator ilGen, - Label elseLabel, - LocalBuilder castedObject, - MethodDescription methodDescription, - string interfaceName, - MethodBodyTypes methodBodyTypes) - { - ilGen.Emit(OpCodes.Ldarg_1); - ilGen.Emit(OpCodes.Ldc_I4, methodDescription.Id); - ilGen.Emit(OpCodes.Bne_Un, elseLabel); - - var invokeTask = ilGen.DeclareLocal(methodDescription.ReturnType); - var requestBody = typeof(IActorRequestMessageBody); - - // now invoke the method on the casted object - ilGen.Emit(OpCodes.Ldloc, castedObject); - - // Check if its WrappedMessage - var elseLabelforWrapped = ilGen.DefineLabel(); - this.AddACheckIfItsWrappedMessage(ilGen, elseLabelforWrapped); - var endlabel = ilGen.DefineLabel(); - - // 2 If true then call GetValue - AddIfWrapMsgGetParameters(ilGen, castedObject, methodBodyTypes); - ilGen.Emit(OpCodes.Br, endlabel); - ilGen.MarkLabel(elseLabelforWrapped); - - // else call GetParameter on IServiceRemotingMessageBody - AddIfNotWrapMsgGetParameter(ilGen, castedObject, methodDescription, requestBody); - - ilGen.MarkLabel(endlabel); - - if (methodDescription.HasCancellationToken) - { - ilGen.Emit(OpCodes.Ldarg, 5); - } - - ilGen.EmitCall(OpCodes.Callvirt, methodDescription.MethodInfo, null); - ilGen.Emit(OpCodes.Stloc, invokeTask); - - // call the base method to return continuation task - if (TypeUtility.IsTaskType(methodDescription.ReturnType) && - methodDescription.ReturnType.GetTypeInfo().IsGenericType) - { - // the return is Task - var continueWithGenericMethodInfo = - this.continueWithResultMethodInfo.MakeGenericMethod(methodDescription.ReturnType - .GenericTypeArguments[0]); - - ilGen.Emit(OpCodes.Ldarg_0); // base - ilGen.Emit(OpCodes.Ldstr, interfaceName); - ilGen.Emit(OpCodes.Ldstr, methodDescription.Name); - ilGen.Emit(OpCodes.Ldc_I4, methodDescription.Id); - ilGen.Emit(OpCodes.Ldarg, 4); // message body factory - ilGen.Emit(OpCodes.Ldloc, invokeTask); - ilGen.EmitCall(OpCodes.Call, continueWithGenericMethodInfo, null); - ilGen.Emit(OpCodes.Ret); - } - else - { - ilGen.Emit(OpCodes.Ldarg_0); // base - ilGen.Emit(OpCodes.Ldloc, invokeTask); - ilGen.EmitCall(OpCodes.Call, this.continueWithMethodInfo, null); - ilGen.Emit(OpCodes.Ret); - } - } - - private void AddACheckIfItsWrappedMessage( - ILGenerator ilGen, Label elseLabelforWrapped) - { - var boolres = ilGen.DeclareLocal(typeof(bool)); - ilGen.Emit(OpCodes.Ldarg_3); // request object - ilGen.Emit(OpCodes.Call, this.checkIfitsWrapped); - ilGen.Emit(OpCodes.Stloc, boolres); - ilGen.Emit(OpCodes.Ldloc, boolres); - ilGen.Emit(OpCodes.Brfalse, elseLabelforWrapped); - } - - private void AddCreateResponseBodyMethod( - TypeBuilder classBuilder, - InterfaceDescription interfaceDescription, - MethodBodyTypesBuildResult methodBodyTypesBuildResult) - { - var methodBuilder = CodeBuilderUtils.CreateProtectedMethodBuilder( - classBuilder, - "CreateWrappedResponseBody", - typeof(object), // responseBody - return value - typeof(int), // methodId - typeof(object)); // retval from the invoked method on the remoted object - - var ilGen = methodBuilder.GetILGenerator(); - - foreach (var methodDescription in interfaceDescription.Methods) - { - var methodBodyTypes = methodBodyTypesBuildResult.MethodBodyTypesMap[methodDescription.Name]; - if (methodBodyTypes.ResponseBodyType == null) - { - continue; - } - - var elseLabel = ilGen.DefineLabel(); - - this.AddIfMethodIdCreateResponseBlock( - ilGen, - elseLabel, - methodDescription.Id, - methodBodyTypes.ResponseBodyType); - - ilGen.MarkLabel(elseLabel); - } - - // return null; (if method id's do not match) - ilGen.Emit(OpCodes.Ldnull); - ilGen.Emit(OpCodes.Ret); - } - - private void AddIfMethodIdCreateResponseBlock( - ILGenerator ilGen, - Label elseLabel, - int methodId, - Type responseType) - { - // if (methodId == ) - ilGen.Emit(OpCodes.Ldarg_1); - ilGen.Emit(OpCodes.Ldc_I4, methodId); - ilGen.Emit(OpCodes.Bne_Un, elseLabel); - - var ctorInfo = responseType.GetConstructor(Type.EmptyTypes); - if (ctorInfo != null) - { - var localBuilder = ilGen.DeclareLocal(responseType); - - // new - ilGen.Emit(OpCodes.Newobj, ctorInfo); - ilGen.Emit(OpCodes.Stloc, localBuilder); - ilGen.Emit(OpCodes.Ldloc, localBuilder); - - // responseBody.retval = ()retval; - var fInfo = responseType.GetField(this.CodeBuilder.Names.RetVal); - ilGen.Emit(OpCodes.Ldarg_2); - ilGen.Emit( - fInfo.FieldType.GetTypeInfo().IsClass ? OpCodes.Castclass : OpCodes.Unbox_Any, - fInfo.FieldType); - ilGen.Emit(OpCodes.Stfld, fInfo); - ilGen.Emit(OpCodes.Ldloc, localBuilder); - ilGen.Emit(OpCodes.Ret); - } - else - { - ilGen.Emit(OpCodes.Ldnull); - ilGen.Emit(OpCodes.Ret); + // ReSharper disable once AssignNullToNotNullAttribute + // castedRequestBody is set to non-null in the previous if check on the same condition + ilGen.Emit(OpCodes.Ldarg_3); + ilGen.Emit(OpCodes.Ldc_I4, i); + ilGen.Emit(OpCodes.Ldstr, argument.Name); + ilGen.Emit(OpCodes.Ldtoken, argument.ArgumentType); + ilGen.Emit(OpCodes.Call, getTypeFromHandle); + ilGen.Emit(OpCodes.Callvirt, method); + ilGen.Emit(OpCodes.Unbox_Any, argument.ArgumentType); } } } -} + + private static void AddIfWrapMsgGetParameters( + ILGenerator ilGen, + LocalBuilder castedObject, + MethodBodyTypes methodBodyTypes) + { + var wrappedRequest = ilGen.DeclareLocal(typeof(object)); + + var getValueMethod = typeof(WrappedMessage).GetProperty("Value").GetGetMethod(); + ilGen.Emit(OpCodes.Ldarg_3); // request object + ilGen.Emit(OpCodes.Callvirt, getValueMethod); + ilGen.Emit(OpCodes.Stloc, wrappedRequest); + + // then cast and call GetField + LocalBuilder castedRequestBody = null; + + if (methodBodyTypes.RequestBodyType != null) + { + // cast the request body + // var castedRequestBody = ()requestBody; + castedRequestBody = ilGen.DeclareLocal(methodBodyTypes.RequestBodyType); + ilGen.Emit(OpCodes.Ldloc, wrappedRequest); // wrapped request + ilGen.Emit(OpCodes.Castclass, methodBodyTypes.RequestBodyType); + ilGen.Emit(OpCodes.Stloc, castedRequestBody); + } + + // now invoke the method on the casted object + ilGen.Emit(OpCodes.Ldloc, castedObject); + + if (methodBodyTypes.RequestBodyType != null) + { + foreach (var field in methodBodyTypes.RequestBodyType.GetFields()) + { + // ReSharper disable once AssignNullToNotNullAttribute + // castedRequestBody is set to non-null in the previous if check on the same condition + ilGen.Emit(OpCodes.Ldloc, castedRequestBody); + ilGen.Emit(OpCodes.Ldfld, field); + } + } + } + + private void AddOnDispatchMethod( + TypeBuilder classBuilder, + InterfaceDescription interfaceDescription, + MethodBodyTypesBuildResult methodBodyTypesBuildResult) + { + var dispatchMethodImpl = CodeBuilderUtils.CreateProtectedMethodBuilder( + classBuilder, + "OnDispatch", + typeof(void), + typeof(int), // methodid + typeof(object), // remoted object + typeof(IActorRequestMessageBody)); // requestBody + + var ilGen = dispatchMethodImpl.GetILGenerator(); + + var castedObject = ilGen.DeclareLocal(interfaceDescription.InterfaceType); + ilGen.Emit(OpCodes.Ldarg_2); // load remoted object + ilGen.Emit(OpCodes.Castclass, interfaceDescription.InterfaceType); + ilGen.Emit(OpCodes.Stloc, castedObject); // store casted result to local 0 + + foreach (var methodDescription in interfaceDescription.Methods) + { + if (!TypeUtility.IsVoidType(methodDescription.ReturnType)) + { + continue; + } + + var elseLable = ilGen.DefineLabel(); + + this.AddIfMethodIdInvokeBlock( + ilGen: ilGen, + elseLabel: elseLable, + castedObject: castedObject, + methodDescription: methodDescription, + methodBodyTypes: methodBodyTypesBuildResult.MethodBodyTypesMap[methodDescription.Name]); + + ilGen.MarkLabel(elseLable); + } + + ilGen.ThrowException(typeof(MissingMethodException)); + } + + private void AddIfMethodIdInvokeBlock( + ILGenerator ilGen, + Label elseLabel, + LocalBuilder castedObject, + MethodDescription methodDescription, + MethodBodyTypes methodBodyTypes) + { + ilGen.Emit(OpCodes.Ldarg_1); + ilGen.Emit(OpCodes.Ldc_I4, methodDescription.Id); + ilGen.Emit(OpCodes.Bne_Un, elseLabel); + + // Check If its Wrapped , then call getparam + var requestBody = typeof(IActorRequestMessageBody); + + // now invoke the method on the casted object + ilGen.Emit(OpCodes.Ldloc, castedObject); + + // Check if its WrappedMessage + var elseLabelforWrapped = ilGen.DefineLabel(); + this.AddACheckIfItsWrappedMessage(ilGen, elseLabelforWrapped); + var endlabel = ilGen.DefineLabel(); + + // 2 If true then call GetValue + AddIfWrapMsgGetParameters(ilGen, castedObject, methodBodyTypes); + ilGen.Emit(OpCodes.Br, endlabel); + ilGen.MarkLabel(elseLabelforWrapped); + + // else call GetParameter on IServiceRemotingMessageBody + AddIfNotWrapMsgGetParameter(ilGen, castedObject, methodDescription, requestBody); + + ilGen.MarkLabel(endlabel); + + ilGen.EmitCall(OpCodes.Callvirt, methodDescription.MethodInfo, null); + ilGen.Emit(OpCodes.Ret); + } + + private void AddOnDispatchAsyncMethod( + TypeBuilder classBuilder, + InterfaceDescription interfaceDescription, + MethodBodyTypesBuildResult methodBodyTypesBuildResult) + { + var dispatchMethodImpl = CodeBuilderUtils.CreateProtectedMethodBuilder( + classBuilder, + "OnDispatchAsync", + typeof(Task), + typeof(int), // methodid + typeof(object), // remoted object + typeof(IActorRequestMessageBody), // requestBody + typeof(IActorMessageBodyFactory), // remotingmessageBodyFactory + typeof(CancellationToken)); // CancellationToken + + var ilGen = dispatchMethodImpl.GetILGenerator(); + + var castedObject = ilGen.DeclareLocal(interfaceDescription.InterfaceType); + ilGen.Emit(OpCodes.Ldarg_2); // load remoted object + ilGen.Emit(OpCodes.Castclass, interfaceDescription.InterfaceType); + ilGen.Emit(OpCodes.Stloc, castedObject); // store casted result to local 0 + + foreach (var methodDescription in interfaceDescription.Methods) + { + if (!TypeUtility.IsTaskType(methodDescription.ReturnType)) + { + continue; + } + + var elseLable = ilGen.DefineLabel(); + + this.AddIfMethodIdInvokeAsyncBlock( + ilGen: ilGen, + elseLabel: elseLable, + castedObject: castedObject, + methodDescription: methodDescription, + interfaceName: interfaceDescription.InterfaceType.FullName, + methodBodyTypes: methodBodyTypesBuildResult.MethodBodyTypesMap[methodDescription.Name]); + + ilGen.MarkLabel(elseLable); + } + + ilGen.ThrowException(typeof(MissingMethodException)); + } + + private void AddIfMethodIdInvokeAsyncBlock( + ILGenerator ilGen, + Label elseLabel, + LocalBuilder castedObject, + MethodDescription methodDescription, + string interfaceName, + MethodBodyTypes methodBodyTypes) + { + ilGen.Emit(OpCodes.Ldarg_1); + ilGen.Emit(OpCodes.Ldc_I4, methodDescription.Id); + ilGen.Emit(OpCodes.Bne_Un, elseLabel); + + var invokeTask = ilGen.DeclareLocal(methodDescription.ReturnType); + var requestBody = typeof(IActorRequestMessageBody); + + // now invoke the method on the casted object + ilGen.Emit(OpCodes.Ldloc, castedObject); + + // Check if its WrappedMessage + var elseLabelforWrapped = ilGen.DefineLabel(); + this.AddACheckIfItsWrappedMessage(ilGen, elseLabelforWrapped); + var endlabel = ilGen.DefineLabel(); + + // 2 If true then call GetValue + AddIfWrapMsgGetParameters(ilGen, castedObject, methodBodyTypes); + ilGen.Emit(OpCodes.Br, endlabel); + ilGen.MarkLabel(elseLabelforWrapped); + + // else call GetParameter on IServiceRemotingMessageBody + AddIfNotWrapMsgGetParameter(ilGen, castedObject, methodDescription, requestBody); + + ilGen.MarkLabel(endlabel); + + if (methodDescription.HasCancellationToken) + { + ilGen.Emit(OpCodes.Ldarg, 5); + } + + ilGen.EmitCall(OpCodes.Callvirt, methodDescription.MethodInfo, null); + ilGen.Emit(OpCodes.Stloc, invokeTask); + + // call the base method to return continuation task + if (TypeUtility.IsTaskType(methodDescription.ReturnType) && + methodDescription.ReturnType.GetTypeInfo().IsGenericType) + { + // the return is Task + var continueWithGenericMethodInfo = + this.continueWithResultMethodInfo.MakeGenericMethod(methodDescription.ReturnType + .GenericTypeArguments[0]); + + ilGen.Emit(OpCodes.Ldarg_0); // base + ilGen.Emit(OpCodes.Ldstr, interfaceName); + ilGen.Emit(OpCodes.Ldstr, methodDescription.Name); + ilGen.Emit(OpCodes.Ldc_I4, methodDescription.Id); + ilGen.Emit(OpCodes.Ldarg, 4); // message body factory + ilGen.Emit(OpCodes.Ldloc, invokeTask); + ilGen.EmitCall(OpCodes.Call, continueWithGenericMethodInfo, null); + ilGen.Emit(OpCodes.Ret); + } + else + { + ilGen.Emit(OpCodes.Ldarg_0); // base + ilGen.Emit(OpCodes.Ldloc, invokeTask); + ilGen.EmitCall(OpCodes.Call, this.continueWithMethodInfo, null); + ilGen.Emit(OpCodes.Ret); + } + } + + private void AddACheckIfItsWrappedMessage( + ILGenerator ilGen, Label elseLabelforWrapped) + { + var boolres = ilGen.DeclareLocal(typeof(bool)); + ilGen.Emit(OpCodes.Ldarg_3); // request object + ilGen.Emit(OpCodes.Call, this.checkIfitsWrapped); + ilGen.Emit(OpCodes.Stloc, boolres); + ilGen.Emit(OpCodes.Ldloc, boolres); + ilGen.Emit(OpCodes.Brfalse, elseLabelforWrapped); + } + + private void AddCreateResponseBodyMethod( + TypeBuilder classBuilder, + InterfaceDescription interfaceDescription, + MethodBodyTypesBuildResult methodBodyTypesBuildResult) + { + var methodBuilder = CodeBuilderUtils.CreateProtectedMethodBuilder( + classBuilder, + "CreateWrappedResponseBody", + typeof(object), // responseBody - return value + typeof(int), // methodId + typeof(object)); // retval from the invoked method on the remoted object + + var ilGen = methodBuilder.GetILGenerator(); + + foreach (var methodDescription in interfaceDescription.Methods) + { + var methodBodyTypes = methodBodyTypesBuildResult.MethodBodyTypesMap[methodDescription.Name]; + if (methodBodyTypes.ResponseBodyType == null) + { + continue; + } + + var elseLabel = ilGen.DefineLabel(); + + this.AddIfMethodIdCreateResponseBlock( + ilGen, + elseLabel, + methodDescription.Id, + methodBodyTypes.ResponseBodyType); + + ilGen.MarkLabel(elseLabel); + } + + // return null; (if method id's do not match) + ilGen.Emit(OpCodes.Ldnull); + ilGen.Emit(OpCodes.Ret); + } + + private void AddIfMethodIdCreateResponseBlock( + ILGenerator ilGen, + Label elseLabel, + int methodId, + Type responseType) + { + // if (methodId == ) + ilGen.Emit(OpCodes.Ldarg_1); + ilGen.Emit(OpCodes.Ldc_I4, methodId); + ilGen.Emit(OpCodes.Bne_Un, elseLabel); + + var ctorInfo = responseType.GetConstructor(Type.EmptyTypes); + if (ctorInfo != null) + { + var localBuilder = ilGen.DeclareLocal(responseType); + + // new + ilGen.Emit(OpCodes.Newobj, ctorInfo); + ilGen.Emit(OpCodes.Stloc, localBuilder); + ilGen.Emit(OpCodes.Ldloc, localBuilder); + + // responseBody.retval = ()retval; + var fInfo = responseType.GetField(this.CodeBuilder.Names.RetVal); + ilGen.Emit(OpCodes.Ldarg_2); + ilGen.Emit( + fInfo.FieldType.GetTypeInfo().IsClass ? OpCodes.Castclass : OpCodes.Unbox_Any, + fInfo.FieldType); + ilGen.Emit(OpCodes.Stfld, fInfo); + ilGen.Emit(OpCodes.Ldloc, localBuilder); + ilGen.Emit(OpCodes.Ret); + } + else + { + ilGen.Emit(OpCodes.Ldnull); + ilGen.Emit(OpCodes.Ret); + } + } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Client/ActorProxy.cs b/src/Dapr.Actors/Client/ActorProxy.cs index 841e3b09..ae024367 100644 --- a/src/Dapr.Actors/Client/ActorProxy.cs +++ b/src/Dapr.Actors/Client/ActorProxy.cs @@ -11,310 +11,309 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Client +namespace Dapr.Actors.Client; + +using System; +using System.IO; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Dapr.Actors.Communication; +using Dapr.Actors.Communication.Client; + +/// +/// Provides the base implementation for the proxy to the remote actor objects implementing interfaces. +/// The proxy object can be used for client-to-actor and actor-to-actor communication. +/// +public class ActorProxy : IActorProxy { - using System; - using System.IO; - using System.Text; - using System.Text.Json; - using System.Threading; - using System.Threading.Tasks; - using Dapr.Actors.Communication; - using Dapr.Actors.Communication.Client; + /// + /// The default factory used to create an actor proxy + /// + public static IActorProxyFactory DefaultProxyFactory { get; } = new ActorProxyFactory(); + + private ActorRemotingClient actorRemotingClient; + private ActorNonRemotingClient actorNonRemotingClient; /// - /// Provides the base implementation for the proxy to the remote actor objects implementing interfaces. - /// The proxy object can be used for client-to-actor and actor-to-actor communication. + /// Initializes a new instance of the class. + /// This constructor is protected so that it can be used by generated class which derives from ActorProxy when making Remoting calls. + /// This constructor is also marked as internal so that it can be called by ActorProxyFactory when making non-remoting calls. /// - public class ActorProxy : IActorProxy + protected internal ActorProxy() { - /// - /// The default factory used to create an actor proxy - /// - public static IActorProxyFactory DefaultProxyFactory { get; } = new ActorProxyFactory(); - - private ActorRemotingClient actorRemotingClient; - private ActorNonRemotingClient actorNonRemotingClient; - - /// - /// Initializes a new instance of the class. - /// This constructor is protected so that it can be used by generated class which derives from ActorProxy when making Remoting calls. - /// This constructor is also marked as internal so that it can be called by ActorProxyFactory when making non-remoting calls. - /// - protected internal ActorProxy() - { - } - - /// - public ActorId ActorId { get; private set; } - - /// - public string ActorType { get; private set; } - - internal IActorMessageBodyFactory ActorMessageBodyFactory { get; set; } - internal JsonSerializerOptions JsonSerializerOptions { get; set; } - internal string DaprApiToken; - - /// - /// Creates a proxy to the actor object that implements an actor interface. - /// - /// - /// The actor interface implemented by the remote actor object. - /// The returned proxy object will implement this interface. - /// - /// The actor ID of the proxy actor object. Methods called on this proxy will result in requests - /// being sent to the actor with this ID. - /// Type of actor implementation. - /// The optional to use when creating the actor proxy. - /// Proxy to the actor object. - public static TActorInterface Create(ActorId actorId, string actorType, ActorProxyOptions options = null) - where TActorInterface : IActor - { - return DefaultProxyFactory.CreateActorProxy(actorId, actorType, options); - } - - /// - /// Creates a proxy to the actor object that implements an actor interface. - /// - /// The actor ID of the proxy actor object. Methods called on this proxy will result in requests - /// being sent to the actor with this ID. - /// - /// The actor interface type implemented by the remote actor object. - /// The returned proxy object will implement this interface. - /// - /// Type of actor implementation. - /// The optional to use when creating the actor proxy. - /// Proxy to the actor object. - public static object Create(ActorId actorId, Type actorInterfaceType, string actorType, ActorProxyOptions options = null) - { - if (!typeof(IActor).IsAssignableFrom(actorInterfaceType)) - { - throw new ArgumentException("The interface must implement IActor.", nameof(actorInterfaceType)); - } - return DefaultProxyFactory.CreateActorProxy(actorId, actorInterfaceType, actorType, options); - } - - /// - /// Creates an Actor Proxy for making calls without Remoting. - /// - /// Actor Id. - /// Type of actor. - /// The optional to use when creating the actor proxy. - /// Actor proxy to interact with remote actor object. - public static ActorProxy Create(ActorId actorId, string actorType, ActorProxyOptions options = null) - { - return DefaultProxyFactory.Create(actorId, actorType, options); - } - - /// - /// Invokes the specified method for the actor with argument. The argument will be serialized as JSON. - /// - /// The data type of the object that will be serialized. - /// Return type of method. - /// Actor method name. - /// Object argument for actor method. - /// Cancellation Token. - /// Response form server. - public async Task InvokeMethodAsync(string method, TRequest data, CancellationToken cancellationToken = default) - { - using var stream = new MemoryStream(); - await JsonSerializer.SerializeAsync(stream, data, JsonSerializerOptions); - await stream.FlushAsync(); - var jsonPayload = Encoding.UTF8.GetString(stream.ToArray()); - var response = await this.actorNonRemotingClient.InvokeActorMethodWithoutRemotingAsync(this.ActorType, this.ActorId.ToString(), method, jsonPayload, cancellationToken); - return await JsonSerializer.DeserializeAsync(response, JsonSerializerOptions); - } - - /// - /// Invokes the specified method for the actor with argument. The argument will be serialized as JSON. - /// - /// The data type of the object that will be serialized. - /// Actor method name. - /// Object argument for actor method. - /// Cancellation Token. - /// Response form server. - public async Task InvokeMethodAsync(string method, TRequest data, CancellationToken cancellationToken = default) - { - using var stream = new MemoryStream(); - await JsonSerializer.SerializeAsync(stream, data, JsonSerializerOptions); - await stream.FlushAsync(); - var jsonPayload = Encoding.UTF8.GetString(stream.ToArray()); - await this.actorNonRemotingClient.InvokeActorMethodWithoutRemotingAsync(this.ActorType, this.ActorId.ToString(), method, jsonPayload, cancellationToken); - } - - /// - /// Invokes the specified method for the actor with argument. - /// - /// Return type of method. - /// Actor method name. - /// Cancellation Token. - /// Response form server. - public async Task InvokeMethodAsync(string method, CancellationToken cancellationToken = default) - { - var response = await this.actorNonRemotingClient.InvokeActorMethodWithoutRemotingAsync(this.ActorType, this.ActorId.ToString(), method, null, cancellationToken); - return await JsonSerializer.DeserializeAsync(response, JsonSerializerOptions); - } - - /// - /// Invokes the specified method for the actor with argument. - /// - /// Actor method name. - /// Cancellation Token. - /// Response form server. - public Task InvokeMethodAsync(string method, CancellationToken cancellationToken = default) - { - return this.actorNonRemotingClient.InvokeActorMethodWithoutRemotingAsync(this.ActorType, this.ActorId.ToString(), method, null, cancellationToken); - } - - /// - /// Initialize when ActorProxy is created for Remoting. - /// - internal void Initialize( - ActorRemotingClient client, - ActorId actorId, - string actorType, - ActorProxyOptions options) - { - this.actorRemotingClient = client; - this.ActorId = actorId; - this.ActorType = actorType; - this.ActorMessageBodyFactory = client.GetRemotingMessageBodyFactory(); - this.JsonSerializerOptions = options?.JsonSerializerOptions ?? new JsonSerializerOptions(JsonSerializerDefaults.Web); - this.DaprApiToken = options?.DaprApiToken; - } - - /// - /// Initialize when ActorProxy is created for non-Remoting calls. - /// - internal void Initialize( - ActorNonRemotingClient client, - ActorId actorId, - string actorType, - ActorProxyOptions options) - { - this.actorNonRemotingClient = client; - this.ActorId = actorId; - this.ActorType = actorType; - this.JsonSerializerOptions = options?.JsonSerializerOptions ?? this.JsonSerializerOptions; - } - - /// - /// Invokes the specified method for the actor with provided request. - /// - /// Interface ID. - /// Method ID. - /// Method Name. - /// Request Message Body Value. - /// Cancellation Token. - /// A representing the result of the asynchronous operation. - protected async Task InvokeMethodAsync( - int interfaceId, - int methodId, - string methodName, - IActorRequestMessageBody requestMsgBodyValue, - CancellationToken cancellationToken) - { - var headers = new ActorRequestMessageHeader - { - ActorId = this.ActorId, - ActorType = this.ActorType, - InterfaceId = interfaceId, - MethodId = methodId, - CallContext = Actors.Helper.GetCallContext(), - MethodName = methodName, - }; - - var responseMsg = await this.actorRemotingClient.InvokeAsync( - new ActorRequestMessage( - headers, - requestMsgBodyValue), - cancellationToken); - - return responseMsg?.GetBody(); - } - - /// - /// Creates the Actor request message Body. - /// - /// Full Name of the service interface for which this call is invoked. - /// Method Name of the service interface for which this call is invoked. - /// Number of Parameters in the service interface Method. - /// Wrapped Request Object. - /// A request message body. - protected IActorRequestMessageBody CreateRequestMessageBody( - string interfaceName, - string methodName, - int parameterCount, - object wrappedRequest) - { - return this.ActorMessageBodyFactory.CreateRequestMessageBody(interfaceName, methodName, parameterCount, wrappedRequest); - } - - /// - /// This method is used by the generated proxy type and should be used directly. This method converts the Task with object - /// return value to a Task without the return value for the void method invocation. - /// - /// A task returned from the method that contains null return value. - /// A task that represents the asynchronous operation for remote method call without the return value. - protected Task ContinueWith(Task task) - { - return task; - } - - /// - /// This method is used by the generated proxy type and should be used directly. This method converts the Task with object - /// return value to a Task without the return value for the void method invocation. - /// - /// Interface Id for the actor interface. - /// Method Id for the actor method. - /// Response body. - /// Return value of method call as . - protected virtual object GetReturnValue(int interfaceId, int methodId, object responseBody) - { - return Task.CompletedTask; - } - - /// - /// Called by the generated proxy class to get the result from the response body. - /// - /// of the remote method return value. - /// InterfaceId of the remoting interface. - /// MethodId of the remoting Method. - /// A task that represents the asynchronous operation for remote method call. - /// A task that represents the asynchronous operation for remote method call. - /// The value of the TRetval contains the remote method return value. - protected async Task ContinueWithResult( - int interfaceId, - int methodId, - Task task) - { - var responseBody = await task; - if (responseBody is WrappedMessage wrappedMessage) - { - var obj = this.GetReturnValue( - interfaceId, - methodId, - wrappedMessage.Value); - - return (TRetval)obj; - } - - return (TRetval)responseBody.Get(typeof(TRetval)); - } - - /// - /// This check if we are wrapping actor message or not. - /// - /// Actor Request Message Body. - /// true or false. - protected bool CheckIfItsWrappedRequest(IActorRequestMessageBody requestMessageBody) - { - if (requestMessageBody is WrappedMessage) - { - return true; - } - - return false; - } } -} + + /// + public ActorId ActorId { get; private set; } + + /// + public string ActorType { get; private set; } + + internal IActorMessageBodyFactory ActorMessageBodyFactory { get; set; } + internal JsonSerializerOptions JsonSerializerOptions { get; set; } + internal string DaprApiToken; + + /// + /// Creates a proxy to the actor object that implements an actor interface. + /// + /// + /// The actor interface implemented by the remote actor object. + /// The returned proxy object will implement this interface. + /// + /// The actor ID of the proxy actor object. Methods called on this proxy will result in requests + /// being sent to the actor with this ID. + /// Type of actor implementation. + /// The optional to use when creating the actor proxy. + /// Proxy to the actor object. + public static TActorInterface Create(ActorId actorId, string actorType, ActorProxyOptions options = null) + where TActorInterface : IActor + { + return DefaultProxyFactory.CreateActorProxy(actorId, actorType, options); + } + + /// + /// Creates a proxy to the actor object that implements an actor interface. + /// + /// The actor ID of the proxy actor object. Methods called on this proxy will result in requests + /// being sent to the actor with this ID. + /// + /// The actor interface type implemented by the remote actor object. + /// The returned proxy object will implement this interface. + /// + /// Type of actor implementation. + /// The optional to use when creating the actor proxy. + /// Proxy to the actor object. + public static object Create(ActorId actorId, Type actorInterfaceType, string actorType, ActorProxyOptions options = null) + { + if (!typeof(IActor).IsAssignableFrom(actorInterfaceType)) + { + throw new ArgumentException("The interface must implement IActor.", nameof(actorInterfaceType)); + } + return DefaultProxyFactory.CreateActorProxy(actorId, actorInterfaceType, actorType, options); + } + + /// + /// Creates an Actor Proxy for making calls without Remoting. + /// + /// Actor Id. + /// Type of actor. + /// The optional to use when creating the actor proxy. + /// Actor proxy to interact with remote actor object. + public static ActorProxy Create(ActorId actorId, string actorType, ActorProxyOptions options = null) + { + return DefaultProxyFactory.Create(actorId, actorType, options); + } + + /// + /// Invokes the specified method for the actor with argument. The argument will be serialized as JSON. + /// + /// The data type of the object that will be serialized. + /// Return type of method. + /// Actor method name. + /// Object argument for actor method. + /// Cancellation Token. + /// Response form server. + public async Task InvokeMethodAsync(string method, TRequest data, CancellationToken cancellationToken = default) + { + using var stream = new MemoryStream(); + await JsonSerializer.SerializeAsync(stream, data, JsonSerializerOptions); + await stream.FlushAsync(); + var jsonPayload = Encoding.UTF8.GetString(stream.ToArray()); + var response = await this.actorNonRemotingClient.InvokeActorMethodWithoutRemotingAsync(this.ActorType, this.ActorId.ToString(), method, jsonPayload, cancellationToken); + return await JsonSerializer.DeserializeAsync(response, JsonSerializerOptions); + } + + /// + /// Invokes the specified method for the actor with argument. The argument will be serialized as JSON. + /// + /// The data type of the object that will be serialized. + /// Actor method name. + /// Object argument for actor method. + /// Cancellation Token. + /// Response form server. + public async Task InvokeMethodAsync(string method, TRequest data, CancellationToken cancellationToken = default) + { + using var stream = new MemoryStream(); + await JsonSerializer.SerializeAsync(stream, data, JsonSerializerOptions); + await stream.FlushAsync(); + var jsonPayload = Encoding.UTF8.GetString(stream.ToArray()); + await this.actorNonRemotingClient.InvokeActorMethodWithoutRemotingAsync(this.ActorType, this.ActorId.ToString(), method, jsonPayload, cancellationToken); + } + + /// + /// Invokes the specified method for the actor with argument. + /// + /// Return type of method. + /// Actor method name. + /// Cancellation Token. + /// Response form server. + public async Task InvokeMethodAsync(string method, CancellationToken cancellationToken = default) + { + var response = await this.actorNonRemotingClient.InvokeActorMethodWithoutRemotingAsync(this.ActorType, this.ActorId.ToString(), method, null, cancellationToken); + return await JsonSerializer.DeserializeAsync(response, JsonSerializerOptions); + } + + /// + /// Invokes the specified method for the actor with argument. + /// + /// Actor method name. + /// Cancellation Token. + /// Response form server. + public Task InvokeMethodAsync(string method, CancellationToken cancellationToken = default) + { + return this.actorNonRemotingClient.InvokeActorMethodWithoutRemotingAsync(this.ActorType, this.ActorId.ToString(), method, null, cancellationToken); + } + + /// + /// Initialize when ActorProxy is created for Remoting. + /// + internal void Initialize( + ActorRemotingClient client, + ActorId actorId, + string actorType, + ActorProxyOptions options) + { + this.actorRemotingClient = client; + this.ActorId = actorId; + this.ActorType = actorType; + this.ActorMessageBodyFactory = client.GetRemotingMessageBodyFactory(); + this.JsonSerializerOptions = options?.JsonSerializerOptions ?? new JsonSerializerOptions(JsonSerializerDefaults.Web); + this.DaprApiToken = options?.DaprApiToken; + } + + /// + /// Initialize when ActorProxy is created for non-Remoting calls. + /// + internal void Initialize( + ActorNonRemotingClient client, + ActorId actorId, + string actorType, + ActorProxyOptions options) + { + this.actorNonRemotingClient = client; + this.ActorId = actorId; + this.ActorType = actorType; + this.JsonSerializerOptions = options?.JsonSerializerOptions ?? this.JsonSerializerOptions; + } + + /// + /// Invokes the specified method for the actor with provided request. + /// + /// Interface ID. + /// Method ID. + /// Method Name. + /// Request Message Body Value. + /// Cancellation Token. + /// A representing the result of the asynchronous operation. + protected async Task InvokeMethodAsync( + int interfaceId, + int methodId, + string methodName, + IActorRequestMessageBody requestMsgBodyValue, + CancellationToken cancellationToken) + { + var headers = new ActorRequestMessageHeader + { + ActorId = this.ActorId, + ActorType = this.ActorType, + InterfaceId = interfaceId, + MethodId = methodId, + CallContext = Actors.Helper.GetCallContext(), + MethodName = methodName, + }; + + var responseMsg = await this.actorRemotingClient.InvokeAsync( + new ActorRequestMessage( + headers, + requestMsgBodyValue), + cancellationToken); + + return responseMsg?.GetBody(); + } + + /// + /// Creates the Actor request message Body. + /// + /// Full Name of the service interface for which this call is invoked. + /// Method Name of the service interface for which this call is invoked. + /// Number of Parameters in the service interface Method. + /// Wrapped Request Object. + /// A request message body. + protected IActorRequestMessageBody CreateRequestMessageBody( + string interfaceName, + string methodName, + int parameterCount, + object wrappedRequest) + { + return this.ActorMessageBodyFactory.CreateRequestMessageBody(interfaceName, methodName, parameterCount, wrappedRequest); + } + + /// + /// This method is used by the generated proxy type and should be used directly. This method converts the Task with object + /// return value to a Task without the return value for the void method invocation. + /// + /// A task returned from the method that contains null return value. + /// A task that represents the asynchronous operation for remote method call without the return value. + protected Task ContinueWith(Task task) + { + return task; + } + + /// + /// This method is used by the generated proxy type and should be used directly. This method converts the Task with object + /// return value to a Task without the return value for the void method invocation. + /// + /// Interface Id for the actor interface. + /// Method Id for the actor method. + /// Response body. + /// Return value of method call as . + protected virtual object GetReturnValue(int interfaceId, int methodId, object responseBody) + { + return Task.CompletedTask; + } + + /// + /// Called by the generated proxy class to get the result from the response body. + /// + /// of the remote method return value. + /// InterfaceId of the remoting interface. + /// MethodId of the remoting Method. + /// A task that represents the asynchronous operation for remote method call. + /// A task that represents the asynchronous operation for remote method call. + /// The value of the TRetval contains the remote method return value. + protected async Task ContinueWithResult( + int interfaceId, + int methodId, + Task task) + { + var responseBody = await task; + if (responseBody is WrappedMessage wrappedMessage) + { + var obj = this.GetReturnValue( + interfaceId, + methodId, + wrappedMessage.Value); + + return (TRetval)obj; + } + + return (TRetval)responseBody.Get(typeof(TRetval)); + } + + /// + /// This check if we are wrapping actor message or not. + /// + /// Actor Request Message Body. + /// true or false. + protected bool CheckIfItsWrappedRequest(IActorRequestMessageBody requestMessageBody) + { + if (requestMessageBody is WrappedMessage) + { + return true; + } + + return false; + } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Client/ActorProxyFactory.cs b/src/Dapr.Actors/Client/ActorProxyFactory.cs index 4a8fe3a0..9b445213 100644 --- a/src/Dapr.Actors/Client/ActorProxyFactory.cs +++ b/src/Dapr.Actors/Client/ActorProxyFactory.cs @@ -11,89 +11,88 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Client +namespace Dapr.Actors.Client; + +using System; +using System.Net.Http; +using Dapr.Actors.Builder; +using Dapr.Actors.Communication; +using Dapr.Actors.Communication.Client; + +/// +/// Represents a factory class to create a proxy to the remote actor objects. +/// +public class ActorProxyFactory : IActorProxyFactory { - using System; - using System.Net.Http; - using Dapr.Actors.Builder; - using Dapr.Actors.Communication; - using Dapr.Actors.Communication.Client; + private ActorProxyOptions defaultOptions; + private readonly HttpMessageHandler handler; - /// - /// Represents a factory class to create a proxy to the remote actor objects. - /// - public class ActorProxyFactory : IActorProxyFactory + /// + public ActorProxyOptions DefaultOptions { - private ActorProxyOptions defaultOptions; - private readonly HttpMessageHandler handler; - - /// - public ActorProxyOptions DefaultOptions + get => this.defaultOptions; + set { - get => this.defaultOptions; - set - { - this.defaultOptions = value ?? - throw new ArgumentNullException(nameof(DefaultOptions), $"{nameof(ActorProxyFactory)}.{nameof(DefaultOptions)} cannot be null"); - } - } - - /// - /// Initializes a new instance of the class. - /// - [Obsolete("Use the constructor that accepts HttpMessageHandler. This will be removed in the future.")] - public ActorProxyFactory(ActorProxyOptions options, HttpClientHandler handler) - : this(options, (HttpMessageHandler)handler) - { - } - - /// - /// Initializes a new instance of the class. - /// - public ActorProxyFactory(ActorProxyOptions options = null, HttpMessageHandler handler = null) - { - this.defaultOptions = options ?? new ActorProxyOptions(); - this.handler = handler; - } - - /// - public TActorInterface CreateActorProxy(ActorId actorId, string actorType, ActorProxyOptions options = null) - where TActorInterface : IActor - => (TActorInterface)this.CreateActorProxy(actorId, typeof(TActorInterface), actorType, options ?? this.defaultOptions); - - /// - public ActorProxy Create(ActorId actorId, string actorType, ActorProxyOptions options = null) - { - options ??= this.DefaultOptions; - - var actorProxy = new ActorProxy(); - var daprInteractor = new DaprHttpInteractor(this.handler, options.HttpEndpoint, options.DaprApiToken, options.RequestTimeout); - var nonRemotingClient = new ActorNonRemotingClient(daprInteractor); - actorProxy.Initialize(nonRemotingClient, actorId, actorType, options); - - return actorProxy; - } - - /// - public object CreateActorProxy(ActorId actorId, Type actorInterfaceType, string actorType, ActorProxyOptions options = null) - { - options ??= this.DefaultOptions; - - var daprInteractor = new DaprHttpInteractor(this.handler, options.HttpEndpoint, options.DaprApiToken, options.RequestTimeout); - - // provide a serializer if 'useJsonSerialization' is true and no serialization provider is provided. - IActorMessageBodySerializationProvider serializationProvider = null; - if (options.UseJsonSerialization) - { - serializationProvider = new ActorMessageBodyJsonSerializationProvider(options.JsonSerializerOptions); - } - - var remotingClient = new ActorRemotingClient(daprInteractor, serializationProvider); - var proxyGenerator = ActorCodeBuilder.GetOrCreateProxyGenerator(actorInterfaceType); - var actorProxy = proxyGenerator.CreateActorProxy(); - actorProxy.Initialize(remotingClient, actorId, actorType, options); - - return actorProxy; + this.defaultOptions = value ?? + throw new ArgumentNullException(nameof(DefaultOptions), $"{nameof(ActorProxyFactory)}.{nameof(DefaultOptions)} cannot be null"); } } -} + + /// + /// Initializes a new instance of the class. + /// + [Obsolete("Use the constructor that accepts HttpMessageHandler. This will be removed in the future.")] + public ActorProxyFactory(ActorProxyOptions options, HttpClientHandler handler) + : this(options, (HttpMessageHandler)handler) + { + } + + /// + /// Initializes a new instance of the class. + /// + public ActorProxyFactory(ActorProxyOptions options = null, HttpMessageHandler handler = null) + { + this.defaultOptions = options ?? new ActorProxyOptions(); + this.handler = handler; + } + + /// + public TActorInterface CreateActorProxy(ActorId actorId, string actorType, ActorProxyOptions options = null) + where TActorInterface : IActor + => (TActorInterface)this.CreateActorProxy(actorId, typeof(TActorInterface), actorType, options ?? this.defaultOptions); + + /// + public ActorProxy Create(ActorId actorId, string actorType, ActorProxyOptions options = null) + { + options ??= this.DefaultOptions; + + var actorProxy = new ActorProxy(); + var daprInteractor = new DaprHttpInteractor(this.handler, options.HttpEndpoint, options.DaprApiToken, options.RequestTimeout); + var nonRemotingClient = new ActorNonRemotingClient(daprInteractor); + actorProxy.Initialize(nonRemotingClient, actorId, actorType, options); + + return actorProxy; + } + + /// + public object CreateActorProxy(ActorId actorId, Type actorInterfaceType, string actorType, ActorProxyOptions options = null) + { + options ??= this.DefaultOptions; + + var daprInteractor = new DaprHttpInteractor(this.handler, options.HttpEndpoint, options.DaprApiToken, options.RequestTimeout); + + // provide a serializer if 'useJsonSerialization' is true and no serialization provider is provided. + IActorMessageBodySerializationProvider serializationProvider = null; + if (options.UseJsonSerialization) + { + serializationProvider = new ActorMessageBodyJsonSerializationProvider(options.JsonSerializerOptions); + } + + var remotingClient = new ActorRemotingClient(daprInteractor, serializationProvider); + var proxyGenerator = ActorCodeBuilder.GetOrCreateProxyGenerator(actorInterfaceType); + var actorProxy = proxyGenerator.CreateActorProxy(); + actorProxy.Initialize(remotingClient, actorId, actorType, options); + + return actorProxy; + } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Client/ActorProxyOptions.cs b/src/Dapr.Actors/Client/ActorProxyOptions.cs index 2afce852..6ad6cf1e 100644 --- a/src/Dapr.Actors/Client/ActorProxyOptions.cs +++ b/src/Dapr.Actors/Client/ActorProxyOptions.cs @@ -11,61 +11,60 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Client +namespace Dapr.Actors.Client; + +using System; +using System.Text.Json; + +/// +/// The class containing customizable options for how the Actor Proxy is initialized. +/// +public class ActorProxyOptions { - using System; - using System.Text.Json; + // TODO: Add actor retry settings + + private JsonSerializerOptions jsonSerializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web); /// - /// The class containing customizable options for how the Actor Proxy is initialized. + /// The constructor /// - public class ActorProxyOptions + public ActorProxyOptions() { - // TODO: Add actor retry settings - - private JsonSerializerOptions jsonSerializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web); - - /// - /// The constructor - /// - public ActorProxyOptions() - { - } - - /// - /// The used for actor proxy message serialization in non-remoting invocation. - /// - public JsonSerializerOptions JsonSerializerOptions - { - get => this.jsonSerializerOptions; - set => this.jsonSerializerOptions = value ?? - throw new ArgumentNullException(nameof(JsonSerializerOptions), $"{nameof(ActorProxyOptions)}.{nameof(JsonSerializerOptions)} cannot be null"); - } - - /// - /// The Dapr Api Token that is added to the header for all requests. - /// - public string DaprApiToken { get; set; } = DaprDefaults.GetDefaultDaprApiToken(null); - - /// - /// Gets or sets the HTTP endpoint URI used to communicate with the Dapr sidecar. - /// - /// - /// The URI endpoint to use for HTTP calls to the Dapr runtime. The default value will be - /// http://127.0.0.1:DAPR_HTTP_PORT where DAPR_HTTP_PORT represents the value of the - /// DAPR_HTTP_PORT environment variable. - /// - /// - public string HttpEndpoint { get; set; } = DaprDefaults.GetDefaultHttpEndpoint(); - - /// - /// The timeout allowed for an actor request. Can be set to System.Threading.Timeout.InfiniteTimeSpan to disable any timeouts. - /// - public TimeSpan? RequestTimeout { get; set; } = null; - - /// - /// Enable JSON serialization for actor proxy message serialization in both remoting and non-remoting invocations. - /// - public bool UseJsonSerialization { get; set; } } -} + + /// + /// The used for actor proxy message serialization in non-remoting invocation. + /// + public JsonSerializerOptions JsonSerializerOptions + { + get => this.jsonSerializerOptions; + set => this.jsonSerializerOptions = value ?? + throw new ArgumentNullException(nameof(JsonSerializerOptions), $"{nameof(ActorProxyOptions)}.{nameof(JsonSerializerOptions)} cannot be null"); + } + + /// + /// The Dapr Api Token that is added to the header for all requests. + /// + public string DaprApiToken { get; set; } = DaprDefaults.GetDefaultDaprApiToken(null); + + /// + /// Gets or sets the HTTP endpoint URI used to communicate with the Dapr sidecar. + /// + /// + /// The URI endpoint to use for HTTP calls to the Dapr runtime. The default value will be + /// http://127.0.0.1:DAPR_HTTP_PORT where DAPR_HTTP_PORT represents the value of the + /// DAPR_HTTP_PORT environment variable. + /// + /// + public string HttpEndpoint { get; set; } = DaprDefaults.GetDefaultHttpEndpoint(); + + /// + /// The timeout allowed for an actor request. Can be set to System.Threading.Timeout.InfiniteTimeSpan to disable any timeouts. + /// + public TimeSpan? RequestTimeout { get; set; } = null; + + /// + /// Enable JSON serialization for actor proxy message serialization in both remoting and non-remoting invocations. + /// + public bool UseJsonSerialization { get; set; } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Client/IActorProxy.cs b/src/Dapr.Actors/Client/IActorProxy.cs index 9d24b47a..d2991617 100644 --- a/src/Dapr.Actors/Client/IActorProxy.cs +++ b/src/Dapr.Actors/Client/IActorProxy.cs @@ -11,23 +11,22 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Client +namespace Dapr.Actors.Client; + +/// +/// Provides the interface for implementation of proxy access for actor service. +/// +public interface IActorProxy { /// - /// Provides the interface for implementation of proxy access for actor service. + /// Gets associated with the proxy object. /// - public interface IActorProxy - { - /// - /// Gets associated with the proxy object. - /// - /// associated with the proxy object. - ActorId ActorId { get; } + /// associated with the proxy object. + ActorId ActorId { get; } - /// - /// Gets actor implementation type of the actor associated with the proxy object. - /// - /// Actor implementation type of the actor associated with the proxy object. - string ActorType { get; } - } -} + /// + /// Gets actor implementation type of the actor associated with the proxy object. + /// + /// Actor implementation type of the actor associated with the proxy object. + string ActorType { get; } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Client/IActorProxyFactory.cs b/src/Dapr.Actors/Client/IActorProxyFactory.cs index 81af4eec..a2e7692e 100644 --- a/src/Dapr.Actors/Client/IActorProxyFactory.cs +++ b/src/Dapr.Actors/Client/IActorProxyFactory.cs @@ -13,48 +13,47 @@ using System; -namespace Dapr.Actors.Client +namespace Dapr.Actors.Client; + +/// +/// Defines the interface containing methods to create actor proxy factory class. +/// +public interface IActorProxyFactory { /// - /// Defines the interface containing methods to create actor proxy factory class. + /// Creates a proxy to the actor object that implements an actor interface. /// - public interface IActorProxyFactory - { - /// - /// Creates a proxy to the actor object that implements an actor interface. - /// - /// - /// The actor interface implemented by the remote actor object. - /// The returned proxy object will implement this interface. - /// - /// Actor Id of the proxy actor object. Methods called on this proxy will result in requests - /// being sent to the actor with this id. - /// Type of actor implementation. - /// The optional to use when creating the actor proxy. - /// An actor proxy object that implements IActorProxy and TActorInterface. - TActorInterface CreateActorProxy( - ActorId actorId, - string actorType, - ActorProxyOptions options = null) - where TActorInterface : IActor; + /// + /// The actor interface implemented by the remote actor object. + /// The returned proxy object will implement this interface. + /// + /// Actor Id of the proxy actor object. Methods called on this proxy will result in requests + /// being sent to the actor with this id. + /// Type of actor implementation. + /// The optional to use when creating the actor proxy. + /// An actor proxy object that implements IActorProxy and TActorInterface. + TActorInterface CreateActorProxy( + ActorId actorId, + string actorType, + ActorProxyOptions options = null) + where TActorInterface : IActor; - /// - /// Create a proxy, this method is also used by ActorReference also to create proxy. - /// - /// Actor Id. - /// Actor Interface Type. - /// Actor implementation Type. - /// The optional to use when creating the actor proxy. - /// Returns Actor Proxy. - object CreateActorProxy(ActorId actorId, Type actorInterfaceType, string actorType, ActorProxyOptions options = null); + /// + /// Create a proxy, this method is also used by ActorReference also to create proxy. + /// + /// Actor Id. + /// Actor Interface Type. + /// Actor implementation Type. + /// The optional to use when creating the actor proxy. + /// Returns Actor Proxy. + object CreateActorProxy(ActorId actorId, Type actorInterfaceType, string actorType, ActorProxyOptions options = null); - /// - /// Creates an Actor Proxy for making calls without Remoting. - /// - /// Actor Id. - /// Type of actor. - /// The optional to use when creating the actor proxy. - /// Actor proxy to interact with remote actor object. - ActorProxy Create(ActorId actorId, string actorType, ActorProxyOptions options = null); - } -} + /// + /// Creates an Actor Proxy for making calls without Remoting. + /// + /// Actor Id. + /// Type of actor. + /// The optional to use when creating the actor proxy. + /// Actor proxy to interact with remote actor object. + ActorProxy Create(ActorId actorId, string actorType, ActorProxyOptions options = null); +} \ No newline at end of file diff --git a/src/Dapr.Actors/ClientSettings.cs b/src/Dapr.Actors/ClientSettings.cs index ce179ff4..912f17b0 100644 --- a/src/Dapr.Actors/ClientSettings.cs +++ b/src/Dapr.Actors/ClientSettings.cs @@ -11,27 +11,26 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors +namespace Dapr.Actors; + +using System; + +/// +/// Represents connection settings for Http/gRPC Client to interact with Dapr runtime. +/// +internal class ClientSettings { - using System; + /// + /// Initializes a new instance of the class. + /// + /// Timespan to wait before the request times out for the client. + public ClientSettings(TimeSpan? clientTimeout = null) + { + this.ClientTimeout = clientTimeout; + } /// - /// Represents connection settings for Http/gRPC Client to interact with Dapr runtime. + /// Gets or sets the Timespan to wait before the request times out for the client. /// - internal class ClientSettings - { - /// - /// Initializes a new instance of the class. - /// - /// Timespan to wait before the request times out for the client. - public ClientSettings(TimeSpan? clientTimeout = null) - { - this.ClientTimeout = clientTimeout; - } - - /// - /// Gets or sets the Timespan to wait before the request times out for the client. - /// - public TimeSpan? ClientTimeout { get; set; } - } -} + public TimeSpan? ClientTimeout { get; set; } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Common/CRC64.cs b/src/Dapr.Actors/Common/CRC64.cs index 5470d125..4f164aeb 100644 --- a/src/Dapr.Actors/Common/CRC64.cs +++ b/src/Dapr.Actors/Common/CRC64.cs @@ -11,132 +11,131 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Common +namespace Dapr.Actors.Common; + +using System.Globalization; + +/// +/// Computes CRC64 for a given byte payload. +/// +internal static class CRC64 { - using System.Globalization; + /// + /// CRC table. + /// + private static readonly ulong[] Crc64Table = + { + 0x0000000000000000, 0x42F0E1EBA9EA3693, 0x85E1C3D753D46D26, 0xC711223CFA3E5BB5, + 0x493366450E42ECDF, 0x0BC387AEA7A8DA4C, 0xCCD2A5925D9681F9, 0x8E224479F47CB76A, + 0x9266CC8A1C85D9BE, 0xD0962D61B56FEF2D, 0x17870F5D4F51B498, 0x5577EEB6E6BB820B, + 0xDB55AACF12C73561, 0x99A54B24BB2D03F2, 0x5EB4691841135847, 0x1C4488F3E8F96ED4, + 0x663D78FF90E185EF, 0x24CD9914390BB37C, 0xE3DCBB28C335E8C9, 0xA12C5AC36ADFDE5A, + 0x2F0E1EBA9EA36930, 0x6DFEFF5137495FA3, 0xAAEFDD6DCD770416, 0xE81F3C86649D3285, + 0xF45BB4758C645C51, 0xB6AB559E258E6AC2, 0x71BA77A2DFB03177, 0x334A9649765A07E4, + 0xBD68D2308226B08E, 0xFF9833DB2BCC861D, 0x388911E7D1F2DDA8, 0x7A79F00C7818EB3B, + 0xCC7AF1FF21C30BDE, 0x8E8A101488293D4D, 0x499B3228721766F8, 0x0B6BD3C3DBFD506B, + 0x854997BA2F81E701, 0xC7B97651866BD192, 0x00A8546D7C558A27, 0x4258B586D5BFBCB4, + 0x5E1C3D753D46D260, 0x1CECDC9E94ACE4F3, 0xDBFDFEA26E92BF46, 0x990D1F49C77889D5, + 0x172F5B3033043EBF, 0x55DFBADB9AEE082C, 0x92CE98E760D05399, 0xD03E790CC93A650A, + 0xAA478900B1228E31, 0xE8B768EB18C8B8A2, 0x2FA64AD7E2F6E317, 0x6D56AB3C4B1CD584, + 0xE374EF45BF6062EE, 0xA1840EAE168A547D, 0x66952C92ECB40FC8, 0x2465CD79455E395B, + 0x3821458AADA7578F, 0x7AD1A461044D611C, 0xBDC0865DFE733AA9, 0xFF3067B657990C3A, + 0x711223CFA3E5BB50, 0x33E2C2240A0F8DC3, 0xF4F3E018F031D676, 0xB60301F359DBE0E5, + 0xDA050215EA6C212F, 0x98F5E3FE438617BC, 0x5FE4C1C2B9B84C09, 0x1D14202910527A9A, + 0x93366450E42ECDF0, 0xD1C685BB4DC4FB63, 0x16D7A787B7FAA0D6, 0x5427466C1E109645, + 0x4863CE9FF6E9F891, 0x0A932F745F03CE02, 0xCD820D48A53D95B7, 0x8F72ECA30CD7A324, + 0x0150A8DAF8AB144E, 0x43A04931514122DD, 0x84B16B0DAB7F7968, 0xC6418AE602954FFB, + 0xBC387AEA7A8DA4C0, 0xFEC89B01D3679253, 0x39D9B93D2959C9E6, 0x7B2958D680B3FF75, + 0xF50B1CAF74CF481F, 0xB7FBFD44DD257E8C, 0x70EADF78271B2539, 0x321A3E938EF113AA, + 0x2E5EB66066087D7E, 0x6CAE578BCFE24BED, 0xABBF75B735DC1058, 0xE94F945C9C3626CB, + 0x676DD025684A91A1, 0x259D31CEC1A0A732, 0xE28C13F23B9EFC87, 0xA07CF2199274CA14, + 0x167FF3EACBAF2AF1, 0x548F120162451C62, 0x939E303D987B47D7, 0xD16ED1D631917144, + 0x5F4C95AFC5EDC62E, 0x1DBC74446C07F0BD, 0xDAAD56789639AB08, 0x985DB7933FD39D9B, + 0x84193F60D72AF34F, 0xC6E9DE8B7EC0C5DC, 0x01F8FCB784FE9E69, 0x43081D5C2D14A8FA, + 0xCD2A5925D9681F90, 0x8FDAB8CE70822903, 0x48CB9AF28ABC72B6, 0x0A3B7B1923564425, + 0x70428B155B4EAF1E, 0x32B26AFEF2A4998D, 0xF5A348C2089AC238, 0xB753A929A170F4AB, + 0x3971ED50550C43C1, 0x7B810CBBFCE67552, 0xBC902E8706D82EE7, 0xFE60CF6CAF321874, + 0xE224479F47CB76A0, 0xA0D4A674EE214033, 0x67C58448141F1B86, 0x253565A3BDF52D15, + 0xAB1721DA49899A7F, 0xE9E7C031E063ACEC, 0x2EF6E20D1A5DF759, 0x6C0603E6B3B7C1CA, + 0xF6FAE5C07D3274CD, 0xB40A042BD4D8425E, 0x731B26172EE619EB, 0x31EBC7FC870C2F78, + 0xBFC9838573709812, 0xFD39626EDA9AAE81, 0x3A28405220A4F534, 0x78D8A1B9894EC3A7, + 0x649C294A61B7AD73, 0x266CC8A1C85D9BE0, 0xE17DEA9D3263C055, 0xA38D0B769B89F6C6, + 0x2DAF4F0F6FF541AC, 0x6F5FAEE4C61F773F, 0xA84E8CD83C212C8A, 0xEABE6D3395CB1A19, + 0x90C79D3FEDD3F122, 0xD2377CD44439C7B1, 0x15265EE8BE079C04, 0x57D6BF0317EDAA97, + 0xD9F4FB7AE3911DFD, 0x9B041A914A7B2B6E, 0x5C1538ADB04570DB, 0x1EE5D94619AF4648, + 0x02A151B5F156289C, 0x4051B05E58BC1E0F, 0x87409262A28245BA, 0xC5B073890B687329, + 0x4B9237F0FF14C443, 0x0962D61B56FEF2D0, 0xCE73F427ACC0A965, 0x8C8315CC052A9FF6, + 0x3A80143F5CF17F13, 0x7870F5D4F51B4980, 0xBF61D7E80F251235, 0xFD913603A6CF24A6, + 0x73B3727A52B393CC, 0x31439391FB59A55F, 0xF652B1AD0167FEEA, 0xB4A25046A88DC879, + 0xA8E6D8B54074A6AD, 0xEA16395EE99E903E, 0x2D071B6213A0CB8B, 0x6FF7FA89BA4AFD18, + 0xE1D5BEF04E364A72, 0xA3255F1BE7DC7CE1, 0x64347D271DE22754, 0x26C49CCCB40811C7, + 0x5CBD6CC0CC10FAFC, 0x1E4D8D2B65FACC6F, 0xD95CAF179FC497DA, 0x9BAC4EFC362EA149, + 0x158E0A85C2521623, 0x577EEB6E6BB820B0, 0x906FC95291867B05, 0xD29F28B9386C4D96, + 0xCEDBA04AD0952342, 0x8C2B41A1797F15D1, 0x4B3A639D83414E64, 0x09CA82762AAB78F7, + 0x87E8C60FDED7CF9D, 0xC51827E4773DF90E, 0x020905D88D03A2BB, 0x40F9E43324E99428, + 0x2CFFE7D5975E55E2, 0x6E0F063E3EB46371, 0xA91E2402C48A38C4, 0xEBEEC5E96D600E57, + 0x65CC8190991CB93D, 0x273C607B30F68FAE, 0xE02D4247CAC8D41B, 0xA2DDA3AC6322E288, + 0xBE992B5F8BDB8C5C, 0xFC69CAB42231BACF, 0x3B78E888D80FE17A, 0x7988096371E5D7E9, + 0xF7AA4D1A85996083, 0xB55AACF12C735610, 0x724B8ECDD64D0DA5, 0x30BB6F267FA73B36, + 0x4AC29F2A07BFD00D, 0x08327EC1AE55E69E, 0xCF235CFD546BBD2B, 0x8DD3BD16FD818BB8, + 0x03F1F96F09FD3CD2, 0x41011884A0170A41, 0x86103AB85A2951F4, 0xC4E0DB53F3C36767, + 0xD8A453A01B3A09B3, 0x9A54B24BB2D03F20, 0x5D45907748EE6495, 0x1FB5719CE1045206, + 0x919735E51578E56C, 0xD367D40EBC92D3FF, 0x1476F63246AC884A, 0x568617D9EF46BED9, + 0xE085162AB69D5E3C, 0xA275F7C11F7768AF, 0x6564D5FDE549331A, 0x279434164CA30589, + 0xA9B6706FB8DFB2E3, 0xEB46918411358470, 0x2C57B3B8EB0BDFC5, 0x6EA7525342E1E956, + 0x72E3DAA0AA188782, 0x30133B4B03F2B111, 0xF7021977F9CCEAA4, 0xB5F2F89C5026DC37, + 0x3BD0BCE5A45A6B5D, 0x79205D0E0DB05DCE, 0xBE317F32F78E067B, 0xFCC19ED95E6430E8, + 0x86B86ED5267CDBD3, 0xC4488F3E8F96ED40, 0x0359AD0275A8B6F5, 0x41A94CE9DC428066, + 0xCF8B0890283E370C, 0x8D7BE97B81D4019F, 0x4A6ACB477BEA5A2A, 0x089A2AACD2006CB9, + 0x14DEA25F3AF9026D, 0x562E43B4931334FE, 0x913F6188692D6F4B, 0xD3CF8063C0C759D8, + 0x5DEDC41A34BBEEB2, 0x1F1D25F19D51D821, 0xD80C07CD676F8394, 0x9AFCE626CE85B507, + }; /// - /// Computes CRC64 for a given byte payload. + /// Returns the CRC64 for the given payload. /// - internal static class CRC64 + /// Byte payload. + /// CRC64 value. + public static ulong ToCRC64(byte[] value) { - /// - /// CRC table. - /// - private static readonly ulong[] Crc64Table = + var crc = 0xffffffffffffffff; + for (var i = 0; i < value.Length; i++) { - 0x0000000000000000, 0x42F0E1EBA9EA3693, 0x85E1C3D753D46D26, 0xC711223CFA3E5BB5, - 0x493366450E42ECDF, 0x0BC387AEA7A8DA4C, 0xCCD2A5925D9681F9, 0x8E224479F47CB76A, - 0x9266CC8A1C85D9BE, 0xD0962D61B56FEF2D, 0x17870F5D4F51B498, 0x5577EEB6E6BB820B, - 0xDB55AACF12C73561, 0x99A54B24BB2D03F2, 0x5EB4691841135847, 0x1C4488F3E8F96ED4, - 0x663D78FF90E185EF, 0x24CD9914390BB37C, 0xE3DCBB28C335E8C9, 0xA12C5AC36ADFDE5A, - 0x2F0E1EBA9EA36930, 0x6DFEFF5137495FA3, 0xAAEFDD6DCD770416, 0xE81F3C86649D3285, - 0xF45BB4758C645C51, 0xB6AB559E258E6AC2, 0x71BA77A2DFB03177, 0x334A9649765A07E4, - 0xBD68D2308226B08E, 0xFF9833DB2BCC861D, 0x388911E7D1F2DDA8, 0x7A79F00C7818EB3B, - 0xCC7AF1FF21C30BDE, 0x8E8A101488293D4D, 0x499B3228721766F8, 0x0B6BD3C3DBFD506B, - 0x854997BA2F81E701, 0xC7B97651866BD192, 0x00A8546D7C558A27, 0x4258B586D5BFBCB4, - 0x5E1C3D753D46D260, 0x1CECDC9E94ACE4F3, 0xDBFDFEA26E92BF46, 0x990D1F49C77889D5, - 0x172F5B3033043EBF, 0x55DFBADB9AEE082C, 0x92CE98E760D05399, 0xD03E790CC93A650A, - 0xAA478900B1228E31, 0xE8B768EB18C8B8A2, 0x2FA64AD7E2F6E317, 0x6D56AB3C4B1CD584, - 0xE374EF45BF6062EE, 0xA1840EAE168A547D, 0x66952C92ECB40FC8, 0x2465CD79455E395B, - 0x3821458AADA7578F, 0x7AD1A461044D611C, 0xBDC0865DFE733AA9, 0xFF3067B657990C3A, - 0x711223CFA3E5BB50, 0x33E2C2240A0F8DC3, 0xF4F3E018F031D676, 0xB60301F359DBE0E5, - 0xDA050215EA6C212F, 0x98F5E3FE438617BC, 0x5FE4C1C2B9B84C09, 0x1D14202910527A9A, - 0x93366450E42ECDF0, 0xD1C685BB4DC4FB63, 0x16D7A787B7FAA0D6, 0x5427466C1E109645, - 0x4863CE9FF6E9F891, 0x0A932F745F03CE02, 0xCD820D48A53D95B7, 0x8F72ECA30CD7A324, - 0x0150A8DAF8AB144E, 0x43A04931514122DD, 0x84B16B0DAB7F7968, 0xC6418AE602954FFB, - 0xBC387AEA7A8DA4C0, 0xFEC89B01D3679253, 0x39D9B93D2959C9E6, 0x7B2958D680B3FF75, - 0xF50B1CAF74CF481F, 0xB7FBFD44DD257E8C, 0x70EADF78271B2539, 0x321A3E938EF113AA, - 0x2E5EB66066087D7E, 0x6CAE578BCFE24BED, 0xABBF75B735DC1058, 0xE94F945C9C3626CB, - 0x676DD025684A91A1, 0x259D31CEC1A0A732, 0xE28C13F23B9EFC87, 0xA07CF2199274CA14, - 0x167FF3EACBAF2AF1, 0x548F120162451C62, 0x939E303D987B47D7, 0xD16ED1D631917144, - 0x5F4C95AFC5EDC62E, 0x1DBC74446C07F0BD, 0xDAAD56789639AB08, 0x985DB7933FD39D9B, - 0x84193F60D72AF34F, 0xC6E9DE8B7EC0C5DC, 0x01F8FCB784FE9E69, 0x43081D5C2D14A8FA, - 0xCD2A5925D9681F90, 0x8FDAB8CE70822903, 0x48CB9AF28ABC72B6, 0x0A3B7B1923564425, - 0x70428B155B4EAF1E, 0x32B26AFEF2A4998D, 0xF5A348C2089AC238, 0xB753A929A170F4AB, - 0x3971ED50550C43C1, 0x7B810CBBFCE67552, 0xBC902E8706D82EE7, 0xFE60CF6CAF321874, - 0xE224479F47CB76A0, 0xA0D4A674EE214033, 0x67C58448141F1B86, 0x253565A3BDF52D15, - 0xAB1721DA49899A7F, 0xE9E7C031E063ACEC, 0x2EF6E20D1A5DF759, 0x6C0603E6B3B7C1CA, - 0xF6FAE5C07D3274CD, 0xB40A042BD4D8425E, 0x731B26172EE619EB, 0x31EBC7FC870C2F78, - 0xBFC9838573709812, 0xFD39626EDA9AAE81, 0x3A28405220A4F534, 0x78D8A1B9894EC3A7, - 0x649C294A61B7AD73, 0x266CC8A1C85D9BE0, 0xE17DEA9D3263C055, 0xA38D0B769B89F6C6, - 0x2DAF4F0F6FF541AC, 0x6F5FAEE4C61F773F, 0xA84E8CD83C212C8A, 0xEABE6D3395CB1A19, - 0x90C79D3FEDD3F122, 0xD2377CD44439C7B1, 0x15265EE8BE079C04, 0x57D6BF0317EDAA97, - 0xD9F4FB7AE3911DFD, 0x9B041A914A7B2B6E, 0x5C1538ADB04570DB, 0x1EE5D94619AF4648, - 0x02A151B5F156289C, 0x4051B05E58BC1E0F, 0x87409262A28245BA, 0xC5B073890B687329, - 0x4B9237F0FF14C443, 0x0962D61B56FEF2D0, 0xCE73F427ACC0A965, 0x8C8315CC052A9FF6, - 0x3A80143F5CF17F13, 0x7870F5D4F51B4980, 0xBF61D7E80F251235, 0xFD913603A6CF24A6, - 0x73B3727A52B393CC, 0x31439391FB59A55F, 0xF652B1AD0167FEEA, 0xB4A25046A88DC879, - 0xA8E6D8B54074A6AD, 0xEA16395EE99E903E, 0x2D071B6213A0CB8B, 0x6FF7FA89BA4AFD18, - 0xE1D5BEF04E364A72, 0xA3255F1BE7DC7CE1, 0x64347D271DE22754, 0x26C49CCCB40811C7, - 0x5CBD6CC0CC10FAFC, 0x1E4D8D2B65FACC6F, 0xD95CAF179FC497DA, 0x9BAC4EFC362EA149, - 0x158E0A85C2521623, 0x577EEB6E6BB820B0, 0x906FC95291867B05, 0xD29F28B9386C4D96, - 0xCEDBA04AD0952342, 0x8C2B41A1797F15D1, 0x4B3A639D83414E64, 0x09CA82762AAB78F7, - 0x87E8C60FDED7CF9D, 0xC51827E4773DF90E, 0x020905D88D03A2BB, 0x40F9E43324E99428, - 0x2CFFE7D5975E55E2, 0x6E0F063E3EB46371, 0xA91E2402C48A38C4, 0xEBEEC5E96D600E57, - 0x65CC8190991CB93D, 0x273C607B30F68FAE, 0xE02D4247CAC8D41B, 0xA2DDA3AC6322E288, - 0xBE992B5F8BDB8C5C, 0xFC69CAB42231BACF, 0x3B78E888D80FE17A, 0x7988096371E5D7E9, - 0xF7AA4D1A85996083, 0xB55AACF12C735610, 0x724B8ECDD64D0DA5, 0x30BB6F267FA73B36, - 0x4AC29F2A07BFD00D, 0x08327EC1AE55E69E, 0xCF235CFD546BBD2B, 0x8DD3BD16FD818BB8, - 0x03F1F96F09FD3CD2, 0x41011884A0170A41, 0x86103AB85A2951F4, 0xC4E0DB53F3C36767, - 0xD8A453A01B3A09B3, 0x9A54B24BB2D03F20, 0x5D45907748EE6495, 0x1FB5719CE1045206, - 0x919735E51578E56C, 0xD367D40EBC92D3FF, 0x1476F63246AC884A, 0x568617D9EF46BED9, - 0xE085162AB69D5E3C, 0xA275F7C11F7768AF, 0x6564D5FDE549331A, 0x279434164CA30589, - 0xA9B6706FB8DFB2E3, 0xEB46918411358470, 0x2C57B3B8EB0BDFC5, 0x6EA7525342E1E956, - 0x72E3DAA0AA188782, 0x30133B4B03F2B111, 0xF7021977F9CCEAA4, 0xB5F2F89C5026DC37, - 0x3BD0BCE5A45A6B5D, 0x79205D0E0DB05DCE, 0xBE317F32F78E067B, 0xFCC19ED95E6430E8, - 0x86B86ED5267CDBD3, 0xC4488F3E8F96ED40, 0x0359AD0275A8B6F5, 0x41A94CE9DC428066, - 0xCF8B0890283E370C, 0x8D7BE97B81D4019F, 0x4A6ACB477BEA5A2A, 0x089A2AACD2006CB9, - 0x14DEA25F3AF9026D, 0x562E43B4931334FE, 0x913F6188692D6F4B, 0xD3CF8063C0C759D8, - 0x5DEDC41A34BBEEB2, 0x1F1D25F19D51D821, 0xD80C07CD676F8394, 0x9AFCE626CE85B507, - }; + var tableIndex = (((uint)(crc >> 56)) ^ value[i]) & 0xff; + crc = Crc64Table[tableIndex] ^ (crc << 8); + } - /// - /// Returns the CRC64 for the given payload. - /// - /// Byte payload. - /// CRC64 value. - public static ulong ToCRC64(byte[] value) + return crc ^ 0xffffffffffffffff; + } + + /// + /// Returns the CRC64 for the given payload. + /// + /// Byte payloads. + /// CRC64 value. + public static ulong ToCRC64(params byte[][] values) + { + var crc = 0xffffffffffffffff; + for (var x = 0; x < values.Length; x++) { - var crc = 0xffffffffffffffff; - for (var i = 0; i < value.Length; i++) + for (var i = 0; i < values[x].Length; i++) { - var tableIndex = (((uint)(crc >> 56)) ^ value[i]) & 0xff; + var tableIndex = (((uint)(crc >> 56)) ^ values[x][i]) & 0xff; crc = Crc64Table[tableIndex] ^ (crc << 8); } - - return crc ^ 0xffffffffffffffff; } - /// - /// Returns the CRC64 for the given payload. - /// - /// Byte payloads. - /// CRC64 value. - public static ulong ToCRC64(params byte[][] values) - { - var crc = 0xffffffffffffffff; - for (var x = 0; x < values.Length; x++) - { - for (var i = 0; i < values[x].Length; i++) - { - var tableIndex = (((uint)(crc >> 56)) ^ values[x][i]) & 0xff; - crc = Crc64Table[tableIndex] ^ (crc << 8); - } - } - - return crc ^ 0xffffffffffffffff; - } - - /// - /// Returns the CRC64 in string form for the given payload. - /// - /// Byte payload. - /// CRC64 value. - public static string ToCrc64String(byte[] value) - { - var crc64 = ToCRC64(value); - return crc64.ToString("X", CultureInfo.InvariantCulture); - } + return crc ^ 0xffffffffffffffff; } -} + + /// + /// Returns the CRC64 in string form for the given payload. + /// + /// Byte payload. + /// CRC64 value. + public static string ToCrc64String(byte[] value) + { + var crc64 = ToCRC64(value); + return crc64.ToString("X", CultureInfo.InvariantCulture); + } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Common/IdUtil.cs b/src/Dapr.Actors/Common/IdUtil.cs index 7a5092fc..0d353697 100644 --- a/src/Dapr.Actors/Common/IdUtil.cs +++ b/src/Dapr.Actors/Common/IdUtil.cs @@ -11,95 +11,94 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Common +namespace Dapr.Actors.Common; + +using System; +using System.Reflection; +using System.Text; + +internal static class IdUtil { - using System; - using System.Reflection; - using System.Text; - - internal static class IdUtil + internal static int ComputeId(MethodInfo methodInfo) { - internal static int ComputeId(MethodInfo methodInfo) + var hash = methodInfo.Name.GetHashCode(); + + if (methodInfo.DeclaringType != null) { - var hash = methodInfo.Name.GetHashCode(); - - if (methodInfo.DeclaringType != null) + if (methodInfo.DeclaringType.Namespace != null) { - if (methodInfo.DeclaringType.Namespace != null) - { - hash = HashCombine(methodInfo.DeclaringType.Namespace.GetHashCode(), hash); - } - - hash = HashCombine(methodInfo.DeclaringType.Name.GetHashCode(), hash); + hash = HashCombine(methodInfo.DeclaringType.Namespace.GetHashCode(), hash); } - return hash; + hash = HashCombine(methodInfo.DeclaringType.Name.GetHashCode(), hash); } - internal static int ComputeId(Type type) - { - var hash = type.Name.GetHashCode(); - if (type.Namespace != null) - { - hash = HashCombine(type.Namespace.GetHashCode(), hash); - } - - return hash; - } - - internal static int ComputeIdWithCRC(Type type) - { - var name = type.Name; - - if (type.Namespace != null) - { - name = string.Concat(type.Namespace, name); - } - - return ComputeIdWithCRC(name); - } - - internal static int ComputeIdWithCRC(MethodInfo methodInfo) - { - var name = methodInfo.Name; - - if (methodInfo.DeclaringType != null) - { - if (methodInfo.DeclaringType.Namespace != null) - { - name = string.Concat(methodInfo.DeclaringType.Namespace, name); - } - - name = string.Concat(methodInfo.DeclaringType.Name, name); - } - - return ComputeIdWithCRC(name); - } - - internal static int ComputeIdWithCRC(string typeName) - { - return (int)CRC64.ToCRC64(Encoding.UTF8.GetBytes(typeName)); - } - - internal static int ComputeId(string typeName, string typeNamespace) - { - var hash = typeName.GetHashCode(); - if (typeNamespace != null) - { - hash = HashCombine(typeNamespace.GetHashCode(), hash); - } - - return hash; - } - - /// - /// This is how VB Anonymous Types combine hash values for fields. - /// - internal static int HashCombine(int newKey, int currentKey) - { -#pragma warning disable SA1139 // Use literal suffix notation instead of casting - return unchecked((currentKey * (int)0xA5555529) + newKey); -#pragma warning restore SA1139 // Use literal suffix notation instead of casting - } + return hash; } -} + + internal static int ComputeId(Type type) + { + var hash = type.Name.GetHashCode(); + if (type.Namespace != null) + { + hash = HashCombine(type.Namespace.GetHashCode(), hash); + } + + return hash; + } + + internal static int ComputeIdWithCRC(Type type) + { + var name = type.Name; + + if (type.Namespace != null) + { + name = string.Concat(type.Namespace, name); + } + + return ComputeIdWithCRC(name); + } + + internal static int ComputeIdWithCRC(MethodInfo methodInfo) + { + var name = methodInfo.Name; + + if (methodInfo.DeclaringType != null) + { + if (methodInfo.DeclaringType.Namespace != null) + { + name = string.Concat(methodInfo.DeclaringType.Namespace, name); + } + + name = string.Concat(methodInfo.DeclaringType.Name, name); + } + + return ComputeIdWithCRC(name); + } + + internal static int ComputeIdWithCRC(string typeName) + { + return (int)CRC64.ToCRC64(Encoding.UTF8.GetBytes(typeName)); + } + + internal static int ComputeId(string typeName, string typeNamespace) + { + var hash = typeName.GetHashCode(); + if (typeNamespace != null) + { + hash = HashCombine(typeNamespace.GetHashCode(), hash); + } + + return hash; + } + + /// + /// This is how VB Anonymous Types combine hash values for fields. + /// + internal static int HashCombine(int newKey, int currentKey) + { +#pragma warning disable SA1139 // Use literal suffix notation instead of casting + return unchecked((currentKey * (int)0xA5555529) + newKey); +#pragma warning restore SA1139 // Use literal suffix notation instead of casting + } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Communication/ActorDataContractSurrogate.cs b/src/Dapr.Actors/Communication/ActorDataContractSurrogate.cs index b957a9b8..e83d3eb8 100644 --- a/src/Dapr.Actors/Communication/ActorDataContractSurrogate.cs +++ b/src/Dapr.Actors/Communication/ActorDataContractSurrogate.cs @@ -11,53 +11,52 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Communication +namespace Dapr.Actors.Communication; + +using System; +using System.Runtime.Serialization; + +internal class ActorDataContractSurrogate : ISerializationSurrogateProvider { - using System; - using System.Runtime.Serialization; + public static readonly ISerializationSurrogateProvider Instance = new ActorDataContractSurrogate(); - internal class ActorDataContractSurrogate : ISerializationSurrogateProvider + public Type GetSurrogateType(Type type) { - public static readonly ISerializationSurrogateProvider Instance = new ActorDataContractSurrogate(); - - public Type GetSurrogateType(Type type) + if (typeof(IActor).IsAssignableFrom(type)) { - if (typeof(IActor).IsAssignableFrom(type)) - { - return typeof(ActorReference); - } - - return type; + return typeof(ActorReference); } - public object GetObjectToSerialize(object obj, Type targetType) - { - if (obj == null) - { - return null; - } - else if (obj is IActor) - { - return ActorReference.Get(obj); - } - - return obj; - } - - public object GetDeserializedObject(object obj, Type targetType) - { - if (obj == null) - { - return null; - } - else if (obj is IActorReference reference && - typeof(IActor).IsAssignableFrom(targetType) && - !typeof(IActorReference).IsAssignableFrom(targetType)) - { - return reference.Bind(targetType); - } - - return obj; - } + return type; } -} + + public object GetObjectToSerialize(object obj, Type targetType) + { + if (obj == null) + { + return null; + } + else if (obj is IActor) + { + return ActorReference.Get(obj); + } + + return obj; + } + + public object GetDeserializedObject(object obj, Type targetType) + { + if (obj == null) + { + return null; + } + else if (obj is IActorReference reference && + typeof(IActor).IsAssignableFrom(targetType) && + !typeof(IActorReference).IsAssignableFrom(targetType)) + { + return reference.Bind(targetType); + } + + return obj; + } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Communication/ActorInvokeException.cs b/src/Dapr.Actors/Communication/ActorInvokeException.cs index d58ceace..1c9e00f0 100644 --- a/src/Dapr.Actors/Communication/ActorInvokeException.cs +++ b/src/Dapr.Actors/Communication/ActorInvokeException.cs @@ -11,88 +11,87 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors +namespace Dapr.Actors; + +using System; +using System.IO; +using Microsoft.Extensions.Logging; + +/// +/// Provides information about an exception from the actor service. +/// +public class ActorInvokeException : Exception { - using System; - using System.IO; - using Microsoft.Extensions.Logging; + /// + /// Initializes a new instance of the class. + /// + public ActorInvokeException() + { + } /// - /// Provides information about an exception from the actor service. + /// Initializes a new instance of the class with appropriate message. /// - public class ActorInvokeException : Exception + /// The fully-qualified name of the exception type thrown by the actor. + /// The error message that explains the reason for this exception. + /// + public ActorInvokeException(string actualExceptionType, string message) + : base(message) { - /// - /// Initializes a new instance of the class. - /// - public ActorInvokeException() - { - } - - /// - /// Initializes a new instance of the class with appropriate message. - /// - /// The fully-qualified name of the exception type thrown by the actor. - /// The error message that explains the reason for this exception. - /// - public ActorInvokeException(string actualExceptionType, string message) - : base(message) - { - this.ActualExceptionType = actualExceptionType; - } - - /// - /// Gets the fully-qualified name of the exception type thrown by the actor. - /// - public string ActualExceptionType { get; private set; } - - /// - /// Factory method that constructs the ActorInvokeException from an exception. - /// - /// Exception. - /// Serialized bytes. - internal static byte[] FromException(Exception exception) - { - var exceptionData = new ActorInvokeExceptionData(exception.GetType().FullName, exception.Message); - var exceptionBytes = exceptionData.Serialize(); - - return exceptionBytes; - } - - /// - /// Gets the exception from the ActorInvokeException. - /// - /// The stream that contains the serialized exception or exception message. - /// Exception from the remote side. - /// true if there was a valid exception, false otherwise. - internal static bool ToException(Stream stream, out Exception result) - { - // try to de-serialize the bytes in to exception requestMessage and create service exception - if (ActorInvokeException.TryDeserialize(stream, out result)) - { - return true; - } - - return false; - } - - internal static bool TryDeserialize(Stream stream, out Exception result, ILogger logger = null) - { - try - { - stream.Seek(0, SeekOrigin.Begin); - var eData = ActorInvokeExceptionData.Deserialize(stream); - result = new ActorInvokeException(eData.Type, eData.Message); - return true; - } - catch (Exception e) - { - // swallowing the exception - logger?.LogWarning("RemoteException: DeSerialization failed : Reason {0}", e); - } - - result = null; - return false; - } + this.ActualExceptionType = actualExceptionType; } -} + + /// + /// Gets the fully-qualified name of the exception type thrown by the actor. + /// + public string ActualExceptionType { get; private set; } + + /// + /// Factory method that constructs the ActorInvokeException from an exception. + /// + /// Exception. + /// Serialized bytes. + internal static byte[] FromException(Exception exception) + { + var exceptionData = new ActorInvokeExceptionData(exception.GetType().FullName, exception.Message); + var exceptionBytes = exceptionData.Serialize(); + + return exceptionBytes; + } + + /// + /// Gets the exception from the ActorInvokeException. + /// + /// The stream that contains the serialized exception or exception message. + /// Exception from the remote side. + /// true if there was a valid exception, false otherwise. + internal static bool ToException(Stream stream, out Exception result) + { + // try to de-serialize the bytes in to exception requestMessage and create service exception + if (ActorInvokeException.TryDeserialize(stream, out result)) + { + return true; + } + + return false; + } + + internal static bool TryDeserialize(Stream stream, out Exception result, ILogger logger = null) + { + try + { + stream.Seek(0, SeekOrigin.Begin); + var eData = ActorInvokeExceptionData.Deserialize(stream); + result = new ActorInvokeException(eData.Type, eData.Message); + return true; + } + catch (Exception e) + { + // swallowing the exception + logger?.LogWarning("RemoteException: DeSerialization failed : Reason {0}", e); + } + + result = null; + return false; + } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Communication/ActorInvokeExceptionData.cs b/src/Dapr.Actors/Communication/ActorInvokeExceptionData.cs index 6103c79c..09348768 100644 --- a/src/Dapr.Actors/Communication/ActorInvokeExceptionData.cs +++ b/src/Dapr.Actors/Communication/ActorInvokeExceptionData.cs @@ -11,47 +11,46 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors +namespace Dapr.Actors; + +using System.IO; +using System.Runtime.Serialization; +using System.Xml; + +[DataContract(Name = "ActorInvokeExceptionData", Namespace = Constants.Namespace)] +internal class ActorInvokeExceptionData { - using System.IO; - using System.Runtime.Serialization; - using System.Xml; + private static readonly DataContractSerializer ActorInvokeExceptionDataSerializer = new DataContractSerializer(typeof(ActorInvokeExceptionData)); - [DataContract(Name = "ActorInvokeExceptionData", Namespace = Constants.Namespace)] - internal class ActorInvokeExceptionData + public ActorInvokeExceptionData(string type, string message) { - private static readonly DataContractSerializer ActorInvokeExceptionDataSerializer = new DataContractSerializer(typeof(ActorInvokeExceptionData)); - - public ActorInvokeExceptionData(string type, string message) - { - this.Type = type; - this.Message = message; - } - - [DataMember] - public string Type { get; private set; } - - [DataMember] - public string Message { get; private set; } - - internal static ActorInvokeExceptionData Deserialize(Stream stream) - { - if ((stream == null) || (stream.Length == 0)) - { - return null; - } - - using var reader = XmlDictionaryReader.CreateBinaryReader(stream, XmlDictionaryReaderQuotas.Max); - return (ActorInvokeExceptionData)ActorInvokeExceptionDataSerializer.ReadObject(reader); - } - - internal byte[] Serialize() - { - using var stream = new MemoryStream(); - using var writer = XmlDictionaryWriter.CreateBinaryWriter(stream); - ActorInvokeExceptionDataSerializer.WriteObject(writer, this); - writer.Flush(); - return stream.ToArray(); - } + this.Type = type; + this.Message = message; } -} + + [DataMember] + public string Type { get; private set; } + + [DataMember] + public string Message { get; private set; } + + internal static ActorInvokeExceptionData Deserialize(Stream stream) + { + if ((stream == null) || (stream.Length == 0)) + { + return null; + } + + using var reader = XmlDictionaryReader.CreateBinaryReader(stream, XmlDictionaryReaderQuotas.Max); + return (ActorInvokeExceptionData)ActorInvokeExceptionDataSerializer.ReadObject(reader); + } + + internal byte[] Serialize() + { + using var stream = new MemoryStream(); + using var writer = XmlDictionaryWriter.CreateBinaryWriter(stream); + ActorInvokeExceptionDataSerializer.WriteObject(writer, this); + writer.Flush(); + return stream.ToArray(); + } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Communication/ActorLogicalCallContext.cs b/src/Dapr.Actors/Communication/ActorLogicalCallContext.cs index ab9cc455..c3470887 100644 --- a/src/Dapr.Actors/Communication/ActorLogicalCallContext.cs +++ b/src/Dapr.Actors/Communication/ActorLogicalCallContext.cs @@ -11,33 +11,32 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Communication +namespace Dapr.Actors.Communication; + +using System.Threading; + +internal static class ActorLogicalCallContext { - using System.Threading; + private static readonly AsyncLocal fabActAsyncLocal = new AsyncLocal(); - internal static class ActorLogicalCallContext + public static bool IsPresent() { - private static readonly AsyncLocal fabActAsyncLocal = new AsyncLocal(); - - public static bool IsPresent() - { - return (fabActAsyncLocal.Value != null); - } - - public static bool TryGet(out string callContextValue) - { - callContextValue = fabActAsyncLocal.Value; - return (callContextValue != null); - } - - public static void Set(string callContextValue) - { - fabActAsyncLocal.Value = callContextValue; - } - - public static void Clear() - { - fabActAsyncLocal.Value = null; - } + return (fabActAsyncLocal.Value != null); } -} + + public static bool TryGet(out string callContextValue) + { + callContextValue = fabActAsyncLocal.Value; + return (callContextValue != null); + } + + public static void Set(string callContextValue) + { + fabActAsyncLocal.Value = callContextValue; + } + + public static void Clear() + { + fabActAsyncLocal.Value = null; + } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Communication/ActorMessageBodyDataContractSerializationProvider.cs b/src/Dapr.Actors/Communication/ActorMessageBodyDataContractSerializationProvider.cs index cf16ee2d..687cd84d 100644 --- a/src/Dapr.Actors/Communication/ActorMessageBodyDataContractSerializationProvider.cs +++ b/src/Dapr.Actors/Communication/ActorMessageBodyDataContractSerializationProvider.cs @@ -11,93 +11,232 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Communication +namespace Dapr.Actors.Communication; + +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.Serialization; +using System.Threading.Tasks; +using System.Xml; + +/// +/// This is the implmentation for used by remoting service and client during +/// request/response serialization . It uses request Wrapping and data contract for serialization. +/// +internal class ActorMessageBodyDataContractSerializationProvider : IActorMessageBodySerializationProvider { - using System; - using System.Collections.Generic; - using System.IO; - using System.Runtime.Serialization; - using System.Threading.Tasks; - using System.Xml; + private static readonly IEnumerable DefaultKnownTypes = new[] + { + typeof(ActorReference), + }; /// - /// This is the implmentation for used by remoting service and client during - /// request/response serialization . It uses request Wrapping and data contract for serialization. + /// Initializes a new instance of the class. /// - internal class ActorMessageBodyDataContractSerializationProvider : IActorMessageBodySerializationProvider + public ActorMessageBodyDataContractSerializationProvider() { - private static readonly IEnumerable DefaultKnownTypes = new[] - { - typeof(ActorReference), - }; + } - /// - /// Initializes a new instance of the class. - /// - public ActorMessageBodyDataContractSerializationProvider() + /// + /// Creates a MessageFactory for Wrapped Message DataContract Remoting Types. This is used to create Remoting Request/Response objects. + /// + /// + /// that provides an instance of the factory for creating + /// remoting request and response message bodies. + /// + public IActorMessageBodyFactory CreateMessageBodyFactory() + { + return new WrappedRequestMessageFactory(); + } + + /// + /// Creates IActorRequestMessageBodySerializer for a serviceInterface using Wrapped Message DataContract implementation. + /// + /// The remoted service interface. + /// The union of parameter types of all of the methods of the specified interface. + /// Wrapped Request Types for all Methods. + /// + /// An instance of the that can serialize the service + /// actor request message body to a messaging body for transferring over the transport. + /// + public IActorRequestMessageBodySerializer CreateRequestMessageBodySerializer( + Type serviceInterfaceType, + IEnumerable methodRequestParameterTypes, + IEnumerable wrappedRequestMessageTypes = null) + { + var knownTypes = new List(DefaultKnownTypes); + knownTypes.AddRange(wrappedRequestMessageTypes); + + DataContractSerializer serializer = this.CreateMessageBodyDataContractSerializer( + typeof(WrappedMessageBody), + knownTypes); + + return new MemoryStreamMessageBodySerializer(this, serializer); + } + + /// + /// Creates IActorResponseMessageBodySerializer for a serviceInterface using Wrapped Message DataContract implementation. + /// + /// The remoted service interface. + /// The return types of all of the methods of the specified interface. + /// Wrapped Response Types for all remoting methods. + /// + /// An instance of the that can serialize the service + /// actor response message body to a messaging body for transferring over the transport. + /// + public IActorResponseMessageBodySerializer CreateResponseMessageBodySerializer( + Type serviceInterfaceType, + IEnumerable methodReturnTypes, + IEnumerable wrappedResponseMessageTypes = null) + { + var knownTypes = new List(DefaultKnownTypes); + knownTypes.AddRange(wrappedResponseMessageTypes); + + DataContractSerializer serializer = this.CreateMessageBodyDataContractSerializer( + typeof(WrappedMessageBody), + knownTypes); + + return new MemoryStreamMessageBodySerializer(this, serializer); + } + + /// + /// Create the writer to write to the stream. Use this method to customize how the serialized contents are written to + /// the stream. + /// + /// The stream on which to write the serialized contents. + /// + /// An using which the serializer will write the object on the + /// stream. + /// + internal XmlDictionaryWriter CreateXmlDictionaryWriter(Stream outputStream) + { + return XmlDictionaryWriter.CreateBinaryWriter(outputStream); + } + + /// + /// Create the reader to read from the input stream. Use this method to customize how the serialized contents are read + /// from the stream. + /// + /// The stream from which to read the serialized contents. + /// + /// An using which the serializer will read the object from the + /// stream. + /// + internal XmlDictionaryReader CreateXmlDictionaryReader(Stream inputStream) + { + return XmlDictionaryReader.CreateBinaryReader(inputStream, XmlDictionaryReaderQuotas.Max); + } + + /// + /// Gets the settings used to create DataContractSerializer for serializing and de-serializing request message body. + /// + /// Remoting RequestMessageBody Type. + /// The return types of all of the methods of the specified interface. + /// for serializing and de-serializing request message body. + internal DataContractSerializer CreateMessageBodyDataContractSerializer( + Type remotingRequestType, + IEnumerable knownTypes) + { + var serializer = new DataContractSerializer( + remotingRequestType, + new DataContractSerializerSettings() + { + MaxItemsInObjectGraph = int.MaxValue, + KnownTypes = knownTypes, + }); + + serializer.SetSerializationSurrogateProvider(new ActorDataContractSurrogate()); + return serializer; + } + + /// + /// Default serializer for service remoting request and response message body that uses the + /// memory stream to create outgoing message buffers. + /// + private class MemoryStreamMessageBodySerializer : + IActorRequestMessageBodySerializer, + IActorResponseMessageBodySerializer + where TRequest : IActorRequestMessageBody + where TResponse : IActorResponseMessageBody + { + private readonly ActorMessageBodyDataContractSerializationProvider serializationProvider; + private readonly DataContractSerializer serializer; + + public MemoryStreamMessageBodySerializer( + ActorMessageBodyDataContractSerializationProvider serializationProvider, + DataContractSerializer serializer) { + this.serializationProvider = serializationProvider; + this.serializer = serializer; } - /// - /// Creates a MessageFactory for Wrapped Message DataContract Remoting Types. This is used to create Remoting Request/Response objects. - /// - /// - /// that provides an instance of the factory for creating - /// remoting request and response message bodies. - /// - public IActorMessageBodyFactory CreateMessageBodyFactory() + byte[] IActorRequestMessageBodySerializer.Serialize(IActorRequestMessageBody actorRequestMessageBody) { - return new WrappedRequestMessageFactory(); + if (actorRequestMessageBody == null) + { + return null; + } + + using var stream = new MemoryStream(); + using var writer = this.CreateXmlDictionaryWriter(stream); + this.serializer.WriteObject(writer, actorRequestMessageBody); + writer.Flush(); + + return stream.ToArray(); } - /// - /// Creates IActorRequestMessageBodySerializer for a serviceInterface using Wrapped Message DataContract implementation. - /// - /// The remoted service interface. - /// The union of parameter types of all of the methods of the specified interface. - /// Wrapped Request Types for all Methods. - /// - /// An instance of the that can serialize the service - /// actor request message body to a messaging body for transferring over the transport. - /// - public IActorRequestMessageBodySerializer CreateRequestMessageBodySerializer( - Type serviceInterfaceType, - IEnumerable methodRequestParameterTypes, - IEnumerable wrappedRequestMessageTypes = null) + ValueTask IActorRequestMessageBodySerializer.DeserializeAsync(Stream stream) { - var knownTypes = new List(DefaultKnownTypes); - knownTypes.AddRange(wrappedRequestMessageTypes); + if (stream == null) + { + return default; + } - DataContractSerializer serializer = this.CreateMessageBodyDataContractSerializer( - typeof(WrappedMessageBody), - knownTypes); + if (stream.Length == 0) + { + return default; + } - return new MemoryStreamMessageBodySerializer(this, serializer); + stream.Position = 0; + using var reader = this.CreateXmlDictionaryReader(stream); + return new ValueTask((TRequest)this.serializer.ReadObject(reader)); } - /// - /// Creates IActorResponseMessageBodySerializer for a serviceInterface using Wrapped Message DataContract implementation. - /// - /// The remoted service interface. - /// The return types of all of the methods of the specified interface. - /// Wrapped Response Types for all remoting methods. - /// - /// An instance of the that can serialize the service - /// actor response message body to a messaging body for transferring over the transport. - /// - public IActorResponseMessageBodySerializer CreateResponseMessageBodySerializer( - Type serviceInterfaceType, - IEnumerable methodReturnTypes, - IEnumerable wrappedResponseMessageTypes = null) + byte[] IActorResponseMessageBodySerializer.Serialize(IActorResponseMessageBody actorResponseMessageBody) { - var knownTypes = new List(DefaultKnownTypes); - knownTypes.AddRange(wrappedResponseMessageTypes); + if (actorResponseMessageBody == null) + { + return null; + } - DataContractSerializer serializer = this.CreateMessageBodyDataContractSerializer( - typeof(WrappedMessageBody), - knownTypes); + using var stream = new MemoryStream(); + using var writer = this.CreateXmlDictionaryWriter(stream); + this.serializer.WriteObject(writer, actorResponseMessageBody); + writer.Flush(); - return new MemoryStreamMessageBodySerializer(this, serializer); + return stream.ToArray(); + } + + ValueTask IActorResponseMessageBodySerializer.DeserializeAsync(Stream messageBody) + { + if (messageBody == null) + { + return default; + } + + // TODO check performance + using var stream = new MemoryStream(); + messageBody.CopyTo(stream); + stream.Position = 0; + + if (stream.Capacity == 0) + { + return default; + } + + using var reader = this.CreateXmlDictionaryReader(stream); + return new ValueTask((TResponse)this.serializer.ReadObject(reader)); } /// @@ -109,9 +248,9 @@ namespace Dapr.Actors.Communication /// An using which the serializer will write the object on the /// stream. /// - internal XmlDictionaryWriter CreateXmlDictionaryWriter(Stream outputStream) + private XmlDictionaryWriter CreateXmlDictionaryWriter(Stream outputStream) { - return XmlDictionaryWriter.CreateBinaryWriter(outputStream); + return this.serializationProvider.CreateXmlDictionaryWriter(outputStream); } /// @@ -123,149 +262,9 @@ namespace Dapr.Actors.Communication /// An using which the serializer will read the object from the /// stream. /// - internal XmlDictionaryReader CreateXmlDictionaryReader(Stream inputStream) + private XmlDictionaryReader CreateXmlDictionaryReader(Stream inputStream) { - return XmlDictionaryReader.CreateBinaryReader(inputStream, XmlDictionaryReaderQuotas.Max); - } - - /// - /// Gets the settings used to create DataContractSerializer for serializing and de-serializing request message body. - /// - /// Remoting RequestMessageBody Type. - /// The return types of all of the methods of the specified interface. - /// for serializing and de-serializing request message body. - internal DataContractSerializer CreateMessageBodyDataContractSerializer( - Type remotingRequestType, - IEnumerable knownTypes) - { - var serializer = new DataContractSerializer( - remotingRequestType, - new DataContractSerializerSettings() - { - MaxItemsInObjectGraph = int.MaxValue, - KnownTypes = knownTypes, - }); - - serializer.SetSerializationSurrogateProvider(new ActorDataContractSurrogate()); - return serializer; - } - - /// - /// Default serializer for service remoting request and response message body that uses the - /// memory stream to create outgoing message buffers. - /// - private class MemoryStreamMessageBodySerializer : - IActorRequestMessageBodySerializer, - IActorResponseMessageBodySerializer - where TRequest : IActorRequestMessageBody - where TResponse : IActorResponseMessageBody - { - private readonly ActorMessageBodyDataContractSerializationProvider serializationProvider; - private readonly DataContractSerializer serializer; - - public MemoryStreamMessageBodySerializer( - ActorMessageBodyDataContractSerializationProvider serializationProvider, - DataContractSerializer serializer) - { - this.serializationProvider = serializationProvider; - this.serializer = serializer; - } - - byte[] IActorRequestMessageBodySerializer.Serialize(IActorRequestMessageBody actorRequestMessageBody) - { - if (actorRequestMessageBody == null) - { - return null; - } - - using var stream = new MemoryStream(); - using var writer = this.CreateXmlDictionaryWriter(stream); - this.serializer.WriteObject(writer, actorRequestMessageBody); - writer.Flush(); - - return stream.ToArray(); - } - - ValueTask IActorRequestMessageBodySerializer.DeserializeAsync(Stream stream) - { - if (stream == null) - { - return default; - } - - if (stream.Length == 0) - { - return default; - } - - stream.Position = 0; - using var reader = this.CreateXmlDictionaryReader(stream); - return new ValueTask((TRequest)this.serializer.ReadObject(reader)); - } - - byte[] IActorResponseMessageBodySerializer.Serialize(IActorResponseMessageBody actorResponseMessageBody) - { - if (actorResponseMessageBody == null) - { - return null; - } - - using var stream = new MemoryStream(); - using var writer = this.CreateXmlDictionaryWriter(stream); - this.serializer.WriteObject(writer, actorResponseMessageBody); - writer.Flush(); - - return stream.ToArray(); - } - - ValueTask IActorResponseMessageBodySerializer.DeserializeAsync(Stream messageBody) - { - if (messageBody == null) - { - return default; - } - - // TODO check performance - using var stream = new MemoryStream(); - messageBody.CopyTo(stream); - stream.Position = 0; - - if (stream.Capacity == 0) - { - return default; - } - - using var reader = this.CreateXmlDictionaryReader(stream); - return new ValueTask((TResponse)this.serializer.ReadObject(reader)); - } - - /// - /// Create the writer to write to the stream. Use this method to customize how the serialized contents are written to - /// the stream. - /// - /// The stream on which to write the serialized contents. - /// - /// An using which the serializer will write the object on the - /// stream. - /// - private XmlDictionaryWriter CreateXmlDictionaryWriter(Stream outputStream) - { - return this.serializationProvider.CreateXmlDictionaryWriter(outputStream); - } - - /// - /// Create the reader to read from the input stream. Use this method to customize how the serialized contents are read - /// from the stream. - /// - /// The stream from which to read the serialized contents. - /// - /// An using which the serializer will read the object from the - /// stream. - /// - private XmlDictionaryReader CreateXmlDictionaryReader(Stream inputStream) - { - return this.serializationProvider.CreateXmlDictionaryReader(inputStream); - } + return this.serializationProvider.CreateXmlDictionaryReader(inputStream); } } -} +} \ No newline at end of file diff --git a/src/Dapr.Actors/Communication/ActorMessageBodyJsonConverter.cs b/src/Dapr.Actors/Communication/ActorMessageBodyJsonConverter.cs index 0c77adb1..03fc13c8 100644 --- a/src/Dapr.Actors/Communication/ActorMessageBodyJsonConverter.cs +++ b/src/Dapr.Actors/Communication/ActorMessageBodyJsonConverter.cs @@ -16,80 +16,79 @@ using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization; -namespace Dapr.Actors.Communication +namespace Dapr.Actors.Communication; + +internal class ActorMessageBodyJsonConverter : JsonConverter { - internal class ActorMessageBodyJsonConverter : JsonConverter + private readonly List methodRequestParameterTypes; + private readonly List wrappedRequestMessageTypes; + private readonly Type wrapperMessageType; + + public ActorMessageBodyJsonConverter( + List methodRequestParameterTypes, + List wrappedRequestMessageTypes = null + ) { - private readonly List methodRequestParameterTypes; - private readonly List wrappedRequestMessageTypes; - private readonly Type wrapperMessageType; + this.methodRequestParameterTypes = methodRequestParameterTypes; + this.wrappedRequestMessageTypes = wrappedRequestMessageTypes; - public ActorMessageBodyJsonConverter( - List methodRequestParameterTypes, - List wrappedRequestMessageTypes = null - ) + if (this.wrappedRequestMessageTypes != null && this.wrappedRequestMessageTypes.Count == 1) { - this.methodRequestParameterTypes = methodRequestParameterTypes; - this.wrappedRequestMessageTypes = wrappedRequestMessageTypes; - - if (this.wrappedRequestMessageTypes != null && this.wrappedRequestMessageTypes.Count == 1) - { - this.wrapperMessageType = this.wrappedRequestMessageTypes[0]; - } - } - - public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - // Ensure start-of-object, then advance - if (reader.TokenType != JsonTokenType.StartObject) throw new JsonException(); - reader.Read(); - - // Ensure property name, then advance - if (reader.TokenType != JsonTokenType.PropertyName || reader.GetString() != "value") throw new JsonException(); - reader.Read(); - - // If the value is null, return null. - if (reader.TokenType == JsonTokenType.Null) - { - // Read the end object token. - reader.Read(); - return default; - } - - // If the value is an object, deserialize it to wrapper message type - if (this.wrapperMessageType != null) - { - var value = JsonSerializer.Deserialize(ref reader, this.wrapperMessageType, options); - - // Construct a new WrappedMessageBody with the deserialized value. - var wrapper = new WrappedMessageBody() - { - Value = value, - }; - - // Read the end object token. - reader.Read(); - - // Coerce the type to T; required because WrappedMessageBody inherits from two separate interfaces, which - // cannot both be used as generic constraints - return (T)(object)wrapper; - } - - return JsonSerializer.Deserialize(ref reader, options); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStartObject(); - writer.WritePropertyName("value"); - - if (value is WrappedMessageBody body) - { - JsonSerializer.Serialize(writer, body.Value, body.Value.GetType(), options); - } - else - writer.WriteNullValue(); - writer.WriteEndObject(); + this.wrapperMessageType = this.wrappedRequestMessageTypes[0]; } } -} + + public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + // Ensure start-of-object, then advance + if (reader.TokenType != JsonTokenType.StartObject) throw new JsonException(); + reader.Read(); + + // Ensure property name, then advance + if (reader.TokenType != JsonTokenType.PropertyName || reader.GetString() != "value") throw new JsonException(); + reader.Read(); + + // If the value is null, return null. + if (reader.TokenType == JsonTokenType.Null) + { + // Read the end object token. + reader.Read(); + return default; + } + + // If the value is an object, deserialize it to wrapper message type + if (this.wrapperMessageType != null) + { + var value = JsonSerializer.Deserialize(ref reader, this.wrapperMessageType, options); + + // Construct a new WrappedMessageBody with the deserialized value. + var wrapper = new WrappedMessageBody() + { + Value = value, + }; + + // Read the end object token. + reader.Read(); + + // Coerce the type to T; required because WrappedMessageBody inherits from two separate interfaces, which + // cannot both be used as generic constraints + return (T)(object)wrapper; + } + + return JsonSerializer.Deserialize(ref reader, options); + } + + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + writer.WritePropertyName("value"); + + if (value is WrappedMessageBody body) + { + JsonSerializer.Serialize(writer, body.Value, body.Value.GetType(), options); + } + else + writer.WriteNullValue(); + writer.WriteEndObject(); + } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Communication/ActorMessageBodyJsonSerializationProvider.cs b/src/Dapr.Actors/Communication/ActorMessageBodyJsonSerializationProvider.cs index 88bc11ce..ce84624d 100644 --- a/src/Dapr.Actors/Communication/ActorMessageBodyJsonSerializationProvider.cs +++ b/src/Dapr.Actors/Communication/ActorMessageBodyJsonSerializationProvider.cs @@ -11,167 +11,166 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Communication +namespace Dapr.Actors.Communication; + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text.Json; +using System.Threading.Tasks; + +/// +/// This is the implmentation for used by remoting service and client during +/// request/response serialization . It uses request Wrapping and data contract for serialization. +/// +internal class ActorMessageBodyJsonSerializationProvider : IActorMessageBodySerializationProvider { - using System; - using System.Collections.Generic; - using System.IO; - using System.Text.Json; - using System.Threading.Tasks; + public JsonSerializerOptions Options { get; } /// - /// This is the implmentation for used by remoting service and client during - /// request/response serialization . It uses request Wrapping and data contract for serialization. + /// Initializes a new instance of the class. /// - internal class ActorMessageBodyJsonSerializationProvider : IActorMessageBodySerializationProvider + public ActorMessageBodyJsonSerializationProvider(JsonSerializerOptions options) { - public JsonSerializerOptions Options { get; } + Options = options; + } - /// - /// Initializes a new instance of the class. - /// - public ActorMessageBodyJsonSerializationProvider(JsonSerializerOptions options) - { - Options = options; - } + /// + /// Creates a MessageFactory for Wrapped Message Json Remoting Types. This is used to create Remoting Request/Response objects. + /// + /// + /// that provides an instance of the factory for creating + /// remoting request and response message bodies. + /// + public IActorMessageBodyFactory CreateMessageBodyFactory() + { + return new WrappedRequestMessageFactory(); + } - /// - /// Creates a MessageFactory for Wrapped Message Json Remoting Types. This is used to create Remoting Request/Response objects. - /// - /// - /// that provides an instance of the factory for creating - /// remoting request and response message bodies. - /// - public IActorMessageBodyFactory CreateMessageBodyFactory() - { - return new WrappedRequestMessageFactory(); - } + /// + /// Creates IActorRequestMessageBodySerializer for a serviceInterface using Wrapped Message Json implementation. + /// + /// The remoted service interface. + /// The union of parameter types of all of the methods of the specified interface. + /// Wrapped Request Types for all Methods. + /// + /// An instance of the that can serialize the service + /// actor request message body to a messaging body for transferring over the transport. + /// + public IActorRequestMessageBodySerializer CreateRequestMessageBodySerializer( + Type serviceInterfaceType, + IEnumerable methodRequestParameterTypes, + IEnumerable wrappedRequestMessageTypes = null) + { + return new MemoryStreamMessageBodySerializer(Options, serviceInterfaceType, methodRequestParameterTypes, wrappedRequestMessageTypes); + } - /// - /// Creates IActorRequestMessageBodySerializer for a serviceInterface using Wrapped Message Json implementation. - /// - /// The remoted service interface. - /// The union of parameter types of all of the methods of the specified interface. - /// Wrapped Request Types for all Methods. - /// - /// An instance of the that can serialize the service - /// actor request message body to a messaging body for transferring over the transport. - /// - public IActorRequestMessageBodySerializer CreateRequestMessageBodySerializer( + /// + /// Creates IActorResponseMessageBodySerializer for a serviceInterface using Wrapped Message Json implementation. + /// + /// The remoted service interface. + /// The return types of all of the methods of the specified interface. + /// Wrapped Response Types for all remoting methods. + /// + /// An instance of the that can serialize the service + /// actor response message body to a messaging body for transferring over the transport. + /// + public IActorResponseMessageBodySerializer CreateResponseMessageBodySerializer( + Type serviceInterfaceType, + IEnumerable methodReturnTypes, + IEnumerable wrappedResponseMessageTypes = null) + { + return new MemoryStreamMessageBodySerializer(Options, serviceInterfaceType, methodReturnTypes, wrappedResponseMessageTypes); + } + + /// + /// Default serializer for service remoting request and response message body that uses the + /// memory stream to create outgoing message buffers. + /// + private class MemoryStreamMessageBodySerializer : + IActorRequestMessageBodySerializer, + IActorResponseMessageBodySerializer + where TRequest : IActorRequestMessageBody + where TResponse : IActorResponseMessageBody + { + private readonly JsonSerializerOptions serializerOptions; + + public MemoryStreamMessageBodySerializer( + JsonSerializerOptions serializerOptions, Type serviceInterfaceType, IEnumerable methodRequestParameterTypes, IEnumerable wrappedRequestMessageTypes = null) { - return new MemoryStreamMessageBodySerializer(Options, serviceInterfaceType, methodRequestParameterTypes, wrappedRequestMessageTypes); + var _methodRequestParameterTypes = new List(methodRequestParameterTypes); + var _wrappedRequestMessageTypes = new List(wrappedRequestMessageTypes); + if(_wrappedRequestMessageTypes.Count > 1){ + throw new NotSupportedException("JSON serialisation should always provide the actor method (or nothing), that was called" + + " to support (de)serialisation. This is a Dapr SDK error, open an issue on GitHub."); + } + this.serializerOptions = new(serializerOptions) + { + // Workaround since WrappedMessageBody creates an object + // with parameters as fields + IncludeFields = true, + }; + + this.serializerOptions.Converters.Add(new ActorMessageBodyJsonConverter(_methodRequestParameterTypes, _wrappedRequestMessageTypes)); + this.serializerOptions.Converters.Add(new ActorMessageBodyJsonConverter(_methodRequestParameterTypes, _wrappedRequestMessageTypes)); } - /// - /// Creates IActorResponseMessageBodySerializer for a serviceInterface using Wrapped Message Json implementation. - /// - /// The remoted service interface. - /// The return types of all of the methods of the specified interface. - /// Wrapped Response Types for all remoting methods. - /// - /// An instance of the that can serialize the service - /// actor response message body to a messaging body for transferring over the transport. - /// - public IActorResponseMessageBodySerializer CreateResponseMessageBodySerializer( - Type serviceInterfaceType, - IEnumerable methodReturnTypes, - IEnumerable wrappedResponseMessageTypes = null) + byte[] IActorRequestMessageBodySerializer.Serialize(IActorRequestMessageBody actorRequestMessageBody) { - return new MemoryStreamMessageBodySerializer(Options, serviceInterfaceType, methodReturnTypes, wrappedResponseMessageTypes); + if (actorRequestMessageBody == null) + { + return null; + } + + return JsonSerializer.SerializeToUtf8Bytes(actorRequestMessageBody, this.serializerOptions); } - /// - /// Default serializer for service remoting request and response message body that uses the - /// memory stream to create outgoing message buffers. - /// - private class MemoryStreamMessageBodySerializer : - IActorRequestMessageBodySerializer, - IActorResponseMessageBodySerializer - where TRequest : IActorRequestMessageBody - where TResponse : IActorResponseMessageBody + async ValueTask IActorRequestMessageBodySerializer.DeserializeAsync(Stream stream) { - private readonly JsonSerializerOptions serializerOptions; - - public MemoryStreamMessageBodySerializer( - JsonSerializerOptions serializerOptions, - Type serviceInterfaceType, - IEnumerable methodRequestParameterTypes, - IEnumerable wrappedRequestMessageTypes = null) + if (stream == null) { - var _methodRequestParameterTypes = new List(methodRequestParameterTypes); - var _wrappedRequestMessageTypes = new List(wrappedRequestMessageTypes); - if(_wrappedRequestMessageTypes.Count > 1){ - throw new NotSupportedException("JSON serialisation should always provide the actor method (or nothing), that was called" + - " to support (de)serialisation. This is a Dapr SDK error, open an issue on GitHub."); - } - this.serializerOptions = new(serializerOptions) - { - // Workaround since WrappedMessageBody creates an object - // with parameters as fields - IncludeFields = true, - }; - - this.serializerOptions.Converters.Add(new ActorMessageBodyJsonConverter(_methodRequestParameterTypes, _wrappedRequestMessageTypes)); - this.serializerOptions.Converters.Add(new ActorMessageBodyJsonConverter(_methodRequestParameterTypes, _wrappedRequestMessageTypes)); + return default; } - byte[] IActorRequestMessageBodySerializer.Serialize(IActorRequestMessageBody actorRequestMessageBody) + if (stream.Length == 0) { - if (actorRequestMessageBody == null) - { - return null; - } - - return JsonSerializer.SerializeToUtf8Bytes(actorRequestMessageBody, this.serializerOptions); + return default; } - async ValueTask IActorRequestMessageBodySerializer.DeserializeAsync(Stream stream) + stream.Position = 0; + return await JsonSerializer.DeserializeAsync(stream, this.serializerOptions); + } + + byte[] IActorResponseMessageBodySerializer.Serialize(IActorResponseMessageBody actorResponseMessageBody) + { + if (actorResponseMessageBody == null) { - if (stream == null) - { - return default; - } - - if (stream.Length == 0) - { - return default; - } - - stream.Position = 0; - return await JsonSerializer.DeserializeAsync(stream, this.serializerOptions); + return null; } - byte[] IActorResponseMessageBodySerializer.Serialize(IActorResponseMessageBody actorResponseMessageBody) - { - if (actorResponseMessageBody == null) - { - return null; - } + return JsonSerializer.SerializeToUtf8Bytes(actorResponseMessageBody, this.serializerOptions); + } - return JsonSerializer.SerializeToUtf8Bytes(actorResponseMessageBody, this.serializerOptions); + async ValueTask IActorResponseMessageBodySerializer.DeserializeAsync(Stream messageBody) + { + if (messageBody == null) + { + return null; } - async ValueTask IActorResponseMessageBodySerializer.DeserializeAsync(Stream messageBody) + using var stream = new MemoryStream(); + messageBody.CopyTo(stream); + stream.Position = 0; + + if (stream.Capacity == 0) { - if (messageBody == null) - { - return null; - } - - using var stream = new MemoryStream(); - messageBody.CopyTo(stream); - stream.Position = 0; - - if (stream.Capacity == 0) - { - return null; - } - - return await JsonSerializer.DeserializeAsync(stream, this.serializerOptions); + return null; } + + return await JsonSerializer.DeserializeAsync(stream, this.serializerOptions); } } -} +} \ No newline at end of file diff --git a/src/Dapr.Actors/Communication/ActorMessageHeaderSerializer.cs b/src/Dapr.Actors/Communication/ActorMessageHeaderSerializer.cs index 7d0b2b73..17ead6d9 100644 --- a/src/Dapr.Actors/Communication/ActorMessageHeaderSerializer.cs +++ b/src/Dapr.Actors/Communication/ActorMessageHeaderSerializer.cs @@ -11,97 +11,96 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Communication +namespace Dapr.Actors.Communication; + +using System.IO; +using System.Runtime.Serialization; +using System.Xml; + +internal class ActorMessageHeaderSerializer : IActorMessageHeaderSerializer { - using System.IO; - using System.Runtime.Serialization; - using System.Xml; + private readonly DataContractSerializer requestHeaderSerializer; + private readonly DataContractSerializer responseHeaderSerializer; - internal class ActorMessageHeaderSerializer : IActorMessageHeaderSerializer - { - private readonly DataContractSerializer requestHeaderSerializer; - private readonly DataContractSerializer responseHeaderSerializer; - - public ActorMessageHeaderSerializer() - : this( - new DataContractSerializer( - typeof(IActorRequestMessageHeader), - new DataContractSerializerSettings() - { - MaxItemsInObjectGraph = int.MaxValue, - KnownTypes = new[] - { - typeof(ActorRequestMessageHeader), - }, - })) - { - } - - public ActorMessageHeaderSerializer( - DataContractSerializer headerRequestSerializer) - { - this.requestHeaderSerializer = headerRequestSerializer; - this.responseHeaderSerializer = new DataContractSerializer( - typeof(IActorResponseMessageHeader), + public ActorMessageHeaderSerializer() + : this( + new DataContractSerializer( + typeof(IActorRequestMessageHeader), new DataContractSerializerSettings() { MaxItemsInObjectGraph = int.MaxValue, - KnownTypes = new[] { typeof(ActorResponseMessageHeader) }, - }); - } - - public byte[] SerializeRequestHeader(IActorRequestMessageHeader serviceRemotingRequestMessageHeader) - { - if (serviceRemotingRequestMessageHeader == null) - { - return null; - } - - using var stream = new MemoryStream(); - using var writer = XmlDictionaryWriter.CreateTextWriter(stream); - this.requestHeaderSerializer.WriteObject(writer, serviceRemotingRequestMessageHeader); - writer.Flush(); - return stream.ToArray(); - } - - public IActorRequestMessageHeader DeserializeRequestHeaders(Stream messageHeader) - { - if ((messageHeader == null) || (messageHeader.Length == 0)) - { - return null; - } - - using var reader = XmlDictionaryReader.CreateTextReader( - messageHeader, - XmlDictionaryReaderQuotas.Max); - return (IActorRequestMessageHeader)this.requestHeaderSerializer.ReadObject(reader); - } - - public byte[] SerializeResponseHeader(IActorResponseMessageHeader serviceRemotingResponseMessageHeader) - { - if (serviceRemotingResponseMessageHeader == null || serviceRemotingResponseMessageHeader.CheckIfItsEmpty()) - { - return null; - } - - using var stream = new MemoryStream(); - using var writer = XmlDictionaryWriter.CreateTextWriter(stream); - this.responseHeaderSerializer.WriteObject(writer, serviceRemotingResponseMessageHeader); - writer.Flush(); - return stream.ToArray(); - } - - public IActorResponseMessageHeader DeserializeResponseHeaders(Stream messageHeader) - { - if ((messageHeader == null) || (messageHeader.Length == 0)) - { - return null; - } - - using var reader = XmlDictionaryReader.CreateTextReader( - messageHeader, - XmlDictionaryReaderQuotas.Max); - return (IActorResponseMessageHeader)this.responseHeaderSerializer.ReadObject(reader); - } + KnownTypes = new[] + { + typeof(ActorRequestMessageHeader), + }, + })) + { } -} + + public ActorMessageHeaderSerializer( + DataContractSerializer headerRequestSerializer) + { + this.requestHeaderSerializer = headerRequestSerializer; + this.responseHeaderSerializer = new DataContractSerializer( + typeof(IActorResponseMessageHeader), + new DataContractSerializerSettings() + { + MaxItemsInObjectGraph = int.MaxValue, + KnownTypes = new[] { typeof(ActorResponseMessageHeader) }, + }); + } + + public byte[] SerializeRequestHeader(IActorRequestMessageHeader serviceRemotingRequestMessageHeader) + { + if (serviceRemotingRequestMessageHeader == null) + { + return null; + } + + using var stream = new MemoryStream(); + using var writer = XmlDictionaryWriter.CreateTextWriter(stream); + this.requestHeaderSerializer.WriteObject(writer, serviceRemotingRequestMessageHeader); + writer.Flush(); + return stream.ToArray(); + } + + public IActorRequestMessageHeader DeserializeRequestHeaders(Stream messageHeader) + { + if ((messageHeader == null) || (messageHeader.Length == 0)) + { + return null; + } + + using var reader = XmlDictionaryReader.CreateTextReader( + messageHeader, + XmlDictionaryReaderQuotas.Max); + return (IActorRequestMessageHeader)this.requestHeaderSerializer.ReadObject(reader); + } + + public byte[] SerializeResponseHeader(IActorResponseMessageHeader serviceRemotingResponseMessageHeader) + { + if (serviceRemotingResponseMessageHeader == null || serviceRemotingResponseMessageHeader.CheckIfItsEmpty()) + { + return null; + } + + using var stream = new MemoryStream(); + using var writer = XmlDictionaryWriter.CreateTextWriter(stream); + this.responseHeaderSerializer.WriteObject(writer, serviceRemotingResponseMessageHeader); + writer.Flush(); + return stream.ToArray(); + } + + public IActorResponseMessageHeader DeserializeResponseHeaders(Stream messageHeader) + { + if ((messageHeader == null) || (messageHeader.Length == 0)) + { + return null; + } + + using var reader = XmlDictionaryReader.CreateTextReader( + messageHeader, + XmlDictionaryReaderQuotas.Max); + return (IActorResponseMessageHeader)this.responseHeaderSerializer.ReadObject(reader); + } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Communication/ActorMessageSerializersManager.cs b/src/Dapr.Actors/Communication/ActorMessageSerializersManager.cs index 3355aff1..4955a599 100644 --- a/src/Dapr.Actors/Communication/ActorMessageSerializersManager.cs +++ b/src/Dapr.Actors/Communication/ActorMessageSerializersManager.cs @@ -11,105 +11,104 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Communication +namespace Dapr.Actors.Communication; + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Dapr.Actors.Builder; + +internal class ActorMessageSerializersManager { - using System; - using System.Collections.Concurrent; - using System.Collections.Generic; - using System.Diagnostics.CodeAnalysis; - using System.Linq; - using Dapr.Actors.Builder; + private readonly ConcurrentDictionary<(int, string), CacheEntry> cachedBodySerializers; + private readonly IActorMessageHeaderSerializer headerSerializer; + private readonly IActorMessageBodySerializationProvider serializationProvider; - internal class ActorMessageSerializersManager + public ActorMessageSerializersManager( + IActorMessageBodySerializationProvider serializationProvider, + IActorMessageHeaderSerializer headerSerializer) { - private readonly ConcurrentDictionary<(int, string), CacheEntry> cachedBodySerializers; - private readonly IActorMessageHeaderSerializer headerSerializer; - private readonly IActorMessageBodySerializationProvider serializationProvider; - - public ActorMessageSerializersManager( - IActorMessageBodySerializationProvider serializationProvider, - IActorMessageHeaderSerializer headerSerializer) + if (serializationProvider == null) { - if (serializationProvider == null) - { - serializationProvider = new ActorMessageBodyDataContractSerializationProvider(); - } - - if (headerSerializer == null) - { - headerSerializer = new ActorMessageHeaderSerializer(); - } - - this.serializationProvider = serializationProvider; - this.cachedBodySerializers = new ConcurrentDictionary<(int, string), CacheEntry>(); - this.headerSerializer = headerSerializer; + serializationProvider = new ActorMessageBodyDataContractSerializationProvider(); } - public IActorMessageBodySerializationProvider GetSerializationProvider() + if (headerSerializer == null) { - return this.serializationProvider; + headerSerializer = new ActorMessageHeaderSerializer(); } - public IActorMessageHeaderSerializer GetHeaderSerializer() - { - return this.headerSerializer; - } - - public IActorRequestMessageBodySerializer GetRequestMessageBodySerializer(int interfaceId, [AllowNull] string methodName = null) - { - return this.cachedBodySerializers.GetOrAdd((interfaceId, methodName), this.CreateSerializers).RequestMessageBodySerializer; - } - - public IActorResponseMessageBodySerializer GetResponseMessageBodySerializer(int interfaceId, [AllowNull] string methodName = null) - { - return this.cachedBodySerializers.GetOrAdd((interfaceId, methodName), this.CreateSerializers).ResponseMessageBodySerializer; - } - - internal CacheEntry CreateSerializers((int interfaceId, string methodName) data) - { - var interfaceDetails = this.GetInterfaceDetails(data.interfaceId); - - // get the service interface type from the code gen layer - var serviceInterfaceType = interfaceDetails.ServiceInterfaceType; - - // get the known types from the codegen layer - var requestBodyTypes = interfaceDetails.RequestKnownTypes; - - // get the known types from the codegen layer - var responseBodyTypes = interfaceDetails.ResponseKnownTypes; - if (data.methodName is null) - { - // Path is mainly used for XML serialization - return new CacheEntry( - this.serializationProvider.CreateRequestMessageBodySerializer(serviceInterfaceType, requestBodyTypes, interfaceDetails.RequestWrappedKnownTypes), - this.serializationProvider.CreateResponseMessageBodySerializer(serviceInterfaceType, responseBodyTypes, interfaceDetails.ResponseWrappedKnownTypes)); - } - else - { - // This path should be used for JSON serialization - var requestWrapperTypeAsList = interfaceDetails.RequestWrappedKnownTypes.Where(r => r.Name == $"{data.methodName}ReqBody").ToList(); - if(requestWrapperTypeAsList.Count > 1){ - throw new NotSupportedException($"More then one wrappertype was found for {data.methodName}"); - } - var responseWrapperTypeAsList = interfaceDetails.ResponseWrappedKnownTypes.Where(r => r.Name == $"{data.methodName}RespBody").ToList(); - if(responseWrapperTypeAsList.Count > 1){ - throw new NotSupportedException($"More then one wrappertype was found for {data.methodName}"); - } - return new CacheEntry( - this.serializationProvider.CreateRequestMessageBodySerializer(serviceInterfaceType, requestBodyTypes, requestWrapperTypeAsList), - this.serializationProvider.CreateResponseMessageBodySerializer(serviceInterfaceType, responseBodyTypes, responseWrapperTypeAsList)); - } - - } - - internal InterfaceDetails GetInterfaceDetails(int interfaceId) - { - if (!ActorCodeBuilder.TryGetKnownTypes(interfaceId, out var interfaceDetails)) - { - throw new ArgumentException("No interface found with this Id " + interfaceId); - } - - return interfaceDetails; - } + this.serializationProvider = serializationProvider; + this.cachedBodySerializers = new ConcurrentDictionary<(int, string), CacheEntry>(); + this.headerSerializer = headerSerializer; } -} + + public IActorMessageBodySerializationProvider GetSerializationProvider() + { + return this.serializationProvider; + } + + public IActorMessageHeaderSerializer GetHeaderSerializer() + { + return this.headerSerializer; + } + + public IActorRequestMessageBodySerializer GetRequestMessageBodySerializer(int interfaceId, [AllowNull] string methodName = null) + { + return this.cachedBodySerializers.GetOrAdd((interfaceId, methodName), this.CreateSerializers).RequestMessageBodySerializer; + } + + public IActorResponseMessageBodySerializer GetResponseMessageBodySerializer(int interfaceId, [AllowNull] string methodName = null) + { + return this.cachedBodySerializers.GetOrAdd((interfaceId, methodName), this.CreateSerializers).ResponseMessageBodySerializer; + } + + internal CacheEntry CreateSerializers((int interfaceId, string methodName) data) + { + var interfaceDetails = this.GetInterfaceDetails(data.interfaceId); + + // get the service interface type from the code gen layer + var serviceInterfaceType = interfaceDetails.ServiceInterfaceType; + + // get the known types from the codegen layer + var requestBodyTypes = interfaceDetails.RequestKnownTypes; + + // get the known types from the codegen layer + var responseBodyTypes = interfaceDetails.ResponseKnownTypes; + if (data.methodName is null) + { + // Path is mainly used for XML serialization + return new CacheEntry( + this.serializationProvider.CreateRequestMessageBodySerializer(serviceInterfaceType, requestBodyTypes, interfaceDetails.RequestWrappedKnownTypes), + this.serializationProvider.CreateResponseMessageBodySerializer(serviceInterfaceType, responseBodyTypes, interfaceDetails.ResponseWrappedKnownTypes)); + } + else + { + // This path should be used for JSON serialization + var requestWrapperTypeAsList = interfaceDetails.RequestWrappedKnownTypes.Where(r => r.Name == $"{data.methodName}ReqBody").ToList(); + if(requestWrapperTypeAsList.Count > 1){ + throw new NotSupportedException($"More then one wrappertype was found for {data.methodName}"); + } + var responseWrapperTypeAsList = interfaceDetails.ResponseWrappedKnownTypes.Where(r => r.Name == $"{data.methodName}RespBody").ToList(); + if(responseWrapperTypeAsList.Count > 1){ + throw new NotSupportedException($"More then one wrappertype was found for {data.methodName}"); + } + return new CacheEntry( + this.serializationProvider.CreateRequestMessageBodySerializer(serviceInterfaceType, requestBodyTypes, requestWrapperTypeAsList), + this.serializationProvider.CreateResponseMessageBodySerializer(serviceInterfaceType, responseBodyTypes, responseWrapperTypeAsList)); + } + + } + + internal InterfaceDetails GetInterfaceDetails(int interfaceId) + { + if (!ActorCodeBuilder.TryGetKnownTypes(interfaceId, out var interfaceDetails)) + { + throw new ArgumentException("No interface found with this Id " + interfaceId); + } + + return interfaceDetails; + } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Communication/ActorMethodDispatcherMap.cs b/src/Dapr.Actors/Communication/ActorMethodDispatcherMap.cs index 8f15a271..a9000a8a 100644 --- a/src/Dapr.Actors/Communication/ActorMethodDispatcherMap.cs +++ b/src/Dapr.Actors/Communication/ActorMethodDispatcherMap.cs @@ -11,43 +11,42 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Communication +namespace Dapr.Actors.Communication; + +using System; +using System.Collections.Generic; +using System.Globalization; +using Dapr.Actors.Builder; +using Dapr.Actors.Resources; + +/// +/// Actor method dispatcher map for remoting calls. +/// +internal class ActorMethodDispatcherMap { - using System; - using System.Collections.Generic; - using System.Globalization; - using Dapr.Actors.Builder; - using Dapr.Actors.Resources; + private readonly IDictionary map; - /// - /// Actor method dispatcher map for remoting calls. - /// - internal class ActorMethodDispatcherMap + public ActorMethodDispatcherMap(IEnumerable interfaceTypes) { - private readonly IDictionary map; + this.map = new Dictionary(); - public ActorMethodDispatcherMap(IEnumerable interfaceTypes) + foreach (var actorInterfaceType in interfaceTypes) { - this.map = new Dictionary(); - - foreach (var actorInterfaceType in interfaceTypes) - { - var methodDispatcher = ActorCodeBuilder.GetOrCreateMethodDispatcher(actorInterfaceType); - this.map.Add(methodDispatcher.InterfaceId, methodDispatcher); - } - } - - public ActorMethodDispatcherBase GetDispatcher(int interfaceId) - { - if (!this.map.TryGetValue(interfaceId, out var methodDispatcher)) - { - throw new KeyNotFoundException(string.Format( - CultureInfo.CurrentCulture, - SR.ErrorMethodDispatcherNotFound, - interfaceId)); - } - - return methodDispatcher; + var methodDispatcher = ActorCodeBuilder.GetOrCreateMethodDispatcher(actorInterfaceType); + this.map.Add(methodDispatcher.InterfaceId, methodDispatcher); } } -} + + public ActorMethodDispatcherBase GetDispatcher(int interfaceId) + { + if (!this.map.TryGetValue(interfaceId, out var methodDispatcher)) + { + throw new KeyNotFoundException(string.Format( + CultureInfo.CurrentCulture, + SR.ErrorMethodDispatcherNotFound, + interfaceId)); + } + + return methodDispatcher; + } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Communication/ActorRequestMessage.cs b/src/Dapr.Actors/Communication/ActorRequestMessage.cs index e123f073..94917c59 100644 --- a/src/Dapr.Actors/Communication/ActorRequestMessage.cs +++ b/src/Dapr.Actors/Communication/ActorRequestMessage.cs @@ -11,27 +11,26 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Communication +namespace Dapr.Actors.Communication; + +internal class ActorRequestMessage : IActorRequestMessage { - internal class ActorRequestMessage : IActorRequestMessage + private readonly IActorRequestMessageHeader header; + private readonly IActorRequestMessageBody msgBody; + + public ActorRequestMessage(IActorRequestMessageHeader header, IActorRequestMessageBody msgBody) { - private readonly IActorRequestMessageHeader header; - private readonly IActorRequestMessageBody msgBody; - - public ActorRequestMessage(IActorRequestMessageHeader header, IActorRequestMessageBody msgBody) - { - this.header = header; - this.msgBody = msgBody; - } - - public IActorRequestMessageHeader GetHeader() - { - return this.header; - } - - public IActorRequestMessageBody GetBody() - { - return this.msgBody; - } + this.header = header; + this.msgBody = msgBody; } -} + + public IActorRequestMessageHeader GetHeader() + { + return this.header; + } + + public IActorRequestMessageBody GetBody() + { + return this.msgBody; + } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Communication/ActorRequestMessageBody.cs b/src/Dapr.Actors/Communication/ActorRequestMessageBody.cs index 9355abf7..4d629eef 100644 --- a/src/Dapr.Actors/Communication/ActorRequestMessageBody.cs +++ b/src/Dapr.Actors/Communication/ActorRequestMessageBody.cs @@ -11,31 +11,30 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Communication +namespace Dapr.Actors.Communication; + +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; + +[DataContract(Name = "msgBody", Namespace = Constants.Namespace)] +internal class ActorRequestMessageBody : IActorRequestMessageBody { - using System; - using System.Collections.Generic; - using System.Runtime.Serialization; + [DataMember] + private readonly Dictionary parameters; - [DataContract(Name = "msgBody", Namespace = Constants.Namespace)] - internal class ActorRequestMessageBody : IActorRequestMessageBody + public ActorRequestMessageBody(int parameterInfos) { - [DataMember] - private readonly Dictionary parameters; - - public ActorRequestMessageBody(int parameterInfos) - { - this.parameters = new Dictionary(parameterInfos); - } - - public void SetParameter(int position, string paramName, object parameter) - { - this.parameters[paramName] = parameter; - } - - public object GetParameter(int position, string paramName, Type paramType) - { - return this.parameters[paramName]; - } + this.parameters = new Dictionary(parameterInfos); } -} + + public void SetParameter(int position, string paramName, object parameter) + { + this.parameters[paramName] = parameter; + } + + public object GetParameter(int position, string paramName, Type paramType) + { + return this.parameters[paramName]; + } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Communication/ActorRequestMessageHeader.cs b/src/Dapr.Actors/Communication/ActorRequestMessageHeader.cs index 3141220c..99b9a3a4 100644 --- a/src/Dapr.Actors/Communication/ActorRequestMessageHeader.cs +++ b/src/Dapr.Actors/Communication/ActorRequestMessageHeader.cs @@ -11,94 +11,93 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Communication +namespace Dapr.Actors.Communication; + +using System.Collections.Generic; +using System.Globalization; +using System.Runtime.Serialization; +using Dapr.Actors.Resources; + +[DataContract(Name = "ActorHeader", Namespace = Constants.Namespace)] +internal class ActorRequestMessageHeader : IActorRequestMessageHeader { - using System.Collections.Generic; - using System.Globalization; - using System.Runtime.Serialization; - using Dapr.Actors.Resources; + internal const string CancellationHeaderName = "CancellationHeader"; - [DataContract(Name = "ActorHeader", Namespace = Constants.Namespace)] - internal class ActorRequestMessageHeader : IActorRequestMessageHeader + /// + /// Initializes a new instance of the class. + /// + public ActorRequestMessageHeader() { - internal const string CancellationHeaderName = "CancellationHeader"; + this.headers = new Dictionary(); + this.InvocationId = null; + } - /// - /// Initializes a new instance of the class. - /// - public ActorRequestMessageHeader() - { - this.headers = new Dictionary(); - this.InvocationId = null; - } + /// + /// Gets or sets the methodId of the remote method. + /// + /// Method id. + [DataMember(Name = "MethodId", IsRequired = true, Order = 0)] + public int MethodId { get; set; } - /// - /// Gets or sets the methodId of the remote method. - /// - /// Method id. - [DataMember(Name = "MethodId", IsRequired = true, Order = 0)] - public int MethodId { get; set; } + /// + /// Gets or sets the interface id of the remote interface. + /// + /// Interface id. + [DataMember(Name = "InterfaceId", IsRequired = true, Order = 1)] + public int InterfaceId { get; set; } - /// - /// Gets or sets the interface id of the remote interface. - /// - /// Interface id. - [DataMember(Name = "InterfaceId", IsRequired = true, Order = 1)] - public int InterfaceId { get; set; } + /// + /// Gets or sets identifier for the remote method invocation. + /// + [DataMember(Name = "InvocationId", IsRequired = false, Order = 2, EmitDefaultValue = false)] + public string InvocationId { get; set; } - /// - /// Gets or sets identifier for the remote method invocation. - /// - [DataMember(Name = "InvocationId", IsRequired = false, Order = 2, EmitDefaultValue = false)] - public string InvocationId { get; set; } + [DataMember(IsRequired = false, Order = 3)] + public ActorId ActorId { get; set; } - [DataMember(IsRequired = false, Order = 3)] - public ActorId ActorId { get; set; } + [DataMember(IsRequired = false, Order = 4)] + public string CallContext { get; set; } - [DataMember(IsRequired = false, Order = 4)] - public string CallContext { get; set; } - - [DataMember(Name = "Headers", IsRequired = true, Order = 5)] + [DataMember(Name = "Headers", IsRequired = true, Order = 5)] #pragma warning disable SA1201 // Elements should appear in the correct order. Increases readbility when fields kept in order. - private readonly Dictionary headers; + private readonly Dictionary headers; #pragma warning restore SA1201 // Elements should appear in the correct order - /// - /// Gets or sets the method name of the remote method. - /// - /// Method Name. - [DataMember(Name = "MethodName", IsRequired = false, Order = 6)] - public string MethodName { get; set; } + /// + /// Gets or sets the method name of the remote method. + /// + /// Method Name. + [DataMember(Name = "MethodName", IsRequired = false, Order = 6)] + public string MethodName { get; set; } - [DataMember(IsRequired = false, Order = 7)] - public string ActorType { get; set; } + [DataMember(IsRequired = false, Order = 7)] + public string ActorType { get; set; } - public void AddHeader(string headerName, byte[] headerValue) + public void AddHeader(string headerName, byte[] headerValue) + { + if (this.headers.ContainsKey(headerName)) { - if (this.headers.ContainsKey(headerName)) - { - // TODO throw specific translated exception type - throw new System.Exception( - string.Format( - CultureInfo.CurrentCulture, - SR.ErrorHeaderAlreadyExists, - headerName)); - } - - this.headers[headerName] = headerValue; + // TODO throw specific translated exception type + throw new System.Exception( + string.Format( + CultureInfo.CurrentCulture, + SR.ErrorHeaderAlreadyExists, + headerName)); } - public bool TryGetHeaderValue(string headerName, out byte[] headerValue) - { - headerValue = null; - - if (this.headers == null) - { - return false; - } - - return this.headers.TryGetValue(headerName, out headerValue); - } + this.headers[headerName] = headerValue; } -} + + public bool TryGetHeaderValue(string headerName, out byte[] headerValue) + { + headerValue = null; + + if (this.headers == null) + { + return false; + } + + return this.headers.TryGetValue(headerName, out headerValue); + } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Communication/ActorResponseMessage.cs b/src/Dapr.Actors/Communication/ActorResponseMessage.cs index dcebe830..d9d35edc 100644 --- a/src/Dapr.Actors/Communication/ActorResponseMessage.cs +++ b/src/Dapr.Actors/Communication/ActorResponseMessage.cs @@ -11,29 +11,28 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Communication +namespace Dapr.Actors.Communication; + +internal class ActorResponseMessage : IActorResponseMessage { - internal class ActorResponseMessage : IActorResponseMessage + private readonly IActorResponseMessageHeader header; + private readonly IActorResponseMessageBody msgBody; + + public ActorResponseMessage( + IActorResponseMessageHeader header, + IActorResponseMessageBody msgBody) { - private readonly IActorResponseMessageHeader header; - private readonly IActorResponseMessageBody msgBody; - - public ActorResponseMessage( - IActorResponseMessageHeader header, - IActorResponseMessageBody msgBody) - { - this.header = header; - this.msgBody = msgBody; - } - - public IActorResponseMessageHeader GetHeader() - { - return this.header; - } - - public IActorResponseMessageBody GetBody() - { - return this.msgBody; - } + this.header = header; + this.msgBody = msgBody; } -} + + public IActorResponseMessageHeader GetHeader() + { + return this.header; + } + + public IActorResponseMessageBody GetBody() + { + return this.msgBody; + } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Communication/ActorResponseMessageBody.cs b/src/Dapr.Actors/Communication/ActorResponseMessageBody.cs index 8ed4457a..c5ef0947 100644 --- a/src/Dapr.Actors/Communication/ActorResponseMessageBody.cs +++ b/src/Dapr.Actors/Communication/ActorResponseMessageBody.cs @@ -11,25 +11,24 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Communication +namespace Dapr.Actors.Communication; + +using System; +using System.Runtime.Serialization; + +[DataContract(Name = "msgResponse", Namespace = Constants.Namespace)] +internal class ActorResponseMessageBody : IActorResponseMessageBody { - using System; - using System.Runtime.Serialization; + [DataMember] + private object response; - [DataContract(Name = "msgResponse", Namespace = Constants.Namespace)] - internal class ActorResponseMessageBody : IActorResponseMessageBody + public void Set(object response) { - [DataMember] - private object response; - - public void Set(object response) - { - this.response = response; - } - - public object Get(Type paramType) - { - return this.response; - } + this.response = response; } -} + + public object Get(Type paramType) + { + return this.response; + } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Communication/ActorResponseMessageHeader.cs b/src/Dapr.Actors/Communication/ActorResponseMessageHeader.cs index 00f23fce..ef67c4f2 100644 --- a/src/Dapr.Actors/Communication/ActorResponseMessageHeader.cs +++ b/src/Dapr.Actors/Communication/ActorResponseMessageHeader.cs @@ -11,63 +11,62 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Communication +namespace Dapr.Actors.Communication; + +using System.Collections.Generic; +using System.Globalization; +using System.Runtime.Serialization; +using Dapr.Actors.Resources; + +[DataContract(Name = "ActorResponseMessageHeaders", Namespace = Constants.Namespace)] + +internal class ActorResponseMessageHeader : IActorResponseMessageHeader { - using System.Collections.Generic; - using System.Globalization; - using System.Runtime.Serialization; - using Dapr.Actors.Resources; + [DataMember(Name = "Headers", IsRequired = true, Order = 2)] + private readonly Dictionary headers; - [DataContract(Name = "ActorResponseMessageHeaders", Namespace = Constants.Namespace)] - - internal class ActorResponseMessageHeader : IActorResponseMessageHeader + /// + /// Initializes a new instance of the class. + /// + public ActorResponseMessageHeader() { - [DataMember(Name = "Headers", IsRequired = true, Order = 2)] - private readonly Dictionary headers; - - /// - /// Initializes a new instance of the class. - /// - public ActorResponseMessageHeader() - { - this.headers = new Dictionary(); - } - - public void AddHeader(string headerName, byte[] headerValue) - { - if (this.headers.ContainsKey(headerName)) - { - // TODO throw Dapr specific translated exception type - throw new System.Exception( - string.Format( - CultureInfo.CurrentCulture, - SR.ErrorHeaderAlreadyExists, - headerName)); - } - - this.headers[headerName] = headerValue; - } - - public bool CheckIfItsEmpty() - { - if (this.headers == null || this.headers.Count == 0) - { - return true; - } - - return false; - } - - public bool TryGetHeaderValue(string headerName, out byte[] headerValue) - { - headerValue = null; - - if (this.headers == null) - { - return false; - } - - return this.headers.TryGetValue(headerName, out headerValue); - } + this.headers = new Dictionary(); } -} + + public void AddHeader(string headerName, byte[] headerValue) + { + if (this.headers.ContainsKey(headerName)) + { + // TODO throw Dapr specific translated exception type + throw new System.Exception( + string.Format( + CultureInfo.CurrentCulture, + SR.ErrorHeaderAlreadyExists, + headerName)); + } + + this.headers[headerName] = headerValue; + } + + public bool CheckIfItsEmpty() + { + if (this.headers == null || this.headers.Count == 0) + { + return true; + } + + return false; + } + + public bool TryGetHeaderValue(string headerName, out byte[] headerValue) + { + headerValue = null; + + if (this.headers == null) + { + return false; + } + + return this.headers.TryGetValue(headerName, out headerValue); + } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Communication/ActorStateResponse.cs b/src/Dapr.Actors/Communication/ActorStateResponse.cs index 22b3bf20..bf48467e 100644 --- a/src/Dapr.Actors/Communication/ActorStateResponse.cs +++ b/src/Dapr.Actors/Communication/ActorStateResponse.cs @@ -11,40 +11,39 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Communication +namespace Dapr.Actors.Communication; + +using System; + +/// +/// Represents a response from fetching an actor state key. +/// +public class ActorStateResponse { - using System; + /// + /// Initializes a new instance of the class. + /// + /// The response value. + /// The time to live expiration time. + public ActorStateResponse(T value, DateTimeOffset? ttlExpireTime) + { + this.Value = value; + this.TTLExpireTime = ttlExpireTime; + } /// - /// Represents a response from fetching an actor state key. + /// Gets the response value as a string. /// - public class ActorStateResponse - { - /// - /// Initializes a new instance of the class. - /// - /// The response value. - /// The time to live expiration time. - public ActorStateResponse(T value, DateTimeOffset? ttlExpireTime) - { - this.Value = value; - this.TTLExpireTime = ttlExpireTime; - } + /// + /// The response value as a string. + /// + public T Value { get; } - /// - /// Gets the response value as a string. - /// - /// - /// The response value as a string. - /// - public T Value { get; } - - /// - /// Gets the time to live expiration time. - /// - /// - /// The time to live expiration time. - /// - public DateTimeOffset? TTLExpireTime { get; } - } -} + /// + /// Gets the time to live expiration time. + /// + /// + /// The time to live expiration time. + /// + public DateTimeOffset? TTLExpireTime { get; } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Communication/CacheEntry.cs b/src/Dapr.Actors/Communication/CacheEntry.cs index 41664087..36a3a4da 100644 --- a/src/Dapr.Actors/Communication/CacheEntry.cs +++ b/src/Dapr.Actors/Communication/CacheEntry.cs @@ -11,20 +11,19 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Communication +namespace Dapr.Actors.Communication; + +internal class CacheEntry { - internal class CacheEntry + public CacheEntry( + IActorRequestMessageBodySerializer requestBodySerializer, + IActorResponseMessageBodySerializer responseBodySerializer) { - public CacheEntry( - IActorRequestMessageBodySerializer requestBodySerializer, - IActorResponseMessageBodySerializer responseBodySerializer) - { - this.RequestMessageBodySerializer = requestBodySerializer; - this.ResponseMessageBodySerializer = responseBodySerializer; - } - - public IActorRequestMessageBodySerializer RequestMessageBodySerializer { get; } - - public IActorResponseMessageBodySerializer ResponseMessageBodySerializer { get; } + this.RequestMessageBodySerializer = requestBodySerializer; + this.ResponseMessageBodySerializer = responseBodySerializer; } -} + + public IActorRequestMessageBodySerializer RequestMessageBodySerializer { get; } + + public IActorResponseMessageBodySerializer ResponseMessageBodySerializer { get; } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Communication/Client/ActorNonRemotingClient.cs b/src/Dapr.Actors/Communication/Client/ActorNonRemotingClient.cs index d9e7f840..83989cea 100644 --- a/src/Dapr.Actors/Communication/Client/ActorNonRemotingClient.cs +++ b/src/Dapr.Actors/Communication/Client/ActorNonRemotingClient.cs @@ -11,33 +11,32 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Communication.Client +namespace Dapr.Actors.Communication.Client; + +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +internal class ActorNonRemotingClient { - using System.IO; - using System.Threading; - using System.Threading.Tasks; + private readonly IDaprInteractor daprInteractor; - internal class ActorNonRemotingClient + public ActorNonRemotingClient(IDaprInteractor daprInteractor) { - private readonly IDaprInteractor daprInteractor; - - public ActorNonRemotingClient(IDaprInteractor daprInteractor) - { - this.daprInteractor = daprInteractor; - } - - /// - /// Invokes an Actor method on Dapr runtime without remoting. - /// - /// Type of actor. - /// ActorId. - /// Method name to invoke. - /// Serialized body. - /// Cancels the operation. - /// A task that represents the asynchronous operation. - public Task InvokeActorMethodWithoutRemotingAsync(string actorType, string actorId, string methodName, string jsonPayload, CancellationToken cancellationToken = default) - { - return this.daprInteractor.InvokeActorMethodWithoutRemotingAsync(actorType, actorId, methodName, jsonPayload, cancellationToken); - } + this.daprInteractor = daprInteractor; } -} + + /// + /// Invokes an Actor method on Dapr runtime without remoting. + /// + /// Type of actor. + /// ActorId. + /// Method name to invoke. + /// Serialized body. + /// Cancels the operation. + /// A task that represents the asynchronous operation. + public Task InvokeActorMethodWithoutRemotingAsync(string actorType, string actorId, string methodName, string jsonPayload, CancellationToken cancellationToken = default) + { + return this.daprInteractor.InvokeActorMethodWithoutRemotingAsync(actorType, actorId, methodName, jsonPayload, cancellationToken); + } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Communication/Client/ActorRemotingClient.cs b/src/Dapr.Actors/Communication/Client/ActorRemotingClient.cs index d43accc6..9a019e80 100644 --- a/src/Dapr.Actors/Communication/Client/ActorRemotingClient.cs +++ b/src/Dapr.Actors/Communication/Client/ActorRemotingClient.cs @@ -11,49 +11,48 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Communication.Client +namespace Dapr.Actors.Communication.Client; + +using System.Threading; +using System.Threading.Tasks; + +internal class ActorRemotingClient { - using System.Threading; - using System.Threading.Tasks; + private readonly ActorMessageSerializersManager serializersManager; + private readonly IActorMessageBodyFactory remotingMessageBodyFactory = null; + private readonly IDaprInteractor daprInteractor; - internal class ActorRemotingClient + public ActorRemotingClient( + IDaprInteractor daprInteractor, + IActorMessageBodySerializationProvider serializationProvider = null) { - private readonly ActorMessageSerializersManager serializersManager; - private readonly IActorMessageBodyFactory remotingMessageBodyFactory = null; - private readonly IDaprInteractor daprInteractor; - - public ActorRemotingClient( - IDaprInteractor daprInteractor, - IActorMessageBodySerializationProvider serializationProvider = null) - { - this.daprInteractor = daprInteractor; - this.serializersManager = IntializeSerializationManager(serializationProvider); - this.remotingMessageBodyFactory = this.serializersManager.GetSerializationProvider().CreateMessageBodyFactory(); - } - - /// - /// Gets a factory for creating the remoting message bodies. - /// - /// A factory for creating the remoting message bodies. - public IActorMessageBodyFactory GetRemotingMessageBodyFactory() - { - return this.remotingMessageBodyFactory; - } - - public async Task InvokeAsync( - IActorRequestMessage remotingRequestMessage, - CancellationToken cancellationToken) - { - return await this.daprInteractor.InvokeActorMethodWithRemotingAsync(this.serializersManager, remotingRequestMessage, cancellationToken); - } - - private static ActorMessageSerializersManager IntializeSerializationManager( - IActorMessageBodySerializationProvider serializationProvider) - { - // TODO serializer settings - return new ActorMessageSerializersManager( - serializationProvider, - new ActorMessageHeaderSerializer()); - } + this.daprInteractor = daprInteractor; + this.serializersManager = IntializeSerializationManager(serializationProvider); + this.remotingMessageBodyFactory = this.serializersManager.GetSerializationProvider().CreateMessageBodyFactory(); } -} + + /// + /// Gets a factory for creating the remoting message bodies. + /// + /// A factory for creating the remoting message bodies. + public IActorMessageBodyFactory GetRemotingMessageBodyFactory() + { + return this.remotingMessageBodyFactory; + } + + public async Task InvokeAsync( + IActorRequestMessage remotingRequestMessage, + CancellationToken cancellationToken) + { + return await this.daprInteractor.InvokeActorMethodWithRemotingAsync(this.serializersManager, remotingRequestMessage, cancellationToken); + } + + private static ActorMessageSerializersManager IntializeSerializationManager( + IActorMessageBodySerializationProvider serializationProvider) + { + // TODO serializer settings + return new ActorMessageSerializersManager( + serializationProvider, + new ActorMessageHeaderSerializer()); + } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Communication/DataContractMessageFactory.cs b/src/Dapr.Actors/Communication/DataContractMessageFactory.cs index dd5cebef..a58984b6 100644 --- a/src/Dapr.Actors/Communication/DataContractMessageFactory.cs +++ b/src/Dapr.Actors/Communication/DataContractMessageFactory.cs @@ -11,18 +11,17 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Communication -{ - internal class DataContractMessageFactory : IActorMessageBodyFactory - { - public IActorRequestMessageBody CreateRequestMessageBody(string interfaceName, string methodName, int numberOfParameters, object wrappedRequestObject) - { - return new ActorRequestMessageBody(numberOfParameters); - } +namespace Dapr.Actors.Communication; - public IActorResponseMessageBody CreateResponseMessageBody(string interfaceName, string methodName, object wrappedResponseObject) - { - return new ActorResponseMessageBody(); - } +internal class DataContractMessageFactory : IActorMessageBodyFactory +{ + public IActorRequestMessageBody CreateRequestMessageBody(string interfaceName, string methodName, int numberOfParameters, object wrappedRequestObject) + { + return new ActorRequestMessageBody(numberOfParameters); } -} + + public IActorResponseMessageBody CreateResponseMessageBody(string interfaceName, string methodName, object wrappedResponseObject) + { + return new ActorResponseMessageBody(); + } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Communication/DisposableStream.cs b/src/Dapr.Actors/Communication/DisposableStream.cs index 311f2da3..c6b9abbf 100644 --- a/src/Dapr.Actors/Communication/DisposableStream.cs +++ b/src/Dapr.Actors/Communication/DisposableStream.cs @@ -11,86 +11,85 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Communication +namespace Dapr.Actors.Communication; + +using System.IO; + +/// +/// Wraps an implementation of Stream and provides an idempotent impplementation of Dispose. +/// +internal class DisposableStream : Stream { - using System.IO; + private readonly Stream streamImplementation; + private bool disposed; - /// - /// Wraps an implementation of Stream and provides an idempotent impplementation of Dispose. - /// - internal class DisposableStream : Stream + public DisposableStream(Stream streamImplementation) { - private readonly Stream streamImplementation; - private bool disposed; + this.streamImplementation = streamImplementation; + } - public DisposableStream(Stream streamImplementation) - { - this.streamImplementation = streamImplementation; - } + public override bool CanRead + { + get { return this.streamImplementation.CanRead; } + } - public override bool CanRead - { - get { return this.streamImplementation.CanRead; } - } + public override bool CanSeek + { + get { return this.streamImplementation.CanSeek; } + } - public override bool CanSeek - { - get { return this.streamImplementation.CanSeek; } - } + public override bool CanWrite + { + get { return this.streamImplementation.CanWrite; } + } - public override bool CanWrite - { - get { return this.streamImplementation.CanWrite; } - } + public override long Length + { + get { return this.streamImplementation.Length; } + } - public override long Length - { - get { return this.streamImplementation.Length; } - } + public override long Position + { + get { return this.streamImplementation.Position; } + set { this.streamImplementation.Position = value; } + } - public override long Position - { - get { return this.streamImplementation.Position; } - set { this.streamImplementation.Position = value; } - } + public override void Flush() + { + this.streamImplementation.Flush(); + } - public override void Flush() - { - this.streamImplementation.Flush(); - } + public override long Seek(long offset, SeekOrigin origin) + { + return this.streamImplementation.Seek(offset, origin); + } - public override long Seek(long offset, SeekOrigin origin) - { - return this.streamImplementation.Seek(offset, origin); - } + public override void SetLength(long value) + { + this.streamImplementation.SetLength(value); + } - public override void SetLength(long value) - { - this.streamImplementation.SetLength(value); - } + public override int Read(byte[] buffer, int offset, int count) + { + return this.streamImplementation.Read(buffer, offset, count); + } - public override int Read(byte[] buffer, int offset, int count) - { - return this.streamImplementation.Read(buffer, offset, count); - } + public override void Write(byte[] buffer, int offset, int count) + { + this.streamImplementation.Write(buffer, offset, count); + } - public override void Write(byte[] buffer, int offset, int count) + protected override void Dispose(bool disposing) + { + if (!this.disposed) { - this.streamImplementation.Write(buffer, offset, count); - } - - protected override void Dispose(bool disposing) - { - if (!this.disposed) + base.Dispose(disposing); + if (disposing) { - base.Dispose(disposing); - if (disposing) - { - this.streamImplementation.Dispose(); - } + this.streamImplementation.Dispose(); } - - this.disposed = true; } + + this.disposed = true; } } \ No newline at end of file diff --git a/src/Dapr.Actors/Communication/IActorMessageBodyFactory.cs b/src/Dapr.Actors/Communication/IActorMessageBodyFactory.cs index 2d7b95e6..729ce530 100644 --- a/src/Dapr.Actors/Communication/IActorMessageBodyFactory.cs +++ b/src/Dapr.Actors/Communication/IActorMessageBodyFactory.cs @@ -11,30 +11,29 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Communication +namespace Dapr.Actors.Communication; + +/// +/// Defines the interface that must be implemented for providing factory for creating actor request body and response body objects. +/// +public interface IActorMessageBodyFactory { /// - /// Defines the interface that must be implemented for providing factory for creating actor request body and response body objects. + /// Creates a actor request message body. /// - public interface IActorMessageBodyFactory - { - /// - /// Creates a actor request message body. - /// - /// This is FullName for the service interface for which request body is being constructed. - /// MethodName for the service interface for which request will be sent to. - /// Number of Parameters in that Method. - /// Wrapped Request Object. - /// IActorRequestMessageBody. - IActorRequestMessageBody CreateRequestMessageBody(string interfaceName, string methodName, int numberOfParameters, object wrappedRequestObject); + /// This is FullName for the service interface for which request body is being constructed. + /// MethodName for the service interface for which request will be sent to. + /// Number of Parameters in that Method. + /// Wrapped Request Object. + /// IActorRequestMessageBody. + IActorRequestMessageBody CreateRequestMessageBody(string interfaceName, string methodName, int numberOfParameters, object wrappedRequestObject); - /// - /// Creates a actor response message body. - /// - /// This is FullName for the service interface for which request body is being constructed. - /// MethodName for the service interface for which request will be sent to. - /// Wrapped Response Object. - /// IActorResponseMessageBody. - IActorResponseMessageBody CreateResponseMessageBody(string interfaceName, string methodName, object wrappedResponseObject); - } -} + /// + /// Creates a actor response message body. + /// + /// This is FullName for the service interface for which request body is being constructed. + /// MethodName for the service interface for which request will be sent to. + /// Wrapped Response Object. + /// IActorResponseMessageBody. + IActorResponseMessageBody CreateResponseMessageBody(string interfaceName, string methodName, object wrappedResponseObject); +} \ No newline at end of file diff --git a/src/Dapr.Actors/Communication/IActorMessageBodySerializationProvider.cs b/src/Dapr.Actors/Communication/IActorMessageBodySerializationProvider.cs index f42624a8..2380b335 100644 --- a/src/Dapr.Actors/Communication/IActorMessageBodySerializationProvider.cs +++ b/src/Dapr.Actors/Communication/IActorMessageBodySerializationProvider.cs @@ -11,50 +11,49 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Communication +namespace Dapr.Actors.Communication; + +using System; +using System.Collections.Generic; + +/// +/// Defines the interface that must be implemented for providing custom serialization for the remoting request. +/// +internal interface IActorMessageBodySerializationProvider { - using System; - using System.Collections.Generic; + /// + /// Create a IServiceRemotingMessageBodyFactory used for creating remoting request and response body. + /// + /// A custom that can be used for creating remoting request and response message bodies. + IActorMessageBodyFactory CreateMessageBodyFactory(); /// - /// Defines the interface that must be implemented for providing custom serialization for the remoting request. + /// Creates IActorRequestMessageBodySerializer for a serviceInterface using Wrapped Message DataContract implementation. /// - internal interface IActorMessageBodySerializationProvider - { - /// - /// Create a IServiceRemotingMessageBodyFactory used for creating remoting request and response body. - /// - /// A custom that can be used for creating remoting request and response message bodies. - IActorMessageBodyFactory CreateMessageBodyFactory(); + /// The remoted service interface. + /// The union of parameter types of all of the methods of the specified interface. + /// Wrapped Request Types for all Methods. + /// + /// An instance of the that can serialize the service + /// actor request message body to a messaging body for transferring over the transport. + /// + IActorRequestMessageBodySerializer CreateRequestMessageBodySerializer( + Type serviceInterfaceType, + IEnumerable methodRequestParameterTypes, + IEnumerable wrappedRequestMessageTypes = null); - /// - /// Creates IActorRequestMessageBodySerializer for a serviceInterface using Wrapped Message DataContract implementation. - /// - /// The remoted service interface. - /// The union of parameter types of all of the methods of the specified interface. - /// Wrapped Request Types for all Methods. - /// - /// An instance of the that can serialize the service - /// actor request message body to a messaging body for transferring over the transport. - /// - IActorRequestMessageBodySerializer CreateRequestMessageBodySerializer( - Type serviceInterfaceType, - IEnumerable methodRequestParameterTypes, - IEnumerable wrappedRequestMessageTypes = null); - - /// - /// Creates IActorResponseMessageBodySerializer for a serviceInterface using Wrapped Message DataContract implementation. - /// - /// The remoted service interface. - /// The return types of all of the methods of the specified interface. - /// Wrapped Response Types for all remoting methods. - /// - /// An instance of the that can serialize the service - /// actor response message body to a messaging body for transferring over the transport. - /// - IActorResponseMessageBodySerializer CreateResponseMessageBodySerializer( - Type serviceInterfaceType, - IEnumerable methodReturnTypes, - IEnumerable wrappedResponseMessageTypes = null); - } -} + /// + /// Creates IActorResponseMessageBodySerializer for a serviceInterface using Wrapped Message DataContract implementation. + /// + /// The remoted service interface. + /// The return types of all of the methods of the specified interface. + /// Wrapped Response Types for all remoting methods. + /// + /// An instance of the that can serialize the service + /// actor response message body to a messaging body for transferring over the transport. + /// + IActorResponseMessageBodySerializer CreateResponseMessageBodySerializer( + Type serviceInterfaceType, + IEnumerable methodReturnTypes, + IEnumerable wrappedResponseMessageTypes = null); +} \ No newline at end of file diff --git a/src/Dapr.Actors/Communication/IActorMessageHeaderSerializer.cs b/src/Dapr.Actors/Communication/IActorMessageHeaderSerializer.cs index 82c82f4f..f972ae8c 100644 --- a/src/Dapr.Actors/Communication/IActorMessageHeaderSerializer.cs +++ b/src/Dapr.Actors/Communication/IActorMessageHeaderSerializer.cs @@ -11,41 +11,40 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Communication +namespace Dapr.Actors.Communication; + +using System.IO; + +/// +/// Represents a serializer that can serialize remoting layer message header to messaging layer header. +/// +internal interface IActorMessageHeaderSerializer { - using System.IO; + /// + /// Serializes the remoting request message header to a message header. + /// + /// Remoting header to serialize. + /// Serialized bytes. + byte[] SerializeRequestHeader(IActorRequestMessageHeader serviceRemotingRequestMessageHeader); /// - /// Represents a serializer that can serialize remoting layer message header to messaging layer header. + /// Deserializes a request message header in to remoting header. /// - internal interface IActorMessageHeaderSerializer - { - /// - /// Serializes the remoting request message header to a message header. - /// - /// Remoting header to serialize. - /// Serialized bytes. - byte[] SerializeRequestHeader(IActorRequestMessageHeader serviceRemotingRequestMessageHeader); + /// Messaging layer header to be deserialized. + /// An that has the deserialized contents of the specified message header. + IActorRequestMessageHeader DeserializeRequestHeaders(Stream messageHeader); - /// - /// Deserializes a request message header in to remoting header. - /// - /// Messaging layer header to be deserialized. - /// An that has the deserialized contents of the specified message header. - IActorRequestMessageHeader DeserializeRequestHeaders(Stream messageHeader); + /// + /// Serializes the remoting response message header to a message header. + /// + /// Remoting header to serialize. + /// Serialized bytes. + byte[] SerializeResponseHeader(IActorResponseMessageHeader serviceRemotingResponseMessageHeader); - /// - /// Serializes the remoting response message header to a message header. - /// - /// Remoting header to serialize. - /// Serialized bytes. - byte[] SerializeResponseHeader(IActorResponseMessageHeader serviceRemotingResponseMessageHeader); - - /// - /// Deserializes a response message header in to remoting header. - /// - /// Messaging layer header to be deserialized. - /// An that has the deserialized contents of the specified message header. - IActorResponseMessageHeader DeserializeResponseHeaders(Stream messageHeader); - } -} + /// + /// Deserializes a response message header in to remoting header. + /// + /// Messaging layer header to be deserialized. + /// An that has the deserialized contents of the specified message header. + IActorResponseMessageHeader DeserializeResponseHeaders(Stream messageHeader); +} \ No newline at end of file diff --git a/src/Dapr.Actors/Communication/IActorRequestMessage.cs b/src/Dapr.Actors/Communication/IActorRequestMessage.cs index 0d850a22..11c60312 100644 --- a/src/Dapr.Actors/Communication/IActorRequestMessage.cs +++ b/src/Dapr.Actors/Communication/IActorRequestMessage.cs @@ -11,22 +11,21 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Communication +namespace Dapr.Actors.Communication; + +/// +/// Defines the interface that must be implemented for create Actor Request Message. +/// +public interface IActorRequestMessage { /// - /// Defines the interface that must be implemented for create Actor Request Message. + /// Gets the Actor Request Message Header. /// - public interface IActorRequestMessage - { - /// - /// Gets the Actor Request Message Header. - /// - /// IActorRequestMessageHeader. - IActorRequestMessageHeader GetHeader(); + /// IActorRequestMessageHeader. + IActorRequestMessageHeader GetHeader(); - /// - /// Gets the Actor Request Message Body. - /// IActorRequestMessageBody. - IActorRequestMessageBody GetBody(); - } -} + /// + /// Gets the Actor Request Message Body. + /// IActorRequestMessageBody. + IActorRequestMessageBody GetBody(); +} \ No newline at end of file diff --git a/src/Dapr.Actors/Communication/IActorRequestMessageBody.cs b/src/Dapr.Actors/Communication/IActorRequestMessageBody.cs index f521227e..c2b26872 100644 --- a/src/Dapr.Actors/Communication/IActorRequestMessageBody.cs +++ b/src/Dapr.Actors/Communication/IActorRequestMessageBody.cs @@ -11,31 +11,30 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Communication +namespace Dapr.Actors.Communication; + +using System; + +/// +/// Defines the interface that must be implemented to provide Request Message Body for remoting requests . +/// This contains all the parameters remoting method has. +/// +public interface IActorRequestMessageBody { - using System; + /// + /// This Api gets called to set remoting method parameters before serializing/dispatching the request. + /// + /// Position of the parameter in Remoting Method. + /// Parameter Name in the Remoting Method. + /// Parameter Value. + void SetParameter(int position, string parameName, object parameter); /// - /// Defines the interface that must be implemented to provide Request Message Body for remoting requests . - /// This contains all the parameters remoting method has. + /// This is used to retrive parameter from request body before dispatching to service remoting method. /// - public interface IActorRequestMessageBody - { - /// - /// This Api gets called to set remoting method parameters before serializing/dispatching the request. - /// - /// Position of the parameter in Remoting Method. - /// Parameter Name in the Remoting Method. - /// Parameter Value. - void SetParameter(int position, string parameName, object parameter); - - /// - /// This is used to retrive parameter from request body before dispatching to service remoting method. - /// - /// Position of the parameter in Remoting Method. - /// Parameter Name in the Remoting Method. - /// Parameter Type. - /// The parameter that is at the specified position and has the specified name. - object GetParameter(int position, string parameName, Type paramType); - } -} + /// Position of the parameter in Remoting Method. + /// Parameter Name in the Remoting Method. + /// Parameter Type. + /// The parameter that is at the specified position and has the specified name. + object GetParameter(int position, string parameName, Type paramType); +} \ No newline at end of file diff --git a/src/Dapr.Actors/Communication/IActorRequestMessageBodySerializer.cs b/src/Dapr.Actors/Communication/IActorRequestMessageBodySerializer.cs index b5825738..3d1c96c0 100644 --- a/src/Dapr.Actors/Communication/IActorRequestMessageBodySerializer.cs +++ b/src/Dapr.Actors/Communication/IActorRequestMessageBodySerializer.cs @@ -11,28 +11,27 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Communication +namespace Dapr.Actors.Communication; + +using System.IO; +using System.Threading.Tasks; + +/// +/// Defines the interface that must be implemented to provide a serializer/deserializer for remoting request message body. +/// +internal interface IActorRequestMessageBodySerializer { - using System.IO; - using System.Threading.Tasks; + /// + /// Serialize the remoting request body object to a message body that can be sent over the wire. + /// + /// Actor request message body object. + /// Serialized message body. + byte[] Serialize(IActorRequestMessageBody actorRequestMessageBody); /// - /// Defines the interface that must be implemented to provide a serializer/deserializer for remoting request message body. + /// Deserializes an incoming message body to actor request body object. /// - internal interface IActorRequestMessageBodySerializer - { - /// - /// Serialize the remoting request body object to a message body that can be sent over the wire. - /// - /// Actor request message body object. - /// Serialized message body. - byte[] Serialize(IActorRequestMessageBody actorRequestMessageBody); - - /// - /// Deserializes an incoming message body to actor request body object. - /// - /// Serialized message body. - /// Deserialized remoting request message body object. - ValueTask DeserializeAsync(Stream messageBody); - } -} + /// Serialized message body. + /// Deserialized remoting request message body object. + ValueTask DeserializeAsync(Stream messageBody); +} \ No newline at end of file diff --git a/src/Dapr.Actors/Communication/IActorRequestMessageHeader.cs b/src/Dapr.Actors/Communication/IActorRequestMessageHeader.cs index 1be097dd..ec061c68 100644 --- a/src/Dapr.Actors/Communication/IActorRequestMessageHeader.cs +++ b/src/Dapr.Actors/Communication/IActorRequestMessageHeader.cs @@ -11,63 +11,62 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Communication +namespace Dapr.Actors.Communication; + +/// +/// Specifies the headers that are sent along with a request message. +/// +public interface IActorRequestMessageHeader { /// - /// Specifies the headers that are sent along with a request message. + /// Gets or sets the actorId to which remoting request will dispatch to. /// - public interface IActorRequestMessageHeader - { - /// - /// Gets or sets the actorId to which remoting request will dispatch to. - /// - ActorId ActorId { get; set; } + ActorId ActorId { get; set; } - /// - /// Gets or sets the actorType to which remoting request will dispatch to. - /// - string ActorType { get; set; } + /// + /// Gets or sets the actorType to which remoting request will dispatch to. + /// + string ActorType { get; set; } - /// - /// Gets or sets the call context which is used to limit re-eentrancy in Actors. - /// - string CallContext { get; set; } + /// + /// Gets or sets the call context which is used to limit re-eentrancy in Actors. + /// + string CallContext { get; set; } - /// - /// Gets or sets the methodId of the remote method. - /// - /// The method id. - int MethodId { get; set; } + /// + /// Gets or sets the methodId of the remote method. + /// + /// The method id. + int MethodId { get; set; } - /// - /// Gets or sets the interface id of the remote interface. - /// - /// The interface id. - int InterfaceId { get; set; } + /// + /// Gets or sets the interface id of the remote interface. + /// + /// The interface id. + int InterfaceId { get; set; } - /// - /// Gets or sets the identifier for the remote method invocation. - /// - string InvocationId { get; set; } + /// + /// Gets or sets the identifier for the remote method invocation. + /// + string InvocationId { get; set; } - /// - /// Gets or sets the Method Name of the remoting method. - /// - string MethodName { get; set; } + /// + /// Gets or sets the Method Name of the remoting method. + /// + string MethodName { get; set; } - /// - /// Adds a new header with the specified name and value. - /// - /// The header Name. - /// The header value. - void AddHeader(string headerName, byte[] headerValue); + /// + /// Adds a new header with the specified name and value. + /// + /// The header Name. + /// The header value. + void AddHeader(string headerName, byte[] headerValue); - /// - /// Gets the header with the specified name. - /// - /// The header Name. - /// The header value. - /// true if a header with that name exists; otherwise, false. - bool TryGetHeaderValue(string headerName, out byte[] headerValue); - } -} + /// + /// Gets the header with the specified name. + /// + /// The header Name. + /// The header value. + /// true if a header with that name exists; otherwise, false. + bool TryGetHeaderValue(string headerName, out byte[] headerValue); +} \ No newline at end of file diff --git a/src/Dapr.Actors/Communication/IActorResponseMessage.cs b/src/Dapr.Actors/Communication/IActorResponseMessage.cs index 29c2ad92..189c095c 100644 --- a/src/Dapr.Actors/Communication/IActorResponseMessage.cs +++ b/src/Dapr.Actors/Communication/IActorResponseMessage.cs @@ -11,23 +11,22 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Communication +namespace Dapr.Actors.Communication; + +/// +/// Defines an interface that must be implemented to provide a actor response message for remoting Api. +/// +public interface IActorResponseMessage { /// - /// Defines an interface that must be implemented to provide a actor response message for remoting Api. + /// Gets the header of the response message. /// - public interface IActorResponseMessage - { - /// - /// Gets the header of the response message. - /// - /// The header of this response message. - IActorResponseMessageHeader GetHeader(); + /// The header of this response message. + IActorResponseMessageHeader GetHeader(); - /// - /// Gets the body of the response message. - /// - /// The body of this response message. - IActorResponseMessageBody GetBody(); - } -} + /// + /// Gets the body of the response message. + /// + /// The body of this response message. + IActorResponseMessageBody GetBody(); +} \ No newline at end of file diff --git a/src/Dapr.Actors/Communication/IActorResponseMessageBody.cs b/src/Dapr.Actors/Communication/IActorResponseMessageBody.cs index a7557a95..10f57fb6 100644 --- a/src/Dapr.Actors/Communication/IActorResponseMessageBody.cs +++ b/src/Dapr.Actors/Communication/IActorResponseMessageBody.cs @@ -11,27 +11,26 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Communication +namespace Dapr.Actors.Communication; + +using System; + +/// +/// Defines the interface that must be implemented to provide Request Message Body for remoting requests . +/// This contains all the parameters remoting method has. +/// +public interface IActorResponseMessageBody { - using System; + /// + /// Sets the response of a remoting Method in a remoting response Body. + /// + /// Remoting Method Response. + void Set(object response); /// - /// Defines the interface that must be implemented to provide Request Message Body for remoting requests . - /// This contains all the parameters remoting method has. + /// Gets the response of a remoting Method from a remoting response body before sending it to Client. /// - public interface IActorResponseMessageBody - { - /// - /// Sets the response of a remoting Method in a remoting response Body. - /// - /// Remoting Method Response. - void Set(object response); - - /// - /// Gets the response of a remoting Method from a remoting response body before sending it to Client. - /// - /// Return Type of a Remoting Method. - /// Remoting Method Response. - object Get(Type paramType); - } -} + /// Return Type of a Remoting Method. + /// Remoting Method Response. + object Get(Type paramType); +} \ No newline at end of file diff --git a/src/Dapr.Actors/Communication/IActorResponseMessageBodySerializer.cs b/src/Dapr.Actors/Communication/IActorResponseMessageBodySerializer.cs index b54191f9..90cd19d2 100644 --- a/src/Dapr.Actors/Communication/IActorResponseMessageBodySerializer.cs +++ b/src/Dapr.Actors/Communication/IActorResponseMessageBodySerializer.cs @@ -11,28 +11,27 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Communication +namespace Dapr.Actors.Communication; + +using System.IO; +using System.Threading.Tasks; + +/// +/// Defines the interface that must be implemented to provide a serializer/deserializer for actor response message body. +/// +internal interface IActorResponseMessageBodySerializer { - using System.IO; - using System.Threading.Tasks; + /// + /// Serialize the actor response body object to a message body that can be sent over the wire. + /// + /// Actor request message body object. + /// Serialized message body. + byte[] Serialize(IActorResponseMessageBody actorResponseMessageBody); /// - /// Defines the interface that must be implemented to provide a serializer/deserializer for actor response message body. + /// Deserializes an incoming message body to remoting response body object. /// - internal interface IActorResponseMessageBodySerializer - { - /// - /// Serialize the actor response body object to a message body that can be sent over the wire. - /// - /// Actor request message body object. - /// Serialized message body. - byte[] Serialize(IActorResponseMessageBody actorResponseMessageBody); - - /// - /// Deserializes an incoming message body to remoting response body object. - /// - /// Serialized message body. - /// Deserialized actor response message body object. - ValueTask DeserializeAsync(Stream messageBody); - } -} + /// Serialized message body. + /// Deserialized actor response message body object. + ValueTask DeserializeAsync(Stream messageBody); +} \ No newline at end of file diff --git a/src/Dapr.Actors/Communication/IActorResponseMessageHeader.cs b/src/Dapr.Actors/Communication/IActorResponseMessageHeader.cs index f1f57a0c..134783e9 100644 --- a/src/Dapr.Actors/Communication/IActorResponseMessageHeader.cs +++ b/src/Dapr.Actors/Communication/IActorResponseMessageHeader.cs @@ -11,33 +11,32 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Communication +namespace Dapr.Actors.Communication; + +/// +/// Defines an interfaces that must be implemented to provide header for remoting response message. +/// +/// +public interface IActorResponseMessageHeader { /// - /// Defines an interfaces that must be implemented to provide header for remoting response message. - /// + /// Adds a new header with the specified name and value. /// - public interface IActorResponseMessageHeader - { - /// - /// Adds a new header with the specified name and value. - /// - /// The header Name. - /// The header value. - void AddHeader(string headerName, byte[] headerValue); + /// The header Name. + /// The header value. + void AddHeader(string headerName, byte[] headerValue); - /// - /// Gets the header with the specified name. - /// - /// The header Name. - /// The header value. - /// true if a header with that name exists; otherwise, false. - bool TryGetHeaderValue(string headerName, out byte[] headerValue); + /// + /// Gets the header with the specified name. + /// + /// The header Name. + /// The header value. + /// true if a header with that name exists; otherwise, false. + bool TryGetHeaderValue(string headerName, out byte[] headerValue); - /// - /// Return true if no header exists , else false. - /// - /// true or false. - bool CheckIfItsEmpty(); - } -} + /// + /// Return true if no header exists , else false. + /// + /// true or false. + bool CheckIfItsEmpty(); +} \ No newline at end of file diff --git a/src/Dapr.Actors/Constants.cs b/src/Dapr.Actors/Constants.cs index 038caf10..ddfbd96b 100644 --- a/src/Dapr.Actors/Constants.cs +++ b/src/Dapr.Actors/Constants.cs @@ -11,68 +11,67 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors +namespace Dapr.Actors; + +/// +/// Contains Constant string values used by the library. +/// +internal static class Constants { + public const string RequestIdHeaderName = "X-DaprRequestId"; + public const string RequestHeaderName = "X-DaprRequestHeader"; + public const string ErrorResponseHeaderName = "X-DaprErrorResponseHeader"; + public const string ReentrancyRequestHeaderName = "Dapr-Reentrancy-Id"; + public const string TTLResponseHeaderName = "Metadata.ttlExpireTime"; + public const string Dapr = "dapr"; + public const string Config = "config"; + public const string State = "state"; + public const string Actors = "actors"; + public const string Namespace = "urn:actors"; + public const string DaprDefaultEndpoint = "127.0.0.1"; + public const string DaprDefaultPort = "3500"; + public const string DaprVersion = "v1.0"; + public const string Method = "method"; + public const string Reminders = "reminders"; + public const string Timers = "timers"; + /// - /// Contains Constant string values used by the library. + /// Gets string format for Actors state management relative url. /// - internal static class Constants - { - public const string RequestIdHeaderName = "X-DaprRequestId"; - public const string RequestHeaderName = "X-DaprRequestHeader"; - public const string ErrorResponseHeaderName = "X-DaprErrorResponseHeader"; - public const string ReentrancyRequestHeaderName = "Dapr-Reentrancy-Id"; - public const string TTLResponseHeaderName = "Metadata.ttlExpireTime"; - public const string Dapr = "dapr"; - public const string Config = "config"; - public const string State = "state"; - public const string Actors = "actors"; - public const string Namespace = "urn:actors"; - public const string DaprDefaultEndpoint = "127.0.0.1"; - public const string DaprDefaultPort = "3500"; - public const string DaprVersion = "v1.0"; - public const string Method = "method"; - public const string Reminders = "reminders"; - public const string Timers = "timers"; + public static string ActorStateKeyRelativeUrlFormat => $"{DaprVersion}/{Actors}/{{0}}/{{1}}/{State}/{{2}}"; - /// - /// Gets string format for Actors state management relative url. - /// - public static string ActorStateKeyRelativeUrlFormat => $"{DaprVersion}/{Actors}/{{0}}/{{1}}/{State}/{{2}}"; + /// + /// Gets string format for Actors state management relative url. + /// + public static string ActorStateRelativeUrlFormat => $"{DaprVersion}/{Actors}/{{0}}/{{1}}/{State}"; - /// - /// Gets string format for Actors state management relative url. - /// - public static string ActorStateRelativeUrlFormat => $"{DaprVersion}/{Actors}/{{0}}/{{1}}/{State}"; + /// + /// Gets string format for Actors method invocation relative url. + /// + public static string ActorMethodRelativeUrlFormat => $"{DaprVersion}/{Actors}/{{0}}/{{1}}/{Method}/{{2}}"; - /// - /// Gets string format for Actors method invocation relative url. - /// - public static string ActorMethodRelativeUrlFormat => $"{DaprVersion}/{Actors}/{{0}}/{{1}}/{Method}/{{2}}"; + /// + /// Gets string format for Actors reminder registration relative url.. + /// + public static string ActorReminderRelativeUrlFormat => $"{DaprVersion}/{Actors}/{{0}}/{{1}}/{Reminders}/{{2}}"; - /// - /// Gets string format for Actors reminder registration relative url.. - /// - public static string ActorReminderRelativeUrlFormat => $"{DaprVersion}/{Actors}/{{0}}/{{1}}/{Reminders}/{{2}}"; + /// + /// Gets string format for Actors timer registration relative url.. + /// + public static string ActorTimerRelativeUrlFormat => $"{DaprVersion}/{Actors}/{{0}}/{{1}}/{Timers}/{{2}}"; - /// - /// Gets string format for Actors timer registration relative url.. - /// - public static string ActorTimerRelativeUrlFormat => $"{DaprVersion}/{Actors}/{{0}}/{{1}}/{Timers}/{{2}}"; + /// + /// Error code indicating there was a failure invoking an actor method. + /// + public static string ErrorActorInvokeMethod = "ERR_ACTOR_INVOKE_METHOD"; - /// - /// Error code indicating there was a failure invoking an actor method. - /// - public static string ErrorActorInvokeMethod = "ERR_ACTOR_INVOKE_METHOD"; + /// + /// Error code indicating something does not exist. + /// + public static string ErrorDoesNotExist = "ERR_DOES_NOT_EXIST"; - /// - /// Error code indicating something does not exist. - /// - public static string ErrorDoesNotExist = "ERR_DOES_NOT_EXIST"; - - /// - /// Error code indicating an unknonwn/unspecified error. - /// - public static string Unknown = "UNKNOWN"; - } -} + /// + /// Error code indicating an unknonwn/unspecified error. + /// + public static string Unknown = "UNKNOWN"; +} \ No newline at end of file diff --git a/src/Dapr.Actors/DaprHttpInteractor.cs b/src/Dapr.Actors/DaprHttpInteractor.cs index 7dcbe70c..b7b1f53a 100644 --- a/src/Dapr.Actors/DaprHttpInteractor.cs +++ b/src/Dapr.Actors/DaprHttpInteractor.cs @@ -11,502 +11,501 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors +namespace Dapr.Actors; + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Security.Authentication; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Dapr.Actors.Communication; +using Dapr.Actors.Resources; +using System.Xml; + +/// +/// Class to interact with Dapr runtime over http. +/// +internal class DaprHttpInteractor : IDaprInteractor { - using System; - using System.Collections.Generic; - using System.Globalization; - using System.IO; - using System.Linq; - using System.Net; - using System.Net.Http; - using System.Security.Authentication; - using System.Text; - using System.Text.Json; - using System.Threading; - using System.Threading.Tasks; - using Dapr.Actors.Communication; - using Dapr.Actors.Resources; - using System.Xml; + private readonly JsonSerializerOptions jsonSerializerOptions = JsonSerializerDefaults.Web; + private readonly string httpEndpoint; + private readonly static HttpMessageHandler defaultHandler = new HttpClientHandler(); + private readonly HttpMessageHandler handler; + private HttpClient httpClient; + private bool disposed; + private string daprApiToken; + + private const string EXCEPTION_HEADER_TAG = "b:KeyValueOfstringbase64Binary"; + + public DaprHttpInteractor( + HttpMessageHandler clientHandler, + string httpEndpoint, + string apiToken, + TimeSpan? requestTimeout) + { + this.handler = clientHandler ?? defaultHandler; + this.httpEndpoint = httpEndpoint; + this.daprApiToken = apiToken; + this.httpClient = this.CreateHttpClient(); + this.httpClient.Timeout = requestTimeout ?? this.httpClient.Timeout; + } + + public async Task> GetStateAsync(string actorType, string actorId, string keyName, CancellationToken cancellationToken = default) + { + var relativeUrl = string.Format(CultureInfo.InvariantCulture, Constants.ActorStateKeyRelativeUrlFormat, actorType, actorId, keyName); + + HttpRequestMessage RequestFunc() + { + var request = new HttpRequestMessage() + { + Method = HttpMethod.Get, + }; + return request; + } + + using var response = await this.SendAsync(RequestFunc, relativeUrl, cancellationToken); + var stringResponse = await response.Content.ReadAsStringAsync(); + + DateTimeOffset? ttlExpireTime = null; + if (response.Headers.TryGetValues(Constants.TTLResponseHeaderName, out IEnumerable headerValues)) + { + var ttlExpireTimeString = headerValues.First(); + if (!string.IsNullOrEmpty(ttlExpireTimeString)) + { + ttlExpireTime = DateTime.Parse(ttlExpireTimeString, CultureInfo.InvariantCulture); + } + } + + return new ActorStateResponse(stringResponse, ttlExpireTime); + } + + public Task SaveStateTransactionallyAsync(string actorType, string actorId, string data, CancellationToken cancellationToken = default) + { + var relativeUrl = string.Format(CultureInfo.InvariantCulture, Constants.ActorStateRelativeUrlFormat, actorType, actorId); + + HttpRequestMessage RequestFunc() + { + var request = new HttpRequestMessage() + { + Method = HttpMethod.Put, + Content = new StringContent(data), + }; + + return request; + } + + return this.SendAsync(RequestFunc, relativeUrl, cancellationToken); + } + + public async Task InvokeActorMethodWithRemotingAsync(ActorMessageSerializersManager serializersManager, IActorRequestMessage remotingRequestRequestMessage, CancellationToken cancellationToken = default) + { + var requestMessageHeader = remotingRequestRequestMessage.GetHeader(); + + var actorId = requestMessageHeader.ActorId.ToString(); + var methodName = requestMessageHeader.MethodName; + var actorType = requestMessageHeader.ActorType; + var interfaceId = requestMessageHeader.InterfaceId; + + var serializedHeader = serializersManager.GetHeaderSerializer() + .SerializeRequestHeader(remotingRequestRequestMessage.GetHeader()); + + var msgBodySeriaizer = serializersManager.GetRequestMessageBodySerializer(interfaceId, methodName); + var serializedMsgBody = msgBodySeriaizer.Serialize(remotingRequestRequestMessage.GetBody()); + + // Send Request + var relativeUrl = string.Format(CultureInfo.InvariantCulture, Constants.ActorMethodRelativeUrlFormat, actorType, actorId, methodName); + + HttpRequestMessage RequestFunc() + { + var request = new HttpRequestMessage() + { + Method = HttpMethod.Put, + }; + + if (serializedMsgBody != null) + { + request.Content = new ByteArrayContent(serializedMsgBody); + request.Content.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/octet-stream; charset=utf-8"); + } + + request.Headers.Add(Constants.RequestHeaderName, Encoding.UTF8.GetString(serializedHeader, 0, serializedHeader.Length)); + + var reentrancyId = ActorReentrancyContextAccessor.ReentrancyContext; + if (reentrancyId != null) + { + request.Headers.Add(Constants.ReentrancyRequestHeaderName, reentrancyId); + } + + return request; + } + + var retval = await this.SendAsync(RequestFunc, relativeUrl, cancellationToken); + var header = ""; + IActorResponseMessageHeader actorResponseMessageHeader = null; + if (retval != null && retval.Headers != null) + { + if (retval.Headers.TryGetValues(Constants.ErrorResponseHeaderName, out IEnumerable headerValues)) + { + header = headerValues.First(); + // DeSerialize Actor Response Message Header + actorResponseMessageHeader = + serializersManager.GetHeaderSerializer() + .DeserializeResponseHeaders( + new MemoryStream(Encoding.ASCII.GetBytes(header))); + } + } + + // Get the http response message body content and extract out expected actor response message body + IActorResponseMessageBody actorResponseMessageBody = null; + if (retval != null && retval.Content != null) + { + var responseMessageBody = await retval.Content.ReadAsStreamAsync(); + + // Deserialize Actor Response Message Body + // Deserialize to ActorInvokeException when there is response header otherwise normal path + var responseBodySerializer = serializersManager.GetResponseMessageBodySerializer(interfaceId, methodName); + + // actorResponseMessageHeader is not null, it means there is remote exception + if (actorResponseMessageHeader != null) + { + var isDeserialized = + ActorInvokeException.ToException( + responseMessageBody, + out var remoteMethodException); + if (isDeserialized) + { + var exceptionDetails = GetExceptionDetails(header.ToString()); + throw new ActorMethodInvocationException( + "Remote Actor Method Exception, DETAILS: " + exceptionDetails, + remoteMethodException, + false /* non transient */); + } + else + { + throw new ActorInvokeException(remoteMethodException.GetType().FullName, string.Format( + CultureInfo.InvariantCulture, + SR.ErrorDeserializationFailure, + remoteMethodException.ToString())); + } + } + + actorResponseMessageBody = await responseBodySerializer.DeserializeAsync(responseMessageBody); + } + + return new ActorResponseMessage(actorResponseMessageHeader, actorResponseMessageBody); + } + + private string GetExceptionDetails(string header) { + XmlDocument xmlHeader = new XmlDocument(); + xmlHeader.LoadXml(header); + XmlNodeList exceptionValueXML = xmlHeader.GetElementsByTagName(EXCEPTION_HEADER_TAG); + string exceptionDetails = ""; + if (exceptionValueXML != null && exceptionValueXML.Item(1) != null) + { + exceptionDetails = exceptionValueXML.Item(1).LastChild.InnerText; + } + var base64EncodedBytes = System.Convert.FromBase64String(exceptionDetails); + return Encoding.UTF8.GetString(base64EncodedBytes); + } + + public async Task InvokeActorMethodWithoutRemotingAsync(string actorType, string actorId, string methodName, string jsonPayload, CancellationToken cancellationToken = default) + { + var relativeUrl = string.Format(CultureInfo.InvariantCulture, Constants.ActorMethodRelativeUrlFormat, actorType, actorId, methodName); + + HttpRequestMessage RequestFunc() + { + var request = new HttpRequestMessage() + { + Method = HttpMethod.Put, + }; + + if (jsonPayload != null) + { + request.Content = new StringContent(jsonPayload); + request.Content.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json; charset=utf-8"); + } + + var reentrancyId = ActorReentrancyContextAccessor.ReentrancyContext; + if (reentrancyId != null) + { + request.Headers.Add(Constants.ReentrancyRequestHeaderName, reentrancyId); + } + + return request; + } + + var response = await this.SendAsync(RequestFunc, relativeUrl, cancellationToken); + var stream = await response.Content.ReadAsStreamAsync(); + return stream; + } + + public Task RegisterReminderAsync(string actorType, string actorId, string reminderName, string data, CancellationToken cancellationToken = default) + { + var relativeUrl = string.Format(CultureInfo.InvariantCulture, Constants.ActorReminderRelativeUrlFormat, actorType, actorId, reminderName); + + HttpRequestMessage RequestFunc() + { + var request = new HttpRequestMessage() + { + Method = HttpMethod.Put, + Content = new StringContent(data, Encoding.UTF8), + }; + + request.Content.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json; charset=utf-8"); + return request; + } + + return this.SendAsync(RequestFunc, relativeUrl, cancellationToken); + } + + public async Task GetReminderAsync(string actorType, string actorId, string reminderName, CancellationToken cancellationToken = default) + { + var relativeUrl = string.Format(CultureInfo.InvariantCulture, Constants.ActorReminderRelativeUrlFormat, actorType, actorId, reminderName); + + HttpRequestMessage RequestFunc() + { + var request = new HttpRequestMessage() + { + Method = HttpMethod.Get, + }; + return request; + } + + return await this.SendAsync(RequestFunc, relativeUrl, cancellationToken); + } + + public Task UnregisterReminderAsync(string actorType, string actorId, string reminderName, CancellationToken cancellationToken = default) + { + var relativeUrl = string.Format(CultureInfo.InvariantCulture, Constants.ActorReminderRelativeUrlFormat, actorType, actorId, reminderName); + + HttpRequestMessage RequestFunc() + { + var request = new HttpRequestMessage() + { + Method = HttpMethod.Delete, + }; + + return request; + } + + return this.SendAsync(RequestFunc, relativeUrl, cancellationToken); + } + + public Task RegisterTimerAsync(string actorType, string actorId, string timerName, string data, CancellationToken cancellationToken = default) + { + var relativeUrl = string.Format(CultureInfo.InvariantCulture, Constants.ActorTimerRelativeUrlFormat, actorType, actorId, timerName); + + HttpRequestMessage RequestFunc() + { + var request = new HttpRequestMessage() + { + Method = HttpMethod.Put, + Content = new StringContent(data, Encoding.UTF8), + }; + + request.Content.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json; charset=utf-8"); + return request; + } + + return this.SendAsync(RequestFunc, relativeUrl, cancellationToken); + } + + public Task UnregisterTimerAsync(string actorType, string actorId, string timerName, CancellationToken cancellationToken = default) + { + var relativeUrl = string.Format(CultureInfo.InvariantCulture, Constants.ActorTimerRelativeUrlFormat, actorType, actorId, timerName); + + HttpRequestMessage RequestFunc() + { + var request = new HttpRequestMessage() + { + Method = HttpMethod.Delete, + }; + + return request; + } + + return this.SendAsync(RequestFunc, relativeUrl, cancellationToken); + } /// - /// Class to interact with Dapr runtime over http. + /// Sends an HTTP get request to Dapr. /// - internal class DaprHttpInteractor : IDaprInteractor + /// Func to create HttpRequest to send. + /// The relative URI. + /// The cancellation token. + /// The payload of the GET response. + internal async Task SendAsync( + Func requestFunc, + string relativeUri, + CancellationToken cancellationToken) { - private readonly JsonSerializerOptions jsonSerializerOptions = JsonSerializerDefaults.Web; - private readonly string httpEndpoint; - private readonly static HttpMessageHandler defaultHandler = new HttpClientHandler(); - private readonly HttpMessageHandler handler; - private HttpClient httpClient; - private bool disposed; - private string daprApiToken; + return await this.SendAsyncHandleUnsuccessfulResponse(requestFunc, relativeUri, cancellationToken); + } - private const string EXCEPTION_HEADER_TAG = "b:KeyValueOfstringbase64Binary"; + /// + /// Sends an HTTP get request to Dapr and returns the result as raw JSON. + /// + /// Func to create HttpRequest to send. + /// The relative URI. + /// The cancellation token. + /// The payload of the GET response as string. + internal async Task SendAsyncGetResponseAsRawJson( + Func requestFunc, + string relativeUri, + CancellationToken cancellationToken) + { + using var response = await this.SendAsyncHandleUnsuccessfulResponse(requestFunc, relativeUri, cancellationToken); + var retValue = default(string); - public DaprHttpInteractor( - HttpMessageHandler clientHandler, - string httpEndpoint, - string apiToken, - TimeSpan? requestTimeout) + if (response != null && response.Content != null) { - this.handler = clientHandler ?? defaultHandler; - this.httpEndpoint = httpEndpoint; - this.daprApiToken = apiToken; - this.httpClient = this.CreateHttpClient(); - this.httpClient.Timeout = requestTimeout ?? this.httpClient.Timeout; + retValue = await response.Content.ReadAsStringAsync(); } - public async Task> GetStateAsync(string actorType, string actorId, string keyName, CancellationToken cancellationToken = default) - { - var relativeUrl = string.Format(CultureInfo.InvariantCulture, Constants.ActorStateKeyRelativeUrlFormat, actorType, actorId, keyName); + return retValue; + } - HttpRequestMessage RequestFunc() + /// + /// Disposes resources. + /// + /// False values indicates the method is being called by the runtime, true value indicates the method is called by the user code. + protected virtual void Dispose(bool disposing) + { + if (!this.disposed) + { + if (disposing) { - var request = new HttpRequestMessage() - { - Method = HttpMethod.Get, - }; - return request; + this.httpClient.Dispose(); + this.httpClient = null; } - using var response = await this.SendAsync(RequestFunc, relativeUrl, cancellationToken); - var stringResponse = await response.Content.ReadAsStringAsync(); + this.disposed = true; + } + } - DateTimeOffset? ttlExpireTime = null; - if (response.Headers.TryGetValues(Constants.TTLResponseHeaderName, out IEnumerable headerValues)) + /// + /// Sends an HTTP get request to cluster http gateway. + /// + /// Func to create HttpRequest to send. + /// The relative URI. + /// The cancellation token. + /// The payload of the GET response. + private async Task SendAsyncHandleUnsuccessfulResponse( + Func requestFunc, + string relativeUri, + CancellationToken cancellationToken) + { + HttpRequestMessage FinalRequestFunc() + { + var request = requestFunc.Invoke(); + request.RequestUri = new Uri($"{this.httpEndpoint}/{relativeUri}"); + return request; + } + + var response = await this.SendAsyncHandleSecurityExceptions(FinalRequestFunc, cancellationToken); + + if (response.IsSuccessStatusCode) + { + return response; + } + + // TODO Log Unsuccessful Response. + + // Try to get Error Information if present in response body. + if (response.Content != null) + { + DaprError error = null; + + try { - var ttlExpireTimeString = headerValues.First(); - if (!string.IsNullOrEmpty(ttlExpireTimeString)) + var contentStream = await response.Content.ReadAsStreamAsync(); + if (contentStream.Length != 0) { - ttlExpireTime = DateTime.Parse(ttlExpireTimeString, CultureInfo.InvariantCulture); + error = await JsonSerializer.DeserializeAsync(contentStream, jsonSerializerOptions); } } - - return new ActorStateResponse(stringResponse, ttlExpireTime); - } - - public Task SaveStateTransactionallyAsync(string actorType, string actorId, string data, CancellationToken cancellationToken = default) - { - var relativeUrl = string.Format(CultureInfo.InvariantCulture, Constants.ActorStateRelativeUrlFormat, actorType, actorId); - - HttpRequestMessage RequestFunc() + catch (Exception ex) { - var request = new HttpRequestMessage() - { - Method = HttpMethod.Put, - Content = new StringContent(data), - }; - - return request; + throw new DaprApiException(string.Format("ServerErrorNoMeaningFulResponse", response.StatusCode), ex); } - return this.SendAsync(RequestFunc, relativeUrl, cancellationToken); - } - - public async Task InvokeActorMethodWithRemotingAsync(ActorMessageSerializersManager serializersManager, IActorRequestMessage remotingRequestRequestMessage, CancellationToken cancellationToken = default) - { - var requestMessageHeader = remotingRequestRequestMessage.GetHeader(); - - var actorId = requestMessageHeader.ActorId.ToString(); - var methodName = requestMessageHeader.MethodName; - var actorType = requestMessageHeader.ActorType; - var interfaceId = requestMessageHeader.InterfaceId; - - var serializedHeader = serializersManager.GetHeaderSerializer() - .SerializeRequestHeader(remotingRequestRequestMessage.GetHeader()); - - var msgBodySeriaizer = serializersManager.GetRequestMessageBodySerializer(interfaceId, methodName); - var serializedMsgBody = msgBodySeriaizer.Serialize(remotingRequestRequestMessage.GetBody()); - - // Send Request - var relativeUrl = string.Format(CultureInfo.InvariantCulture, Constants.ActorMethodRelativeUrlFormat, actorType, actorId, methodName); - - HttpRequestMessage RequestFunc() + if (error != null) { - var request = new HttpRequestMessage() - { - Method = HttpMethod.Put, - }; - - if (serializedMsgBody != null) - { - request.Content = new ByteArrayContent(serializedMsgBody); - request.Content.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/octet-stream; charset=utf-8"); - } - - request.Headers.Add(Constants.RequestHeaderName, Encoding.UTF8.GetString(serializedHeader, 0, serializedHeader.Length)); - - var reentrancyId = ActorReentrancyContextAccessor.ReentrancyContext; - if (reentrancyId != null) - { - request.Headers.Add(Constants.ReentrancyRequestHeaderName, reentrancyId); - } - - return request; + throw new DaprApiException(error.Message, error.ErrorCode, false); } - - var retval = await this.SendAsync(RequestFunc, relativeUrl, cancellationToken); - var header = ""; - IActorResponseMessageHeader actorResponseMessageHeader = null; - if (retval != null && retval.Headers != null) + else { - if (retval.Headers.TryGetValues(Constants.ErrorResponseHeaderName, out IEnumerable headerValues)) + // Handle NotFound 404, without any ErrorCode. + if (response.StatusCode.Equals(HttpStatusCode.NotFound)) { - header = headerValues.First(); - // DeSerialize Actor Response Message Header - actorResponseMessageHeader = - serializersManager.GetHeaderSerializer() - .DeserializeResponseHeaders( - new MemoryStream(Encoding.ASCII.GetBytes(header))); + throw new DaprApiException("ErrorMessageHTTP404", Constants.ErrorDoesNotExist, false); } } - - // Get the http response message body content and extract out expected actor response message body - IActorResponseMessageBody actorResponseMessageBody = null; - if (retval != null && retval.Content != null) - { - var responseMessageBody = await retval.Content.ReadAsStreamAsync(); - - // Deserialize Actor Response Message Body - // Deserialize to ActorInvokeException when there is response header otherwise normal path - var responseBodySerializer = serializersManager.GetResponseMessageBodySerializer(interfaceId, methodName); - - // actorResponseMessageHeader is not null, it means there is remote exception - if (actorResponseMessageHeader != null) - { - var isDeserialized = - ActorInvokeException.ToException( - responseMessageBody, - out var remoteMethodException); - if (isDeserialized) - { - var exceptionDetails = GetExceptionDetails(header.ToString()); - throw new ActorMethodInvocationException( - "Remote Actor Method Exception, DETAILS: " + exceptionDetails, - remoteMethodException, - false /* non transient */); - } - else - { - throw new ActorInvokeException(remoteMethodException.GetType().FullName, string.Format( - CultureInfo.InvariantCulture, - SR.ErrorDeserializationFailure, - remoteMethodException.ToString())); - } - } - - actorResponseMessageBody = await responseBodySerializer.DeserializeAsync(responseMessageBody); - } - - return new ActorResponseMessage(actorResponseMessageHeader, actorResponseMessageBody); } - private string GetExceptionDetails(string header) { - XmlDocument xmlHeader = new XmlDocument(); - xmlHeader.LoadXml(header); - XmlNodeList exceptionValueXML = xmlHeader.GetElementsByTagName(EXCEPTION_HEADER_TAG); - string exceptionDetails = ""; - if (exceptionValueXML != null && exceptionValueXML.Item(1) != null) - { - exceptionDetails = exceptionValueXML.Item(1).LastChild.InnerText; - } - var base64EncodedBytes = System.Convert.FromBase64String(exceptionDetails); - return Encoding.UTF8.GetString(base64EncodedBytes); - } + // Couldn't determine Error information from response., throw exception with status code. + throw new DaprApiException(string.Format("ServerErrorNoMeaningFulResponse", response.StatusCode)); + } - public async Task InvokeActorMethodWithoutRemotingAsync(string actorType, string actorId, string methodName, string jsonPayload, CancellationToken cancellationToken = default) + /// + /// Send an HTTP request as an asynchronous operation using HttpClient and handles security exceptions. + /// If UserCode to Dapr calls are over https in future, this method will handle refreshing security. + /// + /// Delegate to get HTTP request message to send. + /// The cancellation token to cancel operation. + /// The task object representing the asynchronous operation. + private async Task SendAsyncHandleSecurityExceptions( + Func requestFunc, + CancellationToken cancellationToken) + { + HttpResponseMessage response; + + // Get the request using the Func as same request cannot be resent when retries are implemented. + using var request = requestFunc.Invoke(); + + // add token for dapr api token based authentication + this.AddDaprApiTokenHeader(request); + + response = await this.httpClient.SendAsync(request, cancellationToken); + + if (!response.IsSuccessStatusCode) { - var relativeUrl = string.Format(CultureInfo.InvariantCulture, Constants.ActorMethodRelativeUrlFormat, actorType, actorId, methodName); - - HttpRequestMessage RequestFunc() + // RefreshSecurity Settings and try again, + if (response.StatusCode == HttpStatusCode.Forbidden || + response.StatusCode == HttpStatusCode.Unauthorized) { - var request = new HttpRequestMessage() - { - Method = HttpMethod.Put, - }; - - if (jsonPayload != null) - { - request.Content = new StringContent(jsonPayload); - request.Content.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json; charset=utf-8"); - } - - var reentrancyId = ActorReentrancyContextAccessor.ReentrancyContext; - if (reentrancyId != null) - { - request.Headers.Add(Constants.ReentrancyRequestHeaderName, reentrancyId); - } - - return request; - } - - var response = await this.SendAsync(RequestFunc, relativeUrl, cancellationToken); - var stream = await response.Content.ReadAsStreamAsync(); - return stream; - } - - public Task RegisterReminderAsync(string actorType, string actorId, string reminderName, string data, CancellationToken cancellationToken = default) - { - var relativeUrl = string.Format(CultureInfo.InvariantCulture, Constants.ActorReminderRelativeUrlFormat, actorType, actorId, reminderName); - - HttpRequestMessage RequestFunc() - { - var request = new HttpRequestMessage() - { - Method = HttpMethod.Put, - Content = new StringContent(data, Encoding.UTF8), - }; - - request.Content.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json; charset=utf-8"); - return request; - } - - return this.SendAsync(RequestFunc, relativeUrl, cancellationToken); - } - - public async Task GetReminderAsync(string actorType, string actorId, string reminderName, CancellationToken cancellationToken = default) - { - var relativeUrl = string.Format(CultureInfo.InvariantCulture, Constants.ActorReminderRelativeUrlFormat, actorType, actorId, reminderName); - - HttpRequestMessage RequestFunc() - { - var request = new HttpRequestMessage() - { - Method = HttpMethod.Get, - }; - return request; - } - - return await this.SendAsync(RequestFunc, relativeUrl, cancellationToken); - } - - public Task UnregisterReminderAsync(string actorType, string actorId, string reminderName, CancellationToken cancellationToken = default) - { - var relativeUrl = string.Format(CultureInfo.InvariantCulture, Constants.ActorReminderRelativeUrlFormat, actorType, actorId, reminderName); - - HttpRequestMessage RequestFunc() - { - var request = new HttpRequestMessage() - { - Method = HttpMethod.Delete, - }; - - return request; - } - - return this.SendAsync(RequestFunc, relativeUrl, cancellationToken); - } - - public Task RegisterTimerAsync(string actorType, string actorId, string timerName, string data, CancellationToken cancellationToken = default) - { - var relativeUrl = string.Format(CultureInfo.InvariantCulture, Constants.ActorTimerRelativeUrlFormat, actorType, actorId, timerName); - - HttpRequestMessage RequestFunc() - { - var request = new HttpRequestMessage() - { - Method = HttpMethod.Put, - Content = new StringContent(data, Encoding.UTF8), - }; - - request.Content.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json; charset=utf-8"); - return request; - } - - return this.SendAsync(RequestFunc, relativeUrl, cancellationToken); - } - - public Task UnregisterTimerAsync(string actorType, string actorId, string timerName, CancellationToken cancellationToken = default) - { - var relativeUrl = string.Format(CultureInfo.InvariantCulture, Constants.ActorTimerRelativeUrlFormat, actorType, actorId, timerName); - - HttpRequestMessage RequestFunc() - { - var request = new HttpRequestMessage() - { - Method = HttpMethod.Delete, - }; - - return request; - } - - return this.SendAsync(RequestFunc, relativeUrl, cancellationToken); - } - - /// - /// Sends an HTTP get request to Dapr. - /// - /// Func to create HttpRequest to send. - /// The relative URI. - /// The cancellation token. - /// The payload of the GET response. - internal async Task SendAsync( - Func requestFunc, - string relativeUri, - CancellationToken cancellationToken) - { - return await this.SendAsyncHandleUnsuccessfulResponse(requestFunc, relativeUri, cancellationToken); - } - - /// - /// Sends an HTTP get request to Dapr and returns the result as raw JSON. - /// - /// Func to create HttpRequest to send. - /// The relative URI. - /// The cancellation token. - /// The payload of the GET response as string. - internal async Task SendAsyncGetResponseAsRawJson( - Func requestFunc, - string relativeUri, - CancellationToken cancellationToken) - { - using var response = await this.SendAsyncHandleUnsuccessfulResponse(requestFunc, relativeUri, cancellationToken); - var retValue = default(string); - - if (response != null && response.Content != null) - { - retValue = await response.Content.ReadAsStringAsync(); - } - - return retValue; - } - - /// - /// Disposes resources. - /// - /// False values indicates the method is being called by the runtime, true value indicates the method is called by the user code. - protected virtual void Dispose(bool disposing) - { - if (!this.disposed) - { - if (disposing) - { - this.httpClient.Dispose(); - this.httpClient = null; - } - - this.disposed = true; - } - } - - /// - /// Sends an HTTP get request to cluster http gateway. - /// - /// Func to create HttpRequest to send. - /// The relative URI. - /// The cancellation token. - /// The payload of the GET response. - private async Task SendAsyncHandleUnsuccessfulResponse( - Func requestFunc, - string relativeUri, - CancellationToken cancellationToken) - { - HttpRequestMessage FinalRequestFunc() - { - var request = requestFunc.Invoke(); - request.RequestUri = new Uri($"{this.httpEndpoint}/{relativeUri}"); - return request; - } - - var response = await this.SendAsyncHandleSecurityExceptions(FinalRequestFunc, cancellationToken); - - if (response.IsSuccessStatusCode) - { - return response; - } - - // TODO Log Unsuccessful Response. - - // Try to get Error Information if present in response body. - if (response.Content != null) - { - DaprError error = null; - - try - { - var contentStream = await response.Content.ReadAsStreamAsync(); - if (contentStream.Length != 0) - { - error = await JsonSerializer.DeserializeAsync(contentStream, jsonSerializerOptions); - } - } - catch (Exception ex) - { - throw new DaprApiException(string.Format("ServerErrorNoMeaningFulResponse", response.StatusCode), ex); - } - - if (error != null) - { - throw new DaprApiException(error.Message, error.ErrorCode, false); - } - else - { - // Handle NotFound 404, without any ErrorCode. - if (response.StatusCode.Equals(HttpStatusCode.NotFound)) - { - throw new DaprApiException("ErrorMessageHTTP404", Constants.ErrorDoesNotExist, false); - } - } - } - - // Couldn't determine Error information from response., throw exception with status code. - throw new DaprApiException(string.Format("ServerErrorNoMeaningFulResponse", response.StatusCode)); - } - - /// - /// Send an HTTP request as an asynchronous operation using HttpClient and handles security exceptions. - /// If UserCode to Dapr calls are over https in future, this method will handle refreshing security. - /// - /// Delegate to get HTTP request message to send. - /// The cancellation token to cancel operation. - /// The task object representing the asynchronous operation. - private async Task SendAsyncHandleSecurityExceptions( - Func requestFunc, - CancellationToken cancellationToken) - { - HttpResponseMessage response; - - // Get the request using the Func as same request cannot be resent when retries are implemented. - using var request = requestFunc.Invoke(); - - // add token for dapr api token based authentication - this.AddDaprApiTokenHeader(request); - - response = await this.httpClient.SendAsync(request, cancellationToken); - - if (!response.IsSuccessStatusCode) - { - // RefreshSecurity Settings and try again, - if (response.StatusCode == HttpStatusCode.Forbidden || - response.StatusCode == HttpStatusCode.Unauthorized) - { - // TODO Log - throw new AuthenticationException("Invalid client credentials"); - } - else - { - return response; - } + // TODO Log + throw new AuthenticationException("Invalid client credentials"); } else { return response; } } - - private HttpClient CreateHttpClient() + else { - return new HttpClient(this.handler, false); - } - - private void AddDaprApiTokenHeader(HttpRequestMessage request) - { - if (!string.IsNullOrWhiteSpace(this.daprApiToken)) - { - request.Headers.Add("dapr-api-token", this.daprApiToken); - return; - } + return response; } } -} + + private HttpClient CreateHttpClient() + { + return new HttpClient(this.handler, false); + } + + private void AddDaprApiTokenHeader(HttpRequestMessage request) + { + if (!string.IsNullOrWhiteSpace(this.daprApiToken)) + { + request.Headers.Add("dapr-api-token", this.daprApiToken); + return; + } + } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Description/ActorInterfaceDescription.cs b/src/Dapr.Actors/Description/ActorInterfaceDescription.cs index 69885984..02b4f84a 100644 --- a/src/Dapr.Actors/Description/ActorInterfaceDescription.cs +++ b/src/Dapr.Actors/Description/ActorInterfaceDescription.cs @@ -11,73 +11,72 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Description +namespace Dapr.Actors.Description; + +using System; +using System.Globalization; +using System.Reflection; +using Dapr.Actors; +using Dapr.Actors.Resources; +using Dapr.Actors.Runtime; + +internal class ActorInterfaceDescription : InterfaceDescription { - using System; - using System.Globalization; - using System.Reflection; - using Dapr.Actors; - using Dapr.Actors.Resources; - using Dapr.Actors.Runtime; - - internal class ActorInterfaceDescription : InterfaceDescription + private ActorInterfaceDescription(Type actorInterfaceType, bool useCRCIdGeneration) + : base("actor", actorInterfaceType, useCRCIdGeneration, MethodReturnCheck.EnsureReturnsTask) { - private ActorInterfaceDescription(Type actorInterfaceType, bool useCRCIdGeneration) - : base("actor", actorInterfaceType, useCRCIdGeneration, MethodReturnCheck.EnsureReturnsTask) + } + + public static ActorInterfaceDescription Create(Type actorInterfaceType) + { + EnsureActorInterface(actorInterfaceType); + return new ActorInterfaceDescription(actorInterfaceType, false); + } + + public static ActorInterfaceDescription CreateUsingCRCId(Type actorInterfaceType) + { + EnsureActorInterface(actorInterfaceType); + + return new ActorInterfaceDescription(actorInterfaceType, true); + } + + private static void EnsureActorInterface(Type actorInterfaceType) + { + if (!actorInterfaceType.GetTypeInfo().IsInterface) { + throw new ArgumentException( + string.Format( + CultureInfo.CurrentCulture, + SR.ErrorNotAnActorInterface_InterfaceCheck, + actorInterfaceType.FullName, + typeof(IActor).FullName), + "actorInterfaceType"); } - public static ActorInterfaceDescription Create(Type actorInterfaceType) + var nonActorParentInterface = actorInterfaceType.GetNonActorParentType(); + if (nonActorParentInterface != null) { - EnsureActorInterface(actorInterfaceType); - return new ActorInterfaceDescription(actorInterfaceType, false); - } - - public static ActorInterfaceDescription CreateUsingCRCId(Type actorInterfaceType) - { - EnsureActorInterface(actorInterfaceType); - - return new ActorInterfaceDescription(actorInterfaceType, true); - } - - private static void EnsureActorInterface(Type actorInterfaceType) - { - if (!actorInterfaceType.GetTypeInfo().IsInterface) + if (nonActorParentInterface == actorInterfaceType) { throw new ArgumentException( string.Format( CultureInfo.CurrentCulture, - SR.ErrorNotAnActorInterface_InterfaceCheck, + SR.ErrorNotAnActorInterface_DerivationCheck1, actorInterfaceType.FullName, typeof(IActor).FullName), "actorInterfaceType"); } - - var nonActorParentInterface = actorInterfaceType.GetNonActorParentType(); - if (nonActorParentInterface != null) + else { - if (nonActorParentInterface == actorInterfaceType) - { - throw new ArgumentException( - string.Format( - CultureInfo.CurrentCulture, - SR.ErrorNotAnActorInterface_DerivationCheck1, - actorInterfaceType.FullName, - typeof(IActor).FullName), - "actorInterfaceType"); - } - else - { - throw new ArgumentException( - string.Format( - CultureInfo.CurrentCulture, - SR.ErrorNotAnActorInterface_DerivationCheck2, - actorInterfaceType.FullName, - nonActorParentInterface.FullName, - typeof(IActor).FullName), - "actorInterfaceType"); - } + throw new ArgumentException( + string.Format( + CultureInfo.CurrentCulture, + SR.ErrorNotAnActorInterface_DerivationCheck2, + actorInterfaceType.FullName, + nonActorParentInterface.FullName, + typeof(IActor).FullName), + "actorInterfaceType"); } } } -} +} \ No newline at end of file diff --git a/src/Dapr.Actors/Description/InterfaceDescription.cs b/src/Dapr.Actors/Description/InterfaceDescription.cs index 898a70ed..41c8a145 100644 --- a/src/Dapr.Actors/Description/InterfaceDescription.cs +++ b/src/Dapr.Actors/Description/InterfaceDescription.cs @@ -11,224 +11,223 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Description +namespace Dapr.Actors.Description; + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Reflection; +using Dapr.Actors.Common; +using Dapr.Actors.Resources; + +internal abstract class InterfaceDescription { - using System; - using System.Collections.Generic; - using System.Globalization; - using System.Reflection; - using Dapr.Actors.Common; - using Dapr.Actors.Resources; + private readonly Type remotedInterfaceType; + private readonly bool useCRCIdGeneration; + private readonly int interfaceId; + private readonly int interfaceIdV1; - internal abstract class InterfaceDescription + private readonly MethodDescription[] methods; + + protected InterfaceDescription( + string remotedInterfaceKindName, + Type remotedInterfaceType, + bool useCRCIdGeneration, + MethodReturnCheck methodReturnCheck = MethodReturnCheck.EnsureReturnsTask) { - private readonly Type remotedInterfaceType; - private readonly bool useCRCIdGeneration; - private readonly int interfaceId; - private readonly int interfaceIdV1; + EnsureNotGeneric(remotedInterfaceKindName, remotedInterfaceType); - private readonly MethodDescription[] methods; - - protected InterfaceDescription( - string remotedInterfaceKindName, - Type remotedInterfaceType, - bool useCRCIdGeneration, - MethodReturnCheck methodReturnCheck = MethodReturnCheck.EnsureReturnsTask) + this.remotedInterfaceType = remotedInterfaceType; + this.useCRCIdGeneration = useCRCIdGeneration; + if (this.useCRCIdGeneration) { - EnsureNotGeneric(remotedInterfaceKindName, remotedInterfaceType); + this.interfaceId = IdUtil.ComputeIdWithCRC(remotedInterfaceType); - this.remotedInterfaceType = remotedInterfaceType; - this.useCRCIdGeneration = useCRCIdGeneration; - if (this.useCRCIdGeneration) - { - this.interfaceId = IdUtil.ComputeIdWithCRC(remotedInterfaceType); - - // This is needed for backward compatibility support to V1 Stack like ActorEventproxy - this.interfaceIdV1 = IdUtil.ComputeId(remotedInterfaceType); - } - else - { - this.interfaceId = IdUtil.ComputeId(remotedInterfaceType); - } - - this.methods = GetMethodDescriptions(remotedInterfaceKindName, remotedInterfaceType, methodReturnCheck, useCRCIdGeneration); + // This is needed for backward compatibility support to V1 Stack like ActorEventproxy + this.interfaceIdV1 = IdUtil.ComputeId(remotedInterfaceType); + } + else + { + this.interfaceId = IdUtil.ComputeId(remotedInterfaceType); } - public int V1Id - { - get { return this.interfaceIdV1; } - } + this.methods = GetMethodDescriptions(remotedInterfaceKindName, remotedInterfaceType, methodReturnCheck, useCRCIdGeneration); + } - public int Id - { - get { return this.interfaceId; } - } + public int V1Id + { + get { return this.interfaceIdV1; } + } - public Type InterfaceType - { - get { return this.remotedInterfaceType; } - } + public int Id + { + get { return this.interfaceId; } + } - public MethodDescription[] Methods - { - get { return this.methods; } - } + public Type InterfaceType + { + get { return this.remotedInterfaceType; } + } - private static void EnsureNotGeneric( - string remotedInterfaceKindName, - Type remotedInterfaceType) - { - if (remotedInterfaceType.GetTypeInfo().IsGenericType || - remotedInterfaceType.GetTypeInfo().IsGenericTypeDefinition) - { - throw new ArgumentException( - string.Format( - CultureInfo.CurrentCulture, - SR.ErrorRemotedInterfaceIsGeneric, - remotedInterfaceKindName, - remotedInterfaceType.FullName), - remotedInterfaceKindName + "InterfaceType"); - } - } + public MethodDescription[] Methods + { + get { return this.methods; } + } - private static MethodDescription[] GetMethodDescriptions( - string remotedInterfaceKindName, - Type remotedInterfaceType, - MethodReturnCheck methodReturnCheck, - bool useCRCIdGeneration) - { - EnsureValidMethods(remotedInterfaceKindName, remotedInterfaceType, methodReturnCheck); - var methods = remotedInterfaceType.GetMethods(); - var methodDescriptions = new MethodDescription[methods.Length]; - for (var i = 0; i < methods.Length; i++) - { - methodDescriptions[i] = MethodDescription.Create(remotedInterfaceKindName, methods[i], useCRCIdGeneration); - } - - return methodDescriptions; - } - - private static void EnsureValidMethods( - string remotedInterfaceKindName, - Type remotedInterfaceType, - MethodReturnCheck methodReturnCheck) - { - var methodNameSet = new HashSet(); - foreach (var m in remotedInterfaceType.GetMethods()) - { - EnsureNotOverloaded(remotedInterfaceKindName, remotedInterfaceType, m, methodNameSet); - EnsureNotGeneric(remotedInterfaceKindName, remotedInterfaceType, m); - EnsureNotVariableArgs(remotedInterfaceKindName, remotedInterfaceType, m); - - if (methodReturnCheck == MethodReturnCheck.EnsureReturnsTask) - { - EnsureReturnsTask(remotedInterfaceKindName, remotedInterfaceType, m); - } - - if (methodReturnCheck == MethodReturnCheck.EnsureReturnsVoid) - { - EnsureReturnsVoid(remotedInterfaceKindName, remotedInterfaceType, m); - } - } - } - - private static void EnsureNotOverloaded( - string remotedInterfaceKindName, - Type remotedInterfaceType, - MethodInfo methodInfo, - ISet methodNameSet) - { - if (methodNameSet.Contains(methodInfo.Name)) - { - ThrowArgumentExceptionForMethodChecks( - remotedInterfaceKindName, - remotedInterfaceType, - methodInfo, - SR.ErrorRemotedMethodsIsOverloaded); - } - - methodNameSet.Add((methodInfo.Name)); - } - - private static void EnsureNotGeneric( - string remotedInterfaceKindName, - Type remotedInterfaceType, - MethodInfo methodInfo) - { - if (methodInfo.IsGenericMethod) - { - ThrowArgumentExceptionForMethodChecks( - remotedInterfaceKindName, - remotedInterfaceType, - methodInfo, - SR.ErrorRemotedMethodHasGenerics); - } - } - - private static void EnsureNotVariableArgs( - string remotedInterfaceKindName, - Type remotedInterfaceType, - MethodInfo methodInfo) - { - if (methodInfo.CallingConvention.HasFlag(CallingConventions.VarArgs)) - { - ThrowArgumentExceptionForMethodChecks( - remotedInterfaceKindName, - remotedInterfaceType, - methodInfo, - SR.ErrorRemotedMethodHasVarArgs); - } - } - - private static void EnsureReturnsTask( - string remotedInterfaceKindName, - Type remotedInterfaceType, - MethodInfo methodInfo) - { - if (!TypeUtility.IsTaskType(methodInfo.ReturnType)) - { - ThrowArgumentExceptionForMethodChecks( - remotedInterfaceKindName, - remotedInterfaceType, - methodInfo, - SR.ErrorRemotedMethodDoesNotReturnTask); - } - } - - private static void EnsureReturnsVoid( - string remotedInterfaceKindName, - Type remotedInterfaceType, - MethodInfo methodInfo) - { - if (!TypeUtility.IsVoidType(methodInfo.ReturnType)) - { - throw new ArgumentException( - string.Format( - CultureInfo.CurrentCulture, - SR.ErrorRemotedMethodDoesNotReturnVoid, - remotedInterfaceKindName, - methodInfo.Name, - remotedInterfaceType.FullName, - methodInfo.ReturnType.FullName, - typeof(void)), - remotedInterfaceKindName + "InterfaceType"); - } - } - - private static void ThrowArgumentExceptionForMethodChecks( - string remotedInterfaceKindName, - Type remotedInterfaceType, - MethodInfo methodInfo, - string resourceName) + private static void EnsureNotGeneric( + string remotedInterfaceKindName, + Type remotedInterfaceType) + { + if (remotedInterfaceType.GetTypeInfo().IsGenericType || + remotedInterfaceType.GetTypeInfo().IsGenericTypeDefinition) { throw new ArgumentException( string.Format( CultureInfo.CurrentCulture, - resourceName, + SR.ErrorRemotedInterfaceIsGeneric, remotedInterfaceKindName, - methodInfo.Name, remotedInterfaceType.FullName), remotedInterfaceKindName + "InterfaceType"); } } -} + + private static MethodDescription[] GetMethodDescriptions( + string remotedInterfaceKindName, + Type remotedInterfaceType, + MethodReturnCheck methodReturnCheck, + bool useCRCIdGeneration) + { + EnsureValidMethods(remotedInterfaceKindName, remotedInterfaceType, methodReturnCheck); + var methods = remotedInterfaceType.GetMethods(); + var methodDescriptions = new MethodDescription[methods.Length]; + for (var i = 0; i < methods.Length; i++) + { + methodDescriptions[i] = MethodDescription.Create(remotedInterfaceKindName, methods[i], useCRCIdGeneration); + } + + return methodDescriptions; + } + + private static void EnsureValidMethods( + string remotedInterfaceKindName, + Type remotedInterfaceType, + MethodReturnCheck methodReturnCheck) + { + var methodNameSet = new HashSet(); + foreach (var m in remotedInterfaceType.GetMethods()) + { + EnsureNotOverloaded(remotedInterfaceKindName, remotedInterfaceType, m, methodNameSet); + EnsureNotGeneric(remotedInterfaceKindName, remotedInterfaceType, m); + EnsureNotVariableArgs(remotedInterfaceKindName, remotedInterfaceType, m); + + if (methodReturnCheck == MethodReturnCheck.EnsureReturnsTask) + { + EnsureReturnsTask(remotedInterfaceKindName, remotedInterfaceType, m); + } + + if (methodReturnCheck == MethodReturnCheck.EnsureReturnsVoid) + { + EnsureReturnsVoid(remotedInterfaceKindName, remotedInterfaceType, m); + } + } + } + + private static void EnsureNotOverloaded( + string remotedInterfaceKindName, + Type remotedInterfaceType, + MethodInfo methodInfo, + ISet methodNameSet) + { + if (methodNameSet.Contains(methodInfo.Name)) + { + ThrowArgumentExceptionForMethodChecks( + remotedInterfaceKindName, + remotedInterfaceType, + methodInfo, + SR.ErrorRemotedMethodsIsOverloaded); + } + + methodNameSet.Add((methodInfo.Name)); + } + + private static void EnsureNotGeneric( + string remotedInterfaceKindName, + Type remotedInterfaceType, + MethodInfo methodInfo) + { + if (methodInfo.IsGenericMethod) + { + ThrowArgumentExceptionForMethodChecks( + remotedInterfaceKindName, + remotedInterfaceType, + methodInfo, + SR.ErrorRemotedMethodHasGenerics); + } + } + + private static void EnsureNotVariableArgs( + string remotedInterfaceKindName, + Type remotedInterfaceType, + MethodInfo methodInfo) + { + if (methodInfo.CallingConvention.HasFlag(CallingConventions.VarArgs)) + { + ThrowArgumentExceptionForMethodChecks( + remotedInterfaceKindName, + remotedInterfaceType, + methodInfo, + SR.ErrorRemotedMethodHasVarArgs); + } + } + + private static void EnsureReturnsTask( + string remotedInterfaceKindName, + Type remotedInterfaceType, + MethodInfo methodInfo) + { + if (!TypeUtility.IsTaskType(methodInfo.ReturnType)) + { + ThrowArgumentExceptionForMethodChecks( + remotedInterfaceKindName, + remotedInterfaceType, + methodInfo, + SR.ErrorRemotedMethodDoesNotReturnTask); + } + } + + private static void EnsureReturnsVoid( + string remotedInterfaceKindName, + Type remotedInterfaceType, + MethodInfo methodInfo) + { + if (!TypeUtility.IsVoidType(methodInfo.ReturnType)) + { + throw new ArgumentException( + string.Format( + CultureInfo.CurrentCulture, + SR.ErrorRemotedMethodDoesNotReturnVoid, + remotedInterfaceKindName, + methodInfo.Name, + remotedInterfaceType.FullName, + methodInfo.ReturnType.FullName, + typeof(void)), + remotedInterfaceKindName + "InterfaceType"); + } + } + + private static void ThrowArgumentExceptionForMethodChecks( + string remotedInterfaceKindName, + Type remotedInterfaceType, + MethodInfo methodInfo, + string resourceName) + { + throw new ArgumentException( + string.Format( + CultureInfo.CurrentCulture, + resourceName, + remotedInterfaceKindName, + methodInfo.Name, + remotedInterfaceType.FullName), + remotedInterfaceKindName + "InterfaceType"); + } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Description/MethodArgumentDescription.cs b/src/Dapr.Actors/Description/MethodArgumentDescription.cs index 7685c361..b7eaae30 100644 --- a/src/Dapr.Actors/Description/MethodArgumentDescription.cs +++ b/src/Dapr.Actors/Description/MethodArgumentDescription.cs @@ -11,91 +11,90 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Description +namespace Dapr.Actors.Description; + +using System; +using System.Globalization; +using System.Reflection; +using Dapr.Actors.Resources; + +internal sealed class MethodArgumentDescription { - using System; - using System.Globalization; - using System.Reflection; - using Dapr.Actors.Resources; + private readonly ParameterInfo parameterInfo; - internal sealed class MethodArgumentDescription + private MethodArgumentDescription(ParameterInfo parameterInfo) { - private readonly ParameterInfo parameterInfo; + this.parameterInfo = parameterInfo; + } - private MethodArgumentDescription(ParameterInfo parameterInfo) + public string Name + { + get { return this.parameterInfo.Name; } + } + + public Type ArgumentType + { + get { return this.parameterInfo.ParameterType; } + } + + internal static MethodArgumentDescription Create(string remotedInterfaceKindName, MethodInfo methodInfo, ParameterInfo parameter) + { + var remotedInterfaceType = methodInfo.DeclaringType; + EnsureNotOutRefOptional(remotedInterfaceKindName, remotedInterfaceType, methodInfo, parameter); + EnsureNotVariableLength(remotedInterfaceKindName, remotedInterfaceType, methodInfo, parameter); + + return new MethodArgumentDescription(parameter); + } + + private static void EnsureNotVariableLength( + string remotedInterfaceKindName, + Type remotedInterfaceType, + MethodInfo methodInfo, + ParameterInfo param) + { + if (param.GetCustomAttributes(typeof(ParamArrayAttribute), false).Length > 0) { - this.parameterInfo = parameterInfo; - } - - public string Name - { - get { return this.parameterInfo.Name; } - } - - public Type ArgumentType - { - get { return this.parameterInfo.ParameterType; } - } - - internal static MethodArgumentDescription Create(string remotedInterfaceKindName, MethodInfo methodInfo, ParameterInfo parameter) - { - var remotedInterfaceType = methodInfo.DeclaringType; - EnsureNotOutRefOptional(remotedInterfaceKindName, remotedInterfaceType, methodInfo, parameter); - EnsureNotVariableLength(remotedInterfaceKindName, remotedInterfaceType, methodInfo, parameter); - - return new MethodArgumentDescription(parameter); - } - - private static void EnsureNotVariableLength( - string remotedInterfaceKindName, - Type remotedInterfaceType, - MethodInfo methodInfo, - ParameterInfo param) - { - if (param.GetCustomAttributes(typeof(ParamArrayAttribute), false).Length > 0) - { - ThrowArgumentExceptionForParamChecks( - remotedInterfaceKindName, - remotedInterfaceType, - methodInfo, - param, - SR.ErrorRemotedMethodHasVarArgParameter); - } - } - - private static void EnsureNotOutRefOptional( - string remotedInterfaceKindName, - Type remotedInterfaceType, - MethodInfo methodInfo, - ParameterInfo param) - { - if (param.IsOut || param.IsIn || param.IsOptional) - { - ThrowArgumentExceptionForParamChecks( - remotedInterfaceKindName, - remotedInterfaceType, - methodInfo, - param, - SR.ErrorRemotedMethodHasOutRefOptionalParameter); - } - } - - private static void ThrowArgumentExceptionForParamChecks( - string remotedInterfaceKindName, - Type remotedInterfaceType, - MethodInfo methodInfo, - ParameterInfo param, - string resourceName) - { - throw new ArgumentException( - string.Format( - CultureInfo.CurrentCulture, - resourceName, - remotedInterfaceKindName, - methodInfo.Name, - remotedInterfaceType.FullName, - param.Name), - remotedInterfaceKindName + "InterfaceType"); + ThrowArgumentExceptionForParamChecks( + remotedInterfaceKindName, + remotedInterfaceType, + methodInfo, + param, + SR.ErrorRemotedMethodHasVarArgParameter); } } -} + + private static void EnsureNotOutRefOptional( + string remotedInterfaceKindName, + Type remotedInterfaceType, + MethodInfo methodInfo, + ParameterInfo param) + { + if (param.IsOut || param.IsIn || param.IsOptional) + { + ThrowArgumentExceptionForParamChecks( + remotedInterfaceKindName, + remotedInterfaceType, + methodInfo, + param, + SR.ErrorRemotedMethodHasOutRefOptionalParameter); + } + } + + private static void ThrowArgumentExceptionForParamChecks( + string remotedInterfaceKindName, + Type remotedInterfaceType, + MethodInfo methodInfo, + ParameterInfo param, + string resourceName) + { + throw new ArgumentException( + string.Format( + CultureInfo.CurrentCulture, + resourceName, + remotedInterfaceKindName, + methodInfo.Name, + remotedInterfaceType.FullName, + param.Name), + remotedInterfaceKindName + "InterfaceType"); + } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Description/MethodDescription.cs b/src/Dapr.Actors/Description/MethodDescription.cs index aca6e223..3ee7b0bf 100644 --- a/src/Dapr.Actors/Description/MethodDescription.cs +++ b/src/Dapr.Actors/Description/MethodDescription.cs @@ -11,97 +11,96 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Description +namespace Dapr.Actors.Description; + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Reflection; +using System.Threading; +using Dapr.Actors.Common; +using Dapr.Actors.Resources; + +internal class MethodDescription { - using System; - using System.Collections.Generic; - using System.Globalization; - using System.Reflection; - using System.Threading; - using Dapr.Actors.Common; - using Dapr.Actors.Resources; + private readonly bool useCRCIdGeneration; - internal class MethodDescription + private MethodDescription( + MethodInfo methodInfo, + MethodArgumentDescription[] arguments, + bool hasCancellationToken, + bool useCRCIdGeneration) { - private readonly bool useCRCIdGeneration; - - private MethodDescription( - MethodInfo methodInfo, - MethodArgumentDescription[] arguments, - bool hasCancellationToken, - bool useCRCIdGeneration) + this.MethodInfo = methodInfo; + this.useCRCIdGeneration = useCRCIdGeneration; + if (this.useCRCIdGeneration) { - this.MethodInfo = methodInfo; - this.useCRCIdGeneration = useCRCIdGeneration; - if (this.useCRCIdGeneration) + this.Id = IdUtil.ComputeIdWithCRC(methodInfo); + } + else + { + this.Id = IdUtil.ComputeId(methodInfo); + } + + this.Arguments = arguments; + this.HasCancellationToken = hasCancellationToken; + } + + public int Id { get; } + + public string Name + { + get { return this.MethodInfo.Name; } + } + + public Type ReturnType + { + get { return this.MethodInfo.ReturnType; } + } + + public bool HasCancellationToken { get; } + + public MethodArgumentDescription[] Arguments { get; } + + public MethodInfo MethodInfo { get; } + + internal static MethodDescription Create(string remotedInterfaceKindName, MethodInfo methodInfo, bool useCRCIdGeneration) + { + var parameters = methodInfo.GetParameters(); + var argumentList = new List(parameters.Length); + var hasCancellationToken = false; + + foreach (var param in parameters) + { + if (hasCancellationToken) { - this.Id = IdUtil.ComputeIdWithCRC(methodInfo); + // If the method has a cancellation token, then it must be the last argument. + throw new ArgumentException( + string.Format( + CultureInfo.CurrentCulture, + SR.ErrorRemotedMethodCancellationTokenOutOfOrder, + remotedInterfaceKindName, + methodInfo.Name, + methodInfo.DeclaringType.FullName, + param.Name, + typeof(CancellationToken)), + remotedInterfaceKindName + "InterfaceType"); + } + + if (param.ParameterType == typeof(CancellationToken)) + { + hasCancellationToken = true; } else { - this.Id = IdUtil.ComputeId(methodInfo); + argumentList.Add(MethodArgumentDescription.Create(remotedInterfaceKindName, methodInfo, param)); } - - this.Arguments = arguments; - this.HasCancellationToken = hasCancellationToken; } - public int Id { get; } - - public string Name - { - get { return this.MethodInfo.Name; } - } - - public Type ReturnType - { - get { return this.MethodInfo.ReturnType; } - } - - public bool HasCancellationToken { get; } - - public MethodArgumentDescription[] Arguments { get; } - - public MethodInfo MethodInfo { get; } - - internal static MethodDescription Create(string remotedInterfaceKindName, MethodInfo methodInfo, bool useCRCIdGeneration) - { - var parameters = methodInfo.GetParameters(); - var argumentList = new List(parameters.Length); - var hasCancellationToken = false; - - foreach (var param in parameters) - { - if (hasCancellationToken) - { - // If the method has a cancellation token, then it must be the last argument. - throw new ArgumentException( - string.Format( - CultureInfo.CurrentCulture, - SR.ErrorRemotedMethodCancellationTokenOutOfOrder, - remotedInterfaceKindName, - methodInfo.Name, - methodInfo.DeclaringType.FullName, - param.Name, - typeof(CancellationToken)), - remotedInterfaceKindName + "InterfaceType"); - } - - if (param.ParameterType == typeof(CancellationToken)) - { - hasCancellationToken = true; - } - else - { - argumentList.Add(MethodArgumentDescription.Create(remotedInterfaceKindName, methodInfo, param)); - } - } - - return new MethodDescription( - methodInfo, - argumentList.ToArray(), - hasCancellationToken, - useCRCIdGeneration); - } + return new MethodDescription( + methodInfo, + argumentList.ToArray(), + hasCancellationToken, + useCRCIdGeneration); } -} +} \ No newline at end of file diff --git a/src/Dapr.Actors/Description/MethodReturnCheck.cs b/src/Dapr.Actors/Description/MethodReturnCheck.cs index 2aa4205a..d3693e7c 100644 --- a/src/Dapr.Actors/Description/MethodReturnCheck.cs +++ b/src/Dapr.Actors/Description/MethodReturnCheck.cs @@ -11,11 +11,10 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Description +namespace Dapr.Actors.Description; + +internal enum MethodReturnCheck { - internal enum MethodReturnCheck - { - EnsureReturnsTask, - EnsureReturnsVoid, - } -} + EnsureReturnsTask, + EnsureReturnsVoid, +} \ No newline at end of file diff --git a/src/Dapr.Actors/Description/TypeUtility.cs b/src/Dapr.Actors/Description/TypeUtility.cs index 4890d1ca..d4a23ffa 100644 --- a/src/Dapr.Actors/Description/TypeUtility.cs +++ b/src/Dapr.Actors/Description/TypeUtility.cs @@ -11,23 +11,22 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Description +namespace Dapr.Actors.Description; + +using System; +using System.Reflection; +using System.Threading.Tasks; + +internal static class TypeUtility { - using System; - using System.Reflection; - using System.Threading.Tasks; - - internal static class TypeUtility + public static bool IsTaskType(Type type) { - public static bool IsTaskType(Type type) - { - return ((type == typeof(Task)) || - (type.GetTypeInfo().IsGenericType && (type.GetGenericTypeDefinition() == typeof(Task<>)))); - } - - public static bool IsVoidType(Type type) - { - return ((type == typeof(void)) || (type == null)); - } + return ((type == typeof(Task)) || + (type.GetTypeInfo().IsGenericType && (type.GetGenericTypeDefinition() == typeof(Task<>)))); } -} + + public static bool IsVoidType(Type type) + { + return ((type == typeof(void)) || (type == null)); + } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Helper.cs b/src/Dapr.Actors/Helper.cs index 9cd4c602..06afaac2 100644 --- a/src/Dapr.Actors/Helper.cs +++ b/src/Dapr.Actors/Helper.cs @@ -11,28 +11,27 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors -{ - using System; - using System.Globalization; - using Dapr.Actors.Communication; +namespace Dapr.Actors; - internal class Helper +using System; +using System.Globalization; +using Dapr.Actors.Communication; + +internal class Helper +{ + public static string GetCallContext() { - public static string GetCallContext() + if (ActorLogicalCallContext.TryGet(out var callContextValue)) { - if (ActorLogicalCallContext.TryGet(out var callContextValue)) - { - return string.Format( - CultureInfo.InvariantCulture, - "{0}{1}", - callContextValue, - Guid.NewGuid().ToString()); - } - else - { - return Guid.NewGuid().ToString(); - } + return string.Format( + CultureInfo.InvariantCulture, + "{0}{1}", + callContextValue, + Guid.NewGuid().ToString()); + } + else + { + return Guid.NewGuid().ToString(); } } -} +} \ No newline at end of file diff --git a/src/Dapr.Actors/IActor.cs b/src/Dapr.Actors/IActor.cs index 652d88af..6e6ce124 100644 --- a/src/Dapr.Actors/IActor.cs +++ b/src/Dapr.Actors/IActor.cs @@ -11,12 +11,11 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors +namespace Dapr.Actors; + +/// +/// Base interface for inheriting reliable actor interfaces. +/// +public interface IActor { - /// - /// Base interface for inheriting reliable actor interfaces. - /// - public interface IActor - { - } -} +} \ No newline at end of file diff --git a/src/Dapr.Actors/IActorReference.cs b/src/Dapr.Actors/IActorReference.cs index 12a7c103..560f4a8b 100644 --- a/src/Dapr.Actors/IActorReference.cs +++ b/src/Dapr.Actors/IActorReference.cs @@ -11,23 +11,22 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors -{ - using System; - using Dapr.Actors.Client; +namespace Dapr.Actors; +using System; +using Dapr.Actors.Client; + +/// +/// Interface for ActorReference. +/// +internal interface IActorReference +{ /// - /// Interface for ActorReference. + /// Creates an that implements an actor interface for the actor using the + /// + /// method. /// - internal interface IActorReference - { - /// - /// Creates an that implements an actor interface for the actor using the - /// - /// method. - /// - /// Actor interface for the created to implement. - /// An actor proxy object that implements and TActorInterface. - object Bind(Type actorInterfaceType); - } -} + /// Actor interface for the created to implement. + /// An actor proxy object that implements and TActorInterface. + object Bind(Type actorInterfaceType); +} \ No newline at end of file diff --git a/src/Dapr.Actors/IDaprInteractor.cs b/src/Dapr.Actors/IDaprInteractor.cs index e0d91c44..f857f90f 100644 --- a/src/Dapr.Actors/IDaprInteractor.cs +++ b/src/Dapr.Actors/IDaprInteractor.cs @@ -13,108 +13,107 @@ using System.Net.Http; -namespace Dapr.Actors +namespace Dapr.Actors; + +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Dapr.Actors.Communication; + +/// +/// Interface for interacting with Dapr runtime. +/// +internal interface IDaprInteractor { - using System.IO; - using System.Threading; - using System.Threading.Tasks; - using Dapr.Actors.Communication; + /// + /// Invokes an Actor method on Dapr without remoting. + /// + /// Type of actor. + /// ActorId. + /// Method name to invoke. + /// Serialized body. + /// Cancels the operation. + /// A task that represents the asynchronous operation. + Task InvokeActorMethodWithoutRemotingAsync(string actorType, string actorId, string methodName, string jsonPayload, CancellationToken cancellationToken = default); /// - /// Interface for interacting with Dapr runtime. + /// Saves state batch to Dapr. /// - internal interface IDaprInteractor - { - /// - /// Invokes an Actor method on Dapr without remoting. - /// - /// Type of actor. - /// ActorId. - /// Method name to invoke. - /// Serialized body. - /// Cancels the operation. - /// A task that represents the asynchronous operation. - Task InvokeActorMethodWithoutRemotingAsync(string actorType, string actorId, string methodName, string jsonPayload, CancellationToken cancellationToken = default); + /// Type of actor. + /// ActorId. + /// JSON data with state changes as per the Dapr spec for transaction state update. + /// Cancels the operation. + /// A task that represents the asynchronous operation. + Task SaveStateTransactionallyAsync(string actorType, string actorId, string data, CancellationToken cancellationToken = default); - /// - /// Saves state batch to Dapr. - /// - /// Type of actor. - /// ActorId. - /// JSON data with state changes as per the Dapr spec for transaction state update. - /// Cancels the operation. - /// A task that represents the asynchronous operation. - Task SaveStateTransactionallyAsync(string actorType, string actorId, string data, CancellationToken cancellationToken = default); + /// + /// Saves a state to Dapr. + /// + /// Type of actor. + /// ActorId. + /// Name of key to get value for. + /// Cancels the operation. + /// A task that represents the asynchronous operation. + Task> GetStateAsync(string actorType, string actorId, string keyName, CancellationToken cancellationToken = default); - /// - /// Saves a state to Dapr. - /// - /// Type of actor. - /// ActorId. - /// Name of key to get value for. - /// Cancels the operation. - /// A task that represents the asynchronous operation. - Task> GetStateAsync(string actorType, string actorId, string keyName, CancellationToken cancellationToken = default); + /// + /// Invokes Actor method. + /// + /// Serializers manager for remoting calls. + /// Actor Request Message. + /// Cancels the operation. + /// A representing the result of the asynchronous operation. + Task InvokeActorMethodWithRemotingAsync(ActorMessageSerializersManager serializersManager, IActorRequestMessage remotingRequestRequestMessage, CancellationToken cancellationToken = default); - /// - /// Invokes Actor method. - /// - /// Serializers manager for remoting calls. - /// Actor Request Message. - /// Cancels the operation. - /// A representing the result of the asynchronous operation. - Task InvokeActorMethodWithRemotingAsync(ActorMessageSerializersManager serializersManager, IActorRequestMessage remotingRequestRequestMessage, CancellationToken cancellationToken = default); + /// + /// Register a reminder. + /// + /// Type of actor. + /// ActorId. + /// Name of reminder to register. + /// JSON reminder data as per the Dapr spec. + /// Cancels the operation. + /// A representing the result of the asynchronous operation. + Task RegisterReminderAsync(string actorType, string actorId, string reminderName, string data, CancellationToken cancellationToken = default); - /// - /// Register a reminder. - /// - /// Type of actor. - /// ActorId. - /// Name of reminder to register. - /// JSON reminder data as per the Dapr spec. - /// Cancels the operation. - /// A representing the result of the asynchronous operation. - Task RegisterReminderAsync(string actorType, string actorId, string reminderName, string data, CancellationToken cancellationToken = default); + /// + /// Gets a reminder. + /// + /// Type of actor. + /// ActorId. + /// Name of reminder to unregister. + /// Cancels the operation. + /// A containing the response of the asynchronous HTTP operation. + Task GetReminderAsync(string actorType, string actorId, string reminderName, CancellationToken cancellationToken = default); - /// - /// Gets a reminder. - /// - /// Type of actor. - /// ActorId. - /// Name of reminder to unregister. - /// Cancels the operation. - /// A containing the response of the asynchronous HTTP operation. - Task GetReminderAsync(string actorType, string actorId, string reminderName, CancellationToken cancellationToken = default); + /// + /// Unregisters a reminder. + /// + /// Type of actor. + /// ActorId. + /// Name of reminder to unregister. + /// Cancels the operation. + /// A representing the result of the asynchronous operation. + Task UnregisterReminderAsync(string actorType, string actorId, string reminderName, CancellationToken cancellationToken = default); - /// - /// Unregisters a reminder. - /// - /// Type of actor. - /// ActorId. - /// Name of reminder to unregister. - /// Cancels the operation. - /// A representing the result of the asynchronous operation. - Task UnregisterReminderAsync(string actorType, string actorId, string reminderName, CancellationToken cancellationToken = default); + /// + /// Registers a timer. + /// + /// Type of actor. + /// ActorId. + /// Name of timer to register. + /// JSON reminder data as per the Dapr spec. + /// Cancels the operation. + /// A representing the result of the asynchronous operation. + Task RegisterTimerAsync(string actorType, string actorId, string timerName, string data, CancellationToken cancellationToken = default); - /// - /// Registers a timer. - /// - /// Type of actor. - /// ActorId. - /// Name of timer to register. - /// JSON reminder data as per the Dapr spec. - /// Cancels the operation. - /// A representing the result of the asynchronous operation. - Task RegisterTimerAsync(string actorType, string actorId, string timerName, string data, CancellationToken cancellationToken = default); - - /// - /// Unegisters a timer. - /// - /// Type of actor. - /// ActorId. - /// Name of timer to register. - /// Cancels the operation. - /// A representing the result of the asynchronous operation. - Task UnregisterTimerAsync(string actorType, string actorId, string timerName, CancellationToken cancellationToken = default); - } -} + /// + /// Unegisters a timer. + /// + /// Type of actor. + /// ActorId. + /// Name of timer to register. + /// Cancels the operation. + /// A representing the result of the asynchronous operation. + Task UnregisterTimerAsync(string actorType, string actorId, string timerName, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/Dapr.Actors/JsonSerializerDefaults.cs b/src/Dapr.Actors/JsonSerializerDefaults.cs index 9dd222bd..711e359a 100644 --- a/src/Dapr.Actors/JsonSerializerDefaults.cs +++ b/src/Dapr.Actors/JsonSerializerDefaults.cs @@ -13,20 +13,19 @@ using System.Text.Json; -namespace Dapr.Actors +namespace Dapr.Actors; + +/// +/// A helper used to standardize the defaults +/// +internal static class JsonSerializerDefaults { /// - /// A helper used to standardize the defaults + /// defaults with Camel Casing and case insensitive properties /// - internal static class JsonSerializerDefaults + internal static JsonSerializerOptions Web => new JsonSerializerOptions { - /// - /// defaults with Camel Casing and case insensitive properties - /// - internal static JsonSerializerOptions Web => new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - PropertyNameCaseInsensitive = true - }; - } + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = true + }; } \ No newline at end of file diff --git a/src/Dapr.Actors/Runtime/Actor.cs b/src/Dapr.Actors/Runtime/Actor.cs index ddfc266e..bdd5ac52 100644 --- a/src/Dapr.Actors/Runtime/Actor.cs +++ b/src/Dapr.Actors/Runtime/Actor.cs @@ -11,572 +11,571 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Runtime +namespace Dapr.Actors.Runtime; + +using System; +using System.Reflection; +using System.Threading.Tasks; +using Dapr.Actors.Client; +using Microsoft.Extensions.Logging; + +/// +/// Represents the base class for actors. +/// +/// +/// The base type for actors, that provides the common functionality +/// for actors that derive from . +/// The state is preserved across actor garbage collections and fail-overs. +/// +public abstract class Actor { - using System; - using System.Reflection; - using System.Threading.Tasks; - using Dapr.Actors.Client; - using Microsoft.Extensions.Logging; + private readonly string actorTypeName; /// - /// Represents the base class for actors. + /// The Logger /// - /// - /// The base type for actors, that provides the common functionality - /// for actors that derive from . - /// The state is preserved across actor garbage collections and fail-overs. - /// - public abstract class Actor + protected ILogger Logger { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The that will host this actor instance. + protected Actor(ActorHost host) { - private readonly string actorTypeName; + this.Host = host; + this.StateManager = new ActorStateManager(this); + this.actorTypeName = this.Host.ActorTypeInfo.ActorTypeName; + this.Logger = host.LoggerFactory.CreateLogger(this.GetType()); + } - /// - /// The Logger - /// - protected ILogger Logger { get; } + /// + /// Gets the identity of this actor. + /// + /// The for the actor. + public ActorId Id => Host.Id; - /// - /// Initializes a new instance of the class. - /// - /// The that will host this actor instance. - protected Actor(ActorHost host) + /// + /// Gets the host of this actor within the actor runtime. + /// + /// The for the actor. + public ActorHost Host { get; } + + /// + /// Gets the used to create proxy client instances for communicating + /// with other actors. + /// + public IActorProxyFactory ProxyFactory => this.Host.ProxyFactory; + + /// + /// Gets the StateManager for the actor. + /// + public IActorStateManager StateManager { get; protected set; } + + internal async Task OnActivateInternalAsync() + { + await this.ResetStateAsync(); + await this.OnActivateAsync(); + + this.Logger.LogDebug("Activated"); + + // Save any state modifications done in user overridden Activate method. + await this.SaveStateAsync(); + } + + internal async Task OnDeactivateInternalAsync() + { + this.Logger.LogDebug("Deactivating ..."); + await this.ResetStateAsync(); + await this.OnDeactivateAsync(); + this.Logger.LogDebug("Deactivated"); + } + + internal Task OnPreActorMethodAsyncInternal(ActorMethodContext actorMethodContext) + { + return this.OnPreActorMethodAsync(actorMethodContext); + } + + internal async Task OnPostActorMethodAsyncInternal(ActorMethodContext actorMethodContext) + { + await this.OnPostActorMethodAsync(actorMethodContext); + await this.SaveStateAsync(); + } + + internal async Task OnActorMethodFailedInternalAsync(ActorMethodContext actorMethodContext, Exception e) + { + await this.OnActorMethodFailedAsync(actorMethodContext, e); + // Exception has been thrown by user code, reset the state in state manager. + await this.ResetStateAsync(); + } + + internal Task ResetStateAsync() + { + // Exception has been thrown by user code, reset the state in state manager. + return this.StateManager.ClearCacheAsync(); + } + + /// + /// Saves all the state changes (add/update/remove) that were made since last call to + /// , + /// to the actor state provider associated with the actor. + /// + /// A task that represents the asynchronous save operation. + protected async Task SaveStateAsync() + { + await this.StateManager.SaveStateAsync(); + } + + /// + /// Override this method to initialize the members, initialize state or register timers. This method is called right after the actor is activated + /// and before any method call or reminders are dispatched on it. + /// + /// A Task that represents outstanding OnActivateAsync operation. + protected virtual Task OnActivateAsync() + { + return Task.CompletedTask; + } + + /// + /// Override this method to release any resources. This method is called when actor is deactivated (garbage collected by Actor Runtime). + /// Actor operations like state changes should not be called from this method. + /// + /// A Task that represents outstanding OnDeactivateAsync operation. + protected virtual Task OnDeactivateAsync() + { + return Task.CompletedTask; + } + + /// + /// Override this method for performing any action prior to an actor method is invoked. + /// This method is invoked by actor runtime just before invoking an actor method. + /// + /// + /// An describing the method that will be invoked by actor runtime after this method finishes. + /// + /// + /// Returns a Task representing pre-actor-method operation. + /// + /// + /// This method is invoked by actor runtime prior to: + /// + /// Invoking an actor interface method when a client request comes. + /// Invoking a method when a reminder fires. + /// Invoking a timer callback when timer fires. + /// + /// + protected virtual Task OnPreActorMethodAsync(ActorMethodContext actorMethodContext) + { + return Task.CompletedTask; + } + + /// + /// Override this method for performing any action after an actor method has finished execution. + /// This method is invoked by actor runtime an actor method has finished execution. + /// + /// + /// An describing the method that was invoked by actor runtime prior to this method. + /// + /// + /// Returns a Task representing post-actor-method operation. + /// + /// /// + /// This method is invoked by actor runtime prior to: + /// + /// Invoking an actor interface method when a client request comes. + /// Invoking a method when a reminder fires. + /// Invoking a timer callback when timer fires. + /// + /// + protected virtual Task OnPostActorMethodAsync(ActorMethodContext actorMethodContext) + { + return Task.CompletedTask; + } + + /// + /// Override this method for performing any action when invoking actor method has thrown an exception. + /// This method is invoked by actor runtime when invoking actor method has thrown an exception. + /// + /// + /// An describing the method that was invoked by actor runtime prior to this method. + /// + /// Exception thrown by either actor method or by OnPreActorMethodAsync/OnPostActorMethodAsync overriden methods. + /// + /// Returns a Task representing post-actor-method operation. + /// + /// /// + /// This method is invoked by actor runtime prior to: + /// + /// Invoking an actor interface method when a client request comes. + /// Invoking a method when a reminder fires. + /// Invoking a timer callback when timer fires. + /// + /// + protected virtual Task OnActorMethodFailedAsync(ActorMethodContext actorMethodContext, Exception e) + { + return Task.CompletedTask; + } + + /// + /// Registers a reminder with the actor. + /// + /// The name of the reminder to register. The name must be unique per actor. + /// User state passed to the reminder invocation. + /// The amount of time to delay before invoking the reminder for the first time. Specify negative one (-1) milliseconds to disable invocation. Specify zero (0) to invoke the reminder immediately after registration. + /// + /// + /// The time interval between reminder invocations after the first invocation. Specify negative one (-1) milliseconds to disable periodic invocation. + /// + /// + /// A task that represents the asynchronous registration operation. The result of the task provides information about the registered reminder and is used to unregister the reminder using UnregisterReminderAsync />. + /// + /// + /// + /// The class deriving from must implement to consume reminder invocations. Multiple reminders can be registered at any time, uniquely identified by . Existing reminders can also be updated by calling this method again. Reminder invocations are synchronized both with other reminders and other actor method callbacks. + /// + /// + protected async Task RegisterReminderAsync( + string reminderName, + byte[] state, + TimeSpan dueTime, + TimeSpan period) + { + return await RegisterReminderAsync(new ActorReminderOptions { - this.Host = host; - this.StateManager = new ActorStateManager(this); - this.actorTypeName = this.Host.ActorTypeInfo.ActorTypeName; - this.Logger = host.LoggerFactory.CreateLogger(this.GetType()); - } + ActorTypeName = this.actorTypeName, + Id = this.Id, + ReminderName = reminderName, + State = state, + DueTime = dueTime, + Period = period, + Ttl = null + }); + } - /// - /// Gets the identity of this actor. - /// - /// The for the actor. - public ActorId Id => Host.Id; - - /// - /// Gets the host of this actor within the actor runtime. - /// - /// The for the actor. - public ActorHost Host { get; } - - /// - /// Gets the used to create proxy client instances for communicating - /// with other actors. - /// - public IActorProxyFactory ProxyFactory => this.Host.ProxyFactory; - - /// - /// Gets the StateManager for the actor. - /// - public IActorStateManager StateManager { get; protected set; } - - internal async Task OnActivateInternalAsync() + /// + /// Registers a reminder with the actor. + /// + /// The name of the reminder to register. The name must be unique per actor. + /// User state passed to the reminder invocation. + /// The amount of time to delay before invoking the reminder for the first time. Specify negative one (-1) milliseconds to disable invocation. Specify zero (0) to invoke the reminder immediately after registration. + /// + /// + /// The time interval between reminder invocations after the first invocation. Specify negative one (-1) milliseconds to disable periodic invocation. + /// + /// The time interval after which the reminder will expire. + /// + /// A task that represents the asynchronous registration operation. The result of the task provides information about the registered reminder and is used to unregister the reminder using UnregisterReminderAsync />. + /// + /// + /// + /// The class deriving from must implement to consume reminder invocations. Multiple reminders can be registered at any time, uniquely identified by . Existing reminders can also be updated by calling this method again. Reminder invocations are synchronized both with other reminders and other actor method callbacks. + /// + /// + protected async Task RegisterReminderAsync( + string reminderName, + byte[] state, + TimeSpan dueTime, + TimeSpan period, + TimeSpan ttl) + { + return await RegisterReminderAsync(new ActorReminderOptions { - await this.ResetStateAsync(); - await this.OnActivateAsync(); - - this.Logger.LogDebug("Activated"); - - // Save any state modifications done in user overridden Activate method. - await this.SaveStateAsync(); - } - - internal async Task OnDeactivateInternalAsync() - { - this.Logger.LogDebug("Deactivating ..."); - await this.ResetStateAsync(); - await this.OnDeactivateAsync(); - this.Logger.LogDebug("Deactivated"); - } - - internal Task OnPreActorMethodAsyncInternal(ActorMethodContext actorMethodContext) - { - return this.OnPreActorMethodAsync(actorMethodContext); - } - - internal async Task OnPostActorMethodAsyncInternal(ActorMethodContext actorMethodContext) - { - await this.OnPostActorMethodAsync(actorMethodContext); - await this.SaveStateAsync(); - } - - internal async Task OnActorMethodFailedInternalAsync(ActorMethodContext actorMethodContext, Exception e) - { - await this.OnActorMethodFailedAsync(actorMethodContext, e); - // Exception has been thrown by user code, reset the state in state manager. - await this.ResetStateAsync(); - } - - internal Task ResetStateAsync() - { - // Exception has been thrown by user code, reset the state in state manager. - return this.StateManager.ClearCacheAsync(); - } - - /// - /// Saves all the state changes (add/update/remove) that were made since last call to - /// , - /// to the actor state provider associated with the actor. - /// - /// A task that represents the asynchronous save operation. - protected async Task SaveStateAsync() - { - await this.StateManager.SaveStateAsync(); - } - - /// - /// Override this method to initialize the members, initialize state or register timers. This method is called right after the actor is activated - /// and before any method call or reminders are dispatched on it. - /// - /// A Task that represents outstanding OnActivateAsync operation. - protected virtual Task OnActivateAsync() - { - return Task.CompletedTask; - } - - /// - /// Override this method to release any resources. This method is called when actor is deactivated (garbage collected by Actor Runtime). - /// Actor operations like state changes should not be called from this method. - /// - /// A Task that represents outstanding OnDeactivateAsync operation. - protected virtual Task OnDeactivateAsync() - { - return Task.CompletedTask; - } - - /// - /// Override this method for performing any action prior to an actor method is invoked. - /// This method is invoked by actor runtime just before invoking an actor method. - /// - /// - /// An describing the method that will be invoked by actor runtime after this method finishes. - /// - /// - /// Returns a Task representing pre-actor-method operation. - /// - /// - /// This method is invoked by actor runtime prior to: - /// - /// Invoking an actor interface method when a client request comes. - /// Invoking a method when a reminder fires. - /// Invoking a timer callback when timer fires. - /// - /// - protected virtual Task OnPreActorMethodAsync(ActorMethodContext actorMethodContext) - { - return Task.CompletedTask; - } - - /// - /// Override this method for performing any action after an actor method has finished execution. - /// This method is invoked by actor runtime an actor method has finished execution. - /// - /// - /// An describing the method that was invoked by actor runtime prior to this method. - /// - /// - /// Returns a Task representing post-actor-method operation. - /// - /// /// - /// This method is invoked by actor runtime prior to: - /// - /// Invoking an actor interface method when a client request comes. - /// Invoking a method when a reminder fires. - /// Invoking a timer callback when timer fires. - /// - /// - protected virtual Task OnPostActorMethodAsync(ActorMethodContext actorMethodContext) - { - return Task.CompletedTask; - } - - /// - /// Override this method for performing any action when invoking actor method has thrown an exception. - /// This method is invoked by actor runtime when invoking actor method has thrown an exception. - /// - /// - /// An describing the method that was invoked by actor runtime prior to this method. - /// - /// Exception thrown by either actor method or by OnPreActorMethodAsync/OnPostActorMethodAsync overriden methods. - /// - /// Returns a Task representing post-actor-method operation. - /// - /// /// - /// This method is invoked by actor runtime prior to: - /// - /// Invoking an actor interface method when a client request comes. - /// Invoking a method when a reminder fires. - /// Invoking a timer callback when timer fires. - /// - /// - protected virtual Task OnActorMethodFailedAsync(ActorMethodContext actorMethodContext, Exception e) - { - return Task.CompletedTask; - } - - /// - /// Registers a reminder with the actor. - /// - /// The name of the reminder to register. The name must be unique per actor. - /// User state passed to the reminder invocation. - /// The amount of time to delay before invoking the reminder for the first time. Specify negative one (-1) milliseconds to disable invocation. Specify zero (0) to invoke the reminder immediately after registration. - /// - /// - /// The time interval between reminder invocations after the first invocation. Specify negative one (-1) milliseconds to disable periodic invocation. - /// - /// - /// A task that represents the asynchronous registration operation. The result of the task provides information about the registered reminder and is used to unregister the reminder using UnregisterReminderAsync />. - /// - /// - /// - /// The class deriving from must implement to consume reminder invocations. Multiple reminders can be registered at any time, uniquely identified by . Existing reminders can also be updated by calling this method again. Reminder invocations are synchronized both with other reminders and other actor method callbacks. - /// - /// - protected async Task RegisterReminderAsync( - string reminderName, - byte[] state, - TimeSpan dueTime, - TimeSpan period) - { - return await RegisterReminderAsync(new ActorReminderOptions - { - ActorTypeName = this.actorTypeName, - Id = this.Id, - ReminderName = reminderName, - State = state, - DueTime = dueTime, - Period = period, - Ttl = null - }); - } - - /// - /// Registers a reminder with the actor. - /// - /// The name of the reminder to register. The name must be unique per actor. - /// User state passed to the reminder invocation. - /// The amount of time to delay before invoking the reminder for the first time. Specify negative one (-1) milliseconds to disable invocation. Specify zero (0) to invoke the reminder immediately after registration. - /// - /// - /// The time interval between reminder invocations after the first invocation. Specify negative one (-1) milliseconds to disable periodic invocation. - /// - /// The time interval after which the reminder will expire. - /// - /// A task that represents the asynchronous registration operation. The result of the task provides information about the registered reminder and is used to unregister the reminder using UnregisterReminderAsync />. - /// - /// - /// - /// The class deriving from must implement to consume reminder invocations. Multiple reminders can be registered at any time, uniquely identified by . Existing reminders can also be updated by calling this method again. Reminder invocations are synchronized both with other reminders and other actor method callbacks. - /// - /// - protected async Task RegisterReminderAsync( - string reminderName, - byte[] state, - TimeSpan dueTime, - TimeSpan period, - TimeSpan ttl) - { - return await RegisterReminderAsync(new ActorReminderOptions - { - ActorTypeName = this.actorTypeName, - Id = this.Id, - ReminderName = reminderName, - State = state, - DueTime = dueTime, - Period = period, - Ttl = ttl - }); - } + ActorTypeName = this.actorTypeName, + Id = this.Id, + ReminderName = reminderName, + State = state, + DueTime = dueTime, + Period = period, + Ttl = ttl + }); + } - /// - /// Registers a reminder with the actor. - /// - /// The name of the reminder to register. The name must be unique per actor. - /// User state passed to the reminder invocation. - /// The amount of time to delay before invoking the reminder for the first time. Specify negative one (-1) milliseconds to disable invocation. Specify zero (0) to invoke the reminder immediately after registration. - /// - /// The time interval between reminder invocations after the first invocation. - /// - /// The number of repetitions for which the reminder should be invoked. - /// The time interval after which the reminder will expire. - /// - /// A task that represents the asynchronous registration operation. The result of the task provides information about the registered reminder and is used to unregister the reminder using UnregisterReminderAsync. - /// - /// - /// - /// The class deriving from must implement to consume reminder invocations. Multiple reminders can be registered at any time, uniquely identified by . Existing reminders can also be updated by calling this method again. Reminder invocations are synchronized both with other reminders and other actor method callbacks. - /// - /// - protected async Task RegisterReminderAsync( - string reminderName, - byte[] state, - TimeSpan dueTime, - TimeSpan period, - int repetitions, - TimeSpan ttl) + /// + /// Registers a reminder with the actor. + /// + /// The name of the reminder to register. The name must be unique per actor. + /// User state passed to the reminder invocation. + /// The amount of time to delay before invoking the reminder for the first time. Specify negative one (-1) milliseconds to disable invocation. Specify zero (0) to invoke the reminder immediately after registration. + /// + /// The time interval between reminder invocations after the first invocation. + /// + /// The number of repetitions for which the reminder should be invoked. + /// The time interval after which the reminder will expire. + /// + /// A task that represents the asynchronous registration operation. The result of the task provides information about the registered reminder and is used to unregister the reminder using UnregisterReminderAsync. + /// + /// + /// + /// The class deriving from must implement to consume reminder invocations. Multiple reminders can be registered at any time, uniquely identified by . Existing reminders can also be updated by calling this method again. Reminder invocations are synchronized both with other reminders and other actor method callbacks. + /// + /// + protected async Task RegisterReminderAsync( + string reminderName, + byte[] state, + TimeSpan dueTime, + TimeSpan period, + int repetitions, + TimeSpan ttl) + { + return await RegisterReminderAsync(new ActorReminderOptions { - return await RegisterReminderAsync(new ActorReminderOptions - { - ActorTypeName = this.actorTypeName, - Id = this.Id, - ReminderName = reminderName, - State = state, - DueTime = dueTime, - Period = period, - Repetitions = repetitions, - Ttl = ttl - }); - } + ActorTypeName = this.actorTypeName, + Id = this.Id, + ReminderName = reminderName, + State = state, + DueTime = dueTime, + Period = period, + Repetitions = repetitions, + Ttl = ttl + }); + } - /// - /// Registers a reminder with the actor. - /// - /// The name of the reminder to register. The name must be unique per actor. - /// User state passed to the reminder invocation. - /// The amount of time to delay before invoking the reminder for the first time. Specify negative one (-1) milliseconds to disable invocation. Specify zero (0) to invoke the reminder immediately after registration. - /// - /// The time interval between reminder invocations after the first invocation. - /// - /// The number of repetitions for which the reminder should be invoked. - /// - /// A task that represents the asynchronous registration operation. The result of the task provides information about the registered reminder and is used to unregister the reminder using UnregisterReminderAsync. - /// - /// - /// - /// The class deriving from must implement to consume reminder invocations. Multiple reminders can be registered at any time, uniquely identified by . Existing reminders can also be updated by calling this method again. Reminder invocations are synchronized both with other reminders and other actor method callbacks. - /// - /// - protected async Task RegisterReminderAsync( - string reminderName, - byte[] state, - TimeSpan dueTime, - TimeSpan period, - int repetitions) + /// + /// Registers a reminder with the actor. + /// + /// The name of the reminder to register. The name must be unique per actor. + /// User state passed to the reminder invocation. + /// The amount of time to delay before invoking the reminder for the first time. Specify negative one (-1) milliseconds to disable invocation. Specify zero (0) to invoke the reminder immediately after registration. + /// + /// The time interval between reminder invocations after the first invocation. + /// + /// The number of repetitions for which the reminder should be invoked. + /// + /// A task that represents the asynchronous registration operation. The result of the task provides information about the registered reminder and is used to unregister the reminder using UnregisterReminderAsync. + /// + /// + /// + /// The class deriving from must implement to consume reminder invocations. Multiple reminders can be registered at any time, uniquely identified by . Existing reminders can also be updated by calling this method again. Reminder invocations are synchronized both with other reminders and other actor method callbacks. + /// + /// + protected async Task RegisterReminderAsync( + string reminderName, + byte[] state, + TimeSpan dueTime, + TimeSpan period, + int repetitions) + { + return await RegisterReminderAsync(new ActorReminderOptions { - return await RegisterReminderAsync(new ActorReminderOptions - { - ActorTypeName = this.actorTypeName, - Id = this.Id, - ReminderName = reminderName, - State = state, - DueTime = dueTime, - Period = period, - Repetitions = repetitions - }); + ActorTypeName = this.actorTypeName, + Id = this.Id, + ReminderName = reminderName, + State = state, + DueTime = dueTime, + Period = period, + Repetitions = repetitions + }); + } + + /// + /// Registers a reminder with the actor. + /// + /// A containing the various settings for an . + /// + /// A task that represents the asynchronous registration operation. The result of the task provides information about the registered reminder and is used to unregister the reminder using UnregisterReminderAsync />. + /// + /// + /// + /// The class deriving from must implement to consume reminder invocations. Multiple reminders can be registered at any time, uniquely identified by . Existing reminders can also be updated by calling this method again. Reminder invocations are synchronized both with other reminders and other actor method callbacks. + /// + /// + internal async Task RegisterReminderAsync(ActorReminderOptions options) + { + var reminder = new ActorReminder(options); + await this.Host.TimerManager.RegisterReminderAsync(reminder); + return reminder; + } + + /// + /// Gets a reminder previously registered using . + /// + /// The name of the reminder to get. + /// + /// Returns a task that represents the asynchronous get operation. The result of the task contains the reminder if it exists, otherwise null. + /// + protected async Task GetReminderAsync(string reminderName) + { + return await this.Host.TimerManager.GetReminderAsync(new ActorReminderToken(this.actorTypeName, this.Id, reminderName)); + } + + /// + /// Unregisters a reminder previously registered using . + /// + /// The actor reminder to unregister. + /// + /// Returns a task that represents the asynchronous unregistration operation. + /// + protected Task UnregisterReminderAsync(IActorReminder reminder) + { + var token = reminder is ActorReminderToken ? (ActorReminderToken)reminder : new ActorReminderToken(this.actorTypeName, this.Id, reminder.Name); + return this.Host.TimerManager.UnregisterReminderAsync(token); + } + + /// + /// Unregisters a reminder previously registered using . + /// + /// The actor reminder name to unregister. + /// + /// Returns a task that represents the asynchronous unregistration operation. + /// + protected Task UnregisterReminderAsync(string reminderName) + { + var token = new ActorReminderToken(this.actorTypeName, this.Id, reminderName); + return this.Host.TimerManager.UnregisterReminderAsync(token); + } + + /// + /// Registers a Timer for the actor. A timer name is autogenerated by the runtime to keep track of it. + /// + /// Timer Name. If a timer name is not provided, a timer is autogenerated. + /// + /// The name of the method to be called when the timer fires. + /// It has one parameter: the state object passed to RegisterTimer. + /// It returns a representing the asynchronous operation. + /// + /// An object containing information to be used by the callback method, or null. + /// The amount of time to delay before the async callback is first invoked. + /// Specify negative one (-1) milliseconds to prevent the timer from starting. + /// Specify zero (0) to start the timer immediately. + /// + /// + /// The time interval between invocations of the async callback. + /// Specify negative one (-1) milliseconds to disable periodic signaling. + /// Returns IActorTimer object. + public async Task RegisterTimerAsync( + string timerName, + string callback, + byte[] callbackParams, + TimeSpan dueTime, + TimeSpan period) + { + return await RegisterTimerAsync(new ActorTimerOptions + { + ActorTypeName = this.actorTypeName, + Id = this.Id, + TimerName = timerName, + TimerCallback = callback, + Data = callbackParams, + DueTime = dueTime, + Period = period, + Ttl = null + }); + } + + /// + /// Registers a Timer for the actor. A timer name is autogenerated by the runtime to keep track of it. + /// + /// Timer Name. If a timer name is not provided, a timer is autogenerated. + /// + /// The name of the method to be called when the timer fires. + /// It has one parameter: the state object passed to RegisterTimer. + /// It returns a representing the asynchronous operation. + /// + /// An object containing information to be used by the callback method, or null. + /// The amount of time to delay before the async callback is first invoked. + /// Specify negative one (-1) milliseconds to prevent the timer from starting. + /// Specify zero (0) to start the timer immediately. + /// + /// + /// The time interval between invocations of the async callback. + /// Specify negative one (-1) milliseconds to disable periodic signaling. + /// The time interval after which a Timer will expire. + /// Returns IActorTimer object. + public async Task RegisterTimerAsync( + string timerName, + string callback, + byte[] callbackParams, + TimeSpan dueTime, + TimeSpan period, + TimeSpan ttl) + { + return await RegisterTimerAsync(new ActorTimerOptions + { + ActorTypeName = this.actorTypeName, + Id = this.Id, + TimerName = timerName, + TimerCallback = callback, + Data = callbackParams, + DueTime = dueTime, + Period = period, + Ttl = ttl + }); + } + + internal async Task RegisterTimerAsync(ActorTimerOptions options) + { + // Validate that the timer callback specified meets all the required criteria for a valid callback method + this.ValidateTimerCallback(this.Host, options.TimerCallback); + + // create a timer name to register with Dapr runtime. + if (string.IsNullOrEmpty(options.TimerName)) + { + options.TimerName = $"{this.Id}_Timer_{Guid.NewGuid()}"; } - /// - /// Registers a reminder with the actor. - /// - /// A containing the various settings for an . - /// - /// A task that represents the asynchronous registration operation. The result of the task provides information about the registered reminder and is used to unregister the reminder using UnregisterReminderAsync />. - /// - /// - /// - /// The class deriving from must implement to consume reminder invocations. Multiple reminders can be registered at any time, uniquely identified by . Existing reminders can also be updated by calling this method again. Reminder invocations are synchronized both with other reminders and other actor method callbacks. - /// - /// - internal async Task RegisterReminderAsync(ActorReminderOptions options) + var actorTimer = new ActorTimer(options); + await this.Host.TimerManager.RegisterTimerAsync(actorTimer); + return actorTimer; + } + + /// + /// Unregisters a Timer previously set on this actor. + /// + /// An IActorTimer representing timer that needs to be unregistered. + /// Task representing the Unregister timer operation. + protected async Task UnregisterTimerAsync(ActorTimer timer) + { + await this.Host.TimerManager.UnregisterTimerAsync(timer); + } + + /// + /// Unregisters a Timer previously set on this actor. + /// + /// Name of timer to unregister. + /// Task representing the Unregister timer operation. + protected async Task UnregisterTimerAsync(string timerName) + { + var token = new ActorTimerToken(this.actorTypeName, this.Id, timerName); + await this.Host.TimerManager.UnregisterTimerAsync(token); + } + + internal MethodInfo GetMethodInfoUsingReflection(Type actorType, string callback) + { + return actorType.GetMethod(callback, BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Static); + } + + internal void ValidateTimerCallback(ActorHost host, string callback) + { + var actorTypeName = host.ActorTypeInfo.ActorTypeName; + var actorType = host.ActorTypeInfo.ImplementationType; + + MethodInfo methodInfo; + try { - var reminder = new ActorReminder(options); - await this.Host.TimerManager.RegisterReminderAsync(reminder); - return reminder; + methodInfo = this.GetMethodInfoUsingReflection(actorType, callback); + } + catch (AmbiguousMatchException) + { + // GetMethod will throw an AmbiguousMatchException if more than one methods are found with the same name + throw new ArgumentException($"Timer callback method: {callback} cannot be overloaded."); } - /// - /// Gets a reminder previously registered using . - /// - /// The name of the reminder to get. - /// - /// Returns a task that represents the asynchronous get operation. The result of the task contains the reminder if it exists, otherwise null. - /// - protected async Task GetReminderAsync(string reminderName) + // Check if the method exists + if (methodInfo == null) { - return await this.Host.TimerManager.GetReminderAsync(new ActorReminderToken(this.actorTypeName, this.Id, reminderName)); + throw new ArgumentException($"Timer callback method: {callback} does not exist in the Actor class: {actorTypeName}"); } - /// - /// Unregisters a reminder previously registered using . - /// - /// The actor reminder to unregister. - /// - /// Returns a task that represents the asynchronous unregistration operation. - /// - protected Task UnregisterReminderAsync(IActorReminder reminder) + // The timer callback can accept only 0 or 1 parameters + var parameters = methodInfo.GetParameters(); + if (parameters.Length > 1) { - var token = reminder is ActorReminderToken ? (ActorReminderToken)reminder : new ActorReminderToken(this.actorTypeName, this.Id, reminder.Name); - return this.Host.TimerManager.UnregisterReminderAsync(token); + throw new ArgumentException("Timer callback can accept only zero or one parameters"); } - /// - /// Unregisters a reminder previously registered using . - /// - /// The actor reminder name to unregister. - /// - /// Returns a task that represents the asynchronous unregistration operation. - /// - protected Task UnregisterReminderAsync(string reminderName) + // The timer callback should return only Task return type + if (methodInfo.ReturnType != typeof(Task)) { - var token = new ActorReminderToken(this.actorTypeName, this.Id, reminderName); - return this.Host.TimerManager.UnregisterReminderAsync(token); - } - - /// - /// Registers a Timer for the actor. A timer name is autogenerated by the runtime to keep track of it. - /// - /// Timer Name. If a timer name is not provided, a timer is autogenerated. - /// - /// The name of the method to be called when the timer fires. - /// It has one parameter: the state object passed to RegisterTimer. - /// It returns a representing the asynchronous operation. - /// - /// An object containing information to be used by the callback method, or null. - /// The amount of time to delay before the async callback is first invoked. - /// Specify negative one (-1) milliseconds to prevent the timer from starting. - /// Specify zero (0) to start the timer immediately. - /// - /// - /// The time interval between invocations of the async callback. - /// Specify negative one (-1) milliseconds to disable periodic signaling. - /// Returns IActorTimer object. - public async Task RegisterTimerAsync( - string timerName, - string callback, - byte[] callbackParams, - TimeSpan dueTime, - TimeSpan period) - { - return await RegisterTimerAsync(new ActorTimerOptions - { - ActorTypeName = this.actorTypeName, - Id = this.Id, - TimerName = timerName, - TimerCallback = callback, - Data = callbackParams, - DueTime = dueTime, - Period = period, - Ttl = null - }); - } - - /// - /// Registers a Timer for the actor. A timer name is autogenerated by the runtime to keep track of it. - /// - /// Timer Name. If a timer name is not provided, a timer is autogenerated. - /// - /// The name of the method to be called when the timer fires. - /// It has one parameter: the state object passed to RegisterTimer. - /// It returns a representing the asynchronous operation. - /// - /// An object containing information to be used by the callback method, or null. - /// The amount of time to delay before the async callback is first invoked. - /// Specify negative one (-1) milliseconds to prevent the timer from starting. - /// Specify zero (0) to start the timer immediately. - /// - /// - /// The time interval between invocations of the async callback. - /// Specify negative one (-1) milliseconds to disable periodic signaling. - /// The time interval after which a Timer will expire. - /// Returns IActorTimer object. - public async Task RegisterTimerAsync( - string timerName, - string callback, - byte[] callbackParams, - TimeSpan dueTime, - TimeSpan period, - TimeSpan ttl) - { - return await RegisterTimerAsync(new ActorTimerOptions - { - ActorTypeName = this.actorTypeName, - Id = this.Id, - TimerName = timerName, - TimerCallback = callback, - Data = callbackParams, - DueTime = dueTime, - Period = period, - Ttl = ttl - }); - } - - internal async Task RegisterTimerAsync(ActorTimerOptions options) - { - // Validate that the timer callback specified meets all the required criteria for a valid callback method - this.ValidateTimerCallback(this.Host, options.TimerCallback); - - // create a timer name to register with Dapr runtime. - if (string.IsNullOrEmpty(options.TimerName)) - { - options.TimerName = $"{this.Id}_Timer_{Guid.NewGuid()}"; - } - - var actorTimer = new ActorTimer(options); - await this.Host.TimerManager.RegisterTimerAsync(actorTimer); - return actorTimer; - } - - /// - /// Unregisters a Timer previously set on this actor. - /// - /// An IActorTimer representing timer that needs to be unregistered. - /// Task representing the Unregister timer operation. - protected async Task UnregisterTimerAsync(ActorTimer timer) - { - await this.Host.TimerManager.UnregisterTimerAsync(timer); - } - - /// - /// Unregisters a Timer previously set on this actor. - /// - /// Name of timer to unregister. - /// Task representing the Unregister timer operation. - protected async Task UnregisterTimerAsync(string timerName) - { - var token = new ActorTimerToken(this.actorTypeName, this.Id, timerName); - await this.Host.TimerManager.UnregisterTimerAsync(token); - } - - internal MethodInfo GetMethodInfoUsingReflection(Type actorType, string callback) - { - return actorType.GetMethod(callback, BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Static); - } - - internal void ValidateTimerCallback(ActorHost host, string callback) - { - var actorTypeName = host.ActorTypeInfo.ActorTypeName; - var actorType = host.ActorTypeInfo.ImplementationType; - - MethodInfo methodInfo; - try - { - methodInfo = this.GetMethodInfoUsingReflection(actorType, callback); - } - catch (AmbiguousMatchException) - { - // GetMethod will throw an AmbiguousMatchException if more than one methods are found with the same name - throw new ArgumentException($"Timer callback method: {callback} cannot be overloaded."); - } - - // Check if the method exists - if (methodInfo == null) - { - throw new ArgumentException($"Timer callback method: {callback} does not exist in the Actor class: {actorTypeName}"); - } - - // The timer callback can accept only 0 or 1 parameters - var parameters = methodInfo.GetParameters(); - if (parameters.Length > 1) - { - throw new ArgumentException("Timer callback can accept only zero or one parameters"); - } - - // The timer callback should return only Task return type - if (methodInfo.ReturnType != typeof(Task)) - { - throw new ArgumentException("Timer callback can only return type Task"); - } + throw new ArgumentException("Timer callback can only return type Task"); } } -} +} \ No newline at end of file diff --git a/src/Dapr.Actors/Runtime/ActorActivator.cs b/src/Dapr.Actors/Runtime/ActorActivator.cs index 1d7708ce..b0fa29ae 100644 --- a/src/Dapr.Actors/Runtime/ActorActivator.cs +++ b/src/Dapr.Actors/Runtime/ActorActivator.cs @@ -13,40 +13,39 @@ using System.Threading.Tasks; -namespace Dapr.Actors.Runtime +namespace Dapr.Actors.Runtime; + +/// +/// An abstraction for implementing the creation and deletion of actor instances. +/// +public abstract class ActorActivator { /// - /// An abstraction for implementing the creation and deletion of actor instances. + /// Creates the actor instance and returns it inside an instance of . /// - public abstract class ActorActivator - { - /// - /// Creates the actor instance and returns it inside an instance of . - /// - /// The actor host specifying information needed for the creation of the actor. - /// - /// Asynchronously returns n instance of . The - /// instance will be provided to when the actor is ready for deletion. - /// - /// - /// Implementations should not interact with lifecycle callback methods on the type. - /// These methods will be called by the runtime. - /// - public abstract Task CreateAsync(ActorHost host); + /// The actor host specifying information needed for the creation of the actor. + /// + /// Asynchronously returns n instance of . The + /// instance will be provided to when the actor is ready for deletion. + /// + /// + /// Implementations should not interact with lifecycle callback methods on the type. + /// These methods will be called by the runtime. + /// + public abstract Task CreateAsync(ActorHost host); - /// - /// Deletes the actor instance and cleans up all associated resources. - /// - /// - /// The instance that was created during creation of the actor. - /// - /// - /// A task that represents the asynchronous completion of the operation. - /// - /// - /// Implementations should not interact with lifecycle callback methods on the type. - /// These methods will be called by the runtime. - /// - public abstract Task DeleteAsync(ActorActivatorState state); - } -} + /// + /// Deletes the actor instance and cleans up all associated resources. + /// + /// + /// The instance that was created during creation of the actor. + /// + /// + /// A task that represents the asynchronous completion of the operation. + /// + /// + /// Implementations should not interact with lifecycle callback methods on the type. + /// These methods will be called by the runtime. + /// + public abstract Task DeleteAsync(ActorActivatorState state); +} \ No newline at end of file diff --git a/src/Dapr.Actors/Runtime/ActorActivatorFactory.cs b/src/Dapr.Actors/Runtime/ActorActivatorFactory.cs index 00394a5b..e25d12a1 100644 --- a/src/Dapr.Actors/Runtime/ActorActivatorFactory.cs +++ b/src/Dapr.Actors/Runtime/ActorActivatorFactory.cs @@ -11,19 +11,18 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Runtime +namespace Dapr.Actors.Runtime; + +/// +/// An abstraction used to construct an for a given +/// . +/// +public abstract class ActorActivatorFactory { /// - /// An abstraction used to construct an for a given - /// . + /// Creates the for the provided . /// - public abstract class ActorActivatorFactory - { - /// - /// Creates the for the provided . - /// - /// The . - /// An . - public abstract ActorActivator CreateActivator(ActorTypeInformation type); - } -} + /// The . + /// An . + public abstract ActorActivator CreateActivator(ActorTypeInformation type); +} \ No newline at end of file diff --git a/src/Dapr.Actors/Runtime/ActorActivatorState.cs b/src/Dapr.Actors/Runtime/ActorActivatorState.cs index e9e33e0a..4021657d 100644 --- a/src/Dapr.Actors/Runtime/ActorActivatorState.cs +++ b/src/Dapr.Actors/Runtime/ActorActivatorState.cs @@ -11,27 +11,26 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Runtime +namespace Dapr.Actors.Runtime; + +/// +/// A state object created by an implementation of . Implementations +/// can return a subclass of to associate additional data +/// with an Actor instance. +/// +public class ActorActivatorState { /// - /// A state object created by an implementation of . Implementations - /// can return a subclass of to associate additional data - /// with an Actor instance. + /// Initializes a new instance of . /// - public class ActorActivatorState + /// The instance. + public ActorActivatorState(Actor actor) { - /// - /// Initializes a new instance of . - /// - /// The instance. - public ActorActivatorState(Actor actor) - { - Actor = actor; - } - - /// - /// Gets the instance. - /// - public Actor Actor { get; } + Actor = actor; } -} + + /// + /// Gets the instance. + /// + public Actor Actor { get; } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Runtime/ActorAttribute.cs b/src/Dapr.Actors/Runtime/ActorAttribute.cs index f4869dbe..4cf6b539 100644 --- a/src/Dapr.Actors/Runtime/ActorAttribute.cs +++ b/src/Dapr.Actors/Runtime/ActorAttribute.cs @@ -11,22 +11,21 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Runtime -{ - using System; +namespace Dapr.Actors.Runtime; +using System; + +/// +/// Contains optional properties related to an actor implementation. +/// +/// Intended to be attached to actor implementation types (i.e.those derived from ). +[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] +public sealed class ActorAttribute : Attribute +{ /// - /// Contains optional properties related to an actor implementation. + /// Gets or sets the name of the actor type represented by the actor. /// - /// Intended to be attached to actor implementation types (i.e.those derived from ). - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] - public sealed class ActorAttribute : Attribute - { - /// - /// Gets or sets the name of the actor type represented by the actor. - /// - /// The name of the actor type represented by the actor. - /// If set, this value will override the default actor type name derived from the actor implementation type. - public string TypeName { get; set; } - } + /// The name of the actor type represented by the actor. + /// If set, this value will override the default actor type name derived from the actor implementation type. + public string TypeName { get; set; } } \ No newline at end of file diff --git a/src/Dapr.Actors/Runtime/ActorCallType.cs b/src/Dapr.Actors/Runtime/ActorCallType.cs index 0b961d49..c128f695 100644 --- a/src/Dapr.Actors/Runtime/ActorCallType.cs +++ b/src/Dapr.Actors/Runtime/ActorCallType.cs @@ -11,30 +11,29 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Runtime +namespace Dapr.Actors.Runtime; + +/// +/// Represents the call-type associated with the method invoked by actor runtime. +/// +/// +/// This is provided as part of which is passed as argument to +/// and . +/// +public enum ActorCallType { /// - /// Represents the call-type associated with the method invoked by actor runtime. + /// Specifies that the method invoked is an actor interface method for a given client request. /// - /// - /// This is provided as part of which is passed as argument to - /// and . - /// - public enum ActorCallType - { - /// - /// Specifies that the method invoked is an actor interface method for a given client request. - /// - ActorInterfaceMethod = 0, + ActorInterfaceMethod = 0, - /// - /// Specifies that the method invoked is a timer callback method. - /// - TimerMethod = 1, + /// + /// Specifies that the method invoked is a timer callback method. + /// + TimerMethod = 1, - /// - /// Specifies that the method is when a reminder fires. - /// - ReminderMethod = 2, - } -} + /// + /// Specifies that the method is when a reminder fires. + /// + ReminderMethod = 2, +} \ No newline at end of file diff --git a/src/Dapr.Actors/Runtime/ActorHost.cs b/src/Dapr.Actors/Runtime/ActorHost.cs index dca0edb6..fab64e67 100644 --- a/src/Dapr.Actors/Runtime/ActorHost.cs +++ b/src/Dapr.Actors/Runtime/ActorHost.cs @@ -11,156 +11,155 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Runtime +namespace Dapr.Actors.Runtime; + +using System; +using System.Text.Json; +using Dapr.Actors.Client; +using Microsoft.Extensions.Logging; + +/// +/// Represents a host for an actor type within the actor runtime. +/// +public sealed class ActorHost { - using System; - using System.Text.Json; - using Dapr.Actors.Client; - using Microsoft.Extensions.Logging; + /// + /// Creates an instance of for unit testing an actor instance. + /// + /// The for configuring the host. + /// The actor type. + /// An instance. + public static ActorHost CreateForTest(ActorTestOptions options = null) + where TActor : Actor + { + return CreateForTest(typeof(TActor), actorTypeName: null, options); + } /// - /// Represents a host for an actor type within the actor runtime. + /// Creates an instance of for unit testing an actor instance. /// - public sealed class ActorHost + /// The name of the actor type represented by the actor. + /// The for configuring the host. + /// The actor type. + /// An instance. + public static ActorHost CreateForTest(string actorTypeName, ActorTestOptions options = null) + where TActor : Actor { - /// - /// Creates an instance of for unit testing an actor instance. - /// - /// The for configuring the host. - /// The actor type. - /// An instance. - public static ActorHost CreateForTest(ActorTestOptions options = null) - where TActor : Actor - { - return CreateForTest(typeof(TActor), actorTypeName: null, options); - } - - /// - /// Creates an instance of for unit testing an actor instance. - /// - /// The name of the actor type represented by the actor. - /// The for configuring the host. - /// The actor type. - /// An instance. - public static ActorHost CreateForTest(string actorTypeName, ActorTestOptions options = null) - where TActor : Actor - { - return CreateForTest(typeof(TActor), actorTypeName, options); - } - - /// - /// Creates an instance of for unit testing an actor instance. - /// - /// The actor type. - /// The for configuring the host. - /// An instance. - public static ActorHost CreateForTest(Type actorType, ActorTestOptions options = null) - { - return CreateForTest(actorType, actorTypeName: null, options); - } - - /// - /// Creates an instance of for unit testing an actor instance. - /// - /// The actor type. - /// The name of the actor type represented by the actor. - /// The for configuring the host. - /// An instance. - public static ActorHost CreateForTest(Type actorType, string actorTypeName, ActorTestOptions options = null) - { - if (actorType == null) - { - throw new ArgumentNullException(nameof(actorType)); - } - - options ??= new ActorTestOptions(); - - return new ActorHost( - ActorTypeInformation.Get(actorType, actorTypeName), - options.ActorId, - options.JsonSerializerOptions, - options.LoggerFactory, - options.ProxyFactory, - options.TimerManager); - } - - /// - /// Initializes a new instance of the class. - /// - /// The type information of the Actor. - /// The id of the Actor instance. - /// The to use for actor state persistence and message deserialization. - /// The logger factory. - /// The . - [Obsolete("Application code should not call this method. Use CreateForTest for unit testing.")] - public ActorHost( - ActorTypeInformation actorTypeInfo, - ActorId id, - JsonSerializerOptions jsonSerializerOptions, - ILoggerFactory loggerFactory, - IActorProxyFactory proxyFactory) - { - this.ActorTypeInfo = actorTypeInfo; - this.Id = id; - this.LoggerFactory = loggerFactory; - this.ProxyFactory = proxyFactory; - } - - /// - /// Initializes a new instance of the class. - /// - /// The type information of the Actor. - /// The id of the Actor instance. - /// The to use for actor state persistence and message deserialization. - /// The logger factory. - /// The . - /// The . - internal ActorHost( - ActorTypeInformation actorTypeInfo, - ActorId id, - JsonSerializerOptions jsonSerializerOptions, - ILoggerFactory loggerFactory, - IActorProxyFactory proxyFactory, - ActorTimerManager timerManager) - { - this.ActorTypeInfo = actorTypeInfo; - this.Id = id; - this.JsonSerializerOptions = jsonSerializerOptions; - this.LoggerFactory = loggerFactory; - this.ProxyFactory = proxyFactory; - this.TimerManager = timerManager; - } - - /// - /// Gets the ActorTypeInformation for actor service. - /// - public ActorTypeInformation ActorTypeInfo { get; } - - /// - /// Gets the . - /// - public ActorId Id { get; } - - /// - /// Gets the LoggerFactory for actor service - /// - public ILoggerFactory LoggerFactory { get; } - - /// - /// Gets the . - /// - public JsonSerializerOptions JsonSerializerOptions { get; } - - /// - /// Gets the . - /// - public IActorProxyFactory ProxyFactory { get; } - - /// - /// Gets the . - /// - public ActorTimerManager TimerManager { get; } - - internal DaprStateProvider StateProvider { get; set; } + return CreateForTest(typeof(TActor), actorTypeName, options); } -} + + /// + /// Creates an instance of for unit testing an actor instance. + /// + /// The actor type. + /// The for configuring the host. + /// An instance. + public static ActorHost CreateForTest(Type actorType, ActorTestOptions options = null) + { + return CreateForTest(actorType, actorTypeName: null, options); + } + + /// + /// Creates an instance of for unit testing an actor instance. + /// + /// The actor type. + /// The name of the actor type represented by the actor. + /// The for configuring the host. + /// An instance. + public static ActorHost CreateForTest(Type actorType, string actorTypeName, ActorTestOptions options = null) + { + if (actorType == null) + { + throw new ArgumentNullException(nameof(actorType)); + } + + options ??= new ActorTestOptions(); + + return new ActorHost( + ActorTypeInformation.Get(actorType, actorTypeName), + options.ActorId, + options.JsonSerializerOptions, + options.LoggerFactory, + options.ProxyFactory, + options.TimerManager); + } + + /// + /// Initializes a new instance of the class. + /// + /// The type information of the Actor. + /// The id of the Actor instance. + /// The to use for actor state persistence and message deserialization. + /// The logger factory. + /// The . + [Obsolete("Application code should not call this method. Use CreateForTest for unit testing.")] + public ActorHost( + ActorTypeInformation actorTypeInfo, + ActorId id, + JsonSerializerOptions jsonSerializerOptions, + ILoggerFactory loggerFactory, + IActorProxyFactory proxyFactory) + { + this.ActorTypeInfo = actorTypeInfo; + this.Id = id; + this.LoggerFactory = loggerFactory; + this.ProxyFactory = proxyFactory; + } + + /// + /// Initializes a new instance of the class. + /// + /// The type information of the Actor. + /// The id of the Actor instance. + /// The to use for actor state persistence and message deserialization. + /// The logger factory. + /// The . + /// The . + internal ActorHost( + ActorTypeInformation actorTypeInfo, + ActorId id, + JsonSerializerOptions jsonSerializerOptions, + ILoggerFactory loggerFactory, + IActorProxyFactory proxyFactory, + ActorTimerManager timerManager) + { + this.ActorTypeInfo = actorTypeInfo; + this.Id = id; + this.JsonSerializerOptions = jsonSerializerOptions; + this.LoggerFactory = loggerFactory; + this.ProxyFactory = proxyFactory; + this.TimerManager = timerManager; + } + + /// + /// Gets the ActorTypeInformation for actor service. + /// + public ActorTypeInformation ActorTypeInfo { get; } + + /// + /// Gets the . + /// + public ActorId Id { get; } + + /// + /// Gets the LoggerFactory for actor service + /// + public ILoggerFactory LoggerFactory { get; } + + /// + /// Gets the . + /// + public JsonSerializerOptions JsonSerializerOptions { get; } + + /// + /// Gets the . + /// + public IActorProxyFactory ProxyFactory { get; } + + /// + /// Gets the . + /// + public ActorTimerManager TimerManager { get; } + + internal DaprStateProvider StateProvider { get; set; } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Runtime/ActorManager.cs b/src/Dapr.Actors/Runtime/ActorManager.cs index c78126cc..e60cd455 100644 --- a/src/Dapr.Actors/Runtime/ActorManager.cs +++ b/src/Dapr.Actors/Runtime/ActorManager.cs @@ -11,391 +11,390 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Runtime +namespace Dapr.Actors.Runtime; + +using System; +using System.Collections.Concurrent; +using System.IO; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Dapr.Actors; +using Dapr.Actors.Client; +using Dapr.Actors.Communication; +using Microsoft.Extensions.Logging; + +// The ActorManager serves as a cache for a variety of different concerns related to an Actor type +// as well as the runtime managment for Actor instances of that type. +internal sealed class ActorManager { - using System; - using System.Collections.Concurrent; - using System.IO; - using System.Text; - using System.Text.Json; - using System.Threading; - using System.Threading.Tasks; - using Dapr.Actors; - using Dapr.Actors.Client; - using Dapr.Actors.Communication; - using Microsoft.Extensions.Logging; + private const string ReceiveReminderMethodName = "ReceiveReminderAsync"; + private const string TimerMethodName = "FireTimerAsync"; + private readonly ActorRegistration registration; + private readonly ActorActivator activator; + private readonly ILoggerFactory loggerFactory; + private readonly IActorProxyFactory proxyFactory; + private readonly ActorTimerManager timerManager; + private readonly ConcurrentDictionary activeActors; + private readonly ActorMethodContext reminderMethodContext; + private readonly ActorMethodContext timerMethodContext; + private readonly ActorMessageSerializersManager serializersManager; + private readonly IActorMessageBodyFactory messageBodyFactory; + private readonly JsonSerializerOptions jsonSerializerOptions; - // The ActorManager serves as a cache for a variety of different concerns related to an Actor type - // as well as the runtime managment for Actor instances of that type. - internal sealed class ActorManager + // method dispatchermap used by remoting calls. + private readonly ActorMethodDispatcherMap methodDispatcherMap; + + // method info map used by non-remoting calls. + private readonly ActorMethodInfoMap actorMethodInfoMap; + + private readonly ILogger logger; + private IDaprInteractor daprInteractor { get; } + + + internal ActorManager( + ActorRegistration registration, + ActorActivator activator, + JsonSerializerOptions jsonSerializerOptions, + bool useJsonSerialization, + ILoggerFactory loggerFactory, + IActorProxyFactory proxyFactory, + IDaprInteractor daprInteractor) { - private const string ReceiveReminderMethodName = "ReceiveReminderAsync"; - private const string TimerMethodName = "FireTimerAsync"; - private readonly ActorRegistration registration; - private readonly ActorActivator activator; - private readonly ILoggerFactory loggerFactory; - private readonly IActorProxyFactory proxyFactory; - private readonly ActorTimerManager timerManager; - private readonly ConcurrentDictionary activeActors; - private readonly ActorMethodContext reminderMethodContext; - private readonly ActorMethodContext timerMethodContext; - private readonly ActorMessageSerializersManager serializersManager; - private readonly IActorMessageBodyFactory messageBodyFactory; - private readonly JsonSerializerOptions jsonSerializerOptions; + this.registration = registration; + this.activator = activator; + this.jsonSerializerOptions = jsonSerializerOptions; + this.loggerFactory = loggerFactory; + this.proxyFactory = proxyFactory; + this.daprInteractor = daprInteractor; - // method dispatchermap used by remoting calls. - private readonly ActorMethodDispatcherMap methodDispatcherMap; + this.timerManager = new DefaultActorTimerManager(this.daprInteractor); - // method info map used by non-remoting calls. - private readonly ActorMethodInfoMap actorMethodInfoMap; + // map for remoting calls. + this.methodDispatcherMap = new ActorMethodDispatcherMap(this.registration.Type.InterfaceTypes); - private readonly ILogger logger; - private IDaprInteractor daprInteractor { get; } + // map for non-remoting calls. + this.actorMethodInfoMap = new ActorMethodInfoMap(this.registration.Type.InterfaceTypes); + this.activeActors = new ConcurrentDictionary(); + this.reminderMethodContext = ActorMethodContext.CreateForReminder(ReceiveReminderMethodName); + this.timerMethodContext = ActorMethodContext.CreateForTimer(TimerMethodName); - - internal ActorManager( - ActorRegistration registration, - ActorActivator activator, - JsonSerializerOptions jsonSerializerOptions, - bool useJsonSerialization, - ILoggerFactory loggerFactory, - IActorProxyFactory proxyFactory, - IDaprInteractor daprInteractor) + // provide a serializer if 'useJsonSerialization' is true and no serialization provider is provided. + IActorMessageBodySerializationProvider serializationProvider = null; + if (useJsonSerialization) { - this.registration = registration; - this.activator = activator; - this.jsonSerializerOptions = jsonSerializerOptions; - this.loggerFactory = loggerFactory; - this.proxyFactory = proxyFactory; - this.daprInteractor = daprInteractor; - - this.timerManager = new DefaultActorTimerManager(this.daprInteractor); - - // map for remoting calls. - this.methodDispatcherMap = new ActorMethodDispatcherMap(this.registration.Type.InterfaceTypes); - - // map for non-remoting calls. - this.actorMethodInfoMap = new ActorMethodInfoMap(this.registration.Type.InterfaceTypes); - this.activeActors = new ConcurrentDictionary(); - this.reminderMethodContext = ActorMethodContext.CreateForReminder(ReceiveReminderMethodName); - this.timerMethodContext = ActorMethodContext.CreateForTimer(TimerMethodName); - - // provide a serializer if 'useJsonSerialization' is true and no serialization provider is provided. - IActorMessageBodySerializationProvider serializationProvider = null; - if (useJsonSerialization) - { - serializationProvider = new ActorMessageBodyJsonSerializationProvider(jsonSerializerOptions); - } - - this.serializersManager = IntializeSerializationManager(serializationProvider); - this.messageBodyFactory = new WrappedRequestMessageFactory(); - - this.logger = loggerFactory.CreateLogger(this.GetType()); + serializationProvider = new ActorMessageBodyJsonSerializationProvider(jsonSerializerOptions); } - internal ActorTypeInformation ActorTypeInfo => this.registration.Type; + this.serializersManager = IntializeSerializationManager(serializationProvider); + this.messageBodyFactory = new WrappedRequestMessageFactory(); - internal async Task> DispatchWithRemotingAsync(ActorId actorId, string actorMethodName, string daprActorheader, Stream data, CancellationToken cancellationToken) - { - var actorMethodContext = ActorMethodContext.CreateForActor(actorMethodName); + this.logger = loggerFactory.CreateLogger(this.GetType()); + } - // Get the serialized header - var actorMessageHeader = this.serializersManager.GetHeaderSerializer() - .DeserializeRequestHeaders(new MemoryStream(Encoding.ASCII.GetBytes(daprActorheader))); + internal ActorTypeInformation ActorTypeInfo => this.registration.Type; - var interfaceId = actorMessageHeader.InterfaceId; + internal async Task> DispatchWithRemotingAsync(ActorId actorId, string actorMethodName, string daprActorheader, Stream data, CancellationToken cancellationToken) + { + var actorMethodContext = ActorMethodContext.CreateForActor(actorMethodName); - // Get the deserialized Body. - var msgBodySerializer = this.serializersManager.GetRequestMessageBodySerializer(actorMessageHeader.InterfaceId, actorMethodContext.MethodName); + // Get the serialized header + var actorMessageHeader = this.serializersManager.GetHeaderSerializer() + .DeserializeRequestHeaders(new MemoryStream(Encoding.ASCII.GetBytes(daprActorheader))); + + var interfaceId = actorMessageHeader.InterfaceId; + + // Get the deserialized Body. + var msgBodySerializer = this.serializersManager.GetRequestMessageBodySerializer(actorMessageHeader.InterfaceId, actorMethodContext.MethodName); - IActorRequestMessageBody actorMessageBody; - using (var stream = new MemoryStream()) - { - await data.CopyToAsync(stream); - actorMessageBody = await msgBodySerializer.DeserializeAsync(stream); - } - - // Call the method on the method dispatcher using the Func below. - var methodDispatcher = this.methodDispatcherMap.GetDispatcher(actorMessageHeader.InterfaceId); - - // Create a Func to be invoked by common method. - async Task> RequestFunc(Actor actor, CancellationToken ct) - { - IActorResponseMessageBody responseMsgBody = null; - - responseMsgBody = (IActorResponseMessageBody)await methodDispatcher.DispatchAsync( - actor, - actorMessageHeader.MethodId, - actorMessageBody, - this.messageBodyFactory, - ct); - - return this.CreateResponseMessage(responseMsgBody, interfaceId, actorMethodContext.MethodName); - } - - return await this.DispatchInternalAsync(actorId, actorMethodContext, RequestFunc, cancellationToken); + IActorRequestMessageBody actorMessageBody; + using (var stream = new MemoryStream()) + { + await data.CopyToAsync(stream); + actorMessageBody = await msgBodySerializer.DeserializeAsync(stream); } - internal async Task DispatchWithoutRemotingAsync(ActorId actorId, string actorMethodName, Stream requestBodyStream, Stream responseBodyStream, CancellationToken cancellationToken) + // Call the method on the method dispatcher using the Func below. + var methodDispatcher = this.methodDispatcherMap.GetDispatcher(actorMessageHeader.InterfaceId); + + // Create a Func to be invoked by common method. + async Task> RequestFunc(Actor actor, CancellationToken ct) { - var actorMethodContext = ActorMethodContext.CreateForActor(actorMethodName); + IActorResponseMessageBody responseMsgBody = null; - // Create a Func to be invoked by common method. - var methodInfo = this.actorMethodInfoMap.LookupActorMethodInfo(actorMethodName); + responseMsgBody = (IActorResponseMessageBody)await methodDispatcher.DispatchAsync( + actor, + actorMessageHeader.MethodId, + actorMessageBody, + this.messageBodyFactory, + ct); - async Task RequestFunc(Actor actor, CancellationToken ct) + return this.CreateResponseMessage(responseMsgBody, interfaceId, actorMethodContext.MethodName); + } + + return await this.DispatchInternalAsync(actorId, actorMethodContext, RequestFunc, cancellationToken); + } + + internal async Task DispatchWithoutRemotingAsync(ActorId actorId, string actorMethodName, Stream requestBodyStream, Stream responseBodyStream, CancellationToken cancellationToken) + { + var actorMethodContext = ActorMethodContext.CreateForActor(actorMethodName); + + // Create a Func to be invoked by common method. + var methodInfo = this.actorMethodInfoMap.LookupActorMethodInfo(actorMethodName); + + async Task RequestFunc(Actor actor, CancellationToken ct) + { + var parameters = methodInfo.GetParameters(); + dynamic awaitable; + + if (parameters.Length == 0 || (parameters.Length == 1 && parameters[0].ParameterType == typeof(CancellationToken))) { - var parameters = methodInfo.GetParameters(); - dynamic awaitable; - - if (parameters.Length == 0 || (parameters.Length == 1 && parameters[0].ParameterType == typeof(CancellationToken))) - { - awaitable = methodInfo.Invoke(actor, parameters.Length == 0 ? null : new object[] { ct }); - } - else if (parameters.Length == 1 || (parameters.Length == 2 && parameters[1].ParameterType == typeof(CancellationToken))) - { - // deserialize using stream. - var type = parameters[0].ParameterType; - var deserializedType = await JsonSerializer.DeserializeAsync(requestBodyStream, type, jsonSerializerOptions); - awaitable = methodInfo.Invoke(actor, parameters.Length == 1 ? new object[] { deserializedType } : new object[] { deserializedType, ct }); - } - else - { - var errorMsg = $"Method {string.Concat(methodInfo.DeclaringType.Name, ".", methodInfo.Name)} has more than one parameter and can't be invoked through http"; - throw new ArgumentException(errorMsg); - } - - await awaitable; - - // Handle the return type of method correctly. - if (methodInfo.ReturnType.Name != typeof(Task).Name) - { - // already await, Getting result will be non blocking. - var x = awaitable.GetAwaiter().GetResult(); - return x; - } - else - { - return default; - } + awaitable = methodInfo.Invoke(actor, parameters.Length == 0 ? null : new object[] { ct }); + } + else if (parameters.Length == 1 || (parameters.Length == 2 && parameters[1].ParameterType == typeof(CancellationToken))) + { + // deserialize using stream. + var type = parameters[0].ParameterType; + var deserializedType = await JsonSerializer.DeserializeAsync(requestBodyStream, type, jsonSerializerOptions); + awaitable = methodInfo.Invoke(actor, parameters.Length == 1 ? new object[] { deserializedType } : new object[] { deserializedType, ct }); + } + else + { + var errorMsg = $"Method {string.Concat(methodInfo.DeclaringType.Name, ".", methodInfo.Name)} has more than one parameter and can't be invoked through http"; + throw new ArgumentException(errorMsg); } - var result = await this.DispatchInternalAsync(actorId, actorMethodContext, RequestFunc, cancellationToken); + await awaitable; - // Write Response back if method's return type is other than Task. - // Serialize result if it has result (return type was not just Task.) + // Handle the return type of method correctly. if (methodInfo.ReturnType.Name != typeof(Task).Name) { + // already await, Getting result will be non blocking. + var x = awaitable.GetAwaiter().GetResult(); + return x; + } + else + { + return default; + } + } + + var result = await this.DispatchInternalAsync(actorId, actorMethodContext, RequestFunc, cancellationToken); + + // Write Response back if method's return type is other than Task. + // Serialize result if it has result (return type was not just Task.) + if (methodInfo.ReturnType.Name != typeof(Task).Name) + { #if NET7_0_OR_GREATER - var resultType = methodInfo.ReturnType.GenericTypeArguments[0]; - await JsonSerializer.SerializeAsync(responseBodyStream, result, resultType, jsonSerializerOptions); + var resultType = methodInfo.ReturnType.GenericTypeArguments[0]; + await JsonSerializer.SerializeAsync(responseBodyStream, result, resultType, jsonSerializerOptions); #else await JsonSerializer.SerializeAsync(responseBodyStream, result, jsonSerializerOptions); #endif - } } + } - internal async Task FireReminderAsync(ActorId actorId, string reminderName, Stream requestBodyStream, CancellationToken cancellationToken = default) + internal async Task FireReminderAsync(ActorId actorId, string reminderName, Stream requestBodyStream, CancellationToken cancellationToken = default) + { + // Only FireReminder if its IRemindable, else ignore it. + if (this.ActorTypeInfo.IsRemindable) { - // Only FireReminder if its IRemindable, else ignore it. - if (this.ActorTypeInfo.IsRemindable) - { - var reminderdata = await ReminderInfo.DeserializeAsync(requestBodyStream); - - // Create a Func to be invoked by common method. - async Task RequestFunc(Actor actor, CancellationToken ct) - { - await - (actor as IRemindable).ReceiveReminderAsync( - reminderName, - reminderdata.Data, - reminderdata.DueTime, - reminderdata.Period); - - return null; - } - - await this.DispatchInternalAsync(actorId, this.reminderMethodContext, RequestFunc, cancellationToken); - } - } - - internal async Task FireTimerAsync(ActorId actorId, Stream requestBodyStream, CancellationToken cancellationToken = default) - { - #pragma warning disable 0618 - var timerData = await JsonSerializer.DeserializeAsync(requestBodyStream); - #pragma warning restore 0618 + var reminderdata = await ReminderInfo.DeserializeAsync(requestBodyStream); // Create a Func to be invoked by common method. async Task RequestFunc(Actor actor, CancellationToken ct) { - var actorType = actor.Host.ActorTypeInfo.ImplementationType; - var methodInfo = actor.GetMethodInfoUsingReflection(actorType, timerData.Callback); + await + (actor as IRemindable).ReceiveReminderAsync( + reminderName, + reminderdata.Data, + reminderdata.DueTime, + reminderdata.Period); - var parameters = methodInfo.GetParameters(); - - // The timer callback routine needs to return a type Task - await (Task)(methodInfo.Invoke(actor, (parameters.Length == 0) ? null : new object[] { timerData.Data })); - - return default; + return null; } - await this.DispatchInternalAsync(actorId, this.timerMethodContext, RequestFunc, cancellationToken); - } - - internal async Task ActivateActorAsync(ActorId actorId) - { - // An actor is activated by "Dapr" runtime when a call is to be made for an actor. - var state = await this.CreateActorAsync(actorId); - - try - { - await state.Actor.OnActivateInternalAsync(); - } - catch - { - // Ensure we don't leak resources if user-code throws during activation. - await DeleteActorAsync(state); - throw; - } - - // Add actor to activeActors only after OnActivate succeeds (user code can throw error from its override of Activate method.) - // - // In theory the Dapr runtime protects us from double-activation - there's no case - // where we *expect* to see the *update* code path taken. However it's a possiblity and - // we should handle it. - // - // The policy we have chosen is to always keep the registered instance if we hit a double-activation - // so that means we have to destroy the 'new' instance. - var current = this.activeActors.AddOrUpdate(actorId, state, (key, oldValue) => oldValue); - if (object.ReferenceEquals(state, current)) - { - // On this code path it was an *Add*. Nothing left to do. - return; - } - - // On this code path it was an *Update*. We need to destroy the new instance and clean up. - await DeactivateActorCore(state); - } - - private async Task CreateActorAsync(ActorId actorId) - { - this.logger.LogDebug("Creating Actor of type {ActorType} with ActorId {ActorId}", this.ActorTypeInfo.ImplementationType, actorId); - var host = new ActorHost(this.ActorTypeInfo, actorId, this.jsonSerializerOptions, this.loggerFactory, this.proxyFactory, this.timerManager) - { - StateProvider = new DaprStateProvider(this.daprInteractor, this.jsonSerializerOptions), - }; - var state = await this.activator.CreateAsync(host); - this.logger.LogDebug("Finished creating Actor of type {ActorType} with ActorId {ActorId}", this.ActorTypeInfo.ImplementationType, actorId); - return state; - } - - internal async Task DeactivateActorAsync(ActorId actorId) - { - if (this.activeActors.TryRemove(actorId, out var deactivatedActor)) - { - await DeactivateActorCore(deactivatedActor); - } - } - - private async Task DeactivateActorCore(ActorActivatorState state) - { - try - { - await state.Actor.OnDeactivateInternalAsync(); - } - finally - { - // Ensure we don't leak resources if user-code throws during deactivation. - await DeleteActorAsync(state); - } - } - - private async Task DeleteActorAsync(ActorActivatorState state) - { - this.logger.LogDebug("Deleting Actor of type {ActorType} with ActorId {ActorId}", this.ActorTypeInfo.ImplementationType, state.Actor.Id); - await this.activator.DeleteAsync(state); - this.logger.LogDebug("Finished deleting Actor of type {ActorType} with ActorId {ActorId}", this.ActorTypeInfo.ImplementationType, state.Actor.Id); - } - - // Used for testing - do not leak the actor instances outside of this method in library code. - public bool TryGetActorAsync(ActorId id, out Actor actor) - { - var found = this.activeActors.TryGetValue(id, out var state); - actor = found ? state.Actor : default; - return found; - } - - private static ActorMessageSerializersManager IntializeSerializationManager( - IActorMessageBodySerializationProvider serializationProvider) - { - // TODO serializer settings - return new ActorMessageSerializersManager( - serializationProvider, - new ActorMessageHeaderSerializer()); - } - - private async Task DispatchInternalAsync(ActorId actorId, ActorMethodContext actorMethodContext, Func> actorFunc, CancellationToken cancellationToken) - { - if (!this.activeActors.ContainsKey(actorId)) - { - await this.ActivateActorAsync(actorId); - } - - if (!this.activeActors.TryGetValue(actorId, out var state)) - { - var errorMsg = $"Actor {actorId} is not yet activated."; - throw new InvalidOperationException(errorMsg); - } - - var actor = state.Actor; - - T retval; - try - { - // Set the state context of the request, if required and possible. - if (ActorReentrancyContextAccessor.ReentrancyContext != null) - { - if (state.Actor.StateManager is IActorContextualState contextualStateManager) - { - await contextualStateManager.SetStateContext(Guid.NewGuid().ToString()); - } - } - - // invoke the function of the actor - await actor.OnPreActorMethodAsyncInternal(actorMethodContext); - retval = await actorFunc.Invoke(actor, cancellationToken); - - // PostActivate will save the state, its not invoked when actorFunc invocation throws. - await actor.OnPostActorMethodAsyncInternal(actorMethodContext); - } - catch (Exception e) - { - await actor.OnActorMethodFailedInternalAsync(actorMethodContext, e); - throw; - } - finally - { - // Set the state context of the request, if possible. - if (state.Actor.StateManager is IActorContextualState contextualStateManager) - { - await contextualStateManager.SetStateContext(null); - } - } - - return retval; - } - - private Tuple CreateResponseMessage(IActorResponseMessageBody msgBody, int interfaceId, string methodName) - { - var responseMsgBodyBytes = Array.Empty(); - if (msgBody != null) - { - var responseSerializer = this.serializersManager.GetResponseMessageBodySerializer(interfaceId, methodName); - responseMsgBodyBytes = responseSerializer.Serialize(msgBody); - } - - return new Tuple(string.Empty, responseMsgBodyBytes); + await this.DispatchInternalAsync(actorId, this.reminderMethodContext, RequestFunc, cancellationToken); } } -} + + internal async Task FireTimerAsync(ActorId actorId, Stream requestBodyStream, CancellationToken cancellationToken = default) + { +#pragma warning disable 0618 + var timerData = await JsonSerializer.DeserializeAsync(requestBodyStream); +#pragma warning restore 0618 + + // Create a Func to be invoked by common method. + async Task RequestFunc(Actor actor, CancellationToken ct) + { + var actorType = actor.Host.ActorTypeInfo.ImplementationType; + var methodInfo = actor.GetMethodInfoUsingReflection(actorType, timerData.Callback); + + var parameters = methodInfo.GetParameters(); + + // The timer callback routine needs to return a type Task + await (Task)(methodInfo.Invoke(actor, (parameters.Length == 0) ? null : new object[] { timerData.Data })); + + return default; + } + + await this.DispatchInternalAsync(actorId, this.timerMethodContext, RequestFunc, cancellationToken); + } + + internal async Task ActivateActorAsync(ActorId actorId) + { + // An actor is activated by "Dapr" runtime when a call is to be made for an actor. + var state = await this.CreateActorAsync(actorId); + + try + { + await state.Actor.OnActivateInternalAsync(); + } + catch + { + // Ensure we don't leak resources if user-code throws during activation. + await DeleteActorAsync(state); + throw; + } + + // Add actor to activeActors only after OnActivate succeeds (user code can throw error from its override of Activate method.) + // + // In theory the Dapr runtime protects us from double-activation - there's no case + // where we *expect* to see the *update* code path taken. However it's a possiblity and + // we should handle it. + // + // The policy we have chosen is to always keep the registered instance if we hit a double-activation + // so that means we have to destroy the 'new' instance. + var current = this.activeActors.AddOrUpdate(actorId, state, (key, oldValue) => oldValue); + if (object.ReferenceEquals(state, current)) + { + // On this code path it was an *Add*. Nothing left to do. + return; + } + + // On this code path it was an *Update*. We need to destroy the new instance and clean up. + await DeactivateActorCore(state); + } + + private async Task CreateActorAsync(ActorId actorId) + { + this.logger.LogDebug("Creating Actor of type {ActorType} with ActorId {ActorId}", this.ActorTypeInfo.ImplementationType, actorId); + var host = new ActorHost(this.ActorTypeInfo, actorId, this.jsonSerializerOptions, this.loggerFactory, this.proxyFactory, this.timerManager) + { + StateProvider = new DaprStateProvider(this.daprInteractor, this.jsonSerializerOptions), + }; + var state = await this.activator.CreateAsync(host); + this.logger.LogDebug("Finished creating Actor of type {ActorType} with ActorId {ActorId}", this.ActorTypeInfo.ImplementationType, actorId); + return state; + } + + internal async Task DeactivateActorAsync(ActorId actorId) + { + if (this.activeActors.TryRemove(actorId, out var deactivatedActor)) + { + await DeactivateActorCore(deactivatedActor); + } + } + + private async Task DeactivateActorCore(ActorActivatorState state) + { + try + { + await state.Actor.OnDeactivateInternalAsync(); + } + finally + { + // Ensure we don't leak resources if user-code throws during deactivation. + await DeleteActorAsync(state); + } + } + + private async Task DeleteActorAsync(ActorActivatorState state) + { + this.logger.LogDebug("Deleting Actor of type {ActorType} with ActorId {ActorId}", this.ActorTypeInfo.ImplementationType, state.Actor.Id); + await this.activator.DeleteAsync(state); + this.logger.LogDebug("Finished deleting Actor of type {ActorType} with ActorId {ActorId}", this.ActorTypeInfo.ImplementationType, state.Actor.Id); + } + + // Used for testing - do not leak the actor instances outside of this method in library code. + public bool TryGetActorAsync(ActorId id, out Actor actor) + { + var found = this.activeActors.TryGetValue(id, out var state); + actor = found ? state.Actor : default; + return found; + } + + private static ActorMessageSerializersManager IntializeSerializationManager( + IActorMessageBodySerializationProvider serializationProvider) + { + // TODO serializer settings + return new ActorMessageSerializersManager( + serializationProvider, + new ActorMessageHeaderSerializer()); + } + + private async Task DispatchInternalAsync(ActorId actorId, ActorMethodContext actorMethodContext, Func> actorFunc, CancellationToken cancellationToken) + { + if (!this.activeActors.ContainsKey(actorId)) + { + await this.ActivateActorAsync(actorId); + } + + if (!this.activeActors.TryGetValue(actorId, out var state)) + { + var errorMsg = $"Actor {actorId} is not yet activated."; + throw new InvalidOperationException(errorMsg); + } + + var actor = state.Actor; + + T retval; + try + { + // Set the state context of the request, if required and possible. + if (ActorReentrancyContextAccessor.ReentrancyContext != null) + { + if (state.Actor.StateManager is IActorContextualState contextualStateManager) + { + await contextualStateManager.SetStateContext(Guid.NewGuid().ToString()); + } + } + + // invoke the function of the actor + await actor.OnPreActorMethodAsyncInternal(actorMethodContext); + retval = await actorFunc.Invoke(actor, cancellationToken); + + // PostActivate will save the state, its not invoked when actorFunc invocation throws. + await actor.OnPostActorMethodAsyncInternal(actorMethodContext); + } + catch (Exception e) + { + await actor.OnActorMethodFailedInternalAsync(actorMethodContext, e); + throw; + } + finally + { + // Set the state context of the request, if possible. + if (state.Actor.StateManager is IActorContextualState contextualStateManager) + { + await contextualStateManager.SetStateContext(null); + } + } + + return retval; + } + + private Tuple CreateResponseMessage(IActorResponseMessageBody msgBody, int interfaceId, string methodName) + { + var responseMsgBodyBytes = Array.Empty(); + if (msgBody != null) + { + var responseSerializer = this.serializersManager.GetResponseMessageBodySerializer(interfaceId, methodName); + responseMsgBodyBytes = responseSerializer.Serialize(msgBody); + } + + return new Tuple(string.Empty, responseMsgBodyBytes); + } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Runtime/ActorMethodContext.cs b/src/Dapr.Actors/Runtime/ActorMethodContext.cs index 4e07633b..821399dc 100644 --- a/src/Dapr.Actors/Runtime/ActorMethodContext.cs +++ b/src/Dapr.Actors/Runtime/ActorMethodContext.cs @@ -11,56 +11,55 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Runtime +namespace Dapr.Actors.Runtime; + +/// +/// Contains information about the method that is invoked by actor runtime and +/// is passed as an argument to and . +/// +public struct ActorMethodContext { - /// - /// Contains information about the method that is invoked by actor runtime and - /// is passed as an argument to and . - /// - public struct ActorMethodContext + private readonly string actorMethodName; + private readonly ActorCallType actorCallType; + + private ActorMethodContext(string methodName, ActorCallType callType) { - private readonly string actorMethodName; - private readonly ActorCallType actorCallType; - - private ActorMethodContext(string methodName, ActorCallType callType) - { - this.actorMethodName = methodName; - this.actorCallType = callType; - } - - /// - /// Gets the name of the method invoked by actor runtime. - /// - /// The name of method. - public string MethodName - { - get { return this.actorMethodName; } - } - - /// - /// Gets the type of call by actor runtime (e.g. actor interface method, timer callback etc.). - /// - /// - /// An representing the call type. - /// - public ActorCallType CallType - { - get { return this.actorCallType; } - } - - internal static ActorMethodContext CreateForActor(string methodName) - { - return new ActorMethodContext(methodName, ActorCallType.ActorInterfaceMethod); - } - - internal static ActorMethodContext CreateForTimer(string methodName) - { - return new ActorMethodContext(methodName, ActorCallType.TimerMethod); - } - - internal static ActorMethodContext CreateForReminder(string methodName) - { - return new ActorMethodContext(methodName, ActorCallType.ReminderMethod); - } + this.actorMethodName = methodName; + this.actorCallType = callType; } -} + + /// + /// Gets the name of the method invoked by actor runtime. + /// + /// The name of method. + public string MethodName + { + get { return this.actorMethodName; } + } + + /// + /// Gets the type of call by actor runtime (e.g. actor interface method, timer callback etc.). + /// + /// + /// An representing the call type. + /// + public ActorCallType CallType + { + get { return this.actorCallType; } + } + + internal static ActorMethodContext CreateForActor(string methodName) + { + return new ActorMethodContext(methodName, ActorCallType.ActorInterfaceMethod); + } + + internal static ActorMethodContext CreateForTimer(string methodName) + { + return new ActorMethodContext(methodName, ActorCallType.TimerMethod); + } + + internal static ActorMethodContext CreateForReminder(string methodName) + { + return new ActorMethodContext(methodName, ActorCallType.ReminderMethod); + } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Runtime/ActorMethodInfoMap.cs b/src/Dapr.Actors/Runtime/ActorMethodInfoMap.cs index 0085e819..a677fbb7 100644 --- a/src/Dapr.Actors/Runtime/ActorMethodInfoMap.cs +++ b/src/Dapr.Actors/Runtime/ActorMethodInfoMap.cs @@ -11,41 +11,40 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Runtime +namespace Dapr.Actors.Runtime; + +using System; +using System.Collections.Generic; +using System.Reflection; + +/// +/// Actor method dispatcher map for non remoting calls. method_name -> MethodInfo for methods defined in IACtor interfaces. +/// +internal class ActorMethodInfoMap { - using System; - using System.Collections.Generic; - using System.Reflection; + private readonly Dictionary methods; - /// - /// Actor method dispatcher map for non remoting calls. method_name -> MethodInfo for methods defined in IACtor interfaces. - /// - internal class ActorMethodInfoMap + public ActorMethodInfoMap(IEnumerable interfaceTypes) { - private readonly Dictionary methods; + this.methods = new Dictionary(); - public ActorMethodInfoMap(IEnumerable interfaceTypes) + // Find methods which are defined in IActor interface. + foreach (var actorInterface in interfaceTypes) { - this.methods = new Dictionary(); - - // Find methods which are defined in IActor interface. - foreach (var actorInterface in interfaceTypes) + foreach (var methodInfo in actorInterface.GetMethods()) { - foreach (var methodInfo in actorInterface.GetMethods()) - { - this.methods.Add(methodInfo.Name, methodInfo); - } + this.methods.Add(methodInfo.Name, methodInfo); } } - - public MethodInfo LookupActorMethodInfo(string methodName) - { - if (!this.methods.TryGetValue(methodName, out var methodInfo)) - { - throw new MissingMethodException($"Actor type doesn't contain method {methodName}"); - } - - return methodInfo; - } } -} + + public MethodInfo LookupActorMethodInfo(string methodName) + { + if (!this.methods.TryGetValue(methodName, out var methodInfo)) + { + throw new MissingMethodException($"Actor type doesn't contain method {methodName}"); + } + + return methodInfo; + } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Runtime/ActorRegistration.cs b/src/Dapr.Actors/Runtime/ActorRegistration.cs index b8ea7322..279ed032 100644 --- a/src/Dapr.Actors/Runtime/ActorRegistration.cs +++ b/src/Dapr.Actors/Runtime/ActorRegistration.cs @@ -11,47 +11,46 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Runtime +namespace Dapr.Actors.Runtime; + +/// +/// Represents an actor type registered with the runtime. Provides access to per-type +/// options for the actor. +/// +public sealed class ActorRegistration { /// - /// Represents an actor type registered with the runtime. Provides access to per-type - /// options for the actor. + /// Initializes a new instance of . /// - public sealed class ActorRegistration + /// The for the actor type. + public ActorRegistration(ActorTypeInformation type) : this(type, null) { - /// - /// Initializes a new instance of . - /// - /// The for the actor type. - public ActorRegistration(ActorTypeInformation type) : this(type, null) - { - } - - /// - /// Initializes a new instance of . - /// - /// The for the actor type. - /// The optional that are specified for this type only. - public ActorRegistration(ActorTypeInformation type, ActorRuntimeOptions options) - { - this.Type = type; - this.TypeOptions = options; - } - - /// - /// Gets the for the actor type. - /// - public ActorTypeInformation Type { get; } - - /// - /// Gets or sets the to use for the actor. If not set the default - /// activator of the runtime will be used. - /// - public ActorActivator Activator { get; set; } - - /// - /// An optional set of options for this specific actor type. These will override the top level or default values. - /// - public ActorRuntimeOptions TypeOptions { get; } } -} + + /// + /// Initializes a new instance of . + /// + /// The for the actor type. + /// The optional that are specified for this type only. + public ActorRegistration(ActorTypeInformation type, ActorRuntimeOptions options) + { + this.Type = type; + this.TypeOptions = options; + } + + /// + /// Gets the for the actor type. + /// + public ActorTypeInformation Type { get; } + + /// + /// Gets or sets the to use for the actor. If not set the default + /// activator of the runtime will be used. + /// + public ActorActivator Activator { get; set; } + + /// + /// An optional set of options for this specific actor type. These will override the top level or default values. + /// + public ActorRuntimeOptions TypeOptions { get; } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Runtime/ActorRegistrationCollection.cs b/src/Dapr.Actors/Runtime/ActorRegistrationCollection.cs index 69c01646..6eb48154 100644 --- a/src/Dapr.Actors/Runtime/ActorRegistrationCollection.cs +++ b/src/Dapr.Actors/Runtime/ActorRegistrationCollection.cs @@ -14,74 +14,73 @@ using System; using System.Collections.ObjectModel; -namespace Dapr.Actors.Runtime +namespace Dapr.Actors.Runtime; + +/// +/// A collection of instances. +/// +public sealed class ActorRegistrationCollection : KeyedCollection { /// - /// A collection of instances. + /// Returns the key for the item. /// - public sealed class ActorRegistrationCollection : KeyedCollection + /// The item. + /// The key. + protected override ActorTypeInformation GetKeyForItem(ActorRegistration item) { - /// - /// Returns the key for the item. - /// - /// The item. - /// The key. - protected override ActorTypeInformation GetKeyForItem(ActorRegistration item) - { - return item.Type; - } - - /// - /// Registers an actor type in the collection. - /// - /// Type of actor. - /// An optional delegate used to configure the actor registration. - public void RegisterActor(Action configure = null) - where TActor : Actor - { - RegisterActor(actorTypeName: null, configure); - } - - /// - /// Registers an actor type in the collection. - /// - /// Type of actor. - /// An optional that defines values for this type alone. - /// An optional delegate used to configure the actor registration. - public void RegisterActor(ActorRuntimeOptions typeOptions, Action configure = null) - where TActor : Actor - { - RegisterActor(null, typeOptions, configure); - } - - /// - /// Registers an actor type in the collection. - /// - /// Type of actor. - /// The name of the actor type represented by the actor. - /// An optional delegate used to configure the actor registration. - /// The value of will have precedence over the default actor type name derived from the actor implementation type or any type name set via . - public void RegisterActor(string actorTypeName, Action configure = null) - where TActor : Actor - { - RegisterActor(actorTypeName, null, configure); - } - - /// - /// Registers an actor type in the collection. - /// - /// Type of actor. - /// The name of the actor type represented by the actor. - /// An optional that defines values for this type alone. - /// An optional delegate used to configure the actor registration. - /// The value of will have precedence over the default actor type name derived from the actor implementation type or any type name set via . - public void RegisterActor(string actorTypeName, ActorRuntimeOptions typeOptions, Action configure = null) - where TActor : Actor - { - var actorTypeInfo = ActorTypeInformation.Get(typeof(TActor), actorTypeName); - var registration = new ActorRegistration(actorTypeInfo, typeOptions); - configure?.Invoke(registration); - this.Add(registration); - } + return item.Type; } -} + + /// + /// Registers an actor type in the collection. + /// + /// Type of actor. + /// An optional delegate used to configure the actor registration. + public void RegisterActor(Action configure = null) + where TActor : Actor + { + RegisterActor(actorTypeName: null, configure); + } + + /// + /// Registers an actor type in the collection. + /// + /// Type of actor. + /// An optional that defines values for this type alone. + /// An optional delegate used to configure the actor registration. + public void RegisterActor(ActorRuntimeOptions typeOptions, Action configure = null) + where TActor : Actor + { + RegisterActor(null, typeOptions, configure); + } + + /// + /// Registers an actor type in the collection. + /// + /// Type of actor. + /// The name of the actor type represented by the actor. + /// An optional delegate used to configure the actor registration. + /// The value of will have precedence over the default actor type name derived from the actor implementation type or any type name set via . + public void RegisterActor(string actorTypeName, Action configure = null) + where TActor : Actor + { + RegisterActor(actorTypeName, null, configure); + } + + /// + /// Registers an actor type in the collection. + /// + /// Type of actor. + /// The name of the actor type represented by the actor. + /// An optional that defines values for this type alone. + /// An optional delegate used to configure the actor registration. + /// The value of will have precedence over the default actor type name derived from the actor implementation type or any type name set via . + public void RegisterActor(string actorTypeName, ActorRuntimeOptions typeOptions, Action configure = null) + where TActor : Actor + { + var actorTypeInfo = ActorTypeInformation.Get(typeof(TActor), actorTypeName); + var registration = new ActorRegistration(actorTypeInfo, typeOptions); + configure?.Invoke(registration); + this.Add(registration); + } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Runtime/ActorReminder.cs b/src/Dapr.Actors/Runtime/ActorReminder.cs index 930df29b..8dea2a11 100644 --- a/src/Dapr.Actors/Runtime/ActorReminder.cs +++ b/src/Dapr.Actors/Runtime/ActorReminder.cs @@ -16,212 +16,211 @@ using System.Globalization; using System.Threading; using Dapr.Actors.Resources; -namespace Dapr.Actors.Runtime +namespace Dapr.Actors.Runtime; + +/// +/// Represents a reminder registered by an actor. +/// +public class ActorReminder : ActorReminderToken, IActorReminder { + private static readonly TimeSpan MiniumPeriod = Timeout.InfiniteTimeSpan; + /// - /// Represents a reminder registered by an actor. + /// Initializes a new instance of . /// - public class ActorReminder : ActorReminderToken, IActorReminder + /// The actor type. + /// The actor id. + /// The reminder name. + /// The state associated with the reminder. + /// The reminder due time. + /// The reminder period. + public ActorReminder( + string actorType, + ActorId actorId, + string name, + byte[] state, + TimeSpan dueTime, + TimeSpan period) + : this(new ActorReminderOptions + { + ActorTypeName = actorType, + Id = actorId, + ReminderName = name, + State = state, + DueTime = dueTime, + Period = period, + Ttl = null + }) { - private static readonly TimeSpan MiniumPeriod = Timeout.InfiniteTimeSpan; - - /// - /// Initializes a new instance of . - /// - /// The actor type. - /// The actor id. - /// The reminder name. - /// The state associated with the reminder. - /// The reminder due time. - /// The reminder period. - public ActorReminder( - string actorType, - ActorId actorId, - string name, - byte[] state, - TimeSpan dueTime, - TimeSpan period) - : this(new ActorReminderOptions - { - ActorTypeName = actorType, - Id = actorId, - ReminderName = name, - State = state, - DueTime = dueTime, - Period = period, - Ttl = null - }) - { - } - - /// - /// Initializes a new instance of . - /// - /// The actor type. - /// The actor id. - /// The reminder name. - /// The state associated with the reminder. - /// The reminder due time. - /// The reminder period. - /// The reminder ttl. - public ActorReminder( - string actorType, - ActorId actorId, - string name, - byte[] state, - TimeSpan dueTime, - TimeSpan period, - TimeSpan ttl) - : this(new ActorReminderOptions - { - ActorTypeName = actorType, - Id = actorId, - ReminderName = name, - State = state, - DueTime = dueTime, - Period = period, - Ttl = ttl - }) - { - } - - /// - /// Initializes a new instance of . - /// - /// The actor type. - /// The actor id. - /// The reminder name. - /// The state associated with the reminder. - /// The reminder due time. - /// The reminder period. - /// The number of times reminder should be invoked. - /// The reminder ttl. - public ActorReminder( - string actorType, - ActorId actorId, - string name, - byte[] state, - TimeSpan dueTime, - TimeSpan period, - int? repetitions, - TimeSpan? ttl) - : this(new ActorReminderOptions - { - ActorTypeName = actorType, - Id = actorId, - ReminderName = name, - State = state, - DueTime = dueTime, - Period = period, - Repetitions = repetitions, - Ttl = ttl - }) - { - } - - /// - /// Initializes a new instance of . - /// - /// The actor type. - /// The actor id. - /// The reminder name. - /// The state associated with the reminder. - /// The reminder due time. - /// The reminder period. - /// The number of times reminder should be invoked. - public ActorReminder( - string actorType, - ActorId actorId, - string name, - byte[] state, - TimeSpan dueTime, - TimeSpan period, - int? repetitions) - : this(new ActorReminderOptions - { - ActorTypeName = actorType, - Id = actorId, - ReminderName = name, - State = state, - DueTime = dueTime, - Period = period, - Repetitions = repetitions - }) - { - } - - /// - /// Initializes a new instance of . - /// - /// A containing the various settings for an . - internal ActorReminder(ActorReminderOptions options) - : base(options.ActorTypeName, options.Id, options.ReminderName) - { - if (options.DueTime < TimeSpan.Zero) - { - throw new ArgumentOutOfRangeException(nameof(options.DueTime), string.Format( - CultureInfo.CurrentCulture, - SR.TimerArgumentOutOfRange, - TimeSpan.Zero.TotalMilliseconds, - TimeSpan.MaxValue.TotalMilliseconds)); - } - - if (options.Period < MiniumPeriod) - { - throw new ArgumentOutOfRangeException(nameof(options.Period), string.Format( - CultureInfo.CurrentCulture, - SR.TimerArgumentOutOfRange, - MiniumPeriod.TotalMilliseconds, - TimeSpan.MaxValue.TotalMilliseconds)); - } - - if (options.Ttl != null && (options.Ttl < options.DueTime || options.Ttl < TimeSpan.Zero)) - { - throw new ArgumentOutOfRangeException(nameof(options.Ttl), string.Format( - CultureInfo.CurrentCulture, - SR.TimerArgumentOutOfRange, - options.DueTime, - TimeSpan.MaxValue.TotalMilliseconds)); - } - - if (options.Repetitions != null && options.Repetitions <= 0) - { - throw new ArgumentOutOfRangeException(nameof(options.Repetitions), string.Format( - CultureInfo.CurrentCulture, - SR.RepetitionsArgumentOutOfRange, - options.Repetitions)); - } - - this.State = options.State; - this.DueTime = options.DueTime; - this.Period = options.Period; - this.Ttl = options.Ttl; - this.Repetitions = options.Repetitions; - } - - /// - /// Gets the reminder state. - /// - public byte[] State { get; } - - /// - /// Gets the reminder due time. - /// - public TimeSpan DueTime { get; } - - /// - /// Gets the reminder period. - /// - public TimeSpan Period { get; } - - /// - /// The optional that states when the reminder will expire. - /// - public TimeSpan? Ttl { get; } - - /// - /// The optional property that gets the number of invocations of the reminder left. - /// - public int? Repetitions { get; } } -} + + /// + /// Initializes a new instance of . + /// + /// The actor type. + /// The actor id. + /// The reminder name. + /// The state associated with the reminder. + /// The reminder due time. + /// The reminder period. + /// The reminder ttl. + public ActorReminder( + string actorType, + ActorId actorId, + string name, + byte[] state, + TimeSpan dueTime, + TimeSpan period, + TimeSpan ttl) + : this(new ActorReminderOptions + { + ActorTypeName = actorType, + Id = actorId, + ReminderName = name, + State = state, + DueTime = dueTime, + Period = period, + Ttl = ttl + }) + { + } + + /// + /// Initializes a new instance of . + /// + /// The actor type. + /// The actor id. + /// The reminder name. + /// The state associated with the reminder. + /// The reminder due time. + /// The reminder period. + /// The number of times reminder should be invoked. + /// The reminder ttl. + public ActorReminder( + string actorType, + ActorId actorId, + string name, + byte[] state, + TimeSpan dueTime, + TimeSpan period, + int? repetitions, + TimeSpan? ttl) + : this(new ActorReminderOptions + { + ActorTypeName = actorType, + Id = actorId, + ReminderName = name, + State = state, + DueTime = dueTime, + Period = period, + Repetitions = repetitions, + Ttl = ttl + }) + { + } + + /// + /// Initializes a new instance of . + /// + /// The actor type. + /// The actor id. + /// The reminder name. + /// The state associated with the reminder. + /// The reminder due time. + /// The reminder period. + /// The number of times reminder should be invoked. + public ActorReminder( + string actorType, + ActorId actorId, + string name, + byte[] state, + TimeSpan dueTime, + TimeSpan period, + int? repetitions) + : this(new ActorReminderOptions + { + ActorTypeName = actorType, + Id = actorId, + ReminderName = name, + State = state, + DueTime = dueTime, + Period = period, + Repetitions = repetitions + }) + { + } + + /// + /// Initializes a new instance of . + /// + /// A containing the various settings for an . + internal ActorReminder(ActorReminderOptions options) + : base(options.ActorTypeName, options.Id, options.ReminderName) + { + if (options.DueTime < TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException(nameof(options.DueTime), string.Format( + CultureInfo.CurrentCulture, + SR.TimerArgumentOutOfRange, + TimeSpan.Zero.TotalMilliseconds, + TimeSpan.MaxValue.TotalMilliseconds)); + } + + if (options.Period < MiniumPeriod) + { + throw new ArgumentOutOfRangeException(nameof(options.Period), string.Format( + CultureInfo.CurrentCulture, + SR.TimerArgumentOutOfRange, + MiniumPeriod.TotalMilliseconds, + TimeSpan.MaxValue.TotalMilliseconds)); + } + + if (options.Ttl != null && (options.Ttl < options.DueTime || options.Ttl < TimeSpan.Zero)) + { + throw new ArgumentOutOfRangeException(nameof(options.Ttl), string.Format( + CultureInfo.CurrentCulture, + SR.TimerArgumentOutOfRange, + options.DueTime, + TimeSpan.MaxValue.TotalMilliseconds)); + } + + if (options.Repetitions != null && options.Repetitions <= 0) + { + throw new ArgumentOutOfRangeException(nameof(options.Repetitions), string.Format( + CultureInfo.CurrentCulture, + SR.RepetitionsArgumentOutOfRange, + options.Repetitions)); + } + + this.State = options.State; + this.DueTime = options.DueTime; + this.Period = options.Period; + this.Ttl = options.Ttl; + this.Repetitions = options.Repetitions; + } + + /// + /// Gets the reminder state. + /// + public byte[] State { get; } + + /// + /// Gets the reminder due time. + /// + public TimeSpan DueTime { get; } + + /// + /// Gets the reminder period. + /// + public TimeSpan Period { get; } + + /// + /// The optional that states when the reminder will expire. + /// + public TimeSpan? Ttl { get; } + + /// + /// The optional property that gets the number of invocations of the reminder left. + /// + public int? Repetitions { get; } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Runtime/ActorReminderOptions.cs b/src/Dapr.Actors/Runtime/ActorReminderOptions.cs index 5d81d68f..b27bd157 100644 --- a/src/Dapr.Actors/Runtime/ActorReminderOptions.cs +++ b/src/Dapr.Actors/Runtime/ActorReminderOptions.cs @@ -1,49 +1,48 @@ using System; -namespace Dapr.Actors.Runtime +namespace Dapr.Actors.Runtime; + +/// +/// A collection of options used to create an . +/// +internal class ActorReminderOptions { /// - /// A collection of options used to create an . + /// The name of the type of the Actor that the reminder will fire for. /// - internal class ActorReminderOptions - { - /// - /// The name of the type of the Actor that the reminder will fire for. - /// - public string ActorTypeName { get; set; } + public string ActorTypeName { get; set; } - /// - /// The that the reminder will fire for. - /// - public ActorId Id { get; set; } + /// + /// The that the reminder will fire for. + /// + public ActorId Id { get; set; } - /// - /// The name of the reminder. - /// - public string ReminderName { get; set; } + /// + /// The name of the reminder. + /// + public string ReminderName { get; set; } - /// - /// State that is passed to the Actor when the reminder fires. - /// - public byte[] State { get; set; } + /// + /// State that is passed to the Actor when the reminder fires. + /// + public byte[] State { get; set; } - /// - /// that determines when the reminder will first fire. - /// - public TimeSpan DueTime { get; set; } + /// + /// that determines when the reminder will first fire. + /// + public TimeSpan DueTime { get; set; } - /// - /// that determines how much time there is between the reminder firing. Starts after the . - /// - public TimeSpan Period { get; set; } + /// + /// that determines how much time there is between the reminder firing. Starts after the . + /// + public TimeSpan Period { get; set; } - /// - /// An optional that determines when the reminder will expire. - /// - public TimeSpan? Ttl { get; set; } + /// + /// An optional that determines when the reminder will expire. + /// + public TimeSpan? Ttl { get; set; } - /// - /// The number of repetitions for which the reminder should be invoked. - /// - public int? Repetitions { get; set; } - } -} + /// + /// The number of repetitions for which the reminder should be invoked. + /// + public int? Repetitions { get; set; } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Runtime/ActorReminderToken.cs b/src/Dapr.Actors/Runtime/ActorReminderToken.cs index 207fe3fe..a9f601ee 100644 --- a/src/Dapr.Actors/Runtime/ActorReminderToken.cs +++ b/src/Dapr.Actors/Runtime/ActorReminderToken.cs @@ -13,57 +13,56 @@ using System; -namespace Dapr.Actors.Runtime +namespace Dapr.Actors.Runtime; + +/// +/// Represents a reminder that can be unregistered by an actor. +/// +public class ActorReminderToken { /// - /// Represents a reminder that can be unregistered by an actor. + /// Initializes a new instance of . /// - public class ActorReminderToken + /// The actor type. + /// The actor id. + /// The reminder name. + public ActorReminderToken( + string actorType, + ActorId actorId, + string name) { - /// - /// Initializes a new instance of . - /// - /// The actor type. - /// The actor id. - /// The reminder name. - public ActorReminderToken( - string actorType, - ActorId actorId, - string name) + if (actorType == null) { - if (actorType == null) - { - throw new ArgumentNullException(nameof(actorType)); - } - - if (actorId == null) - { - throw new ArgumentNullException(nameof(actorId)); - } - - if (name == null) - { - throw new ArgumentNullException(nameof(name)); - } - - this.ActorType = actorType; - this.ActorId = actorId; - this.Name = name; + throw new ArgumentNullException(nameof(actorType)); } - /// - /// Gets the actor type. - /// - public string ActorType { get; } + if (actorId == null) + { + throw new ArgumentNullException(nameof(actorId)); + } - /// - /// Gets the actor id. - /// - public ActorId ActorId { get; } + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } - /// - /// Gets the reminder name. - /// - public string Name { get; } + this.ActorType = actorType; + this.ActorId = actorId; + this.Name = name; } -} + + /// + /// Gets the actor type. + /// + public string ActorType { get; } + + /// + /// Gets the actor id. + /// + public ActorId ActorId { get; } + + /// + /// Gets the reminder name. + /// + public string Name { get; } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Runtime/ActorRuntime.cs b/src/Dapr.Actors/Runtime/ActorRuntime.cs index 7f01fefb..6c8239ef 100644 --- a/src/Dapr.Actors/Runtime/ActorRuntime.cs +++ b/src/Dapr.Actors/Runtime/ActorRuntime.cs @@ -22,183 +22,182 @@ using System.Threading.Tasks; using Dapr.Actors.Client; using Microsoft.Extensions.Logging; -namespace Dapr.Actors.Runtime +namespace Dapr.Actors.Runtime; + +/// +/// Contains methods to register actor types. Registering the types allows the runtime to create instances of the actor. +/// +public sealed class ActorRuntime { - /// - /// Contains methods to register actor types. Registering the types allows the runtime to create instances of the actor. - /// - public sealed class ActorRuntime + // Map of ActorType --> ActorManager. + private readonly Dictionary actorManagers = new Dictionary(); + private readonly ActorRuntimeOptions options; + private readonly ILogger logger; + private readonly ActorActivatorFactory activatorFactory; + private readonly IActorProxyFactory proxyFactory; + + internal ActorRuntime(ActorRuntimeOptions options, ILoggerFactory loggerFactory, ActorActivatorFactory activatorFactory, IActorProxyFactory proxyFactory) { - // Map of ActorType --> ActorManager. - private readonly Dictionary actorManagers = new Dictionary(); - private readonly ActorRuntimeOptions options; - private readonly ILogger logger; - private readonly ActorActivatorFactory activatorFactory; - private readonly IActorProxyFactory proxyFactory; + this.options = options; + this.logger = loggerFactory.CreateLogger(this.GetType()); + this.activatorFactory = activatorFactory; + this.proxyFactory = proxyFactory; - internal ActorRuntime(ActorRuntimeOptions options, ILoggerFactory loggerFactory, ActorActivatorFactory activatorFactory, IActorProxyFactory proxyFactory) + // Loop through actor registrations and create the actor manager for each one. + // We do this up front so that we can catch initialization errors early, and so + // that access to state can have a simple threading model. + // + // Revisit this if actor initialization becomes a significant source of delay for large projects. + foreach (var actor in options.Actors) { - this.options = options; - this.logger = loggerFactory.CreateLogger(this.GetType()); - this.activatorFactory = activatorFactory; - this.proxyFactory = proxyFactory; - - // Loop through actor registrations and create the actor manager for each one. - // We do this up front so that we can catch initialization errors early, and so - // that access to state can have a simple threading model. - // - // Revisit this if actor initialization becomes a significant source of delay for large projects. - foreach (var actor in options.Actors) - { - var daprInteractor = new DaprHttpInteractor(clientHandler: null, httpEndpoint: options.HttpEndpoint, apiToken: options.DaprApiToken, requestTimeout: null); - this.actorManagers[actor.Type.ActorTypeName] = new ActorManager( - actor, - actor.Activator ?? this.activatorFactory.CreateActivator(actor.Type), - this.options.JsonSerializerOptions, - this.options.UseJsonSerialization, - loggerFactory, - proxyFactory, - daprInteractor); - } - } - - /// - /// Gets actor registrations registered with the runtime. - /// - public IReadOnlyList RegisteredActors => this.options.Actors; - - internal Task SerializeSettingsAndRegisteredTypes(IBufferWriter output) - { - using Utf8JsonWriter writer = new Utf8JsonWriter(output); - writer.WriteStartObject(); - - writer.WritePropertyName("entities"); - writer.WriteStartArray(); - - foreach (var actor in this.RegisteredActors) - { - writer.WriteStringValue(actor.Type.ActorTypeName); - } - - writer.WriteEndArray(); - - writeActorOptions(writer, this.options); - - var actorsWithConfigs = this.options.Actors.Where(actor => actor.TypeOptions != null).ToList(); - - if (actorsWithConfigs.Count > 0) - { - writer.WritePropertyName("entitiesConfig"); - writer.WriteStartArray(); - foreach (var actor in actorsWithConfigs) - { - writer.WriteStartObject(); - writer.WritePropertyName("entities"); - writer.WriteStartArray(); - writer.WriteStringValue(actor.Type.ActorTypeName); - writer.WriteEndArray(); - - writeActorOptions(writer, actor.TypeOptions); - - writer.WriteEndObject(); - } - writer.WriteEndArray(); - } - - writer.WriteEndObject(); - return writer.FlushAsync(); - } - - private void writeActorOptions(Utf8JsonWriter writer, ActorRuntimeOptions actorOptions) - { - if (actorOptions.ActorIdleTimeout != null) - { - writer.WriteString("actorIdleTimeout", ConverterUtils.ConvertTimeSpanValueInDaprFormat(actorOptions.ActorIdleTimeout)); - } - - if (actorOptions.ActorScanInterval != null) - { - writer.WriteString("actorScanInterval", ConverterUtils.ConvertTimeSpanValueInDaprFormat(actorOptions.ActorScanInterval)); - } - - if (actorOptions.DrainOngoingCallTimeout != null) - { - writer.WriteString("drainOngoingCallTimeout", ConverterUtils.ConvertTimeSpanValueInDaprFormat(actorOptions.DrainOngoingCallTimeout)); - } - - // default is false, don't write it if default - if (actorOptions.DrainRebalancedActors != false) - { - writer.WriteBoolean("drainRebalancedActors", (actorOptions.DrainRebalancedActors)); - } - - // default is null, don't write it if default - if (actorOptions.RemindersStoragePartitions != null) - { - writer.WriteNumber("remindersStoragePartitions", actorOptions.RemindersStoragePartitions.Value); - } - - // Reentrancy has a default value so it is always included. - writer.WriteStartObject("reentrancy"); - writer.WriteBoolean("enabled", actorOptions.ReentrancyConfig.Enabled); - if (actorOptions.ReentrancyConfig.MaxStackDepth != null) - { - writer.WriteNumber("maxStackDepth", actorOptions.ReentrancyConfig.MaxStackDepth.Value); - } - writer.WriteEndObject(); - } - - // Deactivates an actor for an actor type with given actor id. - internal async Task DeactivateAsync(string actorTypeName, string actorId) - { - using(this.logger.BeginScope("ActorType: {ActorType}, ActorId: {ActorId}", actorTypeName, actorId)) - { - await GetActorManager(actorTypeName).DeactivateActorAsync(new ActorId(actorId)); - } - } - - // Invokes the specified method for the actor when used with Remoting from CSharp client. - internal Task> DispatchWithRemotingAsync(string actorTypeName, string actorId, string actorMethodName, string daprActorheader, Stream data, CancellationToken cancellationToken = default) - { - using(this.logger.BeginScope("ActorType: {ActorType}, ActorId: {ActorId}, MethodName: {Reminder}", actorTypeName, actorId, actorMethodName)) - { - return GetActorManager(actorTypeName).DispatchWithRemotingAsync(new ActorId(actorId), actorMethodName, daprActorheader, data, cancellationToken); - } - } - - // Invokes the specified method for the actor when used without remoting, this is mainly used for cross language invocation. - internal Task DispatchWithoutRemotingAsync(string actorTypeName, string actorId, string actorMethodName, Stream requestBodyStream, Stream responseBodyStream, CancellationToken cancellationToken = default) - { - return GetActorManager(actorTypeName).DispatchWithoutRemotingAsync(new ActorId(actorId), actorMethodName, requestBodyStream, responseBodyStream, cancellationToken); - } - - // Fires a reminder for the Actor. - internal Task FireReminderAsync(string actorTypeName, string actorId, string reminderName, Stream requestBodyStream, CancellationToken cancellationToken = default) - { - using(this.logger.BeginScope("ActorType: {ActorType}, ActorId: {ActorId}, ReminderName: {Reminder}", actorTypeName, actorId, reminderName)) - { - return GetActorManager(actorTypeName).FireReminderAsync(new ActorId(actorId), reminderName, requestBodyStream, cancellationToken); - } - } - - // Fires a timer for the Actor. - internal Task FireTimerAsync(string actorTypeName, string actorId, string timerName, Stream requestBodyStream, CancellationToken cancellationToken = default) - { - using(this.logger.BeginScope("ActorType: {ActorType}, ActorId: {ActorId}, TimerName: {Timer}", actorTypeName, actorId, timerName)) - { - return GetActorManager(actorTypeName).FireTimerAsync(new ActorId(actorId), requestBodyStream, cancellationToken); - } - } - - private ActorManager GetActorManager(string actorTypeName) - { - if (!this.actorManagers.TryGetValue(actorTypeName, out var actorManager)) - { - var errorMsg = $"Actor type {actorTypeName} is not registered with Actor runtime."; - throw new InvalidOperationException(errorMsg); - } - - return actorManager; + var daprInteractor = new DaprHttpInteractor(clientHandler: null, httpEndpoint: options.HttpEndpoint, apiToken: options.DaprApiToken, requestTimeout: null); + this.actorManagers[actor.Type.ActorTypeName] = new ActorManager( + actor, + actor.Activator ?? this.activatorFactory.CreateActivator(actor.Type), + this.options.JsonSerializerOptions, + this.options.UseJsonSerialization, + loggerFactory, + proxyFactory, + daprInteractor); } } -} + + /// + /// Gets actor registrations registered with the runtime. + /// + public IReadOnlyList RegisteredActors => this.options.Actors; + + internal Task SerializeSettingsAndRegisteredTypes(IBufferWriter output) + { + using Utf8JsonWriter writer = new Utf8JsonWriter(output); + writer.WriteStartObject(); + + writer.WritePropertyName("entities"); + writer.WriteStartArray(); + + foreach (var actor in this.RegisteredActors) + { + writer.WriteStringValue(actor.Type.ActorTypeName); + } + + writer.WriteEndArray(); + + writeActorOptions(writer, this.options); + + var actorsWithConfigs = this.options.Actors.Where(actor => actor.TypeOptions != null).ToList(); + + if (actorsWithConfigs.Count > 0) + { + writer.WritePropertyName("entitiesConfig"); + writer.WriteStartArray(); + foreach (var actor in actorsWithConfigs) + { + writer.WriteStartObject(); + writer.WritePropertyName("entities"); + writer.WriteStartArray(); + writer.WriteStringValue(actor.Type.ActorTypeName); + writer.WriteEndArray(); + + writeActorOptions(writer, actor.TypeOptions); + + writer.WriteEndObject(); + } + writer.WriteEndArray(); + } + + writer.WriteEndObject(); + return writer.FlushAsync(); + } + + private void writeActorOptions(Utf8JsonWriter writer, ActorRuntimeOptions actorOptions) + { + if (actorOptions.ActorIdleTimeout != null) + { + writer.WriteString("actorIdleTimeout", ConverterUtils.ConvertTimeSpanValueInDaprFormat(actorOptions.ActorIdleTimeout)); + } + + if (actorOptions.ActorScanInterval != null) + { + writer.WriteString("actorScanInterval", ConverterUtils.ConvertTimeSpanValueInDaprFormat(actorOptions.ActorScanInterval)); + } + + if (actorOptions.DrainOngoingCallTimeout != null) + { + writer.WriteString("drainOngoingCallTimeout", ConverterUtils.ConvertTimeSpanValueInDaprFormat(actorOptions.DrainOngoingCallTimeout)); + } + + // default is false, don't write it if default + if (actorOptions.DrainRebalancedActors != false) + { + writer.WriteBoolean("drainRebalancedActors", (actorOptions.DrainRebalancedActors)); + } + + // default is null, don't write it if default + if (actorOptions.RemindersStoragePartitions != null) + { + writer.WriteNumber("remindersStoragePartitions", actorOptions.RemindersStoragePartitions.Value); + } + + // Reentrancy has a default value so it is always included. + writer.WriteStartObject("reentrancy"); + writer.WriteBoolean("enabled", actorOptions.ReentrancyConfig.Enabled); + if (actorOptions.ReentrancyConfig.MaxStackDepth != null) + { + writer.WriteNumber("maxStackDepth", actorOptions.ReentrancyConfig.MaxStackDepth.Value); + } + writer.WriteEndObject(); + } + + // Deactivates an actor for an actor type with given actor id. + internal async Task DeactivateAsync(string actorTypeName, string actorId) + { + using(this.logger.BeginScope("ActorType: {ActorType}, ActorId: {ActorId}", actorTypeName, actorId)) + { + await GetActorManager(actorTypeName).DeactivateActorAsync(new ActorId(actorId)); + } + } + + // Invokes the specified method for the actor when used with Remoting from CSharp client. + internal Task> DispatchWithRemotingAsync(string actorTypeName, string actorId, string actorMethodName, string daprActorheader, Stream data, CancellationToken cancellationToken = default) + { + using(this.logger.BeginScope("ActorType: {ActorType}, ActorId: {ActorId}, MethodName: {Reminder}", actorTypeName, actorId, actorMethodName)) + { + return GetActorManager(actorTypeName).DispatchWithRemotingAsync(new ActorId(actorId), actorMethodName, daprActorheader, data, cancellationToken); + } + } + + // Invokes the specified method for the actor when used without remoting, this is mainly used for cross language invocation. + internal Task DispatchWithoutRemotingAsync(string actorTypeName, string actorId, string actorMethodName, Stream requestBodyStream, Stream responseBodyStream, CancellationToken cancellationToken = default) + { + return GetActorManager(actorTypeName).DispatchWithoutRemotingAsync(new ActorId(actorId), actorMethodName, requestBodyStream, responseBodyStream, cancellationToken); + } + + // Fires a reminder for the Actor. + internal Task FireReminderAsync(string actorTypeName, string actorId, string reminderName, Stream requestBodyStream, CancellationToken cancellationToken = default) + { + using(this.logger.BeginScope("ActorType: {ActorType}, ActorId: {ActorId}, ReminderName: {Reminder}", actorTypeName, actorId, reminderName)) + { + return GetActorManager(actorTypeName).FireReminderAsync(new ActorId(actorId), reminderName, requestBodyStream, cancellationToken); + } + } + + // Fires a timer for the Actor. + internal Task FireTimerAsync(string actorTypeName, string actorId, string timerName, Stream requestBodyStream, CancellationToken cancellationToken = default) + { + using(this.logger.BeginScope("ActorType: {ActorType}, ActorId: {ActorId}, TimerName: {Timer}", actorTypeName, actorId, timerName)) + { + return GetActorManager(actorTypeName).FireTimerAsync(new ActorId(actorId), requestBodyStream, cancellationToken); + } + } + + private ActorManager GetActorManager(string actorTypeName) + { + if (!this.actorManagers.TryGetValue(actorTypeName, out var actorManager)) + { + var errorMsg = $"Actor type {actorTypeName} is not registered with Actor runtime."; + throw new InvalidOperationException(errorMsg); + } + + return actorManager; + } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Runtime/ActorRuntimeOptions.cs b/src/Dapr.Actors/Runtime/ActorRuntimeOptions.cs index 21c30201..b03cfc7d 100644 --- a/src/Dapr.Actors/Runtime/ActorRuntimeOptions.cs +++ b/src/Dapr.Actors/Runtime/ActorRuntimeOptions.cs @@ -16,223 +16,222 @@ using System; using System.Text.Json; -namespace Dapr.Actors.Runtime +namespace Dapr.Actors.Runtime; + +/// +/// Represents Dapr actor configuration for this app. Includes the registration of actor types +/// as well as runtime options. +/// +/// See https://docs.dapr.io/reference/api/actors_api/ +/// +public sealed class ActorRuntimeOptions { - /// - /// Represents Dapr actor configuration for this app. Includes the registration of actor types - /// as well as runtime options. - /// - /// See https://docs.dapr.io/reference/api/actors_api/ - /// - public sealed class ActorRuntimeOptions + private TimeSpan? actorIdleTimeout; + private TimeSpan? actorScanInterval; + private TimeSpan? drainOngoingCallTimeout; + private bool drainRebalancedActors; + private ActorReentrancyConfig reentrancyConfig = new ActorReentrancyConfig { - private TimeSpan? actorIdleTimeout; - private TimeSpan? actorScanInterval; - private TimeSpan? drainOngoingCallTimeout; - private bool drainRebalancedActors; - private ActorReentrancyConfig reentrancyConfig = new ActorReentrancyConfig + Enabled = false, + }; + private bool useJsonSerialization = false; + private JsonSerializerOptions jsonSerializerOptions = JsonSerializerDefaults.Web; + private string daprApiToken = string.Empty; + private int? remindersStoragePartitions = null; + + /// + /// Gets the collection of instances. + /// + public ActorRegistrationCollection Actors { get; } = new ActorRegistrationCollection(); + + /// + /// Specifies how long to wait before deactivating an idle actor. An actor is idle + /// if no actor method calls and no reminders have fired on it. + /// See https://docs.dapr.io/reference/api/actors_api/#dapr-calling-to-user-service-code + /// for more including default values. + /// + public TimeSpan? ActorIdleTimeout + { + get { - Enabled = false, - }; - private bool useJsonSerialization = false; - private JsonSerializerOptions jsonSerializerOptions = JsonSerializerDefaults.Web; - private string daprApiToken = string.Empty; - private int? remindersStoragePartitions = null; - - /// - /// Gets the collection of instances. - /// - public ActorRegistrationCollection Actors { get; } = new ActorRegistrationCollection(); - - /// - /// Specifies how long to wait before deactivating an idle actor. An actor is idle - /// if no actor method calls and no reminders have fired on it. - /// See https://docs.dapr.io/reference/api/actors_api/#dapr-calling-to-user-service-code - /// for more including default values. - /// - public TimeSpan? ActorIdleTimeout - { - get - { - return this.actorIdleTimeout; - } - - set - { - if (value <= TimeSpan.Zero) - { - throw new ArgumentOutOfRangeException(nameof(actorIdleTimeout), actorIdleTimeout, "must be positive"); - } - - this.actorIdleTimeout = value; - } + return this.actorIdleTimeout; } - /// - /// A duration which specifies how often to scan for actors to deactivate idle actors. - /// Actors that have been idle longer than the actorIdleTimeout will be deactivated. - /// See https://docs.dapr.io/reference/api/actors_api/#dapr-calling-to-user-service-code - /// for more including default values. - /// - public TimeSpan? ActorScanInterval + set { - get + if (value <= TimeSpan.Zero) { - return this.actorScanInterval; + throw new ArgumentOutOfRangeException(nameof(actorIdleTimeout), actorIdleTimeout, "must be positive"); } - set - { - if (value <= TimeSpan.Zero) - { - throw new ArgumentOutOfRangeException(nameof(actorScanInterval), actorScanInterval, "must be positive"); - } - - this.actorScanInterval = value; - } + this.actorIdleTimeout = value; } - - /// - /// A duration used when in the process of draining rebalanced actors. This specifies - /// how long to wait for the current active actor method to finish. If there is no current actor method call, this is ignored. - /// See https://docs.dapr.io/reference/api/actors_api/#dapr-calling-to-user-service-code - /// for more including default values. - /// - public TimeSpan? DrainOngoingCallTimeout - { - get - { - return this.drainOngoingCallTimeout; - } - - set - { - if (value <= TimeSpan.Zero) - { - throw new ArgumentOutOfRangeException(nameof(drainOngoingCallTimeout), drainOngoingCallTimeout, "must be positive"); - } - - this.drainOngoingCallTimeout = value; - } - } - - /// - /// A bool. If true, Dapr will wait for drainOngoingCallTimeout to allow a current - /// actor call to complete before trying to deactivate an actor. If false, do not wait. - /// See https://docs.dapr.io/reference/api/actors_api/#dapr-calling-to-user-service-code - /// for more including default values. - /// - public bool DrainRebalancedActors - { - get - { - return this.drainRebalancedActors; - } - - set - { - this.drainRebalancedActors = value; - } - } - - /// - /// A configuration that defines the Actor Reentrancy parameters. If set and enabled, Actors - /// will be able to perform reentrant calls to themselves/others. If not set or false, Actors - /// will continue to lock for every request. - /// See https://docs.dapr.io/developing-applications/building-blocks/actors/actor-reentrancy/ - /// - public ActorReentrancyConfig ReentrancyConfig - { - get - { - return this.reentrancyConfig; - } - - set - { - this.reentrancyConfig = value; - } - } - - /// - /// Enable JSON serialization for actor proxy message serialization in both remoting and non-remoting invocations. - /// - public bool UseJsonSerialization - { - get - { - return this.useJsonSerialization; - } - - set - { - this.useJsonSerialization = value; - } - } - - /// - /// The to use for actor state persistence and message deserialization - /// - public JsonSerializerOptions JsonSerializerOptions - { - get - { - return this.jsonSerializerOptions; - } - - set - { - this.jsonSerializerOptions = value ?? throw new ArgumentNullException(nameof(JsonSerializerOptions), $"{nameof(ActorRuntimeOptions)}.{nameof(JsonSerializerOptions)} cannot be null"); - } - } - - /// - /// The to add to the headers in requests to Dapr runtime - /// - public string? DaprApiToken - { - get - { - return this.daprApiToken; - } - - set - { - this.daprApiToken = value ?? throw new ArgumentNullException(nameof(DaprApiToken), $"{nameof(ActorRuntimeOptions)}.{nameof(DaprApiToken)} cannot be null"); - } - } - - /// - /// An int used to determine how many partitions to use for reminders storage. - /// - public int? RemindersStoragePartitions - { - get - { - return this.remindersStoragePartitions; - } - - set - { - if (value < 0) - { - throw new ArgumentOutOfRangeException(nameof(remindersStoragePartitions), remindersStoragePartitions, "must be positive"); - } - - this.remindersStoragePartitions = value; - } - } - - /// - /// Gets or sets the HTTP endpoint URI used to communicate with the Dapr sidecar. - /// - /// - /// The URI endpoint to use for HTTP calls to the Dapr runtime. The default value will be - /// DAPR_HTTP_ENDPOINT first, or http://127.0.0.1:DAPR_HTTP_PORT as fallback - /// where DAPR_HTTP_ENDPOINT and DAPR_HTTP_PORT represents the value of the - /// corresponding environment variables. - /// - /// - public string? HttpEndpoint { get; set; } } -} + + /// + /// A duration which specifies how often to scan for actors to deactivate idle actors. + /// Actors that have been idle longer than the actorIdleTimeout will be deactivated. + /// See https://docs.dapr.io/reference/api/actors_api/#dapr-calling-to-user-service-code + /// for more including default values. + /// + public TimeSpan? ActorScanInterval + { + get + { + return this.actorScanInterval; + } + + set + { + if (value <= TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException(nameof(actorScanInterval), actorScanInterval, "must be positive"); + } + + this.actorScanInterval = value; + } + } + + /// + /// A duration used when in the process of draining rebalanced actors. This specifies + /// how long to wait for the current active actor method to finish. If there is no current actor method call, this is ignored. + /// See https://docs.dapr.io/reference/api/actors_api/#dapr-calling-to-user-service-code + /// for more including default values. + /// + public TimeSpan? DrainOngoingCallTimeout + { + get + { + return this.drainOngoingCallTimeout; + } + + set + { + if (value <= TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException(nameof(drainOngoingCallTimeout), drainOngoingCallTimeout, "must be positive"); + } + + this.drainOngoingCallTimeout = value; + } + } + + /// + /// A bool. If true, Dapr will wait for drainOngoingCallTimeout to allow a current + /// actor call to complete before trying to deactivate an actor. If false, do not wait. + /// See https://docs.dapr.io/reference/api/actors_api/#dapr-calling-to-user-service-code + /// for more including default values. + /// + public bool DrainRebalancedActors + { + get + { + return this.drainRebalancedActors; + } + + set + { + this.drainRebalancedActors = value; + } + } + + /// + /// A configuration that defines the Actor Reentrancy parameters. If set and enabled, Actors + /// will be able to perform reentrant calls to themselves/others. If not set or false, Actors + /// will continue to lock for every request. + /// See https://docs.dapr.io/developing-applications/building-blocks/actors/actor-reentrancy/ + /// + public ActorReentrancyConfig ReentrancyConfig + { + get + { + return this.reentrancyConfig; + } + + set + { + this.reentrancyConfig = value; + } + } + + /// + /// Enable JSON serialization for actor proxy message serialization in both remoting and non-remoting invocations. + /// + public bool UseJsonSerialization + { + get + { + return this.useJsonSerialization; + } + + set + { + this.useJsonSerialization = value; + } + } + + /// + /// The to use for actor state persistence and message deserialization + /// + public JsonSerializerOptions JsonSerializerOptions + { + get + { + return this.jsonSerializerOptions; + } + + set + { + this.jsonSerializerOptions = value ?? throw new ArgumentNullException(nameof(JsonSerializerOptions), $"{nameof(ActorRuntimeOptions)}.{nameof(JsonSerializerOptions)} cannot be null"); + } + } + + /// + /// The to add to the headers in requests to Dapr runtime + /// + public string? DaprApiToken + { + get + { + return this.daprApiToken; + } + + set + { + this.daprApiToken = value ?? throw new ArgumentNullException(nameof(DaprApiToken), $"{nameof(ActorRuntimeOptions)}.{nameof(DaprApiToken)} cannot be null"); + } + } + + /// + /// An int used to determine how many partitions to use for reminders storage. + /// + public int? RemindersStoragePartitions + { + get + { + return this.remindersStoragePartitions; + } + + set + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(remindersStoragePartitions), remindersStoragePartitions, "must be positive"); + } + + this.remindersStoragePartitions = value; + } + } + + /// + /// Gets or sets the HTTP endpoint URI used to communicate with the Dapr sidecar. + /// + /// + /// The URI endpoint to use for HTTP calls to the Dapr runtime. The default value will be + /// DAPR_HTTP_ENDPOINT first, or http://127.0.0.1:DAPR_HTTP_PORT as fallback + /// where DAPR_HTTP_ENDPOINT and DAPR_HTTP_PORT represents the value of the + /// corresponding environment variables. + /// + /// + public string? HttpEndpoint { get; set; } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Runtime/ActorStateChange.cs b/src/Dapr.Actors/Runtime/ActorStateChange.cs index 34fa68fd..0935f679 100644 --- a/src/Dapr.Actors/Runtime/ActorStateChange.cs +++ b/src/Dapr.Actors/Runtime/ActorStateChange.cs @@ -11,75 +11,74 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Runtime +namespace Dapr.Actors.Runtime; + +using System; + +/// +/// Represents a change to an actor state with a given state name. +/// +public sealed class ActorStateChange { - using System; + /// + /// Initializes a new instance of the class. + /// + /// The name of the actor state. + /// The type of value associated with given actor state name. + /// The value associated with given actor state name. + /// The kind of state change for given actor state name. + /// The time to live for the state. + public ActorStateChange(string stateName, Type type, object value, StateChangeKind changeKind, DateTimeOffset? ttlExpireTime) + { + ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName)); + + this.StateName = stateName; + this.Type = type; + this.Value = value; + this.ChangeKind = changeKind; + this.TTLExpireTime = ttlExpireTime; + } /// - /// Represents a change to an actor state with a given state name. + /// Gets the name of the actor state. /// - public sealed class ActorStateChange - { - /// - /// Initializes a new instance of the class. - /// - /// The name of the actor state. - /// The type of value associated with given actor state name. - /// The value associated with given actor state name. - /// The kind of state change for given actor state name. - /// The time to live for the state. - public ActorStateChange(string stateName, Type type, object value, StateChangeKind changeKind, DateTimeOffset? ttlExpireTime) - { - ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName)); + /// + /// The name of the actor state. + /// + public string StateName { get; } - this.StateName = stateName; - this.Type = type; - this.Value = value; - this.ChangeKind = changeKind; - this.TTLExpireTime = ttlExpireTime; - } + /// + /// Gets the type of value associated with given actor state name. + /// + /// + /// The type of value associated with given actor state name. + /// + public Type Type { get; } - /// - /// Gets the name of the actor state. - /// - /// - /// The name of the actor state. - /// - public string StateName { get; } + /// + /// Gets the value associated with given actor state name. + /// + /// + /// The value associated with given actor state name. + /// + public object Value { get; } - /// - /// Gets the type of value associated with given actor state name. - /// - /// - /// The type of value associated with given actor state name. - /// - public Type Type { get; } + /// + /// Gets the kind of state change for given actor state name. + /// + /// + /// The kind of state change for given actor state name. + /// + public StateChangeKind ChangeKind { get; } - /// - /// Gets the value associated with given actor state name. - /// - /// - /// The value associated with given actor state name. - /// - public object Value { get; } - - /// - /// Gets the kind of state change for given actor state name. - /// - /// - /// The kind of state change for given actor state name. - /// - public StateChangeKind ChangeKind { get; } - - /// - /// Gets the time to live for the state. - /// - /// - /// The time to live for the state. - /// - /// - /// If null, the state will not expire. - /// - public DateTimeOffset? TTLExpireTime { get; } - } -} + /// + /// Gets the time to live for the state. + /// + /// + /// The time to live for the state. + /// + /// + /// If null, the state will not expire. + /// + public DateTimeOffset? TTLExpireTime { get; } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Runtime/ActorStateManager.cs b/src/Dapr.Actors/Runtime/ActorStateManager.cs index 31ada443..277f6c4c 100644 --- a/src/Dapr.Actors/Runtime/ActorStateManager.cs +++ b/src/Dapr.Actors/Runtime/ActorStateManager.cs @@ -19,567 +19,566 @@ using System.Threading.Tasks; using Dapr.Actors.Resources; using Dapr.Actors.Communication; -namespace Dapr.Actors.Runtime +namespace Dapr.Actors.Runtime; + +internal sealed class ActorStateManager : IActorStateManager, IActorContextualState { - internal sealed class ActorStateManager : IActorStateManager, IActorContextualState + private readonly Actor actor; + private readonly string actorTypeName; + private readonly Dictionary defaultTracker; + private static AsyncLocal<(string id, Dictionary tracker)> context = new AsyncLocal<(string, Dictionary)>(); + + internal ActorStateManager(Actor actor) { - private readonly Actor actor; - private readonly string actorTypeName; - private readonly Dictionary defaultTracker; - private static AsyncLocal<(string id, Dictionary tracker)> context = new AsyncLocal<(string, Dictionary)>(); + this.actor = actor; + this.actorTypeName = actor.Host.ActorTypeInfo.ActorTypeName; + this.defaultTracker = new Dictionary(); + } - internal ActorStateManager(Actor actor) + public async Task AddStateAsync(string stateName, T value, CancellationToken cancellationToken) + { + EnsureStateProviderInitialized(); + + if (!(await this.TryAddStateAsync(stateName, value, cancellationToken))) { - this.actor = actor; - this.actorTypeName = actor.Host.ActorTypeInfo.ActorTypeName; - this.defaultTracker = new Dictionary(); + throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, SR.ActorStateAlreadyExists, stateName)); + } + } + + public async Task AddStateAsync(string stateName, T value, TimeSpan ttl, CancellationToken cancellationToken) + { + EnsureStateProviderInitialized(); + + if (!(await this.TryAddStateAsync(stateName, value, ttl, cancellationToken))) + { + throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, SR.ActorStateAlreadyExists, stateName)); + } + } + + public async Task TryAddStateAsync(string stateName, T value, CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName)); + + EnsureStateProviderInitialized(); + + var stateChangeTracker = GetContextualStateTracker(); + + if (stateChangeTracker.ContainsKey(stateName)) + { + var stateMetadata = stateChangeTracker[stateName]; + + // Check if the property was marked as remove or is expired in the cache + if (stateMetadata.ChangeKind == StateChangeKind.Remove || (stateMetadata.TTLExpireTime.HasValue && stateMetadata.TTLExpireTime.Value <= DateTimeOffset.UtcNow)) + { + stateChangeTracker[stateName] = StateMetadata.Create(value, StateChangeKind.Update); + return true; + } + + return false; } - public async Task AddStateAsync(string stateName, T value, CancellationToken cancellationToken) + if (await this.actor.Host.StateProvider.ContainsStateAsync(this.actorTypeName, this.actor.Id.ToString(), stateName, cancellationToken)) { - EnsureStateProviderInitialized(); - - if (!(await this.TryAddStateAsync(stateName, value, cancellationToken))) - { - throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, SR.ActorStateAlreadyExists, stateName)); - } + return false; } - public async Task AddStateAsync(string stateName, T value, TimeSpan ttl, CancellationToken cancellationToken) - { - EnsureStateProviderInitialized(); + stateChangeTracker[stateName] = StateMetadata.Create(value, StateChangeKind.Add); + return true; + } - if (!(await this.TryAddStateAsync(stateName, value, ttl, cancellationToken))) + public async Task TryAddStateAsync(string stateName, T value, TimeSpan ttl, CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName)); + + EnsureStateProviderInitialized(); + + var stateChangeTracker = GetContextualStateTracker(); + + if (stateChangeTracker.ContainsKey(stateName)) + { + var stateMetadata = stateChangeTracker[stateName]; + + // Check if the property was marked as remove in the cache or has been expired. + if (stateMetadata.ChangeKind == StateChangeKind.Remove || (stateMetadata.TTLExpireTime.HasValue && stateMetadata.TTLExpireTime.Value <= DateTimeOffset.UtcNow)) { - throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, SR.ActorStateAlreadyExists, stateName)); + stateChangeTracker[stateName] = StateMetadata.Create(value, StateChangeKind.Update, ttl: ttl); + return true; } + + return false; } - public async Task TryAddStateAsync(string stateName, T value, CancellationToken cancellationToken = default) + if (await this.actor.Host.StateProvider.ContainsStateAsync(this.actorTypeName, this.actor.Id.ToString(), stateName, cancellationToken)) { - ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName)); + return false; + } - EnsureStateProviderInitialized(); + stateChangeTracker[stateName] = StateMetadata.Create(value, StateChangeKind.Add, ttl: ttl); + return true; + } - var stateChangeTracker = GetContextualStateTracker(); + public async Task GetStateAsync(string stateName, CancellationToken cancellationToken) + { + EnsureStateProviderInitialized(); - if (stateChangeTracker.ContainsKey(stateName)) + var condRes = await this.TryGetStateAsync(stateName, cancellationToken); + + if (condRes.HasValue) + { + return condRes.Value; + } + + throw new KeyNotFoundException(string.Format(CultureInfo.CurrentCulture, SR.ErrorNamedActorStateNotFound, stateName)); + } + + public async Task> TryGetStateAsync(string stateName, CancellationToken cancellationToken) + { + ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName)); + + EnsureStateProviderInitialized(); + + var stateChangeTracker = GetContextualStateTracker(); + + if (stateChangeTracker.ContainsKey(stateName)) + { + var stateMetadata = stateChangeTracker[stateName]; + + // Check if the property was marked as remove in the cache or is expired + if (stateMetadata.ChangeKind == StateChangeKind.Remove || (stateMetadata.TTLExpireTime.HasValue && stateMetadata.TTLExpireTime.Value <= DateTimeOffset.UtcNow)) { - var stateMetadata = stateChangeTracker[stateName]; - - // Check if the property was marked as remove or is expired in the cache - if (stateMetadata.ChangeKind == StateChangeKind.Remove || (stateMetadata.TTLExpireTime.HasValue && stateMetadata.TTLExpireTime.Value <= DateTimeOffset.UtcNow)) - { - stateChangeTracker[stateName] = StateMetadata.Create(value, StateChangeKind.Update); - return true; - } - - return false; + return new ConditionalValue(false, default); } - if (await this.actor.Host.StateProvider.ContainsStateAsync(this.actorTypeName, this.actor.Id.ToString(), stateName, cancellationToken)) - { - return false; - } + return new ConditionalValue(true, (T)stateMetadata.Value); + } + var conditionalResult = await this.TryGetStateFromStateProviderAsync(stateName, cancellationToken); + if (conditionalResult.HasValue) + { + stateChangeTracker.Add(stateName, StateMetadata.Create(conditionalResult.Value.Value, StateChangeKind.None, ttlExpireTime: conditionalResult.Value.TTLExpireTime)); + return new ConditionalValue(true, conditionalResult.Value.Value); + } + + return new ConditionalValue(false, default); + } + + public async Task SetStateAsync(string stateName, T value, CancellationToken cancellationToken) + { + ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName)); + + EnsureStateProviderInitialized(); + + var stateChangeTracker = GetContextualStateTracker(); + + if (stateChangeTracker.ContainsKey(stateName)) + { + var stateMetadata = stateChangeTracker[stateName]; + stateMetadata.Value = value; + stateMetadata.TTLExpireTime = null; + + if (stateMetadata.ChangeKind == StateChangeKind.None || + stateMetadata.ChangeKind == StateChangeKind.Remove) + { + stateMetadata.ChangeKind = StateChangeKind.Update; + } + } + else if (await this.actor.Host.StateProvider.ContainsStateAsync(this.actorTypeName, this.actor.Id.ToString(), stateName, cancellationToken)) + { + stateChangeTracker.Add(stateName, StateMetadata.Create(value, StateChangeKind.Update)); + } + else + { stateChangeTracker[stateName] = StateMetadata.Create(value, StateChangeKind.Add); - return true; } + } - public async Task TryAddStateAsync(string stateName, T value, TimeSpan ttl, CancellationToken cancellationToken = default) + public async Task SetStateAsync(string stateName, T value, TimeSpan ttl, CancellationToken cancellationToken) + { + ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName)); + + EnsureStateProviderInitialized(); + + var stateChangeTracker = GetContextualStateTracker(); + + if (stateChangeTracker.ContainsKey(stateName)) { - ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName)); + var stateMetadata = stateChangeTracker[stateName]; + stateMetadata.Value = value; + stateMetadata.TTLExpireTime = DateTimeOffset.UtcNow.Add(ttl); - EnsureStateProviderInitialized(); - - var stateChangeTracker = GetContextualStateTracker(); - - if (stateChangeTracker.ContainsKey(stateName)) + if (stateMetadata.ChangeKind == StateChangeKind.None || + stateMetadata.ChangeKind == StateChangeKind.Remove) { - var stateMetadata = stateChangeTracker[stateName]; - - // Check if the property was marked as remove in the cache or has been expired. - if (stateMetadata.ChangeKind == StateChangeKind.Remove || (stateMetadata.TTLExpireTime.HasValue && stateMetadata.TTLExpireTime.Value <= DateTimeOffset.UtcNow)) - { - stateChangeTracker[stateName] = StateMetadata.Create(value, StateChangeKind.Update, ttl: ttl); - return true; - } - - return false; + stateMetadata.ChangeKind = StateChangeKind.Update; } - - if (await this.actor.Host.StateProvider.ContainsStateAsync(this.actorTypeName, this.actor.Id.ToString(), stateName, cancellationToken)) - { - return false; - } - + } + else if (await this.actor.Host.StateProvider.ContainsStateAsync(this.actorTypeName, this.actor.Id.ToString(), stateName, cancellationToken)) + { + stateChangeTracker.Add(stateName, StateMetadata.Create(value, StateChangeKind.Update, ttl: ttl)); + } + else + { stateChangeTracker[stateName] = StateMetadata.Create(value, StateChangeKind.Add, ttl: ttl); - return true; } + } - public async Task GetStateAsync(string stateName, CancellationToken cancellationToken) + public async Task RemoveStateAsync(string stateName, CancellationToken cancellationToken) + { + EnsureStateProviderInitialized(); + + if (!(await this.TryRemoveStateAsync(stateName, cancellationToken))) { - EnsureStateProviderInitialized(); - - var condRes = await this.TryGetStateAsync(stateName, cancellationToken); - - if (condRes.HasValue) - { - return condRes.Value; - } - throw new KeyNotFoundException(string.Format(CultureInfo.CurrentCulture, SR.ErrorNamedActorStateNotFound, stateName)); } + } - public async Task> TryGetStateAsync(string stateName, CancellationToken cancellationToken) + public async Task TryRemoveStateAsync(string stateName, CancellationToken cancellationToken) + { + ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName)); + + EnsureStateProviderInitialized(); + + var stateChangeTracker = GetContextualStateTracker(); + + if (stateChangeTracker.ContainsKey(stateName)) { - ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName)); + var stateMetadata = stateChangeTracker[stateName]; - EnsureStateProviderInitialized(); - - var stateChangeTracker = GetContextualStateTracker(); - - if (stateChangeTracker.ContainsKey(stateName)) + if (stateMetadata.TTLExpireTime.HasValue && stateMetadata.TTLExpireTime.Value <= DateTimeOffset.UtcNow) { - var stateMetadata = stateChangeTracker[stateName]; - - // Check if the property was marked as remove in the cache or is expired - if (stateMetadata.ChangeKind == StateChangeKind.Remove || (stateMetadata.TTLExpireTime.HasValue && stateMetadata.TTLExpireTime.Value <= DateTimeOffset.UtcNow)) - { - return new ConditionalValue(false, default); - } - - return new ConditionalValue(true, (T)stateMetadata.Value); + stateChangeTracker.Remove(stateName); + return false; } - var conditionalResult = await this.TryGetStateFromStateProviderAsync(stateName, cancellationToken); - if (conditionalResult.HasValue) + switch (stateMetadata.ChangeKind) { - stateChangeTracker.Add(stateName, StateMetadata.Create(conditionalResult.Value.Value, StateChangeKind.None, ttlExpireTime: conditionalResult.Value.TTLExpireTime)); - return new ConditionalValue(true, conditionalResult.Value.Value); - } - - return new ConditionalValue(false, default); - } - - public async Task SetStateAsync(string stateName, T value, CancellationToken cancellationToken) - { - ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName)); - - EnsureStateProviderInitialized(); - - var stateChangeTracker = GetContextualStateTracker(); - - if (stateChangeTracker.ContainsKey(stateName)) - { - var stateMetadata = stateChangeTracker[stateName]; - stateMetadata.Value = value; - stateMetadata.TTLExpireTime = null; - - if (stateMetadata.ChangeKind == StateChangeKind.None || - stateMetadata.ChangeKind == StateChangeKind.Remove) - { - stateMetadata.ChangeKind = StateChangeKind.Update; - } - } - else if (await this.actor.Host.StateProvider.ContainsStateAsync(this.actorTypeName, this.actor.Id.ToString(), stateName, cancellationToken)) - { - stateChangeTracker.Add(stateName, StateMetadata.Create(value, StateChangeKind.Update)); - } - else - { - stateChangeTracker[stateName] = StateMetadata.Create(value, StateChangeKind.Add); - } - } - - public async Task SetStateAsync(string stateName, T value, TimeSpan ttl, CancellationToken cancellationToken) - { - ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName)); - - EnsureStateProviderInitialized(); - - var stateChangeTracker = GetContextualStateTracker(); - - if (stateChangeTracker.ContainsKey(stateName)) - { - var stateMetadata = stateChangeTracker[stateName]; - stateMetadata.Value = value; - stateMetadata.TTLExpireTime = DateTimeOffset.UtcNow.Add(ttl); - - if (stateMetadata.ChangeKind == StateChangeKind.None || - stateMetadata.ChangeKind == StateChangeKind.Remove) - { - stateMetadata.ChangeKind = StateChangeKind.Update; - } - } - else if (await this.actor.Host.StateProvider.ContainsStateAsync(this.actorTypeName, this.actor.Id.ToString(), stateName, cancellationToken)) - { - stateChangeTracker.Add(stateName, StateMetadata.Create(value, StateChangeKind.Update, ttl: ttl)); - } - else - { - stateChangeTracker[stateName] = StateMetadata.Create(value, StateChangeKind.Add, ttl: ttl); - } - } - - public async Task RemoveStateAsync(string stateName, CancellationToken cancellationToken) - { - EnsureStateProviderInitialized(); - - if (!(await this.TryRemoveStateAsync(stateName, cancellationToken))) - { - throw new KeyNotFoundException(string.Format(CultureInfo.CurrentCulture, SR.ErrorNamedActorStateNotFound, stateName)); - } - } - - public async Task TryRemoveStateAsync(string stateName, CancellationToken cancellationToken) - { - ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName)); - - EnsureStateProviderInitialized(); - - var stateChangeTracker = GetContextualStateTracker(); - - if (stateChangeTracker.ContainsKey(stateName)) - { - var stateMetadata = stateChangeTracker[stateName]; - - if (stateMetadata.TTLExpireTime.HasValue && stateMetadata.TTLExpireTime.Value <= DateTimeOffset.UtcNow) - { - stateChangeTracker.Remove(stateName); + case StateChangeKind.Remove: return false; - } - - switch (stateMetadata.ChangeKind) - { - case StateChangeKind.Remove: - return false; - case StateChangeKind.Add: - stateChangeTracker.Remove(stateName); - return true; - } - - stateMetadata.ChangeKind = StateChangeKind.Remove; - return true; + case StateChangeKind.Add: + stateChangeTracker.Remove(stateName); + return true; } - if (await this.actor.Host.StateProvider.ContainsStateAsync(this.actorTypeName, this.actor.Id.ToString(), stateName, cancellationToken)) - { - stateChangeTracker.Add(stateName, StateMetadata.CreateForRemove()); - return true; - } - - return false; + stateMetadata.ChangeKind = StateChangeKind.Remove; + return true; } - public async Task ContainsStateAsync(string stateName, CancellationToken cancellationToken) + if (await this.actor.Host.StateProvider.ContainsStateAsync(this.actorTypeName, this.actor.Id.ToString(), stateName, cancellationToken)) { - ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName)); + stateChangeTracker.Add(stateName, StateMetadata.CreateForRemove()); + return true; + } - EnsureStateProviderInitialized(); + return false; + } - var stateChangeTracker = GetContextualStateTracker(); + public async Task ContainsStateAsync(string stateName, CancellationToken cancellationToken) + { + ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName)); - if (stateChangeTracker.ContainsKey(stateName)) + EnsureStateProviderInitialized(); + + var stateChangeTracker = GetContextualStateTracker(); + + if (stateChangeTracker.ContainsKey(stateName)) + { + var stateMetadata = stateChangeTracker[stateName]; + + // Check if the property was marked as remove in the cache + return stateMetadata.ChangeKind != StateChangeKind.Remove; + } + + if (await this.actor.Host.StateProvider.ContainsStateAsync(this.actorTypeName, this.actor.Id.ToString(), stateName, cancellationToken)) + { + return true; + } + + return false; + } + + public async Task GetOrAddStateAsync(string stateName, T value, CancellationToken cancellationToken) + { + EnsureStateProviderInitialized(); + + var condRes = await this.TryGetStateAsync(stateName, cancellationToken); + + if (condRes.HasValue) + { + return condRes.Value; + } + + var changeKind = this.IsStateMarkedForRemove(stateName) ? StateChangeKind.Update : StateChangeKind.Add; + + var stateChangeTracker = GetContextualStateTracker(); + stateChangeTracker[stateName] = StateMetadata.Create(value, changeKind); + return value; + } + + public async Task GetOrAddStateAsync(string stateName, T value, TimeSpan ttl, CancellationToken cancellationToken) + { + EnsureStateProviderInitialized(); + + var condRes = await this.TryGetStateAsync(stateName, cancellationToken); + + if (condRes.HasValue) + { + return condRes.Value; + } + + var changeKind = this.IsStateMarkedForRemove(stateName) ? StateChangeKind.Update : StateChangeKind.Add; + + var stateChangeTracker = GetContextualStateTracker(); + stateChangeTracker[stateName] = StateMetadata.Create(value, changeKind, ttl: ttl); + return value; + } + + public async Task AddOrUpdateStateAsync( + string stateName, + T addValue, + Func updateValueFactory, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName)); + + EnsureStateProviderInitialized(); + + var stateChangeTracker = GetContextualStateTracker(); + + if (stateChangeTracker.ContainsKey(stateName)) + { + var stateMetadata = stateChangeTracker[stateName]; + + // Check if the property was marked as remove in the cache + if (stateMetadata.ChangeKind == StateChangeKind.Remove) + { + stateChangeTracker[stateName] = StateMetadata.Create(addValue, StateChangeKind.Update); + return addValue; + } + + var newValue = updateValueFactory.Invoke(stateName, (T)stateMetadata.Value); + stateMetadata.Value = newValue; + + if (stateMetadata.ChangeKind == StateChangeKind.None) + { + stateMetadata.ChangeKind = StateChangeKind.Update; + } + + return newValue; + } + + var conditionalResult = await this.TryGetStateFromStateProviderAsync(stateName, cancellationToken); + if (conditionalResult.HasValue) + { + var newValue = updateValueFactory.Invoke(stateName, conditionalResult.Value.Value); + stateChangeTracker.Add(stateName, StateMetadata.Create(newValue, StateChangeKind.Update)); + + return newValue; + } + + stateChangeTracker[stateName] = StateMetadata.Create(addValue, StateChangeKind.Add); + return addValue; + } + + public async Task AddOrUpdateStateAsync( + string stateName, + T addValue, + Func updateValueFactory, + TimeSpan ttl, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName)); + + EnsureStateProviderInitialized(); + + var stateChangeTracker = GetContextualStateTracker(); + + if (stateChangeTracker.ContainsKey(stateName)) + { + var stateMetadata = stateChangeTracker[stateName]; + + // Check if the property was marked as remove in the cache + if (stateMetadata.ChangeKind == StateChangeKind.Remove) + { + stateChangeTracker[stateName] = StateMetadata.Create(addValue, StateChangeKind.Update, ttl: ttl); + return addValue; + } + + var newValue = updateValueFactory.Invoke(stateName, (T)stateMetadata.Value); + stateMetadata.Value = newValue; + + if (stateMetadata.ChangeKind == StateChangeKind.None) + { + stateMetadata.ChangeKind = StateChangeKind.Update; + } + + return newValue; + } + + var conditionalResult = await this.TryGetStateFromStateProviderAsync(stateName, cancellationToken); + if (conditionalResult.HasValue) + { + var newValue = updateValueFactory.Invoke(stateName, conditionalResult.Value.Value); + stateChangeTracker.Add(stateName, StateMetadata.Create(newValue, StateChangeKind.Update, ttl: ttl)); + + return newValue; + } + + stateChangeTracker[stateName] = StateMetadata.Create(addValue, StateChangeKind.Add, ttl: ttl); + return addValue; + } + + public Task ClearCacheAsync(CancellationToken cancellationToken) + { + EnsureStateProviderInitialized(); + + var stateChangeTracker = GetContextualStateTracker(); + + stateChangeTracker.Clear(); + return Task.CompletedTask; + } + + public async Task SaveStateAsync(CancellationToken cancellationToken = default) + { + EnsureStateProviderInitialized(); + + var stateChangeTracker = GetContextualStateTracker(); + + if (stateChangeTracker.Count > 0) + { + var stateChangeList = new List(); + var statesToRemove = new List(); + + foreach (var stateName in stateChangeTracker.Keys) { var stateMetadata = stateChangeTracker[stateName]; - // Check if the property was marked as remove in the cache - return stateMetadata.ChangeKind != StateChangeKind.Remove; - } - - if (await this.actor.Host.StateProvider.ContainsStateAsync(this.actorTypeName, this.actor.Id.ToString(), stateName, cancellationToken)) - { - return true; - } - - return false; - } - - public async Task GetOrAddStateAsync(string stateName, T value, CancellationToken cancellationToken) - { - EnsureStateProviderInitialized(); - - var condRes = await this.TryGetStateAsync(stateName, cancellationToken); - - if (condRes.HasValue) - { - return condRes.Value; - } - - var changeKind = this.IsStateMarkedForRemove(stateName) ? StateChangeKind.Update : StateChangeKind.Add; - - var stateChangeTracker = GetContextualStateTracker(); - stateChangeTracker[stateName] = StateMetadata.Create(value, changeKind); - return value; - } - - public async Task GetOrAddStateAsync(string stateName, T value, TimeSpan ttl, CancellationToken cancellationToken) - { - EnsureStateProviderInitialized(); - - var condRes = await this.TryGetStateAsync(stateName, cancellationToken); - - if (condRes.HasValue) - { - return condRes.Value; - } - - var changeKind = this.IsStateMarkedForRemove(stateName) ? StateChangeKind.Update : StateChangeKind.Add; - - var stateChangeTracker = GetContextualStateTracker(); - stateChangeTracker[stateName] = StateMetadata.Create(value, changeKind, ttl: ttl); - return value; - } - - public async Task AddOrUpdateStateAsync( - string stateName, - T addValue, - Func updateValueFactory, - CancellationToken cancellationToken = default) - { - ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName)); - - EnsureStateProviderInitialized(); - - var stateChangeTracker = GetContextualStateTracker(); - - if (stateChangeTracker.ContainsKey(stateName)) - { - var stateMetadata = stateChangeTracker[stateName]; - - // Check if the property was marked as remove in the cache - if (stateMetadata.ChangeKind == StateChangeKind.Remove) + if (stateMetadata.ChangeKind != StateChangeKind.None) { - stateChangeTracker[stateName] = StateMetadata.Create(addValue, StateChangeKind.Update); - return addValue; - } + stateChangeList.Add( + new ActorStateChange(stateName, stateMetadata.Type, stateMetadata.Value, stateMetadata.ChangeKind, stateMetadata.TTLExpireTime)); - var newValue = updateValueFactory.Invoke(stateName, (T)stateMetadata.Value); - stateMetadata.Value = newValue; - - if (stateMetadata.ChangeKind == StateChangeKind.None) - { - stateMetadata.ChangeKind = StateChangeKind.Update; - } - - return newValue; - } - - var conditionalResult = await this.TryGetStateFromStateProviderAsync(stateName, cancellationToken); - if (conditionalResult.HasValue) - { - var newValue = updateValueFactory.Invoke(stateName, conditionalResult.Value.Value); - stateChangeTracker.Add(stateName, StateMetadata.Create(newValue, StateChangeKind.Update)); - - return newValue; - } - - stateChangeTracker[stateName] = StateMetadata.Create(addValue, StateChangeKind.Add); - return addValue; - } - - public async Task AddOrUpdateStateAsync( - string stateName, - T addValue, - Func updateValueFactory, - TimeSpan ttl, - CancellationToken cancellationToken = default) - { - ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName)); - - EnsureStateProviderInitialized(); - - var stateChangeTracker = GetContextualStateTracker(); - - if (stateChangeTracker.ContainsKey(stateName)) - { - var stateMetadata = stateChangeTracker[stateName]; - - // Check if the property was marked as remove in the cache - if (stateMetadata.ChangeKind == StateChangeKind.Remove) - { - stateChangeTracker[stateName] = StateMetadata.Create(addValue, StateChangeKind.Update, ttl: ttl); - return addValue; - } - - var newValue = updateValueFactory.Invoke(stateName, (T)stateMetadata.Value); - stateMetadata.Value = newValue; - - if (stateMetadata.ChangeKind == StateChangeKind.None) - { - stateMetadata.ChangeKind = StateChangeKind.Update; - } - - return newValue; - } - - var conditionalResult = await this.TryGetStateFromStateProviderAsync(stateName, cancellationToken); - if (conditionalResult.HasValue) - { - var newValue = updateValueFactory.Invoke(stateName, conditionalResult.Value.Value); - stateChangeTracker.Add(stateName, StateMetadata.Create(newValue, StateChangeKind.Update, ttl: ttl)); - - return newValue; - } - - stateChangeTracker[stateName] = StateMetadata.Create(addValue, StateChangeKind.Add, ttl: ttl); - return addValue; - } - - public Task ClearCacheAsync(CancellationToken cancellationToken) - { - EnsureStateProviderInitialized(); - - var stateChangeTracker = GetContextualStateTracker(); - - stateChangeTracker.Clear(); - return Task.CompletedTask; - } - - public async Task SaveStateAsync(CancellationToken cancellationToken = default) - { - EnsureStateProviderInitialized(); - - var stateChangeTracker = GetContextualStateTracker(); - - if (stateChangeTracker.Count > 0) - { - var stateChangeList = new List(); - var statesToRemove = new List(); - - foreach (var stateName in stateChangeTracker.Keys) - { - var stateMetadata = stateChangeTracker[stateName]; - - if (stateMetadata.ChangeKind != StateChangeKind.None) + if (stateMetadata.ChangeKind == StateChangeKind.Remove) { - stateChangeList.Add( - new ActorStateChange(stateName, stateMetadata.Type, stateMetadata.Value, stateMetadata.ChangeKind, stateMetadata.TTLExpireTime)); - - if (stateMetadata.ChangeKind == StateChangeKind.Remove) - { - statesToRemove.Add(stateName); - } - - // Mark the states as unmodified so that tracking for next invocation is done correctly. - stateMetadata.ChangeKind = StateChangeKind.None; + statesToRemove.Add(stateName); } - } - if (stateChangeList.Count > 0) - { - await this.actor.Host.StateProvider.SaveStateAsync(this.actorTypeName, this.actor.Id.ToString(), stateChangeList.AsReadOnly(), cancellationToken); - } - - // Remove the states from tracker whcih were marked for removal. - foreach (var stateToRemove in statesToRemove) - { - stateChangeTracker.Remove(stateToRemove); - } - } - } - - public Task SetStateContext(string stateContext) - { - if (stateContext != null) - { - context.Value = (stateContext, new Dictionary()); - } - else - { - context.Value = (null, null); - } - - return Task.CompletedTask; - } - - private bool IsStateMarkedForRemove(string stateName) - { - var stateChangeTracker = GetContextualStateTracker(); - - if (stateChangeTracker.ContainsKey(stateName) && - stateChangeTracker[stateName].ChangeKind == StateChangeKind.Remove) - { - return true; - } - - return false; - } - - private Task>> TryGetStateFromStateProviderAsync(string stateName, CancellationToken cancellationToken) - { - EnsureStateProviderInitialized(); - return this.actor.Host.StateProvider.TryLoadStateAsync(this.actorTypeName, this.actor.Id.ToString(), stateName, cancellationToken); - } - - private void EnsureStateProviderInitialized() - { - if (this.actor.Host.StateProvider == null) - { - throw new InvalidOperationException( - "The actor was initialized without a state provider, and so cannot interact with state. " + - "If this is inside a unit test, replace Actor.StateProvider with a mock."); - } - } - - private Dictionary GetContextualStateTracker() - { - if (context.Value.id != null) - { - return context.Value.tracker; - } - else - { - return defaultTracker; - } - } - - private sealed class StateMetadata - { - private StateMetadata(object value, Type type, StateChangeKind changeKind, DateTimeOffset? ttlExpireTime = null, TimeSpan? ttl = null) - { - this.Value = value; - this.Type = type; - this.ChangeKind = changeKind; - - if (ttlExpireTime.HasValue && ttl.HasValue) { - throw new ArgumentException("Cannot specify both TTLExpireTime and TTL"); - } - if (ttl.HasValue) { - this.TTLExpireTime = DateTimeOffset.UtcNow.Add(ttl.Value); - } else { - this.TTLExpireTime = ttlExpireTime; + // Mark the states as unmodified so that tracking for next invocation is done correctly. + stateMetadata.ChangeKind = StateChangeKind.None; } } - public object Value { get; set; } - - public StateChangeKind ChangeKind { get; set; } - - public Type Type { get; } - - public DateTimeOffset? TTLExpireTime { get; set; } - - public static StateMetadata Create(T value, StateChangeKind changeKind) + if (stateChangeList.Count > 0) { - return new StateMetadata(value, typeof(T), changeKind); + await this.actor.Host.StateProvider.SaveStateAsync(this.actorTypeName, this.actor.Id.ToString(), stateChangeList.AsReadOnly(), cancellationToken); } - public static StateMetadata Create(T value, StateChangeKind changeKind, DateTimeOffset? ttlExpireTime) + // Remove the states from tracker whcih were marked for removal. + foreach (var stateToRemove in statesToRemove) { - return new StateMetadata(value, typeof(T), changeKind, ttlExpireTime: ttlExpireTime); - } - - public static StateMetadata Create(T value, StateChangeKind changeKind, TimeSpan? ttl) - { - return new StateMetadata(value, typeof(T), changeKind, ttl: ttl); - } - - public static StateMetadata CreateForRemove() - { - return new StateMetadata(null, typeof(object), StateChangeKind.Remove); + stateChangeTracker.Remove(stateToRemove); } } } -} + + public Task SetStateContext(string stateContext) + { + if (stateContext != null) + { + context.Value = (stateContext, new Dictionary()); + } + else + { + context.Value = (null, null); + } + + return Task.CompletedTask; + } + + private bool IsStateMarkedForRemove(string stateName) + { + var stateChangeTracker = GetContextualStateTracker(); + + if (stateChangeTracker.ContainsKey(stateName) && + stateChangeTracker[stateName].ChangeKind == StateChangeKind.Remove) + { + return true; + } + + return false; + } + + private Task>> TryGetStateFromStateProviderAsync(string stateName, CancellationToken cancellationToken) + { + EnsureStateProviderInitialized(); + return this.actor.Host.StateProvider.TryLoadStateAsync(this.actorTypeName, this.actor.Id.ToString(), stateName, cancellationToken); + } + + private void EnsureStateProviderInitialized() + { + if (this.actor.Host.StateProvider == null) + { + throw new InvalidOperationException( + "The actor was initialized without a state provider, and so cannot interact with state. " + + "If this is inside a unit test, replace Actor.StateProvider with a mock."); + } + } + + private Dictionary GetContextualStateTracker() + { + if (context.Value.id != null) + { + return context.Value.tracker; + } + else + { + return defaultTracker; + } + } + + private sealed class StateMetadata + { + private StateMetadata(object value, Type type, StateChangeKind changeKind, DateTimeOffset? ttlExpireTime = null, TimeSpan? ttl = null) + { + this.Value = value; + this.Type = type; + this.ChangeKind = changeKind; + + if (ttlExpireTime.HasValue && ttl.HasValue) { + throw new ArgumentException("Cannot specify both TTLExpireTime and TTL"); + } + if (ttl.HasValue) { + this.TTLExpireTime = DateTimeOffset.UtcNow.Add(ttl.Value); + } else { + this.TTLExpireTime = ttlExpireTime; + } + } + + public object Value { get; set; } + + public StateChangeKind ChangeKind { get; set; } + + public Type Type { get; } + + public DateTimeOffset? TTLExpireTime { get; set; } + + public static StateMetadata Create(T value, StateChangeKind changeKind) + { + return new StateMetadata(value, typeof(T), changeKind); + } + + public static StateMetadata Create(T value, StateChangeKind changeKind, DateTimeOffset? ttlExpireTime) + { + return new StateMetadata(value, typeof(T), changeKind, ttlExpireTime: ttlExpireTime); + } + + public static StateMetadata Create(T value, StateChangeKind changeKind, TimeSpan? ttl) + { + return new StateMetadata(value, typeof(T), changeKind, ttl: ttl); + } + + public static StateMetadata CreateForRemove() + { + return new StateMetadata(null, typeof(object), StateChangeKind.Remove); + } + } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Runtime/ActorTestOptions.cs b/src/Dapr.Actors/Runtime/ActorTestOptions.cs index 47e73ce3..a5c965b7 100644 --- a/src/Dapr.Actors/Runtime/ActorTestOptions.cs +++ b/src/Dapr.Actors/Runtime/ActorTestOptions.cs @@ -18,93 +18,92 @@ using Dapr.Actors.Client; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -namespace Dapr.Actors.Runtime +namespace Dapr.Actors.Runtime; + +/// +/// Specifies optional settings settings for the when creating an actor +/// instance for testing. +/// +public sealed class ActorTestOptions { /// - /// Specifies optional settings settings for the when creating an actor - /// instance for testing. + /// Gets or sets the . /// - public sealed class ActorTestOptions + /// + public ActorId ActorId { get; set; } = ActorId.CreateRandom(); + + /// + /// Gets or sets the . + /// + /// + public ILoggerFactory LoggerFactory { get; set; } = NullLoggerFactory.Instance; + + /// + /// Gets or sets the . + /// + public JsonSerializerOptions JsonSerializerOptions { get; set; } = new JsonSerializerOptions(JsonSerializerDefaults.Web); + + /// + /// Gets or sets the . + /// + public IActorProxyFactory ProxyFactory { get; set; } = new InvalidProxyFactory(); + + /// + /// Gets or sets the . + /// + public ActorTimerManager TimerManager { get; set; } = new InvalidTimerManager(); + + private class InvalidProxyFactory : IActorProxyFactory { - /// - /// Gets or sets the . - /// - /// - public ActorId ActorId { get; set; } = ActorId.CreateRandom(); + private static readonly string Message = + "This actor was initialized for tests without providing a replacement for the proxy factory. " + + "Provide a mock implementation of 'IProxyFactory' by setting 'ActorTestOptions.ProxyFactory'."; - /// - /// Gets or sets the . - /// - /// - public ILoggerFactory LoggerFactory { get; set; } = NullLoggerFactory.Instance; - - /// - /// Gets or sets the . - /// - public JsonSerializerOptions JsonSerializerOptions { get; set; } = new JsonSerializerOptions(JsonSerializerDefaults.Web); - - /// - /// Gets or sets the . - /// - public IActorProxyFactory ProxyFactory { get; set; } = new InvalidProxyFactory(); - - /// - /// Gets or sets the . - /// - public ActorTimerManager TimerManager { get; set; } = new InvalidTimerManager(); - - private class InvalidProxyFactory : IActorProxyFactory + public ActorProxy Create(ActorId actorId, string actorType, ActorProxyOptions options = null) { - private static readonly string Message = - "This actor was initialized for tests without providing a replacement for the proxy factory. " + - "Provide a mock implementation of 'IProxyFactory' by setting 'ActorTestOptions.ProxyFactory'."; - - public ActorProxy Create(ActorId actorId, string actorType, ActorProxyOptions options = null) - { - throw new NotImplementedException(Message); - } - - public TActorInterface CreateActorProxy(ActorId actorId, string actorType, ActorProxyOptions options = null) where TActorInterface : IActor - { - throw new NotImplementedException(Message); - } - - public object CreateActorProxy(ActorId actorId, Type actorInterfaceType, string actorType, ActorProxyOptions options = null) - { - throw new NotImplementedException(Message); - } + throw new NotImplementedException(Message); } - private class InvalidTimerManager : ActorTimerManager + public TActorInterface CreateActorProxy(ActorId actorId, string actorType, ActorProxyOptions options = null) where TActorInterface : IActor { - private static readonly string Message = - "This actor was initialized for tests without providing a replacement for the timer manager. " + - "Provide a mock implementation of 'ActorTimerManager' by setting 'ActorTestOptions.TimerManager'."; + throw new NotImplementedException(Message); + } - public override Task RegisterReminderAsync(ActorReminder reminder) - { - throw new NotImplementedException(Message); - } - - public override Task RegisterTimerAsync(ActorTimer timer) - { - throw new NotImplementedException(Message); - } - - public override Task GetReminderAsync(ActorReminderToken reminder) - { - throw new NotImplementedException(Message); - } - - public override Task UnregisterReminderAsync(ActorReminderToken reminder) - { - throw new NotImplementedException(Message); - } - - public override Task UnregisterTimerAsync(ActorTimerToken timer) - { - throw new NotImplementedException(Message); - } + public object CreateActorProxy(ActorId actorId, Type actorInterfaceType, string actorType, ActorProxyOptions options = null) + { + throw new NotImplementedException(Message); } } -} + + private class InvalidTimerManager : ActorTimerManager + { + private static readonly string Message = + "This actor was initialized for tests without providing a replacement for the timer manager. " + + "Provide a mock implementation of 'ActorTimerManager' by setting 'ActorTestOptions.TimerManager'."; + + public override Task RegisterReminderAsync(ActorReminder reminder) + { + throw new NotImplementedException(Message); + } + + public override Task RegisterTimerAsync(ActorTimer timer) + { + throw new NotImplementedException(Message); + } + + public override Task GetReminderAsync(ActorReminderToken reminder) + { + throw new NotImplementedException(Message); + } + + public override Task UnregisterReminderAsync(ActorReminderToken reminder) + { + throw new NotImplementedException(Message); + } + + public override Task UnregisterTimerAsync(ActorTimerToken timer) + { + throw new NotImplementedException(Message); + } + } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Runtime/ActorTimer.cs b/src/Dapr.Actors/Runtime/ActorTimer.cs index 786c05cb..18c68b0b 100644 --- a/src/Dapr.Actors/Runtime/ActorTimer.cs +++ b/src/Dapr.Actors/Runtime/ActorTimer.cs @@ -16,164 +16,163 @@ using System.Globalization; using System.Threading; using Dapr.Actors.Resources; -namespace Dapr.Actors.Runtime +namespace Dapr.Actors.Runtime; + +/// +/// Represents the timer set on an Actor. +/// +public class ActorTimer : ActorTimerToken { + private static readonly TimeSpan MiniumPeriod = Timeout.InfiniteTimeSpan; + /// - /// Represents the timer set on an Actor. + /// Initializes a new instance of . /// - public class ActorTimer : ActorTimerToken + /// The actor type. + /// The actor id. + /// The timer name. + /// The name of the callback associated with the timer. + /// The state associated with the timer. + /// The timer due time. + /// The timer period. + public ActorTimer( + string actorType, + ActorId actorId, + string name, + string timerCallback, + byte[] data, + TimeSpan dueTime, + TimeSpan period) + : this(new ActorTimerOptions + { + ActorTypeName = actorType, + Id = actorId, + TimerName = name, + TimerCallback = timerCallback, + Data = data, + DueTime = dueTime, + Period = period, + Ttl = null + }) { - private static readonly TimeSpan MiniumPeriod = Timeout.InfiniteTimeSpan; - - /// - /// Initializes a new instance of . - /// - /// The actor type. - /// The actor id. - /// The timer name. - /// The name of the callback associated with the timer. - /// The state associated with the timer. - /// The timer due time. - /// The timer period. - public ActorTimer( - string actorType, - ActorId actorId, - string name, - string timerCallback, - byte[] data, - TimeSpan dueTime, - TimeSpan period) - : this(new ActorTimerOptions - { - ActorTypeName = actorType, - Id = actorId, - TimerName = name, - TimerCallback = timerCallback, - Data = data, - DueTime = dueTime, - Period = period, - Ttl = null - }) - { - } - - /// - /// Initializes a new instance of . - /// - /// The actor type. - /// The actor id. - /// The timer name. - /// The name of the callback associated with the timer. - /// The state associated with the timer. - /// The timer due time. - /// The timer period. - /// The timer ttl. - public ActorTimer( - string actorType, - ActorId actorId, - string name, - string timerCallback, - byte[] data, - TimeSpan dueTime, - TimeSpan period, - TimeSpan ttl) - : this(new ActorTimerOptions - { - ActorTypeName = actorType, - Id = actorId, - TimerName = name, - TimerCallback = timerCallback, - Data = data, - DueTime = dueTime, - Period = period, - Ttl = ttl - }) - { - } - - /// - /// Initializes a new instance of . - /// - /// An containing the various settings for an . - internal ActorTimer(ActorTimerOptions options) : base(options.ActorTypeName, options.Id, options.TimerName) - { - if (options.DueTime < TimeSpan.Zero) - { - throw new ArgumentOutOfRangeException(nameof(options.DueTime), string.Format( - CultureInfo.CurrentCulture, - SR.TimerArgumentOutOfRange, - TimeSpan.Zero.TotalMilliseconds, - TimeSpan.MaxValue.TotalMilliseconds)); - } - - if (options.Period < MiniumPeriod) - { - throw new ArgumentOutOfRangeException(nameof(options.Period), string.Format( - CultureInfo.CurrentCulture, - SR.TimerArgumentOutOfRange, - MiniumPeriod.TotalMilliseconds, - TimeSpan.MaxValue.TotalMilliseconds)); - } - - if (options.Ttl != null && (options.Ttl < options.DueTime || options.Ttl < TimeSpan.Zero)) - { - throw new ArgumentOutOfRangeException(nameof(options.Ttl), string.Format( - CultureInfo.CurrentCulture, - SR.TimerArgumentOutOfRange, - options.DueTime, - TimeSpan.MaxValue.TotalMilliseconds)); - } - - this.TimerCallback = options.TimerCallback; - this.Data = options.Data; - this.DueTime = options.DueTime; - this.Period = options.Period; - this.Ttl = options.Ttl; - } - - /// - /// The constructor - /// - [Obsolete("This constructor does not provide all required data and should not be used.")] - public ActorTimer( - string timerName, - TimerInfo timerInfo) - : base("", ActorId.CreateRandom(), timerName) - { - this.TimerInfo = timerInfo; - } - - /// - /// Timer related information - /// - #pragma warning disable 0618 - public TimerInfo TimerInfo { get; } - #pragma warning restore 0618 - - - /// - /// Gets the callback name. - /// - public string TimerCallback { get; } - - /// - /// Gets the state passed to the callback. - /// - public byte[] Data { get; } - - /// - /// Gets the time when the timer is first due to be invoked. - /// - public TimeSpan DueTime { get; } - - /// - /// Gets the time interval at which the timer is invoked periodically. - /// - public TimeSpan Period { get; } - - /// - /// The optional that states when the reminder will expire. - /// - public TimeSpan? Ttl { get; } } -} + + /// + /// Initializes a new instance of . + /// + /// The actor type. + /// The actor id. + /// The timer name. + /// The name of the callback associated with the timer. + /// The state associated with the timer. + /// The timer due time. + /// The timer period. + /// The timer ttl. + public ActorTimer( + string actorType, + ActorId actorId, + string name, + string timerCallback, + byte[] data, + TimeSpan dueTime, + TimeSpan period, + TimeSpan ttl) + : this(new ActorTimerOptions + { + ActorTypeName = actorType, + Id = actorId, + TimerName = name, + TimerCallback = timerCallback, + Data = data, + DueTime = dueTime, + Period = period, + Ttl = ttl + }) + { + } + + /// + /// Initializes a new instance of . + /// + /// An containing the various settings for an . + internal ActorTimer(ActorTimerOptions options) : base(options.ActorTypeName, options.Id, options.TimerName) + { + if (options.DueTime < TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException(nameof(options.DueTime), string.Format( + CultureInfo.CurrentCulture, + SR.TimerArgumentOutOfRange, + TimeSpan.Zero.TotalMilliseconds, + TimeSpan.MaxValue.TotalMilliseconds)); + } + + if (options.Period < MiniumPeriod) + { + throw new ArgumentOutOfRangeException(nameof(options.Period), string.Format( + CultureInfo.CurrentCulture, + SR.TimerArgumentOutOfRange, + MiniumPeriod.TotalMilliseconds, + TimeSpan.MaxValue.TotalMilliseconds)); + } + + if (options.Ttl != null && (options.Ttl < options.DueTime || options.Ttl < TimeSpan.Zero)) + { + throw new ArgumentOutOfRangeException(nameof(options.Ttl), string.Format( + CultureInfo.CurrentCulture, + SR.TimerArgumentOutOfRange, + options.DueTime, + TimeSpan.MaxValue.TotalMilliseconds)); + } + + this.TimerCallback = options.TimerCallback; + this.Data = options.Data; + this.DueTime = options.DueTime; + this.Period = options.Period; + this.Ttl = options.Ttl; + } + + /// + /// The constructor + /// + [Obsolete("This constructor does not provide all required data and should not be used.")] + public ActorTimer( + string timerName, + TimerInfo timerInfo) + : base("", ActorId.CreateRandom(), timerName) + { + this.TimerInfo = timerInfo; + } + + /// + /// Timer related information + /// +#pragma warning disable 0618 + public TimerInfo TimerInfo { get; } +#pragma warning restore 0618 + + + /// + /// Gets the callback name. + /// + public string TimerCallback { get; } + + /// + /// Gets the state passed to the callback. + /// + public byte[] Data { get; } + + /// + /// Gets the time when the timer is first due to be invoked. + /// + public TimeSpan DueTime { get; } + + /// + /// Gets the time interval at which the timer is invoked periodically. + /// + public TimeSpan Period { get; } + + /// + /// The optional that states when the reminder will expire. + /// + public TimeSpan? Ttl { get; } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Runtime/ActorTimerManager.cs b/src/Dapr.Actors/Runtime/ActorTimerManager.cs index 784cf418..65135793 100644 --- a/src/Dapr.Actors/Runtime/ActorTimerManager.cs +++ b/src/Dapr.Actors/Runtime/ActorTimerManager.cs @@ -13,46 +13,45 @@ using System.Threading.Tasks; -namespace Dapr.Actors.Runtime +namespace Dapr.Actors.Runtime; + +/// +/// Provides timer and reminder functionality to an actor instance. +/// +public abstract class ActorTimerManager { /// - /// Provides timer and reminder functionality to an actor instance. + /// Registers the provided reminder with the runtime. /// - public abstract class ActorTimerManager - { - /// - /// Registers the provided reminder with the runtime. - /// - /// The to register. - /// A task which will complete when the operation completes. - public abstract Task RegisterReminderAsync(ActorReminder reminder); + /// The to register. + /// A task which will complete when the operation completes. + public abstract Task RegisterReminderAsync(ActorReminder reminder); - /// - /// Gets a reminder previously registered using - /// - /// The to unregister. - /// A task which will complete when the operation completes. - public abstract Task GetReminderAsync(ActorReminderToken reminder); + /// + /// Gets a reminder previously registered using + /// + /// The to unregister. + /// A task which will complete when the operation completes. + public abstract Task GetReminderAsync(ActorReminderToken reminder); - /// - /// Unregisters the provided reminder with the runtime. - /// - /// The to unregister. - /// A task which will complete when the operation completes. - public abstract Task UnregisterReminderAsync(ActorReminderToken reminder); + /// + /// Unregisters the provided reminder with the runtime. + /// + /// The to unregister. + /// A task which will complete when the operation completes. + public abstract Task UnregisterReminderAsync(ActorReminderToken reminder); - /// - /// Registers the provided timer with the runtime. - /// - /// The to register. - /// A task which will complete when the operation completes. - public abstract Task RegisterTimerAsync(ActorTimer timer); + /// + /// Registers the provided timer with the runtime. + /// + /// The to register. + /// A task which will complete when the operation completes. + public abstract Task RegisterTimerAsync(ActorTimer timer); - /// - /// Unregisters the provided timer with the runtime. - /// - /// The to unregister. - /// A task which will complete when the operation completes. - public abstract Task UnregisterTimerAsync(ActorTimerToken timer); - } -} + /// + /// Unregisters the provided timer with the runtime. + /// + /// The to unregister. + /// A task which will complete when the operation completes. + public abstract Task UnregisterTimerAsync(ActorTimerToken timer); +} \ No newline at end of file diff --git a/src/Dapr.Actors/Runtime/ActorTimerOptions.cs b/src/Dapr.Actors/Runtime/ActorTimerOptions.cs index 6c6746a7..05855c3b 100644 --- a/src/Dapr.Actors/Runtime/ActorTimerOptions.cs +++ b/src/Dapr.Actors/Runtime/ActorTimerOptions.cs @@ -1,49 +1,48 @@ using System; -namespace Dapr.Actors.Runtime +namespace Dapr.Actors.Runtime; + +/// +/// Collection of all the options used for creating a . +/// +internal struct ActorTimerOptions { /// - /// Collection of all the options used for creating a . + /// The name of the type of the Actor that the timer will fire for. /// - internal struct ActorTimerOptions - { - /// - /// The name of the type of the Actor that the timer will fire for. - /// - public string ActorTypeName { get; set; } + public string ActorTypeName { get; set; } - /// - /// The that the timer will fire for. - /// - public ActorId Id { get; set; } + /// + /// The that the timer will fire for. + /// + public ActorId Id { get; set; } - /// - /// The name of the timer. - /// - public string TimerName { get; set; } + /// + /// The name of the timer. + /// + public string TimerName { get; set; } - /// - /// The name of the callback for the timer. - /// - public string TimerCallback { get; set; } + /// + /// The name of the callback for the timer. + /// + public string TimerCallback { get; set; } - /// - /// State that is passed to the Actor when the timer fires. - /// - public byte[] Data { get; set; } + /// + /// State that is passed to the Actor when the timer fires. + /// + public byte[] Data { get; set; } - /// - /// that determines when the timer will first fire. - /// - public TimeSpan DueTime { get; set; } + /// + /// that determines when the timer will first fire. + /// + public TimeSpan DueTime { get; set; } - /// - /// that determines how much time there is between the timer firing. Starts after the . - /// - public TimeSpan Period { get; set; } + /// + /// that determines how much time there is between the timer firing. Starts after the . + /// + public TimeSpan Period { get; set; } - /// - /// An optional that determines when the timer will expire. - /// - public TimeSpan? Ttl { get; set; } - } -} + /// + /// An optional that determines when the timer will expire. + /// + public TimeSpan? Ttl { get; set; } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Runtime/ActorTimerToken.cs b/src/Dapr.Actors/Runtime/ActorTimerToken.cs index 557b8d8b..9f7c0c03 100644 --- a/src/Dapr.Actors/Runtime/ActorTimerToken.cs +++ b/src/Dapr.Actors/Runtime/ActorTimerToken.cs @@ -13,57 +13,56 @@ using System; -namespace Dapr.Actors.Runtime +namespace Dapr.Actors.Runtime; + +/// +/// Represents the timer set for an actor. +/// +public class ActorTimerToken { /// - /// Represents the timer set for an actor. + /// Initializes a new . /// - public class ActorTimerToken + /// The actor type. + /// The actor id. + /// The timer name. + public ActorTimerToken( + string actorType, + ActorId actorId, + string name) { - /// - /// Initializes a new . - /// - /// The actor type. - /// The actor id. - /// The timer name. - public ActorTimerToken( - string actorType, - ActorId actorId, - string name) + if (actorType == null) { - if (actorType == null) - { - throw new ArgumentNullException(nameof(actorType)); - } - - if (actorId == null) - { - throw new ArgumentNullException(nameof(actorId)); - } - - if (name == null) - { - throw new ArgumentNullException(nameof(name)); - } - - this.ActorType = actorType; - this.ActorId = actorId; - this.Name = name; + throw new ArgumentNullException(nameof(actorType)); } - /// - /// Gets the actor type. - /// - public string ActorType { get; } + if (actorId == null) + { + throw new ArgumentNullException(nameof(actorId)); + } - /// - /// Gets the actor id. - /// - public ActorId ActorId { get; } + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } - /// - /// Gets the timer name. - /// - public string Name { get; } + this.ActorType = actorType; + this.ActorId = actorId; + this.Name = name; } -} + + /// + /// Gets the actor type. + /// + public string ActorType { get; } + + /// + /// Gets the actor id. + /// + public ActorId ActorId { get; } + + /// + /// Gets the timer name. + /// + public string Name { get; } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Runtime/ActorTypeExtensions.cs b/src/Dapr.Actors/Runtime/ActorTypeExtensions.cs index 4acaa391..dd8d096e 100644 --- a/src/Dapr.Actors/Runtime/ActorTypeExtensions.cs +++ b/src/Dapr.Actors/Runtime/ActorTypeExtensions.cs @@ -11,94 +11,93 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Runtime +namespace Dapr.Actors.Runtime; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +/// +/// Contains extension method for Actor types. +/// +internal static class ActorTypeExtensions { - using System; - using System.Collections.Generic; - using System.Linq; - using System.Reflection; + /// + /// Gets the actor interfaces implemented by the actor class. + /// + /// The type of class implementing actor. + /// An array containing actor interface which the type implements. + public static Type[] GetActorInterfaces(this Type type) + { + var list = new List(type.GetInterfaces().Where(t => typeof(IActor).IsAssignableFrom(t))); + list.RemoveAll(t => (t.GetNonActorParentType() != null)); + + return list.ToArray(); + } /// - /// Contains extension method for Actor types. + /// Indicates whether the interface type is an actor interface. /// - internal static class ActorTypeExtensions + /// The interface type of the actor. + /// true, if the actorInterfaceType is an interface only implements . + public static bool IsActorInterface(this Type actorInterfaceType) { - /// - /// Gets the actor interfaces implemented by the actor class. - /// - /// The type of class implementing actor. - /// An array containing actor interface which the type implements. - public static Type[] GetActorInterfaces(this Type type) - { - var list = new List(type.GetInterfaces().Where(t => typeof(IActor).IsAssignableFrom(t))); - list.RemoveAll(t => (t.GetNonActorParentType() != null)); - - return list.ToArray(); - } - - /// - /// Indicates whether the interface type is an actor interface. - /// - /// The interface type of the actor. - /// true, if the actorInterfaceType is an interface only implements . - public static bool IsActorInterface(this Type actorInterfaceType) - { - return (actorInterfaceType.GetTypeInfo().IsInterface && (actorInterfaceType.GetNonActorParentType() == null)); - } - - /// - /// Indicates a value whether the actorType is an actor. - /// - /// The type implementing actor. - /// true, if the of actorType is an ; otherwise, false. - public static bool IsActor(this Type actorType) - { - var actorBaseType = actorType.GetTypeInfo().BaseType; - - while (actorBaseType != null) - { - if (actorBaseType == typeof(Actor)) - { - return true; - } - - actorType = actorBaseType; - actorBaseType = actorType.GetTypeInfo().BaseType; - } - - return false; - } - - /// - /// Indicates a value whether an actor type implements interface. - /// - /// The type implementing actor. - /// true, if the implements an interface; otherwise, false. - public static bool IsRemindableActor(this Type actorType) - { - return actorType.IsActor() && actorType.GetInterfaces().Contains(typeof(IRemindable)); - } - - public static Type GetNonActorParentType(this Type type) - { - var list = new List(type.GetInterfaces()); - - // must have IActor as the parent, so removal of it should result in reduction in the count. - if (list.RemoveAll(t => (t == typeof(IActor))) == 0) - { - return type; - } - - foreach (var t in list) - { - var nonActorParent = GetNonActorParentType(t); - if (nonActorParent != null) - { - return nonActorParent; - } - } - - return null; - } + return (actorInterfaceType.GetTypeInfo().IsInterface && (actorInterfaceType.GetNonActorParentType() == null)); } -} + + /// + /// Indicates a value whether the actorType is an actor. + /// + /// The type implementing actor. + /// true, if the of actorType is an ; otherwise, false. + public static bool IsActor(this Type actorType) + { + var actorBaseType = actorType.GetTypeInfo().BaseType; + + while (actorBaseType != null) + { + if (actorBaseType == typeof(Actor)) + { + return true; + } + + actorType = actorBaseType; + actorBaseType = actorType.GetTypeInfo().BaseType; + } + + return false; + } + + /// + /// Indicates a value whether an actor type implements interface. + /// + /// The type implementing actor. + /// true, if the implements an interface; otherwise, false. + public static bool IsRemindableActor(this Type actorType) + { + return actorType.IsActor() && actorType.GetInterfaces().Contains(typeof(IRemindable)); + } + + public static Type GetNonActorParentType(this Type type) + { + var list = new List(type.GetInterfaces()); + + // must have IActor as the parent, so removal of it should result in reduction in the count. + if (list.RemoveAll(t => (t == typeof(IActor))) == 0) + { + return type; + } + + foreach (var t in list) + { + var nonActorParent = GetNonActorParentType(t); + if (nonActorParent != null) + { + return nonActorParent; + } + } + + return null; + } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Runtime/ActorTypeInformation.cs b/src/Dapr.Actors/Runtime/ActorTypeInformation.cs index 80152421..ffa1b5ec 100644 --- a/src/Dapr.Actors/Runtime/ActorTypeInformation.cs +++ b/src/Dapr.Actors/Runtime/ActorTypeInformation.cs @@ -11,153 +11,152 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Runtime +namespace Dapr.Actors.Runtime; + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Reflection; +using Dapr.Actors.Resources; + +/// +/// Contains the information about the type implementing an actor. +/// +public sealed class ActorTypeInformation { - using System; - using System.Collections.Generic; - using System.Globalization; - using System.Reflection; - using Dapr.Actors.Resources; + /// + /// Initializes a new instance of the class. + /// + private ActorTypeInformation() + { + } /// - /// Contains the information about the type implementing an actor. + /// Gets the name of the actor type represented by the actor. /// - public sealed class ActorTypeInformation + /// The name of the actor type represented by the actor. + /// Defaults to the name of the class implementing the actor. Can be overridden using the property. + public string ActorTypeName { get; private set; } + + /// + /// Gets the type of the class implementing the actor. + /// + /// The of the class implementing the actor. + public Type ImplementationType { get; private set; } + + /// + /// Gets the actor interface types which derive from and implemented by actor class. + /// + /// An enumerator that can be used to iterate through the actor interface type. + public IEnumerable InterfaceTypes { get; private set; } + + /// + /// Gets a value indicating whether the class implementing actor is abstract. + /// + /// true if the class implementing actor is abstract, otherwise false. + public bool IsAbstract { get; private set; } + + /// + /// Gets a value indicating whether the actor class implements . + /// + /// true if the actor class implements , otherwise false. + public bool IsRemindable { get; private set; } + + /// + /// Creates the from actorType. + /// + /// The type of class implementing the actor to create ActorTypeInformation for. + /// When this method returns, contains ActorTypeInformation, if the creation of + /// ActorTypeInformation from actorType succeeded, or null if the creation failed. + /// The creation fails if the actorType parameter is null or it does not implement an actor. + /// true if ActorTypeInformation was successfully created for actorType; otherwise, false. + /// + /// Creation of ActorTypeInformation from actorType will fail when: + /// 1. for actorType is not of type . + /// 2. actorType does not implement an interface deriving from and is not marked as abstract. + /// + [Obsolete("Use Get(Type, string) instead. This will be removed in the future.")] + public static bool TryGet(Type actorType, out ActorTypeInformation actorTypeInformation) { - /// - /// Initializes a new instance of the class. - /// - private ActorTypeInformation() + try { + actorTypeInformation = Get(actorType, actorTypeName: null); + return true; } - - /// - /// Gets the name of the actor type represented by the actor. - /// - /// The name of the actor type represented by the actor. - /// Defaults to the name of the class implementing the actor. Can be overridden using the property. - public string ActorTypeName { get; private set; } - - /// - /// Gets the type of the class implementing the actor. - /// - /// The of the class implementing the actor. - public Type ImplementationType { get; private set; } - - /// - /// Gets the actor interface types which derive from and implemented by actor class. - /// - /// An enumerator that can be used to iterate through the actor interface type. - public IEnumerable InterfaceTypes { get; private set; } - - /// - /// Gets a value indicating whether the class implementing actor is abstract. - /// - /// true if the class implementing actor is abstract, otherwise false. - public bool IsAbstract { get; private set; } - - /// - /// Gets a value indicating whether the actor class implements . - /// - /// true if the actor class implements , otherwise false. - public bool IsRemindable { get; private set; } - - /// - /// Creates the from actorType. - /// - /// The type of class implementing the actor to create ActorTypeInformation for. - /// When this method returns, contains ActorTypeInformation, if the creation of - /// ActorTypeInformation from actorType succeeded, or null if the creation failed. - /// The creation fails if the actorType parameter is null or it does not implement an actor. - /// true if ActorTypeInformation was successfully created for actorType; otherwise, false. - /// - /// Creation of ActorTypeInformation from actorType will fail when: - /// 1. for actorType is not of type . - /// 2. actorType does not implement an interface deriving from and is not marked as abstract. - /// - [Obsolete("Use Get(Type, string) instead. This will be removed in the future.")] - public static bool TryGet(Type actorType, out ActorTypeInformation actorTypeInformation) + catch (ArgumentException) { - try - { - actorTypeInformation = Get(actorType, actorTypeName: null); - return true; - } - catch (ArgumentException) - { - actorTypeInformation = null; - return false; - } - } - - /// - /// Creates an from actorType. - /// - /// The type of class implementing the actor to create ActorTypeInformation for. - /// created from actorType. - /// - /// When for actorType is not of type . - /// When actorType does not implement an interface deriving from - /// and is not marked as abstract. - /// - [Obsolete("Use Get(Type, string) instead. This will be removed in the future.")] - public static ActorTypeInformation Get(Type actorType) - { - return Get(actorType, actorTypeName: null); - } - - /// - /// Creates an from actorType. - /// - /// The type of class implementing the actor to create ActorTypeInformation for. - /// The name of the actor type represented by the actor. - /// created from actorType. - /// - /// When for actorType is not of type . - /// When actorType does not implement an interface deriving from - /// and is not marked as abstract. - /// - /// The value of will have precedence over the default actor type name derived from the actor implementation type or any type name set via . - public static ActorTypeInformation Get(Type actorType, string actorTypeName) - { - if (!actorType.IsActor()) - { - throw new ArgumentException( - string.Format( - CultureInfo.CurrentCulture, - SR.ErrorNotAnActor, - actorType.FullName, - typeof(Actor).FullName), - "actorType"); - } - - // get all actor interfaces - var actorInterfaces = actorType.GetActorInterfaces(); - - // ensure that the if the actor type is not abstract it implements at least one actor interface - if ((actorInterfaces.Length == 0) && (!actorType.GetTypeInfo().IsAbstract)) - { - throw new ArgumentException( - string.Format( - CultureInfo.CurrentCulture, - SR.ErrorNoActorInterfaceFound, - actorType.FullName, - typeof(IActor).FullName), - "actorType"); - } - - var actorAttribute = actorType.GetCustomAttribute(); - - actorTypeName ??= actorAttribute?.TypeName ?? actorType.Name; - - return new ActorTypeInformation() - { - ActorTypeName = actorTypeName, - InterfaceTypes = actorInterfaces, - ImplementationType = actorType, - IsAbstract = actorType.GetTypeInfo().IsAbstract, - IsRemindable = actorType.IsRemindableActor(), - }; + actorTypeInformation = null; + return false; } } -} + + /// + /// Creates an from actorType. + /// + /// The type of class implementing the actor to create ActorTypeInformation for. + /// created from actorType. + /// + /// When for actorType is not of type . + /// When actorType does not implement an interface deriving from + /// and is not marked as abstract. + /// + [Obsolete("Use Get(Type, string) instead. This will be removed in the future.")] + public static ActorTypeInformation Get(Type actorType) + { + return Get(actorType, actorTypeName: null); + } + + /// + /// Creates an from actorType. + /// + /// The type of class implementing the actor to create ActorTypeInformation for. + /// The name of the actor type represented by the actor. + /// created from actorType. + /// + /// When for actorType is not of type . + /// When actorType does not implement an interface deriving from + /// and is not marked as abstract. + /// + /// The value of will have precedence over the default actor type name derived from the actor implementation type or any type name set via . + public static ActorTypeInformation Get(Type actorType, string actorTypeName) + { + if (!actorType.IsActor()) + { + throw new ArgumentException( + string.Format( + CultureInfo.CurrentCulture, + SR.ErrorNotAnActor, + actorType.FullName, + typeof(Actor).FullName), + "actorType"); + } + + // get all actor interfaces + var actorInterfaces = actorType.GetActorInterfaces(); + + // ensure that the if the actor type is not abstract it implements at least one actor interface + if ((actorInterfaces.Length == 0) && (!actorType.GetTypeInfo().IsAbstract)) + { + throw new ArgumentException( + string.Format( + CultureInfo.CurrentCulture, + SR.ErrorNoActorInterfaceFound, + actorType.FullName, + typeof(IActor).FullName), + "actorType"); + } + + var actorAttribute = actorType.GetCustomAttribute(); + + actorTypeName ??= actorAttribute?.TypeName ?? actorType.Name; + + return new ActorTypeInformation() + { + ActorTypeName = actorTypeName, + InterfaceTypes = actorInterfaces, + ImplementationType = actorType, + IsAbstract = actorType.GetTypeInfo().IsAbstract, + IsRemindable = actorType.IsRemindableActor(), + }; + } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Runtime/ConditionalValue.cs b/src/Dapr.Actors/Runtime/ConditionalValue.cs index ec4f3a5a..30b5c7a2 100644 --- a/src/Dapr.Actors/Runtime/ConditionalValue.cs +++ b/src/Dapr.Actors/Runtime/ConditionalValue.cs @@ -11,35 +11,34 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Runtime +namespace Dapr.Actors.Runtime; + +/// +/// Result class returned by Reliable Collections APIs that may or may not return a value. +/// +/// The type of the value returned by this . +public struct ConditionalValue { /// - /// Result class returned by Reliable Collections APIs that may or may not return a value. + /// Initializes a new instance of the struct with the given value. /// - /// The type of the value returned by this . - public struct ConditionalValue + /// Indicates whether the value is valid. + /// The value. + public ConditionalValue(bool hasValue, TValue value) { - /// - /// Initializes a new instance of the struct with the given value. - /// - /// Indicates whether the value is valid. - /// The value. - public ConditionalValue(bool hasValue, TValue value) - { - this.HasValue = hasValue; - this.Value = value; - } - - /// - /// Gets a value indicating whether the current object has a valid value of its underlying type. - /// - /// true: Value is valid, false otherwise. - public bool HasValue { get; } - - /// - /// Gets the value of the current object if it has been assigned a valid underlying value. - /// - /// The value of the object. If HasValue is false, returns the default value for type of the TValue parameter. - public TValue Value { get; } + this.HasValue = hasValue; + this.Value = value; } -} + + /// + /// Gets a value indicating whether the current object has a valid value of its underlying type. + /// + /// true: Value is valid, false otherwise. + public bool HasValue { get; } + + /// + /// Gets the value of the current object if it has been assigned a valid underlying value. + /// + /// The value of the object. If HasValue is false, returns the default value for type of the TValue parameter. + public TValue Value { get; } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Runtime/DaprStateProvider.cs b/src/Dapr.Actors/Runtime/DaprStateProvider.cs index e81308db..bddbaf56 100644 --- a/src/Dapr.Actors/Runtime/DaprStateProvider.cs +++ b/src/Dapr.Actors/Runtime/DaprStateProvider.cs @@ -11,171 +11,170 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Runtime +namespace Dapr.Actors.Runtime; + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Dapr.Actors.Communication; + +/// +/// State Provider to interact with Dapr runtime. +/// +internal class DaprStateProvider { - using System; - using System.Collections.Generic; - using System.IO; - using System.Text; - using System.Text.Json; - using System.Threading; - using System.Threading.Tasks; - using Dapr.Actors.Communication; + private readonly IActorStateSerializer actorStateSerializer; + private readonly JsonSerializerOptions jsonSerializerOptions; - /// - /// State Provider to interact with Dapr runtime. - /// - internal class DaprStateProvider + private readonly IDaprInteractor daprInteractor; + + public DaprStateProvider(IDaprInteractor daprInteractor, IActorStateSerializer actorStateSerializer = null) { - private readonly IActorStateSerializer actorStateSerializer; - private readonly JsonSerializerOptions jsonSerializerOptions; + this.actorStateSerializer = actorStateSerializer; + this.daprInteractor = daprInteractor; + } - private readonly IDaprInteractor daprInteractor; + public DaprStateProvider(IDaprInteractor daprInteractor, JsonSerializerOptions jsonSerializerOptions = null) + { + this.jsonSerializerOptions = jsonSerializerOptions; + this.daprInteractor = daprInteractor; + } - public DaprStateProvider(IDaprInteractor daprInteractor, IActorStateSerializer actorStateSerializer = null) + public async Task>> TryLoadStateAsync(string actorType, string actorId, string stateName, CancellationToken cancellationToken = default) + { + var result = new ConditionalValue>(false, default); + var response = await this.daprInteractor.GetStateAsync(actorType, actorId, stateName, cancellationToken); + + if (response.Value.Length != 0 && (!response.TTLExpireTime.HasValue || response.TTLExpireTime.Value > DateTimeOffset.UtcNow)) { - this.actorStateSerializer = actorStateSerializer; - this.daprInteractor = daprInteractor; - } + T typedResult; - public DaprStateProvider(IDaprInteractor daprInteractor, JsonSerializerOptions jsonSerializerOptions = null) - { - this.jsonSerializerOptions = jsonSerializerOptions; - this.daprInteractor = daprInteractor; - } - - public async Task>> TryLoadStateAsync(string actorType, string actorId, string stateName, CancellationToken cancellationToken = default) - { - var result = new ConditionalValue>(false, default); - var response = await this.daprInteractor.GetStateAsync(actorType, actorId, stateName, cancellationToken); - - if (response.Value.Length != 0 && (!response.TTLExpireTime.HasValue || response.TTLExpireTime.Value > DateTimeOffset.UtcNow)) + // perform default json de-serialization if custom serializer was not provided. + if (this.actorStateSerializer != null) { - T typedResult; - - // perform default json de-serialization if custom serializer was not provided. - if (this.actorStateSerializer != null) - { - var byteResult = Convert.FromBase64String(response.Value.Trim('"')); - typedResult = this.actorStateSerializer.Deserialize(byteResult); - } - else - { - typedResult = JsonSerializer.Deserialize(response.Value, jsonSerializerOptions); - } - - result = new ConditionalValue>(true, new ActorStateResponse(typedResult, response.TTLExpireTime)); + var byteResult = Convert.FromBase64String(response.Value.Trim('"')); + typedResult = this.actorStateSerializer.Deserialize(byteResult); + } + else + { + typedResult = JsonSerializer.Deserialize(response.Value, jsonSerializerOptions); } - return result; + result = new ConditionalValue>(true, new ActorStateResponse(typedResult, response.TTLExpireTime)); } - public async Task ContainsStateAsync(string actorType, string actorId, string stateName, CancellationToken cancellationToken = default) - { - var result = await this.daprInteractor.GetStateAsync(actorType, actorId, stateName, cancellationToken); - return (result.Value.Length != 0 && (!result.TTLExpireTime.HasValue || result.TTLExpireTime.Value > DateTimeOffset.UtcNow)); - } + return result; + } - public async Task SaveStateAsync(string actorType, string actorId, IReadOnlyCollection stateChanges, CancellationToken cancellationToken = default) - { - await this.DoStateChangesTransactionallyAsync(actorType, actorId, stateChanges, cancellationToken); - } + public async Task ContainsStateAsync(string actorType, string actorId, string stateName, CancellationToken cancellationToken = default) + { + var result = await this.daprInteractor.GetStateAsync(actorType, actorId, stateName, cancellationToken); + return (result.Value.Length != 0 && (!result.TTLExpireTime.HasValue || result.TTLExpireTime.Value > DateTimeOffset.UtcNow)); + } - private async Task DoStateChangesTransactionallyAsync(string actorType, string actorId, IReadOnlyCollection stateChanges, CancellationToken cancellationToken = default) - { - // Transactional state update request body: - /* - [ - { - "operation": "upsert", - "request": { - "key": "key1", - "value": "myData" - } - }, - { - "operation": "delete", - "request": { - "key": "key2" - } - } - ] - */ - using var stream = new MemoryStream(); - using var writer = new Utf8JsonWriter(stream); - writer.WriteStartArray(); - foreach (var stateChange in stateChanges) + public async Task SaveStateAsync(string actorType, string actorId, IReadOnlyCollection stateChanges, CancellationToken cancellationToken = default) + { + await this.DoStateChangesTransactionallyAsync(actorType, actorId, stateChanges, cancellationToken); + } + + private async Task DoStateChangesTransactionallyAsync(string actorType, string actorId, IReadOnlyCollection stateChanges, CancellationToken cancellationToken = default) + { + // Transactional state update request body: + /* + [ { - writer.WriteStartObject(); - var operation = this.GetDaprStateOperation(stateChange.ChangeKind); - writer.WriteString("operation", operation); - - // write the requestProperty - writer.WritePropertyName("request"); - writer.WriteStartObject(); // start object for request property - switch (stateChange.ChangeKind) - { - case StateChangeKind.Remove: - writer.WriteString("key", stateChange.StateName); - break; - case StateChangeKind.Add: - case StateChangeKind.Update: - writer.WriteString("key", stateChange.StateName); - - // perform default json serialization if custom serializer was not provided. - if (this.actorStateSerializer != null) - { - var buffer = this.actorStateSerializer.Serialize(stateChange.Type, stateChange.Value); - writer.WriteBase64String("value", buffer); - } - else - { - writer.WritePropertyName("value"); - JsonSerializer.Serialize(writer, stateChange.Value, stateChange.Type, jsonSerializerOptions); - } - - if (stateChange.TTLExpireTime.HasValue) { - var ttl = (int)Math.Ceiling((stateChange.TTLExpireTime.Value - DateTimeOffset.UtcNow).TotalSeconds); - writer.WritePropertyName("metadata"); - writer.WriteStartObject(); - writer.WriteString("ttlInSeconds", ttl.ToString()); - writer.WriteEndObject(); - } - - break; - default: - break; + "operation": "upsert", + "request": { + "key": "key1", + "value": "myData" + } + }, + { + "operation": "delete", + "request": { + "key": "key2" } - - writer.WriteEndObject(); // end object for request property - writer.WriteEndObject(); } - - writer.WriteEndArray(); - - await writer.FlushAsync(); - var content = Encoding.UTF8.GetString(stream.ToArray()); - await this.daprInteractor.SaveStateTransactionallyAsync(actorType, actorId, content, cancellationToken); - } - - private string GetDaprStateOperation(StateChangeKind changeKind) + ] + */ + using var stream = new MemoryStream(); + using var writer = new Utf8JsonWriter(stream); + writer.WriteStartArray(); + foreach (var stateChange in stateChanges) { - var operation = string.Empty; + writer.WriteStartObject(); + var operation = this.GetDaprStateOperation(stateChange.ChangeKind); + writer.WriteString("operation", operation); - switch (changeKind) + // write the requestProperty + writer.WritePropertyName("request"); + writer.WriteStartObject(); // start object for request property + switch (stateChange.ChangeKind) { case StateChangeKind.Remove: - operation = "delete"; + writer.WriteString("key", stateChange.StateName); break; case StateChangeKind.Add: case StateChangeKind.Update: - operation = "upsert"; + writer.WriteString("key", stateChange.StateName); + + // perform default json serialization if custom serializer was not provided. + if (this.actorStateSerializer != null) + { + var buffer = this.actorStateSerializer.Serialize(stateChange.Type, stateChange.Value); + writer.WriteBase64String("value", buffer); + } + else + { + writer.WritePropertyName("value"); + JsonSerializer.Serialize(writer, stateChange.Value, stateChange.Type, jsonSerializerOptions); + } + + if (stateChange.TTLExpireTime.HasValue) { + var ttl = (int)Math.Ceiling((stateChange.TTLExpireTime.Value - DateTimeOffset.UtcNow).TotalSeconds); + writer.WritePropertyName("metadata"); + writer.WriteStartObject(); + writer.WriteString("ttlInSeconds", ttl.ToString()); + writer.WriteEndObject(); + } + break; default: break; } - return operation; + writer.WriteEndObject(); // end object for request property + writer.WriteEndObject(); } + + writer.WriteEndArray(); + + await writer.FlushAsync(); + var content = Encoding.UTF8.GetString(stream.ToArray()); + await this.daprInteractor.SaveStateTransactionallyAsync(actorType, actorId, content, cancellationToken); } -} + + private string GetDaprStateOperation(StateChangeKind changeKind) + { + var operation = string.Empty; + + switch (changeKind) + { + case StateChangeKind.Remove: + operation = "delete"; + break; + case StateChangeKind.Add: + case StateChangeKind.Update: + operation = "upsert"; + break; + default: + break; + } + + return operation; + } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Runtime/DefaultActorActivator.cs b/src/Dapr.Actors/Runtime/DefaultActorActivator.cs index 768299da..f766dec1 100644 --- a/src/Dapr.Actors/Runtime/DefaultActorActivator.cs +++ b/src/Dapr.Actors/Runtime/DefaultActorActivator.cs @@ -14,55 +14,54 @@ using System; using System.Threading.Tasks; -namespace Dapr.Actors.Runtime +namespace Dapr.Actors.Runtime; + +/// +/// A default implementation of that uses . +/// +public class DefaultActorActivator : ActorActivator { /// - /// A default implementation of that uses . + /// Creates the actor instance and returns it inside an instance of . /// - public class DefaultActorActivator : ActorActivator + /// The actor host specifying information needed for the creation of the actor. + /// + /// Asynchronously returns n instance of . The + /// instance will be provided to when the actor is ready for deletion. + /// + /// + /// Implementations should not interact with lifecycle callback methods on the type. + /// These methods will be called by the runtime. + /// + public override Task CreateAsync(ActorHost host) { - /// - /// Creates the actor instance and returns it inside an instance of . - /// - /// The actor host specifying information needed for the creation of the actor. - /// - /// Asynchronously returns n instance of . The - /// instance will be provided to when the actor is ready for deletion. - /// - /// - /// Implementations should not interact with lifecycle callback methods on the type. - /// These methods will be called by the runtime. - /// - public override Task CreateAsync(ActorHost host) - { - var type = host.ActorTypeInfo.ImplementationType; - var actor = (Actor)Activator.CreateInstance(type, args: new object[]{ host, }); - return Task.FromResult(new ActorActivatorState(actor)); - } + var type = host.ActorTypeInfo.ImplementationType; + var actor = (Actor)Activator.CreateInstance(type, args: new object[]{ host, }); + return Task.FromResult(new ActorActivatorState(actor)); + } - /// - /// Deletes the actor instance and cleans up all associated resources. - /// - /// - /// The instance that was created during creation of the actor. - /// - /// - /// A task that represents the asynchronous completion of the operation. - /// - /// - /// Implementations should not interact with lifecycle callback methods on the type. - /// These methods will be called by the runtime. - /// - public async override Task DeleteAsync(ActorActivatorState state) + /// + /// Deletes the actor instance and cleans up all associated resources. + /// + /// + /// The instance that was created during creation of the actor. + /// + /// + /// A task that represents the asynchronous completion of the operation. + /// + /// + /// Implementations should not interact with lifecycle callback methods on the type. + /// These methods will be called by the runtime. + /// + public async override Task DeleteAsync(ActorActivatorState state) + { + if (state.Actor is IAsyncDisposable asyncDisposable) { - if (state.Actor is IAsyncDisposable asyncDisposable) - { - await asyncDisposable.DisposeAsync(); - } - else if (state.Actor is IDisposable disposable) - { - disposable.Dispose(); - } + await asyncDisposable.DisposeAsync(); + } + else if (state.Actor is IDisposable disposable) + { + disposable.Dispose(); } } -} +} \ No newline at end of file diff --git a/src/Dapr.Actors/Runtime/DefaultActorActivatorFactory.cs b/src/Dapr.Actors/Runtime/DefaultActorActivatorFactory.cs index 43839c64..4991f7f6 100644 --- a/src/Dapr.Actors/Runtime/DefaultActorActivatorFactory.cs +++ b/src/Dapr.Actors/Runtime/DefaultActorActivatorFactory.cs @@ -11,21 +11,20 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Runtime +namespace Dapr.Actors.Runtime; + +/// +/// A default implementation of that uses . +/// +public class DefaultActorActivatorFactory : ActorActivatorFactory { /// - /// A default implementation of that uses . + /// Creates the for the provided . /// - public class DefaultActorActivatorFactory : ActorActivatorFactory + /// The . + /// An . + public override ActorActivator CreateActivator(ActorTypeInformation type) { - /// - /// Creates the for the provided . - /// - /// The . - /// An . - public override ActorActivator CreateActivator(ActorTypeInformation type) - { - return new DefaultActorActivator(); - } + return new DefaultActorActivator(); } -} +} \ No newline at end of file diff --git a/src/Dapr.Actors/Runtime/DefaultActorTimerManager.cs b/src/Dapr.Actors/Runtime/DefaultActorTimerManager.cs index 79f3eabc..faf3cb59 100644 --- a/src/Dapr.Actors/Runtime/DefaultActorTimerManager.cs +++ b/src/Dapr.Actors/Runtime/DefaultActorTimerManager.cs @@ -17,102 +17,101 @@ using System.Threading.Tasks; using System.IO; using Grpc.Core; -namespace Dapr.Actors.Runtime +namespace Dapr.Actors.Runtime; + +internal class DefaultActorTimerManager : ActorTimerManager { - internal class DefaultActorTimerManager : ActorTimerManager + private readonly IDaprInteractor interactor; + + public DefaultActorTimerManager(IDaprInteractor interactor) { - private readonly IDaprInteractor interactor; - - public DefaultActorTimerManager(IDaprInteractor interactor) - { - this.interactor = interactor; - } - - public override async Task RegisterReminderAsync(ActorReminder reminder) - { - if (reminder == null) - { - throw new ArgumentNullException(nameof(reminder)); - } - - var serialized = await SerializeReminderAsync(reminder); - await this.interactor.RegisterReminderAsync(reminder.ActorType, reminder.ActorId.ToString(), reminder.Name, serialized); - } - - public override async Task GetReminderAsync(ActorReminderToken token) - { - if (token == null) - { - throw new ArgumentNullException(nameof(token)); - } - - var response = await this.interactor.GetReminderAsync(token.ActorType, token.ActorId.ToString(), token.Name); - if ((int)response.StatusCode == 500) - { - return null; - } - - var responseStream = await response.Content.ReadAsStreamAsync(); - return await DeserializeReminderAsync(responseStream, token); - } - - public override async Task UnregisterReminderAsync(ActorReminderToken reminder) - { - if (reminder == null) - { - throw new ArgumentNullException(nameof(reminder)); - } - - await this.interactor.UnregisterReminderAsync(reminder.ActorType, reminder.ActorId.ToString(), reminder.Name); - } - - public override async Task RegisterTimerAsync(ActorTimer timer) - { - if (timer == null) - { - throw new ArgumentNullException(nameof(timer)); - } - - #pragma warning disable 0618 - var timerInfo = new TimerInfo(timer.TimerCallback, timer.Data, timer.DueTime, timer.Period, timer.Ttl); - #pragma warning restore 0618 - var data = JsonSerializer.Serialize(timerInfo); - await this.interactor.RegisterTimerAsync(timer.ActorType, timer.ActorId.ToString(), timer.Name, data); - } - - public override async Task UnregisterTimerAsync(ActorTimerToken timer) - { - if (timer == null) - { - throw new ArgumentNullException(nameof(timer)); - } - - await this.interactor.UnregisterTimerAsync(timer.ActorType, timer.ActorId.ToString(), timer.Name); - } - - private static async ValueTask SerializeReminderAsync(ActorReminder reminder) - { - var info = new ReminderInfo(reminder.State, reminder.DueTime, reminder.Period, reminder.Repetitions, - reminder.Ttl); - return await info.SerializeAsync(); - } - - private static async ValueTask DeserializeReminderAsync(Stream stream, ActorReminderToken token) - { - if (stream == null) - { - throw new ArgumentNullException(nameof(stream)); - } - - var info = await ReminderInfo.DeserializeAsync(stream); - if (info == null) - { - return null; - } - - var reminder = new ActorReminder(token.ActorType, token.ActorId, token.Name, info.Data, info.DueTime, - info.Period); - return reminder; - } + this.interactor = interactor; } -} + + public override async Task RegisterReminderAsync(ActorReminder reminder) + { + if (reminder == null) + { + throw new ArgumentNullException(nameof(reminder)); + } + + var serialized = await SerializeReminderAsync(reminder); + await this.interactor.RegisterReminderAsync(reminder.ActorType, reminder.ActorId.ToString(), reminder.Name, serialized); + } + + public override async Task GetReminderAsync(ActorReminderToken token) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + var response = await this.interactor.GetReminderAsync(token.ActorType, token.ActorId.ToString(), token.Name); + if ((int)response.StatusCode == 500) + { + return null; + } + + var responseStream = await response.Content.ReadAsStreamAsync(); + return await DeserializeReminderAsync(responseStream, token); + } + + public override async Task UnregisterReminderAsync(ActorReminderToken reminder) + { + if (reminder == null) + { + throw new ArgumentNullException(nameof(reminder)); + } + + await this.interactor.UnregisterReminderAsync(reminder.ActorType, reminder.ActorId.ToString(), reminder.Name); + } + + public override async Task RegisterTimerAsync(ActorTimer timer) + { + if (timer == null) + { + throw new ArgumentNullException(nameof(timer)); + } + +#pragma warning disable 0618 + var timerInfo = new TimerInfo(timer.TimerCallback, timer.Data, timer.DueTime, timer.Period, timer.Ttl); +#pragma warning restore 0618 + var data = JsonSerializer.Serialize(timerInfo); + await this.interactor.RegisterTimerAsync(timer.ActorType, timer.ActorId.ToString(), timer.Name, data); + } + + public override async Task UnregisterTimerAsync(ActorTimerToken timer) + { + if (timer == null) + { + throw new ArgumentNullException(nameof(timer)); + } + + await this.interactor.UnregisterTimerAsync(timer.ActorType, timer.ActorId.ToString(), timer.Name); + } + + private static async ValueTask SerializeReminderAsync(ActorReminder reminder) + { + var info = new ReminderInfo(reminder.State, reminder.DueTime, reminder.Period, reminder.Repetitions, + reminder.Ttl); + return await info.SerializeAsync(); + } + + private static async ValueTask DeserializeReminderAsync(Stream stream, ActorReminderToken token) + { + if (stream == null) + { + throw new ArgumentNullException(nameof(stream)); + } + + var info = await ReminderInfo.DeserializeAsync(stream); + if (info == null) + { + return null; + } + + var reminder = new ActorReminder(token.ActorType, token.ActorId, token.Name, info.Data, info.DueTime, + info.Period); + return reminder; + } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Runtime/IActorContextualState.cs b/src/Dapr.Actors/Runtime/IActorContextualState.cs index 8d5ebaa2..5854b9f9 100644 --- a/src/Dapr.Actors/Runtime/IActorContextualState.cs +++ b/src/Dapr.Actors/Runtime/IActorContextualState.cs @@ -13,15 +13,14 @@ using System.Threading.Tasks; -namespace Dapr.Actors.Runtime +namespace Dapr.Actors.Runtime; + +/// +/// +/// +internal interface IActorContextualState { /// - /// /// - internal interface IActorContextualState - { - /// - /// - Task SetStateContext(string stateContext); - } + Task SetStateContext(string stateContext); } \ No newline at end of file diff --git a/src/Dapr.Actors/Runtime/IActorReminder.cs b/src/Dapr.Actors/Runtime/IActorReminder.cs index 0f59efef..b326fae1 100644 --- a/src/Dapr.Actors/Runtime/IActorReminder.cs +++ b/src/Dapr.Actors/Runtime/IActorReminder.cs @@ -11,43 +11,42 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Runtime +namespace Dapr.Actors.Runtime; + +using System; + +/// +/// Represents a reminder registered using . +/// +public interface IActorReminder { - using System; + /// + /// Gets the name of the reminder. The name is unique per actor. + /// + /// The name of the reminder. + string Name { get; } /// - /// Represents a reminder registered using . + /// Gets the time when the reminder is first due to be invoked. /// - public interface IActorReminder - { - /// - /// Gets the name of the reminder. The name is unique per actor. - /// - /// The name of the reminder. - string Name { get; } + /// The time when the reminder is first due to be invoked. + /// + /// A value of negative one (-1) milliseconds means the reminder is not invoked. A value of zero (0) means the reminder is invoked immediately after registration. + /// + TimeSpan DueTime { get; } - /// - /// Gets the time when the reminder is first due to be invoked. - /// - /// The time when the reminder is first due to be invoked. - /// - /// A value of negative one (-1) milliseconds means the reminder is not invoked. A value of zero (0) means the reminder is invoked immediately after registration. - /// - TimeSpan DueTime { get; } + /// + /// Gets the time interval at which the reminder is invoked periodically. + /// + /// The time interval at which the reminder is invoked periodically. + /// + /// The first invocation of the reminder occurs after . All subsequent invocations occur at intervals defined by this property. + /// + TimeSpan Period { get; } - /// - /// Gets the time interval at which the reminder is invoked periodically. - /// - /// The time interval at which the reminder is invoked periodically. - /// - /// The first invocation of the reminder occurs after . All subsequent invocations occur at intervals defined by this property. - /// - TimeSpan Period { get; } - - /// - /// Gets the user state passed to the reminder invocation. - /// - /// The user state passed to the reminder invocation. - byte[] State { get; } - } -} + /// + /// Gets the user state passed to the reminder invocation. + /// + /// The user state passed to the reminder invocation. + byte[] State { get; } +} \ No newline at end of file diff --git a/src/Dapr.Actors/Runtime/IActorStateManager.cs b/src/Dapr.Actors/Runtime/IActorStateManager.cs index 8795e28e..88868d85 100644 --- a/src/Dapr.Actors/Runtime/IActorStateManager.cs +++ b/src/Dapr.Actors/Runtime/IActorStateManager.cs @@ -11,337 +11,336 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Runtime +namespace Dapr.Actors.Runtime; + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +/// +/// Represents an interface that exposes methods to manage state of an . +/// This interface is implemented by . +/// +public interface IActorStateManager { - using System; - using System.Collections.Generic; - using System.Threading; - using System.Threading.Tasks; + /// + /// Adds an actor state with given state name. + /// + /// Type of value associated with given state name. + /// Name of the actor state to add. + /// Value of the actor state to add. + /// The token to monitor for cancellation requests. + /// + /// A task that represents the asynchronous add operation. + /// + /// + /// An actor state with given state name already exists. + /// + /// The specified state name is null. + /// The operation was canceled. + /// + /// The type of state value must be + /// Data Contract serializable. + /// + Task AddStateAsync(string stateName, T value, CancellationToken cancellationToken = default); /// - /// Represents an interface that exposes methods to manage state of an . - /// This interface is implemented by . + /// Adds an actor state with given state name. /// - public interface IActorStateManager - { - /// - /// Adds an actor state with given state name. - /// - /// Type of value associated with given state name. - /// Name of the actor state to add. - /// Value of the actor state to add. - /// The token to monitor for cancellation requests. - /// - /// A task that represents the asynchronous add operation. - /// - /// - /// An actor state with given state name already exists. - /// - /// The specified state name is null. - /// The operation was canceled. - /// - /// The type of state value must be - /// Data Contract serializable. - /// - Task AddStateAsync(string stateName, T value, CancellationToken cancellationToken = default); + /// Type of value associated with given state name. + /// Name of the actor state to add. + /// Value of the actor state to add. + /// The token to monitor for cancellation requests. + /// The time to live for the state. + /// + /// A task that represents the asynchronous add operation. + /// + /// + /// An actor state with given state name already exists. + /// + /// The specified state name is null. + /// The operation was canceled. + /// + /// The type of state value must be + /// Data Contract serializable. + /// + Task AddStateAsync(string stateName, T value, TimeSpan ttl, CancellationToken cancellationToken = default); - /// - /// Adds an actor state with given state name. - /// - /// Type of value associated with given state name. - /// Name of the actor state to add. - /// Value of the actor state to add. - /// The token to monitor for cancellation requests. - /// The time to live for the state. - /// - /// A task that represents the asynchronous add operation. - /// - /// - /// An actor state with given state name already exists. - /// - /// The specified state name is null. - /// The operation was canceled. - /// - /// The type of state value must be - /// Data Contract serializable. - /// - Task AddStateAsync(string stateName, T value, TimeSpan ttl, CancellationToken cancellationToken = default); + /// + /// Gets an actor state with specified state name. + /// + /// Type of value associated with given state name. + /// Name of the actor state to get. + /// The token to monitor for cancellation requests. + /// + /// A task that represents the asynchronous get operation. The value of TResult + /// parameter contains value of actor state with given state name. + /// + /// + /// An actor state with given state name does not exist. + /// + /// The specified state name is null. + /// The operation was canceled. + /// + /// The type of state value must be + /// Data Contract serializable. + /// + Task GetStateAsync(string stateName, CancellationToken cancellationToken = default); - /// - /// Gets an actor state with specified state name. - /// - /// Type of value associated with given state name. - /// Name of the actor state to get. - /// The token to monitor for cancellation requests. - /// - /// A task that represents the asynchronous get operation. The value of TResult - /// parameter contains value of actor state with given state name. - /// - /// - /// An actor state with given state name does not exist. - /// - /// The specified state name is null. - /// The operation was canceled. - /// - /// The type of state value must be - /// Data Contract serializable. - /// - Task GetStateAsync(string stateName, CancellationToken cancellationToken = default); + /// + /// Sets an actor state with given state name to specified value. + /// If an actor state with specified name does not exist, it is added. + /// + /// Type of value associated with given state name. + /// Name of the actor state to set. + /// Value of the actor state. + /// The token to monitor for cancellation requests. + /// + /// A task that represents the asynchronous set operation. + /// + /// The specified state name is null. + /// The operation was canceled. + /// + /// The type of state value must be + /// Data Contract serializable. + /// + Task SetStateAsync(string stateName, T value, CancellationToken cancellationToken = default); - /// - /// Sets an actor state with given state name to specified value. - /// If an actor state with specified name does not exist, it is added. - /// - /// Type of value associated with given state name. - /// Name of the actor state to set. - /// Value of the actor state. - /// The token to monitor for cancellation requests. - /// - /// A task that represents the asynchronous set operation. - /// - /// The specified state name is null. - /// The operation was canceled. - /// - /// The type of state value must be - /// Data Contract serializable. - /// - Task SetStateAsync(string stateName, T value, CancellationToken cancellationToken = default); + /// + /// Sets an actor state with given state name to specified value. + /// If an actor state with specified name does not exist, it is added. + /// + /// Type of value associated with given state name. + /// Name of the actor state to set. + /// Value of the actor state. + /// The time to live for the state. + /// The token to monitor for cancellation requests. + /// + /// A task that represents the asynchronous set operation. + /// + /// The specified state name is null. + /// The operation was canceled. + /// + /// The type of state value must be + /// Data Contract serializable. + /// + Task SetStateAsync(string stateName, T value, TimeSpan ttl, CancellationToken cancellationToken = default); - /// - /// Sets an actor state with given state name to specified value. - /// If an actor state with specified name does not exist, it is added. - /// - /// Type of value associated with given state name. - /// Name of the actor state to set. - /// Value of the actor state. - /// The time to live for the state. - /// The token to monitor for cancellation requests. - /// - /// A task that represents the asynchronous set operation. - /// - /// The specified state name is null. - /// The operation was canceled. - /// - /// The type of state value must be - /// Data Contract serializable. - /// - Task SetStateAsync(string stateName, T value, TimeSpan ttl, CancellationToken cancellationToken = default); + /// + /// Removes an actor state with specified state name. + /// + /// Name of the actor state to remove. + /// The token to monitor for cancellation requests. + /// A task that represents the asynchronous remove operation. + /// + /// An actor state with given state name does not exist. + /// + /// The specified state name is null. + /// The operation was canceled. + Task RemoveStateAsync(string stateName, CancellationToken cancellationToken = default); - /// - /// Removes an actor state with specified state name. - /// - /// Name of the actor state to remove. - /// The token to monitor for cancellation requests. - /// A task that represents the asynchronous remove operation. - /// - /// An actor state with given state name does not exist. - /// - /// The specified state name is null. - /// The operation was canceled. - Task RemoveStateAsync(string stateName, CancellationToken cancellationToken = default); + /// + /// Attempts to add an actor state with given state name and value. Returns false if an actor state with + /// the same name already exists. + /// + /// Type of value associated with given state name. + /// Name of the actor state to add. + /// Value of the actor state to add. + /// The token to monitor for cancellation requests. + /// This is optional and defaults to . + /// + /// A boolean task that represents the asynchronous add operation. Returns true if the + /// value was successfully added and false if an actor state with the same name already exists. + /// + /// The specified state name is null. + /// Provide a valid state name string. + /// The request was canceled using the specified + /// . + /// + /// The type of state value must be + /// Data Contract serializable. + /// + Task TryAddStateAsync(string stateName, T value, CancellationToken cancellationToken = default); - /// - /// Attempts to add an actor state with given state name and value. Returns false if an actor state with - /// the same name already exists. - /// - /// Type of value associated with given state name. - /// Name of the actor state to add. - /// Value of the actor state to add. - /// The token to monitor for cancellation requests. - /// This is optional and defaults to . - /// - /// A boolean task that represents the asynchronous add operation. Returns true if the - /// value was successfully added and false if an actor state with the same name already exists. - /// - /// The specified state name is null. - /// Provide a valid state name string. - /// The request was canceled using the specified - /// . - /// - /// The type of state value must be - /// Data Contract serializable. - /// - Task TryAddStateAsync(string stateName, T value, CancellationToken cancellationToken = default); + /// + /// Attempts to add an actor state with given state name and value. Returns false if an actor state with + /// the same name already exists. + /// + /// Type of value associated with given state name. + /// Name of the actor state to add. + /// Value of the actor state to add. + /// The time to live for the state. + /// The token to monitor for cancellation requests. + /// This is optional and defaults to . + /// + /// A boolean task that represents the asynchronous add operation. Returns true if the + /// value was successfully added and false if an actor state with the same name already exists. + /// + /// The specified state name is null. + /// Provide a valid state name string. + /// The request was canceled using the specified + /// . + /// + /// The type of state value must be + /// Data Contract serializable. + /// + Task TryAddStateAsync(string stateName, T value, TimeSpan ttl, CancellationToken cancellationToken = default); - /// - /// Attempts to add an actor state with given state name and value. Returns false if an actor state with - /// the same name already exists. - /// - /// Type of value associated with given state name. - /// Name of the actor state to add. - /// Value of the actor state to add. - /// The time to live for the state. - /// The token to monitor for cancellation requests. - /// This is optional and defaults to . - /// - /// A boolean task that represents the asynchronous add operation. Returns true if the - /// value was successfully added and false if an actor state with the same name already exists. - /// - /// The specified state name is null. - /// Provide a valid state name string. - /// The request was canceled using the specified - /// . - /// - /// The type of state value must be - /// Data Contract serializable. - /// - Task TryAddStateAsync(string stateName, T value, TimeSpan ttl, CancellationToken cancellationToken = default); + /// + /// Attempts to get an actor state with specified state name. + /// + /// Type of value associated with given state name. + /// Name of the actor state to get. + /// The token to monitor for cancellation requests. + /// + /// A task that represents the asynchronous get operation. The value of TResult + /// parameter contains + /// indicating whether the actor state is present and the value of actor state if it is present. + /// + /// The specified state name is null. + /// The operation was canceled. + /// + /// The type of state value must be + /// Data Contract serializable. + /// + Task> TryGetStateAsync(string stateName, CancellationToken cancellationToken = default); - /// - /// Attempts to get an actor state with specified state name. - /// - /// Type of value associated with given state name. - /// Name of the actor state to get. - /// The token to monitor for cancellation requests. - /// - /// A task that represents the asynchronous get operation. The value of TResult - /// parameter contains - /// indicating whether the actor state is present and the value of actor state if it is present. - /// - /// The specified state name is null. - /// The operation was canceled. - /// - /// The type of state value must be - /// Data Contract serializable. - /// - Task> TryGetStateAsync(string stateName, CancellationToken cancellationToken = default); + /// + /// Attempts to remove an actor state with specified state name. + /// + /// Name of the actor state to remove. + /// The token to monitor for cancellation requests. + /// + /// A task that represents the asynchronous remove operation. The value of TResult + /// parameter indicates if the state was successfully removed. + /// + /// The specified state name is null. + /// The operation was canceled. + Task TryRemoveStateAsync(string stateName, CancellationToken cancellationToken = default); - /// - /// Attempts to remove an actor state with specified state name. - /// - /// Name of the actor state to remove. - /// The token to monitor for cancellation requests. - /// - /// A task that represents the asynchronous remove operation. The value of TResult - /// parameter indicates if the state was successfully removed. - /// - /// The specified state name is null. - /// The operation was canceled. - Task TryRemoveStateAsync(string stateName, CancellationToken cancellationToken = default); + /// + /// Checks if an actor state with specified name exists. + /// + /// Name of the actor state. + /// The token to monitor for cancellation requests. + /// + /// A task that represents the asynchronous check operation. The value of TResult + /// parameter is true if state with specified name exists otherwise false. + /// + /// The specified state name is null. + /// The operation was canceled. + Task ContainsStateAsync(string stateName, CancellationToken cancellationToken = default); - /// - /// Checks if an actor state with specified name exists. - /// - /// Name of the actor state. - /// The token to monitor for cancellation requests. - /// - /// A task that represents the asynchronous check operation. The value of TResult - /// parameter is true if state with specified name exists otherwise false. - /// - /// The specified state name is null. - /// The operation was canceled. - Task ContainsStateAsync(string stateName, CancellationToken cancellationToken = default); + /// + /// Gets an actor state with the given state name if it exists. If it does not + /// exist, creates and new state with the specified name and value. + /// + /// Type of value associated with given state name. + /// Name of the actor state to get or add. + /// Value of the actor state to add if it does not exist. + /// The token to monitor for cancellation requests. + /// + /// A task that represents the asynchronous get or add operation. The value of TResult + /// parameter contains value of actor state with given state name. + /// + /// The specified state name is null. + /// Provide a valid state name string. + /// The request was canceled using the specified + /// . + /// + /// The type of state value must be + /// Data Contract serializable. + /// + Task GetOrAddStateAsync(string stateName, T value, CancellationToken cancellationToken = default); - /// - /// Gets an actor state with the given state name if it exists. If it does not - /// exist, creates and new state with the specified name and value. - /// - /// Type of value associated with given state name. - /// Name of the actor state to get or add. - /// Value of the actor state to add if it does not exist. - /// The token to monitor for cancellation requests. - /// - /// A task that represents the asynchronous get or add operation. The value of TResult - /// parameter contains value of actor state with given state name. - /// - /// The specified state name is null. - /// Provide a valid state name string. - /// The request was canceled using the specified - /// . - /// - /// The type of state value must be - /// Data Contract serializable. - /// - Task GetOrAddStateAsync(string stateName, T value, CancellationToken cancellationToken = default); + /// + /// Gets an actor state with the given state name if it exists. If it does not + /// exist, creates and new state with the specified name and value. + /// + /// Type of value associated with given state name. + /// Name of the actor state to get or add. + /// Value of the actor state to add if it does not exist. + /// The time to live for the state. + /// The token to monitor for cancellation requests. + /// + /// A task that represents the asynchronous get or add operation. The value of TResult + /// parameter contains value of actor state with given state name. + /// + /// The specified state name is null. + /// Provide a valid state name string. + /// The request was canceled using the specified + /// . + /// + /// The type of state value must be + /// Data Contract serializable. + /// + Task GetOrAddStateAsync(string stateName, T value, TimeSpan ttl, CancellationToken cancellationToken = default); - /// - /// Gets an actor state with the given state name if it exists. If it does not - /// exist, creates and new state with the specified name and value. - /// - /// Type of value associated with given state name. - /// Name of the actor state to get or add. - /// Value of the actor state to add if it does not exist. - /// The time to live for the state. - /// The token to monitor for cancellation requests. - /// - /// A task that represents the asynchronous get or add operation. The value of TResult - /// parameter contains value of actor state with given state name. - /// - /// The specified state name is null. - /// Provide a valid state name string. - /// The request was canceled using the specified - /// . - /// - /// The type of state value must be - /// Data Contract serializable. - /// - Task GetOrAddStateAsync(string stateName, T value, TimeSpan ttl, CancellationToken cancellationToken = default); + /// + /// Adds an actor state with given state name, if it does not already exist or updates + /// the state with specified state name, if it exists. + /// + /// Type of value associated with given state name. + /// Name of the actor state to add or update. + /// Value of the actor state to add if it does not exist. + /// Factory function to generate value of actor state to update if it exists. + /// The token to monitor for cancellation requests. + /// + /// A task that represents the asynchronous add/update operation. The value of TResult + /// parameter contains value of actor state that was added/updated. + /// + /// The specified state name is null. + /// The operation was canceled. + /// + /// The type of state value must be + /// Data Contract serializable. + /// + Task AddOrUpdateStateAsync(string stateName, T addValue, Func updateValueFactory, CancellationToken cancellationToken = default); - /// - /// Adds an actor state with given state name, if it does not already exist or updates - /// the state with specified state name, if it exists. - /// - /// Type of value associated with given state name. - /// Name of the actor state to add or update. - /// Value of the actor state to add if it does not exist. - /// Factory function to generate value of actor state to update if it exists. - /// The token to monitor for cancellation requests. - /// - /// A task that represents the asynchronous add/update operation. The value of TResult - /// parameter contains value of actor state that was added/updated. - /// - /// The specified state name is null. - /// The operation was canceled. - /// - /// The type of state value must be - /// Data Contract serializable. - /// - Task AddOrUpdateStateAsync(string stateName, T addValue, Func updateValueFactory, CancellationToken cancellationToken = default); + /// + /// Adds an actor state with given state name, if it does not already exist or updates + /// the state with specified state name, if it exists. + /// + /// Type of value associated with given state name. + /// Name of the actor state to add or update. + /// Value of the actor state to add if it does not exist. + /// Factory function to generate value of actor state to update if it exists. + /// The time to live for the state. + /// The token to monitor for cancellation requests. + /// + /// A task that represents the asynchronous add/update operation. The value of TResult + /// parameter contains value of actor state that was added/updated. + /// + /// The specified state name is null. + /// The operation was canceled. + /// + /// The type of state value must be + /// Data Contract serializable. + /// + Task AddOrUpdateStateAsync(string stateName, T addValue, Func updateValueFactory, TimeSpan ttl, CancellationToken cancellationToken = default); - /// - /// Adds an actor state with given state name, if it does not already exist or updates - /// the state with specified state name, if it exists. - /// - /// Type of value associated with given state name. - /// Name of the actor state to add or update. - /// Value of the actor state to add if it does not exist. - /// Factory function to generate value of actor state to update if it exists. - /// The time to live for the state. - /// The token to monitor for cancellation requests. - /// - /// A task that represents the asynchronous add/update operation. The value of TResult - /// parameter contains value of actor state that was added/updated. - /// - /// The specified state name is null. - /// The operation was canceled. - /// - /// The type of state value must be - /// Data Contract serializable. - /// - Task AddOrUpdateStateAsync(string stateName, T addValue, Func updateValueFactory, TimeSpan ttl, CancellationToken cancellationToken = default); + /// + /// Clears all the cached actor states and any operation(s) performed on + /// since last state save operation. + /// + /// The token to monitor for cancellation requests. + /// + /// A task that represents the asynchronous clear cache operation. + /// + /// + /// All the operation(s) performed on since last save operation are cleared on + /// clearing the cache and will not be included in next save operation. + /// + Task ClearCacheAsync(CancellationToken cancellationToken = default); - /// - /// Clears all the cached actor states and any operation(s) performed on - /// since last state save operation. - /// - /// The token to monitor for cancellation requests. - /// - /// A task that represents the asynchronous clear cache operation. - /// - /// - /// All the operation(s) performed on since last save operation are cleared on - /// clearing the cache and will not be included in next save operation. - /// - Task ClearCacheAsync(CancellationToken cancellationToken = default); - - /// - /// Saves all the cached state changes (add/update/remove) that were made since last call to - /// by actor runtime or by user explicitly. - /// - /// The token to monitor for cancellation requests. - /// - /// A task that represents the asynchronous save operation. - /// - Task SaveStateAsync(CancellationToken cancellationToken = default); - } -} + /// + /// Saves all the cached state changes (add/update/remove) that were made since last call to + /// by actor runtime or by user explicitly. + /// + /// The token to monitor for cancellation requests. + /// + /// A task that represents the asynchronous save operation. + /// + Task SaveStateAsync(CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/Dapr.Actors/Runtime/IActorStateSerializer.cs b/src/Dapr.Actors/Runtime/IActorStateSerializer.cs index cff3b7c2..3d5390c7 100644 --- a/src/Dapr.Actors/Runtime/IActorStateSerializer.cs +++ b/src/Dapr.Actors/Runtime/IActorStateSerializer.cs @@ -11,27 +11,26 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Runtime +namespace Dapr.Actors.Runtime; + +using System; + +/// +/// Represents a custom serializer for actor state. +/// +internal interface IActorStateSerializer { - using System; + /// + /// Deserializes from the given byte array to . + /// + /// The byte array to deserialize from. + /// The deserialized value. + T Deserialize(byte[] buffer); /// - /// Represents a custom serializer for actor state. + /// Serializes an object into a byte array. /// - internal interface IActorStateSerializer - { - /// - /// Deserializes from the given byte array to . - /// - /// The byte array to deserialize from. - /// The deserialized value. - T Deserialize(byte[] buffer); - - /// - /// Serializes an object into a byte array. - /// - /// Type of the state. - /// The state value to serialize. - byte[] Serialize(Type stateType, T state); - } -} + /// Type of the state. + /// The state value to serialize. + byte[] Serialize(Type stateType, T state); +} \ No newline at end of file diff --git a/src/Dapr.Actors/Runtime/IRemindable.cs b/src/Dapr.Actors/Runtime/IRemindable.cs index 0fc66f90..21fb22fe 100644 --- a/src/Dapr.Actors/Runtime/IRemindable.cs +++ b/src/Dapr.Actors/Runtime/IRemindable.cs @@ -11,28 +11,27 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Runtime -{ - using System; - using System.Threading.Tasks; +namespace Dapr.Actors.Runtime; +using System; +using System.Threading.Tasks; + +/// +/// Interface that actors must implement to consume reminders registered using RegisterReminderAsync. />. +/// +public interface IRemindable +{ /// - /// Interface that actors must implement to consume reminders registered using RegisterReminderAsync. />. + /// The reminder call back invoked when an actor reminder is triggered. /// - public interface IRemindable - { - /// - /// The reminder call back invoked when an actor reminder is triggered. - /// - /// The name of reminder provided during registration. - /// The user state provided during registration. - /// The invocation due time provided during registration. - /// The invocation period provided during registration. - /// A task that represents the asynchronous operation performed by this callback. - /// - /// The state of this actor is saved by the actor runtime upon completion of the task returned by this method. If an error occurs while saving the state, then all state cached by this actor's will be discarded and reloaded from previously saved state when the next actor method or reminder invocation occurs. - /// - /// - Task ReceiveReminderAsync(string reminderName, byte[] state, TimeSpan dueTime, TimeSpan period); - } -} + /// The name of reminder provided during registration. + /// The user state provided during registration. + /// The invocation due time provided during registration. + /// The invocation period provided during registration. + /// A task that represents the asynchronous operation performed by this callback. + /// + /// The state of this actor is saved by the actor runtime upon completion of the task returned by this method. If an error occurs while saving the state, then all state cached by this actor's will be discarded and reloaded from previously saved state when the next actor method or reminder invocation occurs. + /// + /// + Task ReceiveReminderAsync(string reminderName, byte[] state, TimeSpan dueTime, TimeSpan period); +} \ No newline at end of file diff --git a/src/Dapr.Actors/Runtime/StateChangeKind.cs b/src/Dapr.Actors/Runtime/StateChangeKind.cs index 29269f2a..3ad79893 100644 --- a/src/Dapr.Actors/Runtime/StateChangeKind.cs +++ b/src/Dapr.Actors/Runtime/StateChangeKind.cs @@ -11,31 +11,30 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Runtime +namespace Dapr.Actors.Runtime; + +/// +/// Represents the kind of state change for an actor state when saves change is called to a set of actor states. +/// +public enum StateChangeKind { /// - /// Represents the kind of state change for an actor state when saves change is called to a set of actor states. + /// No change in state. /// - public enum StateChangeKind - { - /// - /// No change in state. - /// - None = 0, + None = 0, - /// - /// The state needs to be added. - /// - Add = 1, + /// + /// The state needs to be added. + /// + Add = 1, - /// - /// The state needs to be updated. - /// - Update = 2, + /// + /// The state needs to be updated. + /// + Update = 2, - /// - /// The state needs to be removed. - /// - Remove = 3, - } -} + /// + /// The state needs to be removed. + /// + Remove = 3, +} \ No newline at end of file diff --git a/src/Dapr.Actors/Runtime/TimerInfo.cs b/src/Dapr.Actors/Runtime/TimerInfo.cs index bc206915..71da4bf2 100644 --- a/src/Dapr.Actors/Runtime/TimerInfo.cs +++ b/src/Dapr.Actors/Runtime/TimerInfo.cs @@ -11,165 +11,164 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Runtime +namespace Dapr.Actors.Runtime; + +using System; +using System.Globalization; +using System.IO; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading; +using Dapr.Actors.Resources; + +/// +/// Represents the details of the timer set on an Actor. +/// +[Obsolete("This class is an implementation detail of the framework and will be made internal in a future release.")] +[JsonConverter(typeof(TimerInfoConverter))] +public class TimerInfo { - using System; - using System.Globalization; - using System.IO; - using System.Text.Json; - using System.Text.Json.Serialization; - using System.Threading; - using Dapr.Actors.Resources; + private readonly TimeSpan minTimePeriod = Timeout.InfiniteTimeSpan; - /// - /// Represents the details of the timer set on an Actor. - /// - [Obsolete("This class is an implementation detail of the framework and will be made internal in a future release.")] - [JsonConverter(typeof(TimerInfoConverter))] - public class TimerInfo + internal TimerInfo( + string callback, + byte[] state, + TimeSpan dueTime, + TimeSpan period, + TimeSpan? ttl = null) { - private readonly TimeSpan minTimePeriod = Timeout.InfiniteTimeSpan; + this.ValidateDueTime("DueTime", dueTime); + this.ValidatePeriod("Period", period); + this.Callback = callback; + this.Data = state; + this.DueTime = dueTime; + this.Period = period; + this.Ttl = ttl; + } - internal TimerInfo( - string callback, - byte[] state, - TimeSpan dueTime, - TimeSpan period, - TimeSpan? ttl = null) + internal string Callback { get; private set; } + + internal TimeSpan DueTime { get; private set; } + + internal TimeSpan Period { get; private set; } + + internal byte[] Data { get; private set; } + + internal TimeSpan? Ttl { get; private set; } + + private void ValidateDueTime(string argName, TimeSpan value) + { + if (value < TimeSpan.Zero) { - this.ValidateDueTime("DueTime", dueTime); - this.ValidatePeriod("Period", period); - this.Callback = callback; - this.Data = state; - this.DueTime = dueTime; - this.Period = period; - this.Ttl = ttl; - } - - internal string Callback { get; private set; } - - internal TimeSpan DueTime { get; private set; } - - internal TimeSpan Period { get; private set; } - - internal byte[] Data { get; private set; } - - internal TimeSpan? Ttl { get; private set; } - - private void ValidateDueTime(string argName, TimeSpan value) - { - if (value < TimeSpan.Zero) - { - throw new ArgumentOutOfRangeException( - argName, - string.Format( - CultureInfo.CurrentCulture, - SR.TimerArgumentOutOfRange, - this.minTimePeriod.TotalMilliseconds, - TimeSpan.MaxValue.TotalMilliseconds)); - } - } - - private void ValidatePeriod(string argName, TimeSpan value) - { - if (value < this.minTimePeriod) - { - throw new ArgumentOutOfRangeException( - argName, - string.Format( - CultureInfo.CurrentCulture, - SR.TimerArgumentOutOfRange, - this.minTimePeriod.TotalMilliseconds, - TimeSpan.MaxValue.TotalMilliseconds)); - } + throw new ArgumentOutOfRangeException( + argName, + string.Format( + CultureInfo.CurrentCulture, + SR.TimerArgumentOutOfRange, + this.minTimePeriod.TotalMilliseconds, + TimeSpan.MaxValue.TotalMilliseconds)); } } - #pragma warning disable 0618 - internal class TimerInfoConverter : JsonConverter + + private void ValidatePeriod(string argName, TimeSpan value) { - public override TimerInfo Read( - ref Utf8JsonReader reader, - Type typeToConvert, - JsonSerializerOptions options) + if (value < this.minTimePeriod) { - var dueTime = default(TimeSpan); - var period = default(TimeSpan); - var data = default(byte[]); - string callback = null; - TimeSpan? ttl = null; - - using (JsonDocument document = JsonDocument.ParseValue(ref reader)) - { - var json = document.RootElement.Clone(); - - if (json.TryGetProperty("dueTime", out var dueTimeProperty)) - { - var dueTimeString = dueTimeProperty.GetString(); - dueTime = ConverterUtils.ConvertTimeSpanFromDaprFormat(dueTimeString); - } - - if (json.TryGetProperty("period", out var periodProperty)) - { - var periodString = periodProperty.GetString(); - period = ConverterUtils.ConvertTimeSpanFromDaprFormat(periodString); - } - - if (json.TryGetProperty("data", out var dataProperty) && dataProperty.ValueKind != JsonValueKind.Null) - { - data = dataProperty.GetBytesFromBase64(); - } - - if (json.TryGetProperty("callback", out var callbackProperty)) - { - callback = callbackProperty.GetString(); - } - - if (json.TryGetProperty("ttl", out var ttlProperty)) - { - var ttlString = ttlProperty.GetString(); - ttl = ConverterUtils.ConvertTimeSpanFromDaprFormat(ttlString); - } - - return new TimerInfo(callback, data, dueTime, period, ttl); - } - } - - public override async void Write( - Utf8JsonWriter writer, - TimerInfo value, - JsonSerializerOptions options) - { - using var stream = new MemoryStream(); - - writer.WriteStartObject(); - if (value.DueTime != null) - { - writer.WriteString("dueTime", ConverterUtils.ConvertTimeSpanValueInDaprFormat(value.DueTime)); - } - - if (value.Period != null && value.Period >= TimeSpan.Zero) - { - writer.WriteString("period", ConverterUtils.ConvertTimeSpanValueInDaprFormat(value.Period)); - } - - if (value.Data != null) - { - writer.WriteString("data", Convert.ToBase64String(value.Data)); - } - - if (value.Callback != null) - { - writer.WriteString("callback", value.Callback); - } - - if (value.Ttl != null) - { - writer.WriteString("ttl", ConverterUtils.ConvertTimeSpanValueInDaprFormat(value.Ttl)); - } - - writer.WriteEndObject(); - await writer.FlushAsync(); + throw new ArgumentOutOfRangeException( + argName, + string.Format( + CultureInfo.CurrentCulture, + SR.TimerArgumentOutOfRange, + this.minTimePeriod.TotalMilliseconds, + TimeSpan.MaxValue.TotalMilliseconds)); } } - #pragma warning restore 0618 } +#pragma warning disable 0618 +internal class TimerInfoConverter : JsonConverter +{ + public override TimerInfo Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options) + { + var dueTime = default(TimeSpan); + var period = default(TimeSpan); + var data = default(byte[]); + string callback = null; + TimeSpan? ttl = null; + + using (JsonDocument document = JsonDocument.ParseValue(ref reader)) + { + var json = document.RootElement.Clone(); + + if (json.TryGetProperty("dueTime", out var dueTimeProperty)) + { + var dueTimeString = dueTimeProperty.GetString(); + dueTime = ConverterUtils.ConvertTimeSpanFromDaprFormat(dueTimeString); + } + + if (json.TryGetProperty("period", out var periodProperty)) + { + var periodString = periodProperty.GetString(); + period = ConverterUtils.ConvertTimeSpanFromDaprFormat(periodString); + } + + if (json.TryGetProperty("data", out var dataProperty) && dataProperty.ValueKind != JsonValueKind.Null) + { + data = dataProperty.GetBytesFromBase64(); + } + + if (json.TryGetProperty("callback", out var callbackProperty)) + { + callback = callbackProperty.GetString(); + } + + if (json.TryGetProperty("ttl", out var ttlProperty)) + { + var ttlString = ttlProperty.GetString(); + ttl = ConverterUtils.ConvertTimeSpanFromDaprFormat(ttlString); + } + + return new TimerInfo(callback, data, dueTime, period, ttl); + } + } + + public override async void Write( + Utf8JsonWriter writer, + TimerInfo value, + JsonSerializerOptions options) + { + using var stream = new MemoryStream(); + + writer.WriteStartObject(); + if (value.DueTime != null) + { + writer.WriteString("dueTime", ConverterUtils.ConvertTimeSpanValueInDaprFormat(value.DueTime)); + } + + if (value.Period != null && value.Period >= TimeSpan.Zero) + { + writer.WriteString("period", ConverterUtils.ConvertTimeSpanValueInDaprFormat(value.Period)); + } + + if (value.Data != null) + { + writer.WriteString("data", Convert.ToBase64String(value.Data)); + } + + if (value.Callback != null) + { + writer.WriteString("callback", value.Callback); + } + + if (value.Ttl != null) + { + writer.WriteString("ttl", ConverterUtils.ConvertTimeSpanValueInDaprFormat(value.Ttl)); + } + + writer.WriteEndObject(); + await writer.FlushAsync(); + } +} +#pragma warning restore 0618 \ No newline at end of file diff --git a/src/Dapr.Actors/Serialization/ActorIdJsonConverter.cs b/src/Dapr.Actors/Serialization/ActorIdJsonConverter.cs index 5c3838f3..30e60882 100644 --- a/src/Dapr.Actors/Serialization/ActorIdJsonConverter.cs +++ b/src/Dapr.Actors/Serialization/ActorIdJsonConverter.cs @@ -11,49 +11,48 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Seralization +namespace Dapr.Actors.Seralization; + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +// Converter for ActorId - will be serialized as a JSON string +internal class ActorIdJsonConverter : JsonConverter { - using System; - using System.Text.Json; - using System.Text.Json.Serialization; - - // Converter for ActorId - will be serialized as a JSON string - internal class ActorIdJsonConverter : JsonConverter + public override ActorId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - public override ActorId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + if (typeToConvert != typeof(ActorId)) { - if (typeToConvert != typeof(ActorId)) - { - throw new ArgumentException( $"Conversion to the type '{typeToConvert}' is not supported.", nameof(typeToConvert)); - } - - // Note - we generate random Guids for Actor Ids when we're generating them randomly - // but we don't actually enforce a format. Ids could be a number, or a date, or whatever, - // we don't really care. However we always **represent** Ids in JSON as strings. - if (reader.TokenType == JsonTokenType.String && - reader.GetString() is string text && - !string.IsNullOrWhiteSpace(text)) - { - return new ActorId(text); - } - else if (reader.TokenType == JsonTokenType.Null) - { - return null; - } - - throw new JsonException(); // The serializer will provide a default error message. + throw new ArgumentException( $"Conversion to the type '{typeToConvert}' is not supported.", nameof(typeToConvert)); } - public override void Write(Utf8JsonWriter writer, ActorId value, JsonSerializerOptions options) + // Note - we generate random Guids for Actor Ids when we're generating them randomly + // but we don't actually enforce a format. Ids could be a number, or a date, or whatever, + // we don't really care. However we always **represent** Ids in JSON as strings. + if (reader.TokenType == JsonTokenType.String && + reader.GetString() is string text && + !string.IsNullOrWhiteSpace(text)) { - if (value is null) - { - writer.WriteNullValue(); - } - else - { - writer.WriteStringValue(value.GetId()); - } + return new ActorId(text); + } + else if (reader.TokenType == JsonTokenType.Null) + { + return null; + } + + throw new JsonException(); // The serializer will provide a default error message. + } + + public override void Write(Utf8JsonWriter writer, ActorId value, JsonSerializerOptions options) + { + if (value is null) + { + writer.WriteNullValue(); + } + else + { + writer.WriteStringValue(value.GetId()); } } -} +} \ No newline at end of file diff --git a/src/Dapr.AspNetCore/BulkMessageModel.cs b/src/Dapr.AspNetCore/BulkMessageModel.cs index 503101ef..fe795171 100644 --- a/src/Dapr.AspNetCore/BulkMessageModel.cs +++ b/src/Dapr.AspNetCore/BulkMessageModel.cs @@ -11,67 +11,66 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.AspNetCore +namespace Dapr.AspNetCore; + +/// +/// Class representing an entry in the DaprBulkMessage. +/// +/// The type of value contained in the data. +public class BulkMessageModel { /// - /// Class representing an entry in the DaprBulkMessage. + /// Initializes a new instance of the class. /// - /// The type of value contained in the data. - public class BulkMessageModel - { - /// - /// Initializes a new instance of the class. - /// - public BulkMessageModel() { - } - - /// - /// Initializes a new instance of the class. - /// - /// Identifier of the message being processed. - /// Source for this event. - /// Type of event. - /// Version of the event spec. - /// Type of the payload. - /// Payload. - public BulkMessageModel(string id, string source, string type, string specversion, string datacontenttype, - TValue data) { - this.Id = id; - this.Source = source; - this.Type = type; - this.Specversion = specversion; - this.Datacontenttype = datacontenttype; - this.Data = data; - } - - /// - /// Identifier of the message being processed. - /// - public string Id { get; set; } - - /// - /// Source for this event. - /// - public string Source { get; set; } - - /// - /// Type of event. - /// - public string Type { get; set; } - - /// - /// Version of the event spec. - /// - public string Specversion { get; set; } - - /// - /// Type of the payload. - /// - public string Datacontenttype { get; set; } - - /// - /// Payload. - /// - public TValue Data { get; set; } + public BulkMessageModel() { } + + /// + /// Initializes a new instance of the class. + /// + /// Identifier of the message being processed. + /// Source for this event. + /// Type of event. + /// Version of the event spec. + /// Type of the payload. + /// Payload. + public BulkMessageModel(string id, string source, string type, string specversion, string datacontenttype, + TValue data) { + this.Id = id; + this.Source = source; + this.Type = type; + this.Specversion = specversion; + this.Datacontenttype = datacontenttype; + this.Data = data; + } + + /// + /// Identifier of the message being processed. + /// + public string Id { get; set; } + + /// + /// Source for this event. + /// + public string Source { get; set; } + + /// + /// Type of event. + /// + public string Type { get; set; } + + /// + /// Version of the event spec. + /// + public string Specversion { get; set; } + + /// + /// Type of the payload. + /// + public string Datacontenttype { get; set; } + + /// + /// Payload. + /// + public TValue Data { get; set; } } diff --git a/src/Dapr.AspNetCore/BulkSubscribeAppResponse.cs b/src/Dapr.AspNetCore/BulkSubscribeAppResponse.cs index 9a9c3289..93c78dd5 100644 --- a/src/Dapr.AspNetCore/BulkSubscribeAppResponse.cs +++ b/src/Dapr.AspNetCore/BulkSubscribeAppResponse.cs @@ -13,26 +13,25 @@ using System.Collections.Generic; -namespace Dapr.AspNetCore +namespace Dapr.AspNetCore; + +/// +/// Response from the application containing status for each entry in the bulk message. +/// It is posted to the bulk subscribe handler. +/// +public class BulkSubscribeAppResponse { /// - /// Response from the application containing status for each entry in the bulk message. - /// It is posted to the bulk subscribe handler. + /// Initializes a new instance of the class. /// - public class BulkSubscribeAppResponse + /// List of statuses. + public BulkSubscribeAppResponse(List statuses) { - /// - /// Initializes a new instance of the class. - /// - /// List of statuses. - public BulkSubscribeAppResponse(List statuses) - { - this.Statuses = statuses; - } - - /// - /// List of statuses. - /// - public List Statuses { get; } + this.Statuses = statuses; } + + /// + /// List of statuses. + /// + public List Statuses { get; } } diff --git a/src/Dapr.AspNetCore/BulkSubscribeAppResponseEntry.cs b/src/Dapr.AspNetCore/BulkSubscribeAppResponseEntry.cs index b8f8f96d..f7e8de60 100644 --- a/src/Dapr.AspNetCore/BulkSubscribeAppResponseEntry.cs +++ b/src/Dapr.AspNetCore/BulkSubscribeAppResponseEntry.cs @@ -11,32 +11,31 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.AspNetCore +namespace Dapr.AspNetCore; + +/// +/// Maps an entry from bulk subscribe messages to a response status. +/// +public class BulkSubscribeAppResponseEntry { + /// - /// Maps an entry from bulk subscribe messages to a response status. + /// Initializes a new instance of the class. /// - public class BulkSubscribeAppResponseEntry - { - - /// - /// Initializes a new instance of the class. - /// - /// Entry ID of the event. - /// Status of the event processing in application. - public BulkSubscribeAppResponseEntry(string entryId, BulkSubscribeAppResponseStatus status) { - this.EntryId = entryId; - this.Status = status.ToString(); - } - - /// - /// Entry ID of the event. - /// - public string EntryId { get; } - - /// - /// Status of the event processing in application. - /// - public string Status { get; } + /// Entry ID of the event. + /// Status of the event processing in application. + public BulkSubscribeAppResponseEntry(string entryId, BulkSubscribeAppResponseStatus status) { + this.EntryId = entryId; + this.Status = status.ToString(); } + + /// + /// Entry ID of the event. + /// + public string EntryId { get; } + + /// + /// Status of the event processing in application. + /// + public string Status { get; } } diff --git a/src/Dapr.AspNetCore/BulkSubscribeAppResponseStatus.cs b/src/Dapr.AspNetCore/BulkSubscribeAppResponseStatus.cs index 55c8ad46..a275edb3 100644 --- a/src/Dapr.AspNetCore/BulkSubscribeAppResponseStatus.cs +++ b/src/Dapr.AspNetCore/BulkSubscribeAppResponseStatus.cs @@ -11,24 +11,22 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.AspNetCore -{ - /// - /// Status of the message handled in bulk subscribe handler. - /// - public enum BulkSubscribeAppResponseStatus { - /// - /// Success - /// - SUCCESS, - /// - /// Failure - /// - RETRY, - /// - /// Drop - /// - DROP - } - -} +namespace Dapr.AspNetCore; + +/// +/// Status of the message handled in bulk subscribe handler. +/// +public enum BulkSubscribeAppResponseStatus { + /// + /// Success + /// + SUCCESS, + /// + /// Failure + /// + RETRY, + /// + /// Drop + /// + DROP +} \ No newline at end of file diff --git a/src/Dapr.AspNetCore/BulkSubscribeAttribute.cs b/src/Dapr.AspNetCore/BulkSubscribeAttribute.cs index f5331bdf..db63a189 100644 --- a/src/Dapr.AspNetCore/BulkSubscribeAttribute.cs +++ b/src/Dapr.AspNetCore/BulkSubscribeAttribute.cs @@ -13,62 +13,61 @@ using System; -namespace Dapr.AspNetCore +namespace Dapr.AspNetCore; + +/// +/// BulkSubscribeAttribute describes options for a bulk subscriber with respect to a topic. +/// It needs to be paired with at least one [Topic] depending on the use case. +/// +[AttributeUsage(AttributeTargets.All, AllowMultiple = true)] +public class BulkSubscribeAttribute : Attribute, IBulkSubscribeMetadata { /// - /// BulkSubscribeAttribute describes options for a bulk subscriber with respect to a topic. - /// It needs to be paired with at least one [Topic] depending on the use case. + /// Initializes a new instance of the class. /// - [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] - public class BulkSubscribeAttribute : Attribute, IBulkSubscribeMetadata + /// The name of topic. + /// The name of the pubsub component to use. + /// The topic name. + public BulkSubscribeAttribute(string topicName, int maxMessagesCount, int maxAwaitDurationMs) { - /// - /// Initializes a new instance of the class. - /// - /// The name of topic. - /// The name of the pubsub component to use. - /// The topic name. - public BulkSubscribeAttribute(string topicName, int maxMessagesCount, int maxAwaitDurationMs) - { - this.TopicName = topicName; - this.MaxMessagesCount = maxMessagesCount; - this.MaxAwaitDurationMs = maxAwaitDurationMs; - } - - /// - /// Initializes a new instance of the class. - /// - /// The name of topic. - /// The name of the pubsub component to use. - public BulkSubscribeAttribute(string topicName, int maxMessagesCount) - { - this.TopicName = topicName; - this.MaxMessagesCount = maxMessagesCount; - } - - /// - /// Initializes a new instance of the class. - /// - /// The name of topic. - public BulkSubscribeAttribute(string topicName) - { - this.TopicName = topicName; - } - - /// - /// Maximum number of messages in a bulk message from the message bus. - /// - public int MaxMessagesCount { get; } = 100; - - /// - /// Maximum duration to wait for maxBulkSubCount messages by the message bus - /// before sending the messages to Dapr. - /// - public int MaxAwaitDurationMs { get; } = 1000; - - /// - /// The name of the topic to be bulk subscribed. - /// - public string TopicName { get; } + this.TopicName = topicName; + this.MaxMessagesCount = maxMessagesCount; + this.MaxAwaitDurationMs = maxAwaitDurationMs; } -} + + /// + /// Initializes a new instance of the class. + /// + /// The name of topic. + /// The name of the pubsub component to use. + public BulkSubscribeAttribute(string topicName, int maxMessagesCount) + { + this.TopicName = topicName; + this.MaxMessagesCount = maxMessagesCount; + } + + /// + /// Initializes a new instance of the class. + /// + /// The name of topic. + public BulkSubscribeAttribute(string topicName) + { + this.TopicName = topicName; + } + + /// + /// Maximum number of messages in a bulk message from the message bus. + /// + public int MaxMessagesCount { get; } = 100; + + /// + /// Maximum duration to wait for maxBulkSubCount messages by the message bus + /// before sending the messages to Dapr. + /// + public int MaxAwaitDurationMs { get; } = 1000; + + /// + /// The name of the topic to be bulk subscribed. + /// + public string TopicName { get; } +} \ No newline at end of file diff --git a/src/Dapr.AspNetCore/BulkSubscribeMessage.cs b/src/Dapr.AspNetCore/BulkSubscribeMessage.cs index f00ba373..cf39c0ef 100644 --- a/src/Dapr.AspNetCore/BulkSubscribeMessage.cs +++ b/src/Dapr.AspNetCore/BulkSubscribeMessage.cs @@ -13,47 +13,46 @@ using System.Collections.Generic; -namespace Dapr.AspNetCore +namespace Dapr.AspNetCore; + +/// +/// Represents a bulk of messages received from the message bus. +/// +/// The type of value contained in the data. +public class BulkSubscribeMessage { /// - /// Represents a bulk of messages received from the message bus. + /// Initializes a new instance of the class. /// - /// The type of value contained in the data. - public class BulkSubscribeMessage + public BulkSubscribeMessage() { - /// - /// Initializes a new instance of the class. - /// - public BulkSubscribeMessage() - { - } - - /// - /// Initializes a new instance of the class. - /// - /// A list of entries representing the event and other metadata. - /// The name of the pubsub topic. - /// Metadata for the bulk message. - public BulkSubscribeMessage(List> entries, string topic, Dictionary metadata) - { - this.Entries = entries; - this.Topic = topic; - this.Metadata = metadata; - } - - /// - /// A list of entries representing the event and other metadata. - /// - public List> Entries { get; set; } - - /// - /// The name of the pubsub topic. - /// - public string Topic { get; set; } - - /// - /// Metadata for the bulk message. - /// - public Dictionary Metadata { get; set; } } -} + + /// + /// Initializes a new instance of the class. + /// + /// A list of entries representing the event and other metadata. + /// The name of the pubsub topic. + /// Metadata for the bulk message. + public BulkSubscribeMessage(List> entries, string topic, Dictionary metadata) + { + this.Entries = entries; + this.Topic = topic; + this.Metadata = metadata; + } + + /// + /// A list of entries representing the event and other metadata. + /// + public List> Entries { get; set; } + + /// + /// The name of the pubsub topic. + /// + public string Topic { get; set; } + + /// + /// Metadata for the bulk message. + /// + public Dictionary Metadata { get; set; } +} \ No newline at end of file diff --git a/src/Dapr.AspNetCore/BulkSubscribeMessageEntry.cs b/src/Dapr.AspNetCore/BulkSubscribeMessageEntry.cs index 62abe3a1..aba31e71 100644 --- a/src/Dapr.AspNetCore/BulkSubscribeMessageEntry.cs +++ b/src/Dapr.AspNetCore/BulkSubscribeMessageEntry.cs @@ -13,55 +13,54 @@ using System.Collections.Generic; -namespace Dapr.AspNetCore +namespace Dapr.AspNetCore; + +/// +/// Represents a single event from a bulk of messages sent by the message bus. +/// +/// The type of value contained in the data. +public class BulkSubscribeMessageEntry { /// - /// Represents a single event from a bulk of messages sent by the message bus. + /// Initializes a new instance of the class. /// - /// The type of value contained in the data. - public class BulkSubscribeMessageEntry - { - /// - /// Initializes a new instance of the class. - /// - public BulkSubscribeMessageEntry() { - } - - /// - /// Initializes a new instance of the class. - /// - /// A unique identifier for the event. - /// Content type of the event. - /// Metadata for the event. - /// The pubsub event. - public BulkSubscribeMessageEntry(string entryId, string contentType, Dictionary metadata, - TValue eventData) - { - this.EntryId = entryId; - this.ContentType = contentType; - this.Metadata = metadata; - this.Event = eventData; - } - - /// - /// A unique identifier for the event. - /// - public string EntryId { get; set; } - - /// - /// Content type of the event. - /// - public string ContentType { get; set; } - - /// - /// Metadata for the event. - /// - public Dictionary Metadata { get; set; } - - /// - /// The pubsub event. - /// - public TValue Event { get; set; } - + public BulkSubscribeMessageEntry() { } -} + + /// + /// Initializes a new instance of the class. + /// + /// A unique identifier for the event. + /// Content type of the event. + /// Metadata for the event. + /// The pubsub event. + public BulkSubscribeMessageEntry(string entryId, string contentType, Dictionary metadata, + TValue eventData) + { + this.EntryId = entryId; + this.ContentType = contentType; + this.Metadata = metadata; + this.Event = eventData; + } + + /// + /// A unique identifier for the event. + /// + public string EntryId { get; set; } + + /// + /// Content type of the event. + /// + public string ContentType { get; set; } + + /// + /// Metadata for the event. + /// + public Dictionary Metadata { get; set; } + + /// + /// The pubsub event. + /// + public TValue Event { get; set; } + +} \ No newline at end of file diff --git a/src/Dapr.AspNetCore/BulkSubscribeTopicOptions.cs b/src/Dapr.AspNetCore/BulkSubscribeTopicOptions.cs index d70e4cfd..2dfec885 100644 --- a/src/Dapr.AspNetCore/BulkSubscribeTopicOptions.cs +++ b/src/Dapr.AspNetCore/BulkSubscribeTopicOptions.cs @@ -1,24 +1,23 @@ -namespace Dapr.AspNetCore +namespace Dapr.AspNetCore; + +/// +/// This class defines configurations for the bulk subscribe endpoint. +/// +public class BulkSubscribeTopicOptions { /// - /// This class defines configurations for the bulk subscribe endpoint. + /// Maximum number of messages in a bulk message from the message bus. /// - public class BulkSubscribeTopicOptions - { - /// - /// Maximum number of messages in a bulk message from the message bus. - /// - public int MaxMessagesCount { get; set; } = 100; + public int MaxMessagesCount { get; set; } = 100; - /// - /// Maximum duration to wait for maxBulkSubCount messages by the message bus - /// before sending the messages to Dapr. - /// - public int MaxAwaitDurationMs { get; set; } = 1000; + /// + /// Maximum duration to wait for maxBulkSubCount messages by the message bus + /// before sending the messages to Dapr. + /// + public int MaxAwaitDurationMs { get; set; } = 1000; - /// - /// The name of the topic to be bulk subscribed. - /// - public string TopicName { get; set; } - } -} + /// + /// The name of the topic to be bulk subscribed. + /// + public string TopicName { get; set; } +} \ No newline at end of file diff --git a/src/Dapr.AspNetCore/CloudEventPropertyNames.cs b/src/Dapr.AspNetCore/CloudEventPropertyNames.cs index 87e49600..a43d87da 100644 --- a/src/Dapr.AspNetCore/CloudEventPropertyNames.cs +++ b/src/Dapr.AspNetCore/CloudEventPropertyNames.cs @@ -1,9 +1,8 @@ -namespace Dapr +namespace Dapr; + +internal static class CloudEventPropertyNames { - internal static class CloudEventPropertyNames - { - public const string Data = "data"; - public const string DataContentType = "datacontenttype"; - public const string DataBase64 = "data_base64"; - } -} + public const string Data = "data"; + public const string DataContentType = "datacontenttype"; + public const string DataBase64 = "data_base64"; +} \ No newline at end of file diff --git a/src/Dapr.AspNetCore/CloudEventsMiddleware.cs b/src/Dapr.AspNetCore/CloudEventsMiddleware.cs index eac526c2..b9b64b72 100644 --- a/src/Dapr.AspNetCore/CloudEventsMiddleware.cs +++ b/src/Dapr.AspNetCore/CloudEventsMiddleware.cs @@ -14,283 +14,282 @@ using System.Collections.Generic; using System.Linq; -namespace Dapr +namespace Dapr; + +using System; +using System.IO; +using System.Net; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; + +internal class CloudEventsMiddleware { - using System; - using System.IO; - using System.Net; - using System.Text; - using System.Text.Json; - using System.Threading.Tasks; - using Microsoft.AspNetCore.Http; - using Microsoft.AspNetCore.WebUtilities; - using Microsoft.Extensions.Primitives; - using Microsoft.Net.Http.Headers; + private const string ContentType = "application/cloudevents+json"; - internal class CloudEventsMiddleware + // These cloudevent properties are either containing the body of the message or + // are included in the headers by other components of Dapr earlier in the pipeline + private static readonly string[] ExcludedPropertiesFromHeaders = { - private const string ContentType = "application/cloudevents+json"; + CloudEventPropertyNames.DataContentType, CloudEventPropertyNames.Data, + CloudEventPropertyNames.DataBase64, "pubsubname", "traceparent" + }; - // These cloudevent properties are either containing the body of the message or - // are included in the headers by other components of Dapr earlier in the pipeline - private static readonly string[] ExcludedPropertiesFromHeaders = + private readonly RequestDelegate next; + private readonly CloudEventsMiddlewareOptions options; + + public CloudEventsMiddleware(RequestDelegate next, CloudEventsMiddlewareOptions options) + { + this.next = next; + this.options = options; + } + + public Task InvokeAsync(HttpContext httpContext) + { + // This middleware unwraps any requests with a cloud events (JSON) content type + // and replaces the request body + request content type so that it can be read by a + // non-cloud-events-aware piece of code. + // + // This corresponds to cloud events in the *structured* format: + // https://github.com/cloudevents/spec/blob/master/http-transport-binding.md#13-content-modes + // + // For *binary* format, we don't have to do anything + // + // We don't support batching. + // + // The philosophy here is that we don't report an error for things we don't support, because + // that would block someone from implementing their own support for it. We only report an error + // when something we do support isn't correct. + if (!MatchesContentType(httpContext, out var charSet)) { - CloudEventPropertyNames.DataContentType, CloudEventPropertyNames.Data, - CloudEventPropertyNames.DataBase64, "pubsubname", "traceparent" - }; - - private readonly RequestDelegate next; - private readonly CloudEventsMiddlewareOptions options; - - public CloudEventsMiddleware(RequestDelegate next, CloudEventsMiddlewareOptions options) - { - this.next = next; - this.options = options; + return this.next(httpContext); } - public Task InvokeAsync(HttpContext httpContext) + return this.ProcessBodyAsync(httpContext, charSet); + } + + private async Task ProcessBodyAsync(HttpContext httpContext, string charSet) + { + JsonElement json; + if (string.Equals(charSet, Encoding.UTF8.WebName, StringComparison.OrdinalIgnoreCase)) { - // This middleware unwraps any requests with a cloud events (JSON) content type - // and replaces the request body + request content type so that it can be read by a - // non-cloud-events-aware piece of code. - // - // This corresponds to cloud events in the *structured* format: - // https://github.com/cloudevents/spec/blob/master/http-transport-binding.md#13-content-modes - // - // For *binary* format, we don't have to do anything - // - // We don't support batching. - // - // The philosophy here is that we don't report an error for things we don't support, because - // that would block someone from implementing their own support for it. We only report an error - // when something we do support isn't correct. - if (!MatchesContentType(httpContext, out var charSet)) + json = await JsonSerializer.DeserializeAsync(httpContext.Request.Body); + } + else + { + using (var reader = + new HttpRequestStreamReader(httpContext.Request.Body, Encoding.GetEncoding(charSet))) { - return this.next(httpContext); + var text = await reader.ReadToEndAsync(); + json = JsonSerializer.Deserialize(text); } - - return this.ProcessBodyAsync(httpContext, charSet); } - private async Task ProcessBodyAsync(HttpContext httpContext, string charSet) + Stream originalBody; + Stream body; + + string originalContentType; + string contentType; + + // Check whether to use data or data_base64 as per https://github.com/cloudevents/spec/blob/v1.0.1/json-format.md#31-handling-of-data + // Get the property names by OrdinalIgnoreCase comparison to support case insensitive JSON as the Json Serializer for AspCore already supports it by default. + var jsonPropNames = json.EnumerateObject().ToArray(); + + var dataPropName = jsonPropNames + .Select(d => d.Name) + .FirstOrDefault(d => d.Equals(CloudEventPropertyNames.Data, StringComparison.OrdinalIgnoreCase)); + + var dataBase64PropName = jsonPropNames + .Select(d => d.Name) + .FirstOrDefault(d => + d.Equals(CloudEventPropertyNames.DataBase64, StringComparison.OrdinalIgnoreCase)); + + var isDataSet = false; + var isBinaryDataSet = false; + JsonElement data = default; + + if (dataPropName != null) { - JsonElement json; - if (string.Equals(charSet, Encoding.UTF8.WebName, StringComparison.OrdinalIgnoreCase)) + isDataSet = true; + data = json.TryGetProperty(dataPropName, out var dataJsonElement) ? dataJsonElement : data; + } + + if (dataBase64PropName != null) + { + isBinaryDataSet = true; + data = json.TryGetProperty(dataBase64PropName, out var dataJsonElement) ? dataJsonElement : data; + } + + if (isDataSet && isBinaryDataSet) + { + httpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest; + return; + } + + if (isDataSet) + { + contentType = GetDataContentType(json, out var isJson); + + // If the value is anything other than a JSON string, treat it as JSON. Cloud Events requires + // non-JSON text to be enclosed in a JSON string. + isJson |= data.ValueKind != JsonValueKind.String; + + body = new MemoryStream(); + if (isJson || options.SuppressJsonDecodingOfTextPayloads) { - json = await JsonSerializer.DeserializeAsync(httpContext.Request.Body); + // Rehydrate body from JSON payload + await JsonSerializer.SerializeAsync(body, data); } else { - using (var reader = - new HttpRequestStreamReader(httpContext.Request.Body, Encoding.GetEncoding(charSet))) - { - var text = await reader.ReadToEndAsync(); - json = JsonSerializer.Deserialize(text); - } + // Rehydrate body from contents of the string + var text = data.GetString(); + await using var writer = new HttpResponseStreamWriter(body, Encoding.UTF8); + await writer.WriteAsync(text); } - Stream originalBody; - Stream body; - - string originalContentType; - string contentType; - - // Check whether to use data or data_base64 as per https://github.com/cloudevents/spec/blob/v1.0.1/json-format.md#31-handling-of-data - // Get the property names by OrdinalIgnoreCase comparison to support case insensitive JSON as the Json Serializer for AspCore already supports it by default. - var jsonPropNames = json.EnumerateObject().ToArray(); - - var dataPropName = jsonPropNames - .Select(d => d.Name) - .FirstOrDefault(d => d.Equals(CloudEventPropertyNames.Data, StringComparison.OrdinalIgnoreCase)); - - var dataBase64PropName = jsonPropNames - .Select(d => d.Name) - .FirstOrDefault(d => - d.Equals(CloudEventPropertyNames.DataBase64, StringComparison.OrdinalIgnoreCase)); - - var isDataSet = false; - var isBinaryDataSet = false; - JsonElement data = default; - - if (dataPropName != null) - { - isDataSet = true; - data = json.TryGetProperty(dataPropName, out var dataJsonElement) ? dataJsonElement : data; - } - - if (dataBase64PropName != null) - { - isBinaryDataSet = true; - data = json.TryGetProperty(dataBase64PropName, out var dataJsonElement) ? dataJsonElement : data; - } - - if (isDataSet && isBinaryDataSet) - { - httpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest; - return; - } - - if (isDataSet) - { - contentType = GetDataContentType(json, out var isJson); - - // If the value is anything other than a JSON string, treat it as JSON. Cloud Events requires - // non-JSON text to be enclosed in a JSON string. - isJson |= data.ValueKind != JsonValueKind.String; - - body = new MemoryStream(); - if (isJson || options.SuppressJsonDecodingOfTextPayloads) - { - // Rehydrate body from JSON payload - await JsonSerializer.SerializeAsync(body, data); - } - else - { - // Rehydrate body from contents of the string - var text = data.GetString(); - await using var writer = new HttpResponseStreamWriter(body, Encoding.UTF8); - await writer.WriteAsync(text); - } - - body.Seek(0L, SeekOrigin.Begin); - } - else if (isBinaryDataSet) - { - // As per the spec, if the implementation determines that the type of data is Binary, - // the value MUST be represented as a JSON string expression containing the Base64 encoded - // binary value, and use the member name data_base64 to store it inside the JSON object. - var decodedBody = data.GetBytesFromBase64(); - body = new MemoryStream(decodedBody); - body.Seek(0L, SeekOrigin.Begin); - contentType = GetDataContentType(json, out _); - } - else - { - body = new MemoryStream(); - contentType = null; - } - - ForwardCloudEventPropertiesAsHeaders(httpContext, jsonPropNames); - - originalBody = httpContext.Request.Body; - originalContentType = httpContext.Request.ContentType; - - try - { - httpContext.Request.Body = body; - httpContext.Request.ContentType = contentType; - - await this.next(httpContext); - } - finally - { - httpContext.Request.ContentType = originalContentType; - httpContext.Request.Body = originalBody; - } + body.Seek(0L, SeekOrigin.Begin); + } + else if (isBinaryDataSet) + { + // As per the spec, if the implementation determines that the type of data is Binary, + // the value MUST be represented as a JSON string expression containing the Base64 encoded + // binary value, and use the member name data_base64 to store it inside the JSON object. + var decodedBody = data.GetBytesFromBase64(); + body = new MemoryStream(decodedBody); + body.Seek(0L, SeekOrigin.Begin); + contentType = GetDataContentType(json, out _); + } + else + { + body = new MemoryStream(); + contentType = null; } - private void ForwardCloudEventPropertiesAsHeaders( - HttpContext httpContext, - IEnumerable jsonPropNames) + ForwardCloudEventPropertiesAsHeaders(httpContext, jsonPropNames); + + originalBody = httpContext.Request.Body; + originalContentType = httpContext.Request.ContentType; + + try { - if (!options.ForwardCloudEventPropertiesAsHeaders) - { - return; - } + httpContext.Request.Body = body; + httpContext.Request.ContentType = contentType; - var filteredPropertyNames = jsonPropNames - .Where(d => !ExcludedPropertiesFromHeaders.Contains(d.Name, StringComparer.OrdinalIgnoreCase)); - - if (options.IncludedCloudEventPropertiesAsHeaders != null) - { - filteredPropertyNames = filteredPropertyNames - .Where(d => options.IncludedCloudEventPropertiesAsHeaders - .Contains(d.Name, StringComparer.OrdinalIgnoreCase)); - } - else if (options.ExcludedCloudEventPropertiesFromHeaders != null) - { - filteredPropertyNames = filteredPropertyNames - .Where(d => !options.ExcludedCloudEventPropertiesFromHeaders - .Contains(d.Name, StringComparer.OrdinalIgnoreCase)); - } - - foreach (var jsonProperty in filteredPropertyNames) - { - httpContext.Request.Headers.TryAdd($"Cloudevent.{jsonProperty.Name.ToLowerInvariant()}", - jsonProperty.Value.GetRawText().Trim('\"')); - } + await this.next(httpContext); } - - private static string GetDataContentType(JsonElement json, out bool isJson) + finally { - var dataContentTypePropName = json - .EnumerateObject() - .Select(d => d.Name) - .FirstOrDefault(d => - d.Equals(CloudEventPropertyNames.DataContentType, - StringComparison.OrdinalIgnoreCase)); - - string contentType; - - if (dataContentTypePropName != null - && json.TryGetProperty(dataContentTypePropName, out var dataContentType) - && dataContentType.ValueKind == JsonValueKind.String - && MediaTypeHeaderValue.TryParse(dataContentType.GetString(), out var parsed)) - { - contentType = dataContentType.GetString(); - isJson = - parsed.MediaType.Equals("application/json", StringComparison.Ordinal) || - parsed.Suffix.EndsWith("+json", StringComparison.Ordinal); - - // Since S.T.Json always outputs utf-8, we may need to normalize the data content type - // to remove any charset information. We generally just assume utf-8 everywhere, so omitting - // a charset is a safe bet. - if (contentType.Contains("charset")) - { - parsed.Charset = StringSegment.Empty; - contentType = parsed.ToString(); - } - } - else - { - // assume JSON is not specified. - contentType = "application/json"; - isJson = true; - } - - return contentType; - } - - private static bool MatchesContentType(HttpContext httpContext, out string charSet) - { - if (httpContext.Request.ContentType == null) - { - charSet = null; - return false; - } - - // Handle cases where the content type includes additional parameters like charset. - // Doing the string comparison up front so we can avoid allocation. - if (!httpContext.Request.ContentType.StartsWith(ContentType)) - { - charSet = null; - return false; - } - - if (!MediaTypeHeaderValue.TryParse(httpContext.Request.ContentType, out var parsed)) - { - charSet = null; - return false; - } - - if (parsed.MediaType != ContentType) - { - charSet = null; - return false; - } - - charSet = parsed.Charset.Length > 0 ? parsed.Charset.Value : "UTF-8"; - return true; + httpContext.Request.ContentType = originalContentType; + httpContext.Request.Body = originalBody; } } -} + + private void ForwardCloudEventPropertiesAsHeaders( + HttpContext httpContext, + IEnumerable jsonPropNames) + { + if (!options.ForwardCloudEventPropertiesAsHeaders) + { + return; + } + + var filteredPropertyNames = jsonPropNames + .Where(d => !ExcludedPropertiesFromHeaders.Contains(d.Name, StringComparer.OrdinalIgnoreCase)); + + if (options.IncludedCloudEventPropertiesAsHeaders != null) + { + filteredPropertyNames = filteredPropertyNames + .Where(d => options.IncludedCloudEventPropertiesAsHeaders + .Contains(d.Name, StringComparer.OrdinalIgnoreCase)); + } + else if (options.ExcludedCloudEventPropertiesFromHeaders != null) + { + filteredPropertyNames = filteredPropertyNames + .Where(d => !options.ExcludedCloudEventPropertiesFromHeaders + .Contains(d.Name, StringComparer.OrdinalIgnoreCase)); + } + + foreach (var jsonProperty in filteredPropertyNames) + { + httpContext.Request.Headers.TryAdd($"Cloudevent.{jsonProperty.Name.ToLowerInvariant()}", + jsonProperty.Value.GetRawText().Trim('\"')); + } + } + + private static string GetDataContentType(JsonElement json, out bool isJson) + { + var dataContentTypePropName = json + .EnumerateObject() + .Select(d => d.Name) + .FirstOrDefault(d => + d.Equals(CloudEventPropertyNames.DataContentType, + StringComparison.OrdinalIgnoreCase)); + + string contentType; + + if (dataContentTypePropName != null + && json.TryGetProperty(dataContentTypePropName, out var dataContentType) + && dataContentType.ValueKind == JsonValueKind.String + && MediaTypeHeaderValue.TryParse(dataContentType.GetString(), out var parsed)) + { + contentType = dataContentType.GetString(); + isJson = + parsed.MediaType.Equals("application/json", StringComparison.Ordinal) || + parsed.Suffix.EndsWith("+json", StringComparison.Ordinal); + + // Since S.T.Json always outputs utf-8, we may need to normalize the data content type + // to remove any charset information. We generally just assume utf-8 everywhere, so omitting + // a charset is a safe bet. + if (contentType.Contains("charset")) + { + parsed.Charset = StringSegment.Empty; + contentType = parsed.ToString(); + } + } + else + { + // assume JSON is not specified. + contentType = "application/json"; + isJson = true; + } + + return contentType; + } + + private static bool MatchesContentType(HttpContext httpContext, out string charSet) + { + if (httpContext.Request.ContentType == null) + { + charSet = null; + return false; + } + + // Handle cases where the content type includes additional parameters like charset. + // Doing the string comparison up front so we can avoid allocation. + if (!httpContext.Request.ContentType.StartsWith(ContentType)) + { + charSet = null; + return false; + } + + if (!MediaTypeHeaderValue.TryParse(httpContext.Request.ContentType, out var parsed)) + { + charSet = null; + return false; + } + + if (parsed.MediaType != ContentType) + { + charSet = null; + return false; + } + + charSet = parsed.Charset.Length > 0 ? parsed.Charset.Value : "UTF-8"; + return true; + } +} \ No newline at end of file diff --git a/src/Dapr.AspNetCore/CloudEventsMiddlewareOptions.cs b/src/Dapr.AspNetCore/CloudEventsMiddlewareOptions.cs index 84e68adb..98311013 100644 --- a/src/Dapr.AspNetCore/CloudEventsMiddlewareOptions.cs +++ b/src/Dapr.AspNetCore/CloudEventsMiddlewareOptions.cs @@ -11,67 +11,66 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr +namespace Dapr; + +/// +/// Provides optional settings to the cloud events middleware. +/// +public class CloudEventsMiddlewareOptions { /// - /// Provides optional settings to the cloud events middleware. + /// Gets or sets a value that will determine whether non-JSON textual payloads are decoded. /// - public class CloudEventsMiddlewareOptions - { - /// - /// Gets or sets a value that will determine whether non-JSON textual payloads are decoded. - /// - /// - /// - /// In the 1.0 release of the Dapr .NET SDK the cloud events middleware would not JSON-decode - /// a textual cloud events payload. A cloud event payload containing text/plain data - /// of "data": "Hello, \"world!\"" would result in a request body containing "Hello, \"world!\"" - /// instead of the expected JSON-decoded value of Hello, "world!". - /// - /// - /// Setting this property to true restores the previous invalid behavior for compatibility. - /// - /// - public bool SuppressJsonDecodingOfTextPayloads { get; set; } + /// + /// + /// In the 1.0 release of the Dapr .NET SDK the cloud events middleware would not JSON-decode + /// a textual cloud events payload. A cloud event payload containing text/plain data + /// of "data": "Hello, \"world!\"" would result in a request body containing "Hello, \"world!\"" + /// instead of the expected JSON-decoded value of Hello, "world!". + /// + /// + /// Setting this property to true restores the previous invalid behavior for compatibility. + /// + /// + public bool SuppressJsonDecodingOfTextPayloads { get; set; } - /// - /// Gets or sets a value that will determine whether the CloudEvent properties will be forwarded as Request Headers. - /// - /// - /// - /// Setting this property to true will forward all the CloudEvent properties as Request Headers. - /// For more fine grained control of which properties are forwarded you can use either or . - /// - /// - /// Property names will always be prefixed with 'Cloudevent.' and be lower case in the following format:"Cloudevent.type" - /// - /// - /// ie. A CloudEvent property "type": "Example.Type" will be added as "Cloudevent.type": "Example.Type" request header. - /// - /// - public bool ForwardCloudEventPropertiesAsHeaders { get; set; } + /// + /// Gets or sets a value that will determine whether the CloudEvent properties will be forwarded as Request Headers. + /// + /// + /// + /// Setting this property to true will forward all the CloudEvent properties as Request Headers. + /// For more fine grained control of which properties are forwarded you can use either or . + /// + /// + /// Property names will always be prefixed with 'Cloudevent.' and be lower case in the following format:"Cloudevent.type" + /// + /// + /// ie. A CloudEvent property "type": "Example.Type" will be added as "Cloudevent.type": "Example.Type" request header. + /// + /// + public bool ForwardCloudEventPropertiesAsHeaders { get; set; } - /// - /// Gets or sets an array of CloudEvent property names that will be forwarded as Request Headers if is set to true. - /// - /// - /// - /// Note: Setting this will only forwarded the listed property names. - /// - /// - /// ie: ["type", "subject"] - /// - /// - public string[] IncludedCloudEventPropertiesAsHeaders { get; set; } + /// + /// Gets or sets an array of CloudEvent property names that will be forwarded as Request Headers if is set to true. + /// + /// + /// + /// Note: Setting this will only forwarded the listed property names. + /// + /// + /// ie: ["type", "subject"] + /// + /// + public string[] IncludedCloudEventPropertiesAsHeaders { get; set; } - /// - /// Gets or sets an array of CloudEvent property names that will not be forwarded as Request Headers if is set to true. - /// - /// - /// - /// ie: ["type", "subject"] - /// - /// - public string[] ExcludedCloudEventPropertiesFromHeaders { get; set; } - } -} + /// + /// Gets or sets an array of CloudEvent property names that will not be forwarded as Request Headers if is set to true. + /// + /// + /// + /// ie: ["type", "subject"] + /// + /// + public string[] ExcludedCloudEventPropertiesFromHeaders { get; set; } +} \ No newline at end of file diff --git a/src/Dapr.AspNetCore/DaprApplicationBuilderExtensions.cs b/src/Dapr.AspNetCore/DaprApplicationBuilderExtensions.cs index 3ad26a4d..742230fd 100644 --- a/src/Dapr.AspNetCore/DaprApplicationBuilderExtensions.cs +++ b/src/Dapr.AspNetCore/DaprApplicationBuilderExtensions.cs @@ -11,48 +11,47 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.Builder; + +using System; +using Dapr; + +/// +/// Provides extension methods for . +/// +public static class DaprApplicationBuilderExtensions { - using System; - using Dapr; + /// + /// Adds the cloud events middleware to the middleware pipeline. The cloud events middleware will unwrap + /// requests that use the cloud events structured format, allowing the event payload to be read directly. + /// + /// An . + /// The . + public static IApplicationBuilder UseCloudEvents(this IApplicationBuilder builder) + { + if (builder is null) + { + throw new ArgumentNullException(nameof(builder)); + } + + return UseCloudEvents(builder, new CloudEventsMiddlewareOptions()); + } /// - /// Provides extension methods for . + /// Adds the cloud events middleware to the middleware pipeline. The cloud events middleware will unwrap + /// requests that use the cloud events structured format, allowing the event payload to be read directly. /// - public static class DaprApplicationBuilderExtensions + /// An . + /// The to configure optional settings. + /// The . + public static IApplicationBuilder UseCloudEvents(this IApplicationBuilder builder, CloudEventsMiddlewareOptions options) { - /// - /// Adds the cloud events middleware to the middleware pipeline. The cloud events middleware will unwrap - /// requests that use the cloud events structured format, allowing the event payload to be read directly. - /// - /// An . - /// The . - public static IApplicationBuilder UseCloudEvents(this IApplicationBuilder builder) + if (builder is null) { - if (builder is null) - { - throw new ArgumentNullException(nameof(builder)); - } - - return UseCloudEvents(builder, new CloudEventsMiddlewareOptions()); + throw new ArgumentNullException(nameof(builder)); } - /// - /// Adds the cloud events middleware to the middleware pipeline. The cloud events middleware will unwrap - /// requests that use the cloud events structured format, allowing the event payload to be read directly. - /// - /// An . - /// The to configure optional settings. - /// The . - public static IApplicationBuilder UseCloudEvents(this IApplicationBuilder builder, CloudEventsMiddlewareOptions options) - { - if (builder is null) - { - throw new ArgumentNullException(nameof(builder)); - } - - builder.UseMiddleware(options); - return builder; - } + builder.UseMiddleware(options); + return builder; } -} +} \ No newline at end of file diff --git a/src/Dapr.AspNetCore/DaprAuthenticationBuilderExtensions.cs b/src/Dapr.AspNetCore/DaprAuthenticationBuilderExtensions.cs index e55b2916..9155cd94 100644 --- a/src/Dapr.AspNetCore/DaprAuthenticationBuilderExtensions.cs +++ b/src/Dapr.AspNetCore/DaprAuthenticationBuilderExtensions.cs @@ -14,38 +14,37 @@ using System; using Dapr.AspNetCore; -namespace Microsoft.AspNetCore.Authentication +namespace Microsoft.AspNetCore.Authentication; + +/// +/// Provides extension methods for . +/// +public static class DaprAuthenticationBuilderExtensions { /// - /// Provides extension methods for . + /// Adds App API token authentication. + /// See https://docs.dapr.io/operations/security/app-api-token/ for more information about App API token authentication in Dapr. + /// By default, the token will be read from the APP_API_TOKEN environment variable. /// - public static class DaprAuthenticationBuilderExtensions - { - /// - /// Adds App API token authentication. - /// See https://docs.dapr.io/operations/security/app-api-token/ for more information about App API token authentication in Dapr. - /// By default, the token will be read from the APP_API_TOKEN environment variable. - /// - /// The . - /// A reference to after the operation has completed. - public static AuthenticationBuilder AddDapr(this AuthenticationBuilder builder) => builder.AddDapr(configureOptions: null); + /// The . + /// A reference to after the operation has completed. + public static AuthenticationBuilder AddDapr(this AuthenticationBuilder builder) => builder.AddDapr(configureOptions: null); - /// - /// Adds App API token authentication. - /// See https://docs.dapr.io/operations/security/app-api-token/ for more information about App API token authentication in Dapr. - /// - /// The . - /// - /// A delegate that allows configuring . - /// By default, the token will be read from the APP_API_TOKEN environment variable. - /// - /// A reference to after the operation has completed. - public static AuthenticationBuilder AddDapr(this AuthenticationBuilder builder, Action configureOptions) - { - return builder - .AddScheme( - DaprAuthenticationOptions.DefaultScheme, - configureOptions); - } + /// + /// Adds App API token authentication. + /// See https://docs.dapr.io/operations/security/app-api-token/ for more information about App API token authentication in Dapr. + /// + /// The . + /// + /// A delegate that allows configuring . + /// By default, the token will be read from the APP_API_TOKEN environment variable. + /// + /// A reference to after the operation has completed. + public static AuthenticationBuilder AddDapr(this AuthenticationBuilder builder, Action configureOptions) + { + return builder + .AddScheme( + DaprAuthenticationOptions.DefaultScheme, + configureOptions); } -} +} \ No newline at end of file diff --git a/src/Dapr.AspNetCore/DaprAuthenticationHandler.cs b/src/Dapr.AspNetCore/DaprAuthenticationHandler.cs index dc21b592..60a2f974 100644 --- a/src/Dapr.AspNetCore/DaprAuthenticationHandler.cs +++ b/src/Dapr.AspNetCore/DaprAuthenticationHandler.cs @@ -11,27 +11,27 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.AspNetCore -{ - using System.Collections.Generic; - using System.Security.Claims; - using System.Text.Encodings.Web; - using System.Threading.Tasks; - using Microsoft.AspNetCore.Authentication; - using Microsoft.Extensions.Logging; - using Microsoft.Extensions.Options; +namespace Dapr.AspNetCore; - internal class DaprAuthenticationHandler : AuthenticationHandler - { - const string DaprApiToken = "Dapr-Api-Token"; +using System.Collections.Generic; +using System.Security.Claims; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +internal class DaprAuthenticationHandler : AuthenticationHandler +{ + const string DaprApiToken = "Dapr-Api-Token"; #if NET8_0_OR_GREATER - public DaprAuthenticationHandler( - IOptionsMonitor options, - ILoggerFactory logger, - UrlEncoder encoder) : base(options, logger, encoder) - { - } + public DaprAuthenticationHandler( + IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder) : base(options, logger, encoder) + { + } #else public DaprAuthenticationHandler( IOptionsMonitor options, @@ -42,39 +42,38 @@ namespace Dapr.AspNetCore } #endif - protected override Task HandleAuthenticateAsync() - { - return Task.FromResult(HandleAuthenticate()); - } - - private AuthenticateResult HandleAuthenticate() - { - if (!Request.Headers.TryGetValue(DaprApiToken, out var token)) - { - return AuthenticateResult.NoResult(); - } - - var expectedToken = Options.Token; - if (string.IsNullOrWhiteSpace(expectedToken)) - { - return AuthenticateResult.Fail("App API Token not configured."); - } - - if (!string.Equals(token, expectedToken)) - { - return AuthenticateResult.Fail("Not authenticated."); - } - - var claims = new List - { - new Claim(ClaimTypes.Name, "Dapr") - }; - var identity = new ClaimsIdentity(claims, Options.Scheme); - var identities = new List { identity }; - var principal = new ClaimsPrincipal(identities); - var ticket = new AuthenticationTicket(principal, Options.Scheme); - - return AuthenticateResult.Success(ticket); - } + protected override Task HandleAuthenticateAsync() + { + return Task.FromResult(HandleAuthenticate()); } -} + + private AuthenticateResult HandleAuthenticate() + { + if (!Request.Headers.TryGetValue(DaprApiToken, out var token)) + { + return AuthenticateResult.NoResult(); + } + + var expectedToken = Options.Token; + if (string.IsNullOrWhiteSpace(expectedToken)) + { + return AuthenticateResult.Fail("App API Token not configured."); + } + + if (!string.Equals(token, expectedToken)) + { + return AuthenticateResult.Fail("Not authenticated."); + } + + var claims = new List + { + new Claim(ClaimTypes.Name, "Dapr") + }; + var identity = new ClaimsIdentity(claims, Options.Scheme); + var identities = new List { identity }; + var principal = new ClaimsPrincipal(identities); + var ticket = new AuthenticationTicket(principal, Options.Scheme); + + return AuthenticateResult.Success(ticket); + } +} \ No newline at end of file diff --git a/src/Dapr.AspNetCore/DaprAuthenticationOptions.cs b/src/Dapr.AspNetCore/DaprAuthenticationOptions.cs index b12d4d14..fd9f2794 100644 --- a/src/Dapr.AspNetCore/DaprAuthenticationOptions.cs +++ b/src/Dapr.AspNetCore/DaprAuthenticationOptions.cs @@ -13,21 +13,20 @@ using Microsoft.AspNetCore.Authentication; -namespace Dapr.AspNetCore -{ - /// - /// Options class provides information needed to control Dapr Authentication handler behavior. - /// See https://docs.dapr.io/operations/security/app-api-token/ for more information about App API token authentication in Dapr. - /// - public class DaprAuthenticationOptions : AuthenticationSchemeOptions - { - internal const string DefaultScheme = "Dapr"; - internal string Scheme { get; } = DefaultScheme; +namespace Dapr.AspNetCore; - /// - /// Gets or sets the App API token. - /// By default, the token will be read from the APP_API_TOKEN environment variable. - /// - public string Token { get; set; } = DaprDefaults.GetDefaultAppApiToken(null); - } -} +/// +/// Options class provides information needed to control Dapr Authentication handler behavior. +/// See https://docs.dapr.io/operations/security/app-api-token/ for more information about App API token authentication in Dapr. +/// +public class DaprAuthenticationOptions : AuthenticationSchemeOptions +{ + internal const string DefaultScheme = "Dapr"; + internal string Scheme { get; } = DefaultScheme; + + /// + /// Gets or sets the App API token. + /// By default, the token will be read from the APP_API_TOKEN environment variable. + /// + public string Token { get; set; } = DaprDefaults.GetDefaultAppApiToken(null); +} \ No newline at end of file diff --git a/src/Dapr.AspNetCore/DaprAuthorizationOptionsExtensions.cs b/src/Dapr.AspNetCore/DaprAuthorizationOptionsExtensions.cs index 4fd2c2f2..2074fd50 100644 --- a/src/Dapr.AspNetCore/DaprAuthorizationOptionsExtensions.cs +++ b/src/Dapr.AspNetCore/DaprAuthorizationOptionsExtensions.cs @@ -11,27 +11,26 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Microsoft.AspNetCore.Authorization -{ - using Dapr.AspNetCore; +namespace Microsoft.AspNetCore.Authorization; +using Dapr.AspNetCore; + +/// +/// Provides extension methods for . +/// +public static class DaprAuthorizationOptionsExtensions +{ /// - /// Provides extension methods for . + /// Adds Dapr authorization policy. /// - public static class DaprAuthorizationOptionsExtensions + /// The . + /// The name of the policy. + public static void AddDapr(this AuthorizationOptions options, string name = "Dapr") { - /// - /// Adds Dapr authorization policy. - /// - /// The . - /// The name of the policy. - public static void AddDapr(this AuthorizationOptions options, string name = "Dapr") + options.AddPolicy(name, policy => { - options.AddPolicy(name, policy => - { - policy.RequireAuthenticatedUser(); - policy.AddAuthenticationSchemes(DaprAuthenticationOptions.DefaultScheme); - }); - } + policy.RequireAuthenticatedUser(); + policy.AddAuthenticationSchemes(DaprAuthenticationOptions.DefaultScheme); + }); } -} +} \ No newline at end of file diff --git a/src/Dapr.AspNetCore/DaprEndpointConventionBuilderExtensions.cs b/src/Dapr.AspNetCore/DaprEndpointConventionBuilderExtensions.cs index 0ffaf62d..88e015ac 100644 --- a/src/Dapr.AspNetCore/DaprEndpointConventionBuilderExtensions.cs +++ b/src/Dapr.AspNetCore/DaprEndpointConventionBuilderExtensions.cs @@ -11,161 +11,160 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.Builder; + +using Dapr; +using Dapr.AspNetCore; +using System; +using System.Collections.Generic; + +/// +/// Contains extension methods for . +/// +public static class DaprEndpointConventionBuilderExtensions { - using Dapr; - using Dapr.AspNetCore; - using System; - using System.Collections.Generic; + /// + /// Adds metadata to the provided . + /// + /// The .\ + /// The name of the pubsub component to use. + /// The topic name. + /// The type. + /// The builder object. + public static T WithTopic(this T builder, string pubsubName, string name) + where T : IEndpointConventionBuilder + { + return WithTopic(builder, pubsubName, name, false); + } /// - /// Contains extension methods for . + /// Adds metadata to the provided . /// - public static class DaprEndpointConventionBuilderExtensions + /// The .\ + /// The name of the pubsub component to use. + /// The topic name. + /// + /// A collection of metadata key-value pairs that will be provided to the pubsub. The valid metadata keys and values + /// are determined by the type of pubsub component used. + /// + /// The type. + /// The builder object. + public static T WithTopic(this T builder, string pubsubName, string name, IDictionary metadata) + where T : IEndpointConventionBuilder { - /// - /// Adds metadata to the provided . - /// - /// The .\ - /// The name of the pubsub component to use. - /// The topic name. - /// The type. - /// The builder object. - public static T WithTopic(this T builder, string pubsubName, string name) - where T : IEndpointConventionBuilder - { - return WithTopic(builder, pubsubName, name, false); - } - - /// - /// Adds metadata to the provided . - /// - /// The .\ - /// The name of the pubsub component to use. - /// The topic name. - /// - /// A collection of metadata key-value pairs that will be provided to the pubsub. The valid metadata keys and values - /// are determined by the type of pubsub component used. - /// - /// The type. - /// The builder object. - public static T WithTopic(this T builder, string pubsubName, string name, IDictionary metadata) - where T : IEndpointConventionBuilder - { - return WithTopic(builder, pubsubName, name, false, metadata); - } - - /// - /// Adds metadata to the provided . - /// - /// The .\ - /// The name of the pubsub component to use. - /// The topic name. - /// The enable/disable raw pay load flag. - /// The type. - /// The builder object. - public static T WithTopic(this T builder, string pubsubName, string name, bool enableRawPayload) - where T : IEndpointConventionBuilder - { - return WithTopic(builder, pubsubName, name, enableRawPayload, null); - } - - /// - /// Adds metadata to the provided . - /// - /// The .\ - /// The name of the pubsub component to use. - /// The topic name. - /// The enable/disable raw pay load flag. - /// - /// A collection of metadata key-value pairs that will be provided to the pubsub. The valid metadata keys and values - /// are determined by the type of pubsub component used. - /// - /// The type. - /// The builder object. - public static T WithTopic(this T builder, string pubsubName, string name, bool enableRawPayload, IDictionary metadata) - where T : IEndpointConventionBuilder - { - if (builder is null) - { - throw new ArgumentNullException(nameof(builder)); - } - - ArgumentVerifier.ThrowIfNullOrEmpty(pubsubName, nameof(pubsubName)); - ArgumentVerifier.ThrowIfNullOrEmpty(name, nameof(name)); - - builder.WithMetadata(new TopicAttribute(pubsubName, name, enableRawPayload)); - if (metadata is not null) - { - foreach (var md in metadata) - { - builder.WithMetadata(new TopicMetadataAttribute(md.Key, md.Value)); - } - } - return builder; - } - - /// - /// Adds metadata to the provided . - /// - /// The .\ - /// The object of TopicOptions class that provides all topic attributes. - /// The type. - /// The builder object. - public static T WithTopic(this T builder, TopicOptions topicOptions) - where T : IEndpointConventionBuilder - { - if (builder is null) - { - throw new ArgumentNullException(nameof(builder)); - } - - ArgumentVerifier.ThrowIfNullOrEmpty(topicOptions.PubsubName, nameof(topicOptions.PubsubName)); - ArgumentVerifier.ThrowIfNullOrEmpty(topicOptions.Name, nameof(topicOptions.Name)); - - var topicObject = new TopicAttribute(topicOptions.PubsubName, topicOptions.Name, topicOptions.DeadLetterTopic, topicOptions.EnableRawPayload); - - topicObject.Match = topicOptions.Match; - topicObject.Priority = topicOptions.Priority; - topicObject.OwnedMetadatas = topicOptions.OwnedMetadatas; - topicObject.MetadataSeparator = topicObject.MetadataSeparator; - - if (topicOptions.Metadata is not null) - { - foreach (var md in topicOptions.Metadata) - { - builder.WithMetadata(new TopicMetadataAttribute(md.Key, md.Value)); - } - } - - builder.WithMetadata(topicObject); - - return builder; - } - - /// - /// Adds metadata to the provided . - /// - /// The .\ - /// The object of BulkSubscribeTopicOptions class that provides - /// all bulk subscribe topic attributes. - /// The type. - /// The builder object. - public static T WithBulkSubscribe(this T builder, BulkSubscribeTopicOptions bulkSubscribeTopicOptions) - where T : IEndpointConventionBuilder - { - if (builder is null) - { - throw new ArgumentNullException(nameof(builder)); - } - - ArgumentVerifier.ThrowIfNullOrEmpty(bulkSubscribeTopicOptions.TopicName, - nameof(bulkSubscribeTopicOptions.TopicName)); - - builder.WithMetadata(new BulkSubscribeAttribute(bulkSubscribeTopicOptions.TopicName, - bulkSubscribeTopicOptions.MaxMessagesCount, bulkSubscribeTopicOptions.MaxAwaitDurationMs)); - - return builder; - } + return WithTopic(builder, pubsubName, name, false, metadata); } -} + + /// + /// Adds metadata to the provided . + /// + /// The .\ + /// The name of the pubsub component to use. + /// The topic name. + /// The enable/disable raw pay load flag. + /// The type. + /// The builder object. + public static T WithTopic(this T builder, string pubsubName, string name, bool enableRawPayload) + where T : IEndpointConventionBuilder + { + return WithTopic(builder, pubsubName, name, enableRawPayload, null); + } + + /// + /// Adds metadata to the provided . + /// + /// The .\ + /// The name of the pubsub component to use. + /// The topic name. + /// The enable/disable raw pay load flag. + /// + /// A collection of metadata key-value pairs that will be provided to the pubsub. The valid metadata keys and values + /// are determined by the type of pubsub component used. + /// + /// The type. + /// The builder object. + public static T WithTopic(this T builder, string pubsubName, string name, bool enableRawPayload, IDictionary metadata) + where T : IEndpointConventionBuilder + { + if (builder is null) + { + throw new ArgumentNullException(nameof(builder)); + } + + ArgumentVerifier.ThrowIfNullOrEmpty(pubsubName, nameof(pubsubName)); + ArgumentVerifier.ThrowIfNullOrEmpty(name, nameof(name)); + + builder.WithMetadata(new TopicAttribute(pubsubName, name, enableRawPayload)); + if (metadata is not null) + { + foreach (var md in metadata) + { + builder.WithMetadata(new TopicMetadataAttribute(md.Key, md.Value)); + } + } + return builder; + } + + /// + /// Adds metadata to the provided . + /// + /// The .\ + /// The object of TopicOptions class that provides all topic attributes. + /// The type. + /// The builder object. + public static T WithTopic(this T builder, TopicOptions topicOptions) + where T : IEndpointConventionBuilder + { + if (builder is null) + { + throw new ArgumentNullException(nameof(builder)); + } + + ArgumentVerifier.ThrowIfNullOrEmpty(topicOptions.PubsubName, nameof(topicOptions.PubsubName)); + ArgumentVerifier.ThrowIfNullOrEmpty(topicOptions.Name, nameof(topicOptions.Name)); + + var topicObject = new TopicAttribute(topicOptions.PubsubName, topicOptions.Name, topicOptions.DeadLetterTopic, topicOptions.EnableRawPayload); + + topicObject.Match = topicOptions.Match; + topicObject.Priority = topicOptions.Priority; + topicObject.OwnedMetadatas = topicOptions.OwnedMetadatas; + topicObject.MetadataSeparator = topicObject.MetadataSeparator; + + if (topicOptions.Metadata is not null) + { + foreach (var md in topicOptions.Metadata) + { + builder.WithMetadata(new TopicMetadataAttribute(md.Key, md.Value)); + } + } + + builder.WithMetadata(topicObject); + + return builder; + } + + /// + /// Adds metadata to the provided . + /// + /// The .\ + /// The object of BulkSubscribeTopicOptions class that provides + /// all bulk subscribe topic attributes. + /// The type. + /// The builder object. + public static T WithBulkSubscribe(this T builder, BulkSubscribeTopicOptions bulkSubscribeTopicOptions) + where T : IEndpointConventionBuilder + { + if (builder is null) + { + throw new ArgumentNullException(nameof(builder)); + } + + ArgumentVerifier.ThrowIfNullOrEmpty(bulkSubscribeTopicOptions.TopicName, + nameof(bulkSubscribeTopicOptions.TopicName)); + + builder.WithMetadata(new BulkSubscribeAttribute(bulkSubscribeTopicOptions.TopicName, + bulkSubscribeTopicOptions.MaxMessagesCount, bulkSubscribeTopicOptions.MaxAwaitDurationMs)); + + return builder; + } +} \ No newline at end of file diff --git a/src/Dapr.AspNetCore/DaprEndpointRouteBuilderExtensions.cs b/src/Dapr.AspNetCore/DaprEndpointRouteBuilderExtensions.cs index 78c45357..9749a87d 100644 --- a/src/Dapr.AspNetCore/DaprEndpointRouteBuilderExtensions.cs +++ b/src/Dapr.AspNetCore/DaprEndpointRouteBuilderExtensions.cs @@ -11,195 +11,194 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.Builder; + +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; +using Dapr; +using Dapr.AspNetCore; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Patterns; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +/// +/// Contains extension methods for . +/// +public static class DaprEndpointRouteBuilderExtensions { - using System.Collections.Generic; - using System.Linq; - using System.Text.Json; - using System.Text.Json.Serialization; - using Dapr; - using Dapr.AspNetCore; - using Microsoft.AspNetCore.Http; - using Microsoft.AspNetCore.Routing; - using Microsoft.AspNetCore.Routing.Patterns; - using Microsoft.Extensions.DependencyInjection; - using Microsoft.Extensions.Logging; + /// + /// Maps an endpoint that will respond to requests to /dapr/subscribe from the + /// Dapr runtime. + /// + /// The . + /// The . + public static IEndpointConventionBuilder MapSubscribeHandler(this IEndpointRouteBuilder endpoints) + { + return CreateSubscribeEndPoint(endpoints); + } /// - /// Contains extension methods for . + /// Maps an endpoint that will respond to requests to /dapr/subscribe from the + /// Dapr runtime. /// - public static class DaprEndpointRouteBuilderExtensions + /// The . + /// Configuration options + /// The . + /// + public static IEndpointConventionBuilder MapSubscribeHandler(this IEndpointRouteBuilder endpoints, SubscribeOptions options) { - /// - /// Maps an endpoint that will respond to requests to /dapr/subscribe from the - /// Dapr runtime. - /// - /// The . - /// The . - public static IEndpointConventionBuilder MapSubscribeHandler(this IEndpointRouteBuilder endpoints) - { - return CreateSubscribeEndPoint(endpoints); - } - - /// - /// Maps an endpoint that will respond to requests to /dapr/subscribe from the - /// Dapr runtime. - /// - /// The . - /// Configuration options - /// The . - /// - public static IEndpointConventionBuilder MapSubscribeHandler(this IEndpointRouteBuilder endpoints, SubscribeOptions options) - { - return CreateSubscribeEndPoint(endpoints, options); - } - - private static IEndpointConventionBuilder CreateSubscribeEndPoint(IEndpointRouteBuilder endpoints, SubscribeOptions options = null) - { - if (endpoints is null) - { - throw new System.ArgumentNullException(nameof(endpoints)); - } - - return endpoints.MapGet("dapr/subscribe", async context => - { - var logger = context.RequestServices.GetService().CreateLogger("DaprTopicSubscription"); - var dataSource = context.RequestServices.GetRequiredService(); - var subscriptions = dataSource.Endpoints - .OfType() - .Where(e => e.Metadata.GetOrderedMetadata().Any(t => t.Name != null)) // only endpoints which have TopicAttribute with not null Name. - .SelectMany(e => - { - var topicMetadata = e.Metadata.GetOrderedMetadata(); - var originalTopicMetadata = e.Metadata.GetOrderedMetadata(); - var bulkSubscribeMetadata = e.Metadata.GetOrderedMetadata(); - - var subs = new List<(string PubsubName, string Name, string DeadLetterTopic, bool? EnableRawPayload, - string Match, int Priority, Dictionary OriginalTopicMetadata, - string MetadataSeparator, RoutePattern RoutePattern, DaprTopicBulkSubscribe bulkSubscribe)>(); - - for (int i = 0; i < topicMetadata.Count(); i++) - { - DaprTopicBulkSubscribe bulkSubscribe = null; - - foreach (var bulkSubscribeAttr in bulkSubscribeMetadata) - { - if (bulkSubscribeAttr.TopicName != topicMetadata[i].Name) - { - continue; - } - - bulkSubscribe = new DaprTopicBulkSubscribe - { - Enabled = true, - MaxMessagesCount = bulkSubscribeAttr.MaxMessagesCount, - MaxAwaitDurationMs = bulkSubscribeAttr.MaxAwaitDurationMs - }; - break; - } - - subs.Add((topicMetadata[i].PubsubName, - topicMetadata[i].Name, - (topicMetadata[i] as IDeadLetterTopicMetadata)?.DeadLetterTopic, - (topicMetadata[i] as IRawTopicMetadata)?.EnableRawPayload, - topicMetadata[i].Match, - topicMetadata[i].Priority, - originalTopicMetadata.Where(m => (topicMetadata[i] as IOwnedOriginalTopicMetadata)?.OwnedMetadatas?.Any(o => o.Equals(m.Id)) == true || string.IsNullOrEmpty(m.Id)) - .GroupBy(c => c.Name) - .ToDictionary(m => m.Key, m => m.Select(c => c.Value).Distinct().ToArray()), - (topicMetadata[i] as IOwnedOriginalTopicMetadata)?.MetadataSeparator, - e.RoutePattern, - bulkSubscribe)); - } - - return subs; - }) - .Distinct() - .GroupBy(e => new { e.PubsubName, e.Name }) - .Select(e => e.OrderBy(e => e.Priority)) - .Select(e => - { - var first = e.First(); - var rawPayload = e.Any(e => e.EnableRawPayload.GetValueOrDefault()); - var metadataSeparator = e.FirstOrDefault(e => !string.IsNullOrEmpty(e.MetadataSeparator)).MetadataSeparator ?? ","; - var rules = e.Where(e => !string.IsNullOrEmpty(e.Match)).ToList(); - var defaultRoutes = e.Where(e => string.IsNullOrEmpty(e.Match)).Select(e => RoutePatternToString(e.RoutePattern)).ToList(); - var defaultRoute = defaultRoutes.FirstOrDefault(); - - //multiple identical names. use comma separation. - var metadata = new Metadata(e.SelectMany(c => c.OriginalTopicMetadata).GroupBy(c => c.Key).ToDictionary(c => c.Key, c => string.Join(metadataSeparator, c.SelectMany(c => c.Value).Distinct()))); - if (rawPayload || options?.EnableRawPayload is true) - { - metadata.Add(Metadata.RawPayload, "true"); - } - - if (logger != null) - { - if (defaultRoutes.Count > 1) - { - logger.LogError("A default subscription to topic {name} on pubsub {pubsub} already exists.", first.Name, first.PubsubName); - } - - var duplicatePriorities = rules.GroupBy(e => e.Priority) - .Where(g => g.Count() > 1) - .ToDictionary(x => x.Key, y => y.Count()); - - foreach (var entry in duplicatePriorities) - { - logger.LogError("A subscription to topic {name} on pubsub {pubsub} has duplicate priorities for {priority}: found {count} occurrences.", first.Name, first.PubsubName, entry.Key, entry.Value); - } - } - - var subscription = new Subscription - { - Topic = first.Name, - PubsubName = first.PubsubName, - Metadata = metadata.Count > 0 ? metadata : null, - BulkSubscribe = first.bulkSubscribe - }; - - if (first.DeadLetterTopic != null) - { - subscription.DeadLetterTopic = first.DeadLetterTopic; - } - - // Use the V2 routing rules structure - if (rules.Count > 0) - { - subscription.Routes = new Routes - { - Rules = rules.Select(e => new Rule - { - Match = e.Match, - Path = RoutePatternToString(e.RoutePattern), - }).ToList(), - Default = defaultRoute, - }; - } - // Use the V1 structure for backward compatibility. - else - { - subscription.Route = defaultRoute; - } - - return subscription; - }) - .OrderBy(e => (e.PubsubName, e.Topic)); - - await context.Response.WriteAsync(JsonSerializer.Serialize(subscriptions, - new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull - })); - }); - } - - private static string RoutePatternToString(RoutePattern routePattern) - { - return string.Join("/", routePattern.PathSegments - .Select(segment => string.Concat(segment.Parts.Cast() - .Select(part => part.Content)))); - } + return CreateSubscribeEndPoint(endpoints, options); } -} + + private static IEndpointConventionBuilder CreateSubscribeEndPoint(IEndpointRouteBuilder endpoints, SubscribeOptions options = null) + { + if (endpoints is null) + { + throw new System.ArgumentNullException(nameof(endpoints)); + } + + return endpoints.MapGet("dapr/subscribe", async context => + { + var logger = context.RequestServices.GetService().CreateLogger("DaprTopicSubscription"); + var dataSource = context.RequestServices.GetRequiredService(); + var subscriptions = dataSource.Endpoints + .OfType() + .Where(e => e.Metadata.GetOrderedMetadata().Any(t => t.Name != null)) // only endpoints which have TopicAttribute with not null Name. + .SelectMany(e => + { + var topicMetadata = e.Metadata.GetOrderedMetadata(); + var originalTopicMetadata = e.Metadata.GetOrderedMetadata(); + var bulkSubscribeMetadata = e.Metadata.GetOrderedMetadata(); + + var subs = new List<(string PubsubName, string Name, string DeadLetterTopic, bool? EnableRawPayload, + string Match, int Priority, Dictionary OriginalTopicMetadata, + string MetadataSeparator, RoutePattern RoutePattern, DaprTopicBulkSubscribe bulkSubscribe)>(); + + for (int i = 0; i < topicMetadata.Count(); i++) + { + DaprTopicBulkSubscribe bulkSubscribe = null; + + foreach (var bulkSubscribeAttr in bulkSubscribeMetadata) + { + if (bulkSubscribeAttr.TopicName != topicMetadata[i].Name) + { + continue; + } + + bulkSubscribe = new DaprTopicBulkSubscribe + { + Enabled = true, + MaxMessagesCount = bulkSubscribeAttr.MaxMessagesCount, + MaxAwaitDurationMs = bulkSubscribeAttr.MaxAwaitDurationMs + }; + break; + } + + subs.Add((topicMetadata[i].PubsubName, + topicMetadata[i].Name, + (topicMetadata[i] as IDeadLetterTopicMetadata)?.DeadLetterTopic, + (topicMetadata[i] as IRawTopicMetadata)?.EnableRawPayload, + topicMetadata[i].Match, + topicMetadata[i].Priority, + originalTopicMetadata.Where(m => (topicMetadata[i] as IOwnedOriginalTopicMetadata)?.OwnedMetadatas?.Any(o => o.Equals(m.Id)) == true || string.IsNullOrEmpty(m.Id)) + .GroupBy(c => c.Name) + .ToDictionary(m => m.Key, m => m.Select(c => c.Value).Distinct().ToArray()), + (topicMetadata[i] as IOwnedOriginalTopicMetadata)?.MetadataSeparator, + e.RoutePattern, + bulkSubscribe)); + } + + return subs; + }) + .Distinct() + .GroupBy(e => new { e.PubsubName, e.Name }) + .Select(e => e.OrderBy(e => e.Priority)) + .Select(e => + { + var first = e.First(); + var rawPayload = e.Any(e => e.EnableRawPayload.GetValueOrDefault()); + var metadataSeparator = e.FirstOrDefault(e => !string.IsNullOrEmpty(e.MetadataSeparator)).MetadataSeparator ?? ","; + var rules = e.Where(e => !string.IsNullOrEmpty(e.Match)).ToList(); + var defaultRoutes = e.Where(e => string.IsNullOrEmpty(e.Match)).Select(e => RoutePatternToString(e.RoutePattern)).ToList(); + var defaultRoute = defaultRoutes.FirstOrDefault(); + + //multiple identical names. use comma separation. + var metadata = new Metadata(e.SelectMany(c => c.OriginalTopicMetadata).GroupBy(c => c.Key).ToDictionary(c => c.Key, c => string.Join(metadataSeparator, c.SelectMany(c => c.Value).Distinct()))); + if (rawPayload || options?.EnableRawPayload is true) + { + metadata.Add(Metadata.RawPayload, "true"); + } + + if (logger != null) + { + if (defaultRoutes.Count > 1) + { + logger.LogError("A default subscription to topic {name} on pubsub {pubsub} already exists.", first.Name, first.PubsubName); + } + + var duplicatePriorities = rules.GroupBy(e => e.Priority) + .Where(g => g.Count() > 1) + .ToDictionary(x => x.Key, y => y.Count()); + + foreach (var entry in duplicatePriorities) + { + logger.LogError("A subscription to topic {name} on pubsub {pubsub} has duplicate priorities for {priority}: found {count} occurrences.", first.Name, first.PubsubName, entry.Key, entry.Value); + } + } + + var subscription = new Subscription + { + Topic = first.Name, + PubsubName = first.PubsubName, + Metadata = metadata.Count > 0 ? metadata : null, + BulkSubscribe = first.bulkSubscribe + }; + + if (first.DeadLetterTopic != null) + { + subscription.DeadLetterTopic = first.DeadLetterTopic; + } + + // Use the V2 routing rules structure + if (rules.Count > 0) + { + subscription.Routes = new Routes + { + Rules = rules.Select(e => new Rule + { + Match = e.Match, + Path = RoutePatternToString(e.RoutePattern), + }).ToList(), + Default = defaultRoute, + }; + } + // Use the V1 structure for backward compatibility. + else + { + subscription.Route = defaultRoute; + } + + return subscription; + }) + .OrderBy(e => (e.PubsubName, e.Topic)); + + await context.Response.WriteAsync(JsonSerializer.Serialize(subscriptions, + new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + })); + }); + } + + private static string RoutePatternToString(RoutePattern routePattern) + { + return string.Join("/", routePattern.PathSegments + .Select(segment => string.Concat(segment.Parts.Cast() + .Select(part => part.Content)))); + } +} \ No newline at end of file diff --git a/src/Dapr.AspNetCore/DaprMvcBuilderExtensions.cs b/src/Dapr.AspNetCore/DaprMvcBuilderExtensions.cs index 6209fea5..1309c78d 100644 --- a/src/Dapr.AspNetCore/DaprMvcBuilderExtensions.cs +++ b/src/Dapr.AspNetCore/DaprMvcBuilderExtensions.cs @@ -11,47 +11,46 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Microsoft.Extensions.DependencyInjection +namespace Microsoft.Extensions.DependencyInjection; + +using System; +using System.Linq; +using Dapr.AspNetCore; +using Dapr.Client; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.Extensions.DependencyInjection.Extensions; + +/// +/// Provides extension methods for . +/// +public static class DaprMvcBuilderExtensions { - using System; - using System.Linq; - using Dapr.AspNetCore; - using Dapr.Client; - using Microsoft.AspNetCore.Mvc; - using Microsoft.AspNetCore.Mvc.ApplicationModels; - using Microsoft.Extensions.DependencyInjection.Extensions; - /// - /// Provides extension methods for . + /// Adds Dapr integration for MVC to the provided . /// - public static class DaprMvcBuilderExtensions + /// The . + /// The (optional) to use for configuring the DaprClient. + /// The builder. + public static IMvcBuilder AddDapr(this IMvcBuilder builder, Action configureClient = null) { - /// - /// Adds Dapr integration for MVC to the provided . - /// - /// The . - /// The (optional) to use for configuring the DaprClient. - /// The builder. - public static IMvcBuilder AddDapr(this IMvcBuilder builder, Action configureClient = null) + if (builder is null) { - if (builder is null) - { - throw new ArgumentNullException(nameof(builder)); - } - - builder.Services.AddDaprClient(configureClient); - - builder.Services.TryAddSingleton(); - - builder.Services.Configure(options => - { - if (!options.ModelBinderProviders.Any(p => p is StateEntryModelBinderProvider)) - { - options.ModelBinderProviders.Insert(0, new StateEntryModelBinderProvider()); - } - }); - - return builder; + throw new ArgumentNullException(nameof(builder)); } + + builder.Services.AddDaprClient(configureClient); + + builder.Services.TryAddSingleton(); + + builder.Services.Configure(options => + { + if (!options.ModelBinderProviders.Any(p => p is StateEntryModelBinderProvider)) + { + options.ModelBinderProviders.Insert(0, new StateEntryModelBinderProvider()); + } + }); + + return builder; } -} +} \ No newline at end of file diff --git a/src/Dapr.AspNetCore/FromStateAttribute.cs b/src/Dapr.AspNetCore/FromStateAttribute.cs index 16299ba0..cbec456e 100644 --- a/src/Dapr.AspNetCore/FromStateAttribute.cs +++ b/src/Dapr.AspNetCore/FromStateAttribute.cs @@ -11,62 +11,61 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Microsoft.AspNetCore.Mvc +namespace Microsoft.AspNetCore.Mvc; + +using System; +using Dapr; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +/// +/// Attributes a parameter or property as retrieved from the Dapr state store. +/// +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] +public class FromStateAttribute : Attribute, IBindingSourceMetadata { - using System; - using Dapr; - using Microsoft.AspNetCore.Mvc.ModelBinding; + /// + /// Initializes a new instance of the class. + /// + /// The state store name. + public FromStateAttribute(string storeName) + { + if (string.IsNullOrEmpty(storeName)) + { + throw new ArgumentException("The value cannot be null or empty.", nameof(storeName)); + } + + this.StoreName = storeName; + } /// - /// Attributes a parameter or property as retrieved from the Dapr state store. + /// Initializes a new instance of the class. /// - [AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] - public class FromStateAttribute : Attribute, IBindingSourceMetadata + /// The state store name. + /// The state key. + public FromStateAttribute(string storeName, string key) { - /// - /// Initializes a new instance of the class. - /// - /// The state store name. - public FromStateAttribute(string storeName) + this.StoreName = storeName; + this.Key = key; + } + + /// + /// Gets the state store name. + /// + public string StoreName { get; } + + /// + /// Gets the state store key. + /// + public string Key { get; } + + /// + /// Gets the . + /// + public BindingSource BindingSource + { + get { - if (string.IsNullOrEmpty(storeName)) - { - throw new ArgumentException("The value cannot be null or empty.", nameof(storeName)); - } - - this.StoreName = storeName; - } - - /// - /// Initializes a new instance of the class. - /// - /// The state store name. - /// The state key. - public FromStateAttribute(string storeName, string key) - { - this.StoreName = storeName; - this.Key = key; - } - - /// - /// Gets the state store name. - /// - public string StoreName { get; } - - /// - /// Gets the state store key. - /// - public string Key { get; } - - /// - /// Gets the . - /// - public BindingSource BindingSource - { - get - { - return new FromStateBindingSource(this.StoreName, this.Key); - } + return new FromStateBindingSource(this.StoreName, this.Key); } } } \ No newline at end of file diff --git a/src/Dapr.AspNetCore/FromStateBindingSource.cs b/src/Dapr.AspNetCore/FromStateBindingSource.cs index 16837de7..997e53bc 100644 --- a/src/Dapr.AspNetCore/FromStateBindingSource.cs +++ b/src/Dapr.AspNetCore/FromStateBindingSource.cs @@ -11,21 +11,20 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr +namespace Dapr; + +using Microsoft.AspNetCore.Mvc.ModelBinding; + +internal class FromStateBindingSource : BindingSource { - using Microsoft.AspNetCore.Mvc.ModelBinding; - - internal class FromStateBindingSource : BindingSource + public FromStateBindingSource(string storeName, string key) + : base("state", "Dapr state store", isGreedy: true, isFromRequest: false) { - public FromStateBindingSource(string storeName, string key) - : base("state", "Dapr state store", isGreedy: true, isFromRequest: false) - { - this.StoreName = storeName; - this.Key = key; - } - - public string StoreName { get; } - - public string Key { get; } + this.StoreName = storeName; + this.Key = key; } + + public string StoreName { get; } + + public string Key { get; } } \ No newline at end of file diff --git a/src/Dapr.AspNetCore/IBulkSubscribeMetadata.cs b/src/Dapr.AspNetCore/IBulkSubscribeMetadata.cs index 1e4fd006..68c2b04d 100644 --- a/src/Dapr.AspNetCore/IBulkSubscribeMetadata.cs +++ b/src/Dapr.AspNetCore/IBulkSubscribeMetadata.cs @@ -1,24 +1,23 @@ -namespace Dapr.AspNetCore +namespace Dapr.AspNetCore; + +/// +/// Bulk Subscribe Metadata that describes bulk subscribe configuration options. +/// +public interface IBulkSubscribeMetadata { /// - /// Bulk Subscribe Metadata that describes bulk subscribe configuration options. + /// Gets the maximum number of messages in a bulk message from the message bus. /// - public interface IBulkSubscribeMetadata - { - /// - /// Gets the maximum number of messages in a bulk message from the message bus. - /// - int MaxMessagesCount { get; } + int MaxMessagesCount { get; } - /// - /// Gets the Maximum duration to wait for maxBulkSubCount messages by the message bus - /// before sending the messages to Dapr. - /// - int MaxAwaitDurationMs { get; } + /// + /// Gets the Maximum duration to wait for maxBulkSubCount messages by the message bus + /// before sending the messages to Dapr. + /// + int MaxAwaitDurationMs { get; } - /// - /// The name of the topic to be bulk subscribed. - /// - public string TopicName { get; } - } -} + /// + /// The name of the topic to be bulk subscribed. + /// + public string TopicName { get; } +} \ No newline at end of file diff --git a/src/Dapr.AspNetCore/IDeadLetterTopicMetadata.cs b/src/Dapr.AspNetCore/IDeadLetterTopicMetadata.cs index 97707b98..85227e4f 100644 --- a/src/Dapr.AspNetCore/IDeadLetterTopicMetadata.cs +++ b/src/Dapr.AspNetCore/IDeadLetterTopicMetadata.cs @@ -11,17 +11,15 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr +namespace Dapr; + +/// +/// IDeadLetterTopicMetadata that describes the metadata of a dead letter topic. +/// +public interface IDeadLetterTopicMetadata { /// - /// IDeadLetterTopicMetadata that describes the metadata of a dead letter topic. + /// Gets the dead letter topic name /// - public interface IDeadLetterTopicMetadata - { - /// - /// Gets the dead letter topic name - /// - public string DeadLetterTopic { get; } - } -} - + public string DeadLetterTopic { get; } +} \ No newline at end of file diff --git a/src/Dapr.AspNetCore/IOriginalTopicMetadata.cs b/src/Dapr.AspNetCore/IOriginalTopicMetadata.cs index 71aa952c..4d7df6a2 100644 --- a/src/Dapr.AspNetCore/IOriginalTopicMetadata.cs +++ b/src/Dapr.AspNetCore/IOriginalTopicMetadata.cs @@ -11,30 +11,29 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr +namespace Dapr; + +/// +/// IOriginalTopicMetadata that describes subscribe endpoint to a topic original metadata. +/// +public interface IOriginalTopicMetadata { /// - /// IOriginalTopicMetadata that describes subscribe endpoint to a topic original metadata. + /// Gets the topic metadata id. /// - public interface IOriginalTopicMetadata - { - /// - /// Gets the topic metadata id. - /// - /// - /// It is only used for simple identification,. When it is empty, it can be used for all topics in the current context. - /// - string Id { get; } + /// + /// It is only used for simple identification,. When it is empty, it can be used for all topics in the current context. + /// + string Id { get; } - /// - /// Gets the topic metadata name. - /// - /// Multiple identical names. only the first is valid. - string Name { get; } + /// + /// Gets the topic metadata name. + /// + /// Multiple identical names. only the first is valid. + string Name { get; } - /// - /// Gets the topic metadata value. - /// - string Value { get; } - } -} + /// + /// Gets the topic metadata value. + /// + string Value { get; } +} \ No newline at end of file diff --git a/src/Dapr.AspNetCore/IOwnedOriginalTopicMetadata.cs b/src/Dapr.AspNetCore/IOwnedOriginalTopicMetadata.cs index 36ae45e5..378e0f1c 100644 --- a/src/Dapr.AspNetCore/IOwnedOriginalTopicMetadata.cs +++ b/src/Dapr.AspNetCore/IOwnedOriginalTopicMetadata.cs @@ -11,23 +11,22 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr +namespace Dapr; + +/// +/// IOwnedOriginalTopicMetadata that describes subscribe endpoint to topic owned metadata. +/// +public interface IOwnedOriginalTopicMetadata { /// - /// IOwnedOriginalTopicMetadata that describes subscribe endpoint to topic owned metadata. + /// Gets the owned by topic. /// - public interface IOwnedOriginalTopicMetadata - { - /// - /// Gets the owned by topic. - /// - /// When the is not empty, the metadata owned by topic. - string[] OwnedMetadatas { get; } + /// When the is not empty, the metadata owned by topic. + string[] OwnedMetadatas { get; } - /// - /// Get separator to use for metadata - /// - /// Separator to be used for when multiple values exist for a . - string MetadataSeparator { get; } - } -} + /// + /// Get separator to use for metadata + /// + /// Separator to be used for when multiple values exist for a . + string MetadataSeparator { get; } +} \ No newline at end of file diff --git a/src/Dapr.AspNetCore/IRawTopicMetadata.cs b/src/Dapr.AspNetCore/IRawTopicMetadata.cs index 09b682f5..88a6f589 100644 --- a/src/Dapr.AspNetCore/IRawTopicMetadata.cs +++ b/src/Dapr.AspNetCore/IRawTopicMetadata.cs @@ -11,16 +11,15 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr +namespace Dapr; + +/// +/// RawMetadata that describes subscribe endpoint to enable or disable processing raw messages. +/// +public interface IRawTopicMetadata { /// - /// RawMetadata that describes subscribe endpoint to enable or disable processing raw messages. + /// Gets the enable or disable value for processing raw messages. /// - public interface IRawTopicMetadata - { - /// - /// Gets the enable or disable value for processing raw messages. - /// - bool? EnableRawPayload { get; } - } -} + bool? EnableRawPayload { get; } +} \ No newline at end of file diff --git a/src/Dapr.AspNetCore/ITopicMetadata.cs b/src/Dapr.AspNetCore/ITopicMetadata.cs index eb373213..81e0459c 100644 --- a/src/Dapr.AspNetCore/ITopicMetadata.cs +++ b/src/Dapr.AspNetCore/ITopicMetadata.cs @@ -11,31 +11,30 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr +namespace Dapr; + +/// +/// ITopicMetadata that describes an endpoint as a subscriber to a topic. +/// +public interface ITopicMetadata { /// - /// ITopicMetadata that describes an endpoint as a subscriber to a topic. + /// Gets the topic name. /// - public interface ITopicMetadata - { - /// - /// Gets the topic name. - /// - string Name { get; } + string Name { get; } - /// - /// Gets the pubsub component name name. - /// - string PubsubName { get; } + /// + /// Gets the pubsub component name name. + /// + string PubsubName { get; } - /// - /// The CEL expression to use to match events for this handler. - /// - string Match { get; } + /// + /// The CEL expression to use to match events for this handler. + /// + string Match { get; } - /// - /// The priority in which this rule should be evaluated (lower to higher). - /// - int Priority { get; } - } -} + /// + /// The priority in which this rule should be evaluated (lower to higher). + /// + int Priority { get; } +} \ No newline at end of file diff --git a/src/Dapr.AspNetCore/StateEntryApplicationModelProvider.cs b/src/Dapr.AspNetCore/StateEntryApplicationModelProvider.cs index 4760786f..53df0400 100644 --- a/src/Dapr.AspNetCore/StateEntryApplicationModelProvider.cs +++ b/src/Dapr.AspNetCore/StateEntryApplicationModelProvider.cs @@ -11,65 +11,64 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.AspNetCore +namespace Dapr.AspNetCore; + +using System; +using Dapr.AspNetCore.Resources; +using Microsoft.AspNetCore.Mvc.ApplicationModels; + +internal class StateEntryApplicationModelProvider : IApplicationModelProvider { - using System; - using Dapr.AspNetCore.Resources; - using Microsoft.AspNetCore.Mvc.ApplicationModels; + public int Order => 0; - internal class StateEntryApplicationModelProvider : IApplicationModelProvider + public void OnProvidersExecuting(ApplicationModelProviderContext context) { - public int Order => 0; + } - public void OnProvidersExecuting(ApplicationModelProviderContext context) + public void OnProvidersExecuted(ApplicationModelProviderContext context) + { + // Run after default providers, and customize the binding source for StateEntry<>. + foreach (var controller in context.Result.Controllers) { - } - - public void OnProvidersExecuted(ApplicationModelProviderContext context) - { - // Run after default providers, and customize the binding source for StateEntry<>. - foreach (var controller in context.Result.Controllers) + foreach (var property in controller.ControllerProperties) { - foreach (var property in controller.ControllerProperties) + if (property.BindingInfo == null) { - if (property.BindingInfo == null) + // Not bindable. + } + else if (property.BindingInfo.BindingSource?.Id == "state") + { + // Already configured, don't overwrite in case the user customized it. + } + else if (IsStateEntryType(property.ParameterType)) + { + throw new InvalidOperationException(SR.ErrorStateStoreNameNotProvidedForStateEntry); + } + } + + foreach (var action in controller.Actions) + { + foreach (var parameter in action.Parameters) + { + if (parameter.BindingInfo == null) { // Not bindable. } - else if (property.BindingInfo.BindingSource?.Id == "state") + else if (parameter.BindingInfo.BindingSource?.Id == "state") { // Already configured, don't overwrite in case the user customized it. } - else if (IsStateEntryType(property.ParameterType)) + else if (IsStateEntryType(parameter.ParameterType)) { throw new InvalidOperationException(SR.ErrorStateStoreNameNotProvidedForStateEntry); } } - - foreach (var action in controller.Actions) - { - foreach (var parameter in action.Parameters) - { - if (parameter.BindingInfo == null) - { - // Not bindable. - } - else if (parameter.BindingInfo.BindingSource?.Id == "state") - { - // Already configured, don't overwrite in case the user customized it. - } - else if (IsStateEntryType(parameter.ParameterType)) - { - throw new InvalidOperationException(SR.ErrorStateStoreNameNotProvidedForStateEntry); - } - } - } } } - - private static bool IsStateEntryType(Type type) - { - return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(StateEntry<>); - } } -} + + private static bool IsStateEntryType(Type type) + { + return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(StateEntry<>); + } +} \ No newline at end of file diff --git a/src/Dapr.AspNetCore/StateEntryModelBinder.cs b/src/Dapr.AspNetCore/StateEntryModelBinder.cs index abc01d7f..34bc40d6 100644 --- a/src/Dapr.AspNetCore/StateEntryModelBinder.cs +++ b/src/Dapr.AspNetCore/StateEntryModelBinder.cs @@ -11,102 +11,101 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.AspNetCore +namespace Dapr.AspNetCore; + +using System; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; +using Microsoft.Extensions.DependencyInjection; +using Dapr.Client; + +internal class StateEntryModelBinder : IModelBinder { - using System; - using System.Reflection; - using System.Threading.Tasks; - using Microsoft.AspNetCore.Mvc.ModelBinding; - using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; - using Microsoft.Extensions.DependencyInjection; - using Dapr.Client; + private readonly Func> thunk; + private readonly string key; + private readonly string storeName; + private readonly bool isStateEntry; + private readonly Type type; - internal class StateEntryModelBinder : IModelBinder + public StateEntryModelBinder(string storeName, string key, bool isStateEntry, Type type) { - private readonly Func> thunk; - private readonly string key; - private readonly string storeName; - private readonly bool isStateEntry; - private readonly Type type; + this.storeName = storeName; + this.key = key; + this.isStateEntry = isStateEntry; + this.type = type; - public StateEntryModelBinder(string storeName, string key, bool isStateEntry, Type type) + if (isStateEntry) { - this.storeName = storeName; - this.key = key; - this.isStateEntry = isStateEntry; - this.type = type; - - if (isStateEntry) - { - var method = this.GetType().GetMethod(nameof(GetStateEntryAsync), BindingFlags.Static | BindingFlags.NonPublic); - method = method.MakeGenericMethod(type); - this.thunk = (Func>)Delegate.CreateDelegate(typeof(Func>), null, method); - } - else - { - var method = this.GetType().GetMethod(nameof(GetStateAsync), BindingFlags.Static | BindingFlags.NonPublic); - method = method.MakeGenericMethod(type); - this.thunk = (Func>)Delegate.CreateDelegate(typeof(Func>), null, method); - } + var method = this.GetType().GetMethod(nameof(GetStateEntryAsync), BindingFlags.Static | BindingFlags.NonPublic); + method = method.MakeGenericMethod(type); + this.thunk = (Func>)Delegate.CreateDelegate(typeof(Func>), null, method); } - - public async Task BindModelAsync(ModelBindingContext bindingContext) + else { - if (bindingContext is null) - { - throw new ArgumentNullException(nameof(bindingContext)); - } - - var daprClient = bindingContext.HttpContext.RequestServices.GetRequiredService(); - - // Look up route values to use for keys into state. - bool missingKey = false; - var keyName = this.key ?? bindingContext.FieldName; - var key = (string)bindingContext.HttpContext.Request.RouteValues[keyName]; - if (string.IsNullOrEmpty(key)) - { - missingKey = true; - } - - if (missingKey) - { - // If we get here this is a configuration error. The error is somewhat opaque on - // purpose to avoid leaking too much information about the app. - var message = $"Required value {keyName} not present."; - bindingContext.Result = ModelBindingResult.Failed(); - bindingContext.ModelState.TryAddModelError(bindingContext.ModelName, message); - return; - } - - var obj = await this.thunk(daprClient, this.storeName, key); - - // When the state isn't found in the state store: - // - If the StateEntryModelBinder is associated with a value of type StateEntry, then the above call returns an object of type - // StateEntry which is non-null, but StateEntry.Value is null - // - If the StateEntryModelBinder is associated with a value of type T, then the above call returns a null value. - if (obj == null) - { - bindingContext.Result = ModelBindingResult.Failed(); - } - else - { - bindingContext.Result = ModelBindingResult.Success(obj); - bindingContext.ValidationState.Add(bindingContext.Result.Model, new ValidationStateEntry() - { - // Don't do validation since the data came from a trusted source. - SuppressValidation = true, - }); - } - } - - private static async Task GetStateEntryAsync(DaprClient daprClient, string storeName, string key) - { - return await daprClient.GetStateEntryAsync(storeName, key); - } - - private static async Task GetStateAsync(DaprClient daprClient, string storeName, string key) - { - return await daprClient.GetStateAsync(storeName, key); + var method = this.GetType().GetMethod(nameof(GetStateAsync), BindingFlags.Static | BindingFlags.NonPublic); + method = method.MakeGenericMethod(type); + this.thunk = (Func>)Delegate.CreateDelegate(typeof(Func>), null, method); } } -} + + public async Task BindModelAsync(ModelBindingContext bindingContext) + { + if (bindingContext is null) + { + throw new ArgumentNullException(nameof(bindingContext)); + } + + var daprClient = bindingContext.HttpContext.RequestServices.GetRequiredService(); + + // Look up route values to use for keys into state. + bool missingKey = false; + var keyName = this.key ?? bindingContext.FieldName; + var key = (string)bindingContext.HttpContext.Request.RouteValues[keyName]; + if (string.IsNullOrEmpty(key)) + { + missingKey = true; + } + + if (missingKey) + { + // If we get here this is a configuration error. The error is somewhat opaque on + // purpose to avoid leaking too much information about the app. + var message = $"Required value {keyName} not present."; + bindingContext.Result = ModelBindingResult.Failed(); + bindingContext.ModelState.TryAddModelError(bindingContext.ModelName, message); + return; + } + + var obj = await this.thunk(daprClient, this.storeName, key); + + // When the state isn't found in the state store: + // - If the StateEntryModelBinder is associated with a value of type StateEntry, then the above call returns an object of type + // StateEntry which is non-null, but StateEntry.Value is null + // - If the StateEntryModelBinder is associated with a value of type T, then the above call returns a null value. + if (obj == null) + { + bindingContext.Result = ModelBindingResult.Failed(); + } + else + { + bindingContext.Result = ModelBindingResult.Success(obj); + bindingContext.ValidationState.Add(bindingContext.Result.Model, new ValidationStateEntry() + { + // Don't do validation since the data came from a trusted source. + SuppressValidation = true, + }); + } + } + + private static async Task GetStateEntryAsync(DaprClient daprClient, string storeName, string key) + { + return await daprClient.GetStateEntryAsync(storeName, key); + } + + private static async Task GetStateAsync(DaprClient daprClient, string storeName, string key) + { + return await daprClient.GetStateAsync(storeName, key); + } +} \ No newline at end of file diff --git a/src/Dapr.AspNetCore/StateEntryModelBinderProvider.cs b/src/Dapr.AspNetCore/StateEntryModelBinderProvider.cs index 8aeadfc2..620df0fc 100644 --- a/src/Dapr.AspNetCore/StateEntryModelBinderProvider.cs +++ b/src/Dapr.AspNetCore/StateEntryModelBinderProvider.cs @@ -11,52 +11,51 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.AspNetCore +namespace Dapr.AspNetCore; + +using System; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +internal class StateEntryModelBinderProvider : IModelBinderProvider { - using System; - using Microsoft.AspNetCore.Mvc.ModelBinding; - - internal class StateEntryModelBinderProvider : IModelBinderProvider + public IModelBinder GetBinder(ModelBinderProviderContext context) { - public IModelBinder GetBinder(ModelBinderProviderContext context) + if (context is null) { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (!CanBind(context, out var type)) - { - return null; - } - - var storename = (context.BindingInfo.BindingSource as FromStateBindingSource)?.StoreName; - var key = (context.BindingInfo.BindingSource as FromStateBindingSource)?.Key; - return new StateEntryModelBinder(storename, key, type != context.Metadata.ModelType, type); + throw new ArgumentNullException(nameof(context)); } - private static bool CanBind(ModelBinderProviderContext context, out Type type) + if (!CanBind(context, out var type)) { - if (context.BindingInfo.BindingSource?.Id == "state") - { - // [FromState] - type = Unwrap(context.Metadata.ModelType); - return true; - } - - type = null; - return false; + return null; } - private static Type Unwrap(Type type) - { - if (type.IsGenericType && - type.GetGenericTypeDefinition() == typeof(StateEntry<>)) - { - return type.GetGenericArguments()[0]; - } - - return type; - } + var storename = (context.BindingInfo.BindingSource as FromStateBindingSource)?.StoreName; + var key = (context.BindingInfo.BindingSource as FromStateBindingSource)?.Key; + return new StateEntryModelBinder(storename, key, type != context.Metadata.ModelType, type); } -} + + private static bool CanBind(ModelBinderProviderContext context, out Type type) + { + if (context.BindingInfo.BindingSource?.Id == "state") + { + // [FromState] + type = Unwrap(context.Metadata.ModelType); + return true; + } + + type = null; + return false; + } + + private static Type Unwrap(Type type) + { + if (type.IsGenericType && + type.GetGenericTypeDefinition() == typeof(StateEntry<>)) + { + return type.GetGenericArguments()[0]; + } + + return type; + } +} \ No newline at end of file diff --git a/src/Dapr.AspNetCore/SubscribeOptions.cs b/src/Dapr.AspNetCore/SubscribeOptions.cs index eff8a66c..318af15d 100644 --- a/src/Dapr.AspNetCore/SubscribeOptions.cs +++ b/src/Dapr.AspNetCore/SubscribeOptions.cs @@ -11,16 +11,15 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr +namespace Dapr; + +/// +/// This class defines configurations for the subscribe endpoint. +/// +public class SubscribeOptions { /// - /// This class defines configurations for the subscribe endpoint. + /// Gets or Sets a value which indicates whether to enable or disable processing raw messages. /// - public class SubscribeOptions - { - /// - /// Gets or Sets a value which indicates whether to enable or disable processing raw messages. - /// - public bool EnableRawPayload { get; set; } - } -} + public bool EnableRawPayload { get; set; } +} \ No newline at end of file diff --git a/src/Dapr.AspNetCore/Subscription.cs b/src/Dapr.AspNetCore/Subscription.cs index 9fcf237a..7b0e127b 100644 --- a/src/Dapr.AspNetCore/Subscription.cs +++ b/src/Dapr.AspNetCore/Subscription.cs @@ -13,106 +13,105 @@ using System.Collections.Generic; -namespace Dapr +namespace Dapr; + +/// +/// This class defines subscribe endpoint response +/// +internal class Subscription { /// - /// This class defines subscribe endpoint response + /// Gets or sets the topic name. /// - internal class Subscription - { - /// - /// Gets or sets the topic name. - /// - public string Topic { get; set; } - - /// - /// Gets or sets the pubsub name - /// - public string PubsubName { get; set; } - - /// - /// Gets or sets the route - /// - public string Route { get; set; } - - /// - /// Gets or sets the routes - /// - public Routes Routes { get; set; } - - /// - /// Gets or sets the metadata. - /// - public Metadata Metadata { get; set; } - - /// - /// Gets or sets the deadletter topic. - /// - public string DeadLetterTopic { get; set; } - - /// - /// Gets or sets the bulk subscribe options. - /// - public DaprTopicBulkSubscribe BulkSubscribe { get; set; } - } + public string Topic { get; set; } /// - /// This class defines the metadata for subscribe endpoint. + /// Gets or sets the pubsub name /// - internal class Metadata : Dictionary - { - public Metadata() { } + public string PubsubName { get; set; } - public Metadata(IDictionary dictionary) : base(dictionary) { } + /// + /// Gets or sets the route + /// + public string Route { get; set; } - /// - /// RawPayload key - /// - internal const string RawPayload = "rawPayload"; - } + /// + /// Gets or sets the routes + /// + public Routes Routes { get; set; } - internal class Routes - { - /// - /// Gets or sets the default route - /// - public string Default { get; set; } - - /// - /// Gets or sets the routing rules - /// - public List Rules { get; set; } - } - - internal class Rule - { - /// - /// Gets or sets the CEL expression to match this route. - /// - public string Match { get; set; } - - /// - /// Gets or sets the path of the route. - /// - public string Path { get; set; } - } - - internal class DaprTopicBulkSubscribe - { - /// - /// Gets or sets whether bulk subscribe option is enabled for a topic. - /// - public bool Enabled { get; set; } - - /// - /// Gets or sets the maximum number of messages in a bulk message from the message bus. - /// - public int MaxMessagesCount { get; set; } + /// + /// Gets or sets the metadata. + /// + public Metadata Metadata { get; set; } - /// - /// Gets or sets the Maximum duration to wait for maxBulkSubCount messages by the message bus - /// before sending the messages to Dapr. - /// - public int MaxAwaitDurationMs { get; set; } - } + /// + /// Gets or sets the deadletter topic. + /// + public string DeadLetterTopic { get; set; } + + /// + /// Gets or sets the bulk subscribe options. + /// + public DaprTopicBulkSubscribe BulkSubscribe { get; set; } } + +/// +/// This class defines the metadata for subscribe endpoint. +/// +internal class Metadata : Dictionary +{ + public Metadata() { } + + public Metadata(IDictionary dictionary) : base(dictionary) { } + + /// + /// RawPayload key + /// + internal const string RawPayload = "rawPayload"; +} + +internal class Routes +{ + /// + /// Gets or sets the default route + /// + public string Default { get; set; } + + /// + /// Gets or sets the routing rules + /// + public List Rules { get; set; } +} + +internal class Rule +{ + /// + /// Gets or sets the CEL expression to match this route. + /// + public string Match { get; set; } + + /// + /// Gets or sets the path of the route. + /// + public string Path { get; set; } +} + +internal class DaprTopicBulkSubscribe +{ + /// + /// Gets or sets whether bulk subscribe option is enabled for a topic. + /// + public bool Enabled { get; set; } + + /// + /// Gets or sets the maximum number of messages in a bulk message from the message bus. + /// + public int MaxMessagesCount { get; set; } + + /// + /// Gets or sets the Maximum duration to wait for maxBulkSubCount messages by the message bus + /// before sending the messages to Dapr. + /// + public int MaxAwaitDurationMs { get; set; } +} \ No newline at end of file diff --git a/src/Dapr.AspNetCore/TopicAttribute.cs b/src/Dapr.AspNetCore/TopicAttribute.cs index daa5d850..1d1f7a1e 100644 --- a/src/Dapr.AspNetCore/TopicAttribute.cs +++ b/src/Dapr.AspNetCore/TopicAttribute.cs @@ -11,144 +11,143 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr +namespace Dapr; + +using System; + +/// +/// TopicAttribute describes an endpoint as a subscriber to a topic. +/// +[AttributeUsage(AttributeTargets.All, AllowMultiple = true)] +public class TopicAttribute : Attribute, ITopicMetadata, IRawTopicMetadata, IOwnedOriginalTopicMetadata, IDeadLetterTopicMetadata { - using System; + /// + /// Initializes a new instance of the class. + /// + /// The name of the pubsub component to use. + /// The topic name. + /// The topic owned metadata ids. + /// Separator to use for metadata. + public TopicAttribute(string pubsubName, string name, string[] ownedMetadatas = null, string metadataSeparator = null) + { + ArgumentVerifier.ThrowIfNullOrEmpty(pubsubName, nameof(pubsubName)); + ArgumentVerifier.ThrowIfNullOrEmpty(name, nameof(name)); + + this.Name = name; + this.PubsubName = pubsubName; + this.OwnedMetadatas = ownedMetadatas; + this.MetadataSeparator = metadataSeparator; + } /// - /// TopicAttribute describes an endpoint as a subscriber to a topic. + /// Initializes a new instance of the class. /// - [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] - public class TopicAttribute : Attribute, ITopicMetadata, IRawTopicMetadata, IOwnedOriginalTopicMetadata, IDeadLetterTopicMetadata + /// The name of the pubsub component to use. + /// The topic name. + /// The enable/disable raw pay load flag. + /// The topic owned metadata ids. + /// Separator to use for metadata. + public TopicAttribute(string pubsubName, string name, bool enableRawPayload, string[] ownedMetadatas = null, string metadataSeparator = null) { - /// - /// Initializes a new instance of the class. - /// - /// The name of the pubsub component to use. - /// The topic name. - /// The topic owned metadata ids. - /// Separator to use for metadata. - public TopicAttribute(string pubsubName, string name, string[] ownedMetadatas = null, string metadataSeparator = null) - { - ArgumentVerifier.ThrowIfNullOrEmpty(pubsubName, nameof(pubsubName)); - ArgumentVerifier.ThrowIfNullOrEmpty(name, nameof(name)); + ArgumentVerifier.ThrowIfNullOrEmpty(pubsubName, nameof(pubsubName)); + ArgumentVerifier.ThrowIfNullOrEmpty(name, nameof(name)); - this.Name = name; - this.PubsubName = pubsubName; - this.OwnedMetadatas = ownedMetadatas; - this.MetadataSeparator = metadataSeparator; - } - - /// - /// Initializes a new instance of the class. - /// - /// The name of the pubsub component to use. - /// The topic name. - /// The enable/disable raw pay load flag. - /// The topic owned metadata ids. - /// Separator to use for metadata. - public TopicAttribute(string pubsubName, string name, bool enableRawPayload, string[] ownedMetadatas = null, string metadataSeparator = null) - { - ArgumentVerifier.ThrowIfNullOrEmpty(pubsubName, nameof(pubsubName)); - ArgumentVerifier.ThrowIfNullOrEmpty(name, nameof(name)); - - this.Name = name; - this.PubsubName = pubsubName; - this.EnableRawPayload = enableRawPayload; - this.OwnedMetadatas = ownedMetadatas; - this.MetadataSeparator = metadataSeparator; - } - - /// - /// Initializes a new instance of the class. - /// - /// The name of the pubsub component to use. - /// The topic name. - /// The CEL expression to test the cloud event with. - /// The priority of the rule (low-to-high values). - /// The topic owned metadata ids. - /// Separator to use for metadata. - public TopicAttribute(string pubsubName, string name, string match, int priority, string[] ownedMetadatas = null, string metadataSeparator = null) - { - ArgumentVerifier.ThrowIfNullOrEmpty(pubsubName, nameof(pubsubName)); - ArgumentVerifier.ThrowIfNullOrEmpty(name, nameof(name)); - - this.Name = name; - this.PubsubName = pubsubName; - this.Match = match; - this.Priority = priority; - this.OwnedMetadatas = ownedMetadatas; - this.MetadataSeparator = metadataSeparator; - } - - /// - /// Initializes a new instance of the class. - /// - /// The name of the pubsub component to use. - /// The topic name. - /// The enable/disable raw pay load flag. - /// The CEL expression to test the cloud event with. - /// The priority of the rule (low-to-high values). - /// The topic owned metadata ids. - /// Separator to use for metadata. - public TopicAttribute(string pubsubName, string name, bool enableRawPayload, string match, int priority, string[] ownedMetadatas = null, string metadataSeparator = null) - { - ArgumentVerifier.ThrowIfNullOrEmpty(pubsubName, nameof(pubsubName)); - ArgumentVerifier.ThrowIfNullOrEmpty(name, nameof(name)); - - this.Name = name; - this.PubsubName = pubsubName; - this.EnableRawPayload = enableRawPayload; - this.Match = match; - this.Priority = priority; - this.OwnedMetadatas = ownedMetadatas; - this.MetadataSeparator = metadataSeparator; - } - - /// - /// Initializes a new instance of the class. - /// - /// The name of the pubsub component to use. - /// The topic name. - /// The dead letter topic name. - /// The enable/disable raw pay load flag. - /// The topic owned metadata ids. - /// Separator to use for metadata. - public TopicAttribute(string pubsubName, string name, string deadLetterTopic, bool enableRawPayload, string[] ownedMetadatas = null, string metadataSeparator = null) - { - ArgumentVerifier.ThrowIfNullOrEmpty(pubsubName, nameof(pubsubName)); - ArgumentVerifier.ThrowIfNullOrEmpty(name, nameof(name)); - - this.Name = name; - this.PubsubName = pubsubName; - this.DeadLetterTopic = deadLetterTopic; - this.EnableRawPayload = enableRawPayload; - this.OwnedMetadatas = ownedMetadatas; - this.MetadataSeparator = metadataSeparator; - } - - /// - public string Name { get; set; } - - /// - public string PubsubName { get; set; } - - /// - public bool? EnableRawPayload { get; set; } - - /// - public new string Match { get; set; } - - /// - public int Priority { get; set; } - - /// - public string[] OwnedMetadatas { get; set; } - - /// - public string MetadataSeparator { get; set; } - - /// - public string DeadLetterTopic { get; set; } + this.Name = name; + this.PubsubName = pubsubName; + this.EnableRawPayload = enableRawPayload; + this.OwnedMetadatas = ownedMetadatas; + this.MetadataSeparator = metadataSeparator; } -} + + /// + /// Initializes a new instance of the class. + /// + /// The name of the pubsub component to use. + /// The topic name. + /// The CEL expression to test the cloud event with. + /// The priority of the rule (low-to-high values). + /// The topic owned metadata ids. + /// Separator to use for metadata. + public TopicAttribute(string pubsubName, string name, string match, int priority, string[] ownedMetadatas = null, string metadataSeparator = null) + { + ArgumentVerifier.ThrowIfNullOrEmpty(pubsubName, nameof(pubsubName)); + ArgumentVerifier.ThrowIfNullOrEmpty(name, nameof(name)); + + this.Name = name; + this.PubsubName = pubsubName; + this.Match = match; + this.Priority = priority; + this.OwnedMetadatas = ownedMetadatas; + this.MetadataSeparator = metadataSeparator; + } + + /// + /// Initializes a new instance of the class. + /// + /// The name of the pubsub component to use. + /// The topic name. + /// The enable/disable raw pay load flag. + /// The CEL expression to test the cloud event with. + /// The priority of the rule (low-to-high values). + /// The topic owned metadata ids. + /// Separator to use for metadata. + public TopicAttribute(string pubsubName, string name, bool enableRawPayload, string match, int priority, string[] ownedMetadatas = null, string metadataSeparator = null) + { + ArgumentVerifier.ThrowIfNullOrEmpty(pubsubName, nameof(pubsubName)); + ArgumentVerifier.ThrowIfNullOrEmpty(name, nameof(name)); + + this.Name = name; + this.PubsubName = pubsubName; + this.EnableRawPayload = enableRawPayload; + this.Match = match; + this.Priority = priority; + this.OwnedMetadatas = ownedMetadatas; + this.MetadataSeparator = metadataSeparator; + } + + /// + /// Initializes a new instance of the class. + /// + /// The name of the pubsub component to use. + /// The topic name. + /// The dead letter topic name. + /// The enable/disable raw pay load flag. + /// The topic owned metadata ids. + /// Separator to use for metadata. + public TopicAttribute(string pubsubName, string name, string deadLetterTopic, bool enableRawPayload, string[] ownedMetadatas = null, string metadataSeparator = null) + { + ArgumentVerifier.ThrowIfNullOrEmpty(pubsubName, nameof(pubsubName)); + ArgumentVerifier.ThrowIfNullOrEmpty(name, nameof(name)); + + this.Name = name; + this.PubsubName = pubsubName; + this.DeadLetterTopic = deadLetterTopic; + this.EnableRawPayload = enableRawPayload; + this.OwnedMetadatas = ownedMetadatas; + this.MetadataSeparator = metadataSeparator; + } + + /// + public string Name { get; set; } + + /// + public string PubsubName { get; set; } + + /// + public bool? EnableRawPayload { get; set; } + + /// + public new string Match { get; set; } + + /// + public int Priority { get; set; } + + /// + public string[] OwnedMetadatas { get; set; } + + /// + public string MetadataSeparator { get; set; } + + /// + public string DeadLetterTopic { get; set; } +} \ No newline at end of file diff --git a/src/Dapr.AspNetCore/TopicMetadataAttribute.cs b/src/Dapr.AspNetCore/TopicMetadataAttribute.cs index 93d527cc..6d246875 100644 --- a/src/Dapr.AspNetCore/TopicMetadataAttribute.cs +++ b/src/Dapr.AspNetCore/TopicMetadataAttribute.cs @@ -11,52 +11,51 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr +namespace Dapr; + +using System; + +/// +/// IOriginalTopicMetadata that describes subscribe endpoint to a topic original metadata. +/// +[AttributeUsage(AttributeTargets.All, AllowMultiple = true)] +public class TopicMetadataAttribute : Attribute, IOriginalTopicMetadata { - using System; + /// + /// Initializes a new instance of the class. + /// + /// The metadata name. + /// The metadata value. + public TopicMetadataAttribute(string name, string value) + { + ArgumentVerifier.ThrowIfNullOrEmpty(name, nameof(name)); + ArgumentVerifier.ThrowIfNull(value, nameof(value)); + Name = name; + Value = value; + } /// - /// IOriginalTopicMetadata that describes subscribe endpoint to a topic original metadata. + /// Initializes a new instance of the class. /// - [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] - public class TopicMetadataAttribute : Attribute, IOriginalTopicMetadata + /// The metadata id. + /// The metadata name. + /// The metadata value. + public TopicMetadataAttribute(string id, string name, string value) { - /// - /// Initializes a new instance of the class. - /// - /// The metadata name. - /// The metadata value. - public TopicMetadataAttribute(string name, string value) - { - ArgumentVerifier.ThrowIfNullOrEmpty(name, nameof(name)); - ArgumentVerifier.ThrowIfNull(value, nameof(value)); - Name = name; - Value = value; - } - - /// - /// Initializes a new instance of the class. - /// - /// The metadata id. - /// The metadata name. - /// The metadata value. - public TopicMetadataAttribute(string id, string name, string value) - { - ArgumentVerifier.ThrowIfNullOrEmpty(id, nameof(name)); - ArgumentVerifier.ThrowIfNullOrEmpty(name, nameof(name)); - ArgumentVerifier.ThrowIfNull(value, nameof(value)); - Id = id; - Name = name; - Value = value; - } - - /// - public string Id { get; } - - /// - public string Name { get; } - - /// - public string Value { get; } + ArgumentVerifier.ThrowIfNullOrEmpty(id, nameof(name)); + ArgumentVerifier.ThrowIfNullOrEmpty(name, nameof(name)); + ArgumentVerifier.ThrowIfNull(value, nameof(value)); + Id = id; + Name = name; + Value = value; } -} + + /// + public string Id { get; } + + /// + public string Name { get; } + + /// + public string Value { get; } +} \ No newline at end of file diff --git a/src/Dapr.AspNetCore/TopicOptions.cs b/src/Dapr.AspNetCore/TopicOptions.cs index 2ca2eea4..8ed031be 100644 --- a/src/Dapr.AspNetCore/TopicOptions.cs +++ b/src/Dapr.AspNetCore/TopicOptions.cs @@ -11,57 +11,56 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr +namespace Dapr; + +using System.Collections.Generic; +/// +/// This class defines configurations for the subscribe endpoint. +/// +public class TopicOptions { - using System.Collections.Generic; /// - /// This class defines configurations for the subscribe endpoint. + /// Gets or Sets the topic name. /// - public class TopicOptions - { - /// - /// Gets or Sets the topic name. - /// - public string Name { get; set; } + public string Name { get; set; } - /// - /// Gets or Sets the name of the pubsub component to use. - /// - public string PubsubName { get; set; } + /// + /// Gets or Sets the name of the pubsub component to use. + /// + public string PubsubName { get; set; } - /// - /// Gets or Sets a value which indicates whether to enable or disable processing raw messages. - /// - public bool EnableRawPayload { get; set; } + /// + /// Gets or Sets a value which indicates whether to enable or disable processing raw messages. + /// + public bool EnableRawPayload { get; set; } - /// - /// Gets or Sets the CEL expression to use to match events for this handler. - /// - public string Match { get; set; } + /// + /// Gets or Sets the CEL expression to use to match events for this handler. + /// + public string Match { get; set; } - /// - /// Gets or Sets the priority in which this rule should be evaluated (lower to higher). - /// - public int Priority { get; set; } + /// + /// Gets or Sets the priority in which this rule should be evaluated (lower to higher). + /// + public int Priority { get; set; } - /// - /// Gets or Sets the owned by topic. - /// - public string[] OwnedMetadatas { get; set; } + /// + /// Gets or Sets the owned by topic. + /// + public string[] OwnedMetadatas { get; set; } - /// - /// Get or Sets the separator to use for metadata. - /// - public string MetadataSeparator { get; set; } + /// + /// Get or Sets the separator to use for metadata. + /// + public string MetadataSeparator { get; set; } - /// - /// Gets or Sets the dead letter topic. - /// - public string DeadLetterTopic { get; set; } + /// + /// Gets or Sets the dead letter topic. + /// + public string DeadLetterTopic { get; set; } - /// - /// Gets or Sets the original topic metadata. - /// - public IDictionary Metadata; - } -} + /// + /// Gets or Sets the original topic metadata. + /// + public IDictionary Metadata; +} \ No newline at end of file diff --git a/src/Dapr.Client/BindingRequest.cs b/src/Dapr.Client/BindingRequest.cs index 5448588a..a12aab69 100644 --- a/src/Dapr.Client/BindingRequest.cs +++ b/src/Dapr.Client/BindingRequest.cs @@ -14,50 +14,34 @@ using System; using System.Collections.Generic; -namespace Dapr.Client +namespace Dapr.Client; + +/// +/// Represents the request used to invoke a binding. +/// +/// The name of the binding. +/// The type of operation to perform on the binding. +public sealed class BindingRequest(string bindingName, string operation) { /// - /// Represents the request used to invoke a binding. + /// Gets the name of the binding. /// - public sealed class BindingRequest - { - /// - /// Initializes a new for the provided and - /// . - /// - /// The name of the binding. - /// The type of operation to perform on the binding. - public BindingRequest(string bindingName, string operation) - { - ArgumentVerifier.ThrowIfNullOrEmpty(bindingName, nameof(bindingName)); - ArgumentVerifier.ThrowIfNullOrEmpty(operation, nameof(operation)); + /// + public string BindingName { get; } = bindingName ?? throw new ArgumentNullException(nameof(bindingName)); - this.BindingName = bindingName; - this.Operation = operation; + /// + /// Gets the type of operation to perform on the binding. + /// + public string Operation { get; } = operation ?? throw new ArgumentNullException(nameof(operation)); + + /// + /// Gets or sets the binding request payload. + /// + public ReadOnlyMemory Data { get; set; } - this.Metadata = new Dictionary(); - } - - /// - /// Gets the name of the binding. - /// - /// - public string BindingName { get; } - - /// - /// Gets the type of operation to perform on the binding. - /// - public string Operation { get; } - - /// - /// Gets or sets the binding request payload. - /// - public ReadOnlyMemory Data { get; set; } - - /// - /// Gets the metadata; a collection of metadata key-value pairs that will be provided to the binding. - /// The valid metadata keys and values are determined by the type of binding used. - /// - public Dictionary Metadata { get; } - } + /// + /// Gets the metadata; a collection of metadata key-value pairs that will be provided to the binding. + /// The valid metadata keys and values are determined by the type of binding used. + /// + public Dictionary Metadata { get; } = new(); } diff --git a/src/Dapr.Client/BindingResponse.cs b/src/Dapr.Client/BindingResponse.cs index f362533e..cf5be5b6 100644 --- a/src/Dapr.Client/BindingResponse.cs +++ b/src/Dapr.Client/BindingResponse.cs @@ -11,46 +11,33 @@ // limitations under the License. // ------------------------------------------------------------------------ +#nullable enable using System; using System.Collections.Generic; -namespace Dapr.Client +namespace Dapr.Client; + +/// +/// Represents the response from invoking a binding. +/// +/// The assocated with this response. +/// The response payload. +/// The response metadata. +public sealed class BindingResponse(BindingRequest request, ReadOnlyMemory data, IReadOnlyDictionary metadata) { /// - /// Represents the response from invoking a binding. + /// Gets the assocated with this response. /// - public sealed class BindingResponse - { - /// - /// Initializes a new .` - /// - /// The assocated with this response. - /// The response payload. - /// The response metadata. - public BindingResponse(BindingRequest request, ReadOnlyMemory data, IReadOnlyDictionary metadata) - { - ArgumentVerifier.ThrowIfNull(request, nameof(request)); - ArgumentVerifier.ThrowIfNull(data, nameof(data)); - ArgumentVerifier.ThrowIfNull(metadata, nameof(metadata)); + public BindingRequest Request { get; } = request ?? throw new ArgumentNullException(nameof(request)); - this.Request = request; - this.Data = data; - this.Metadata = metadata; - } + /// + /// Gets the response payload. + /// + public ReadOnlyMemory Data { get; } = data; - /// - /// Gets the assocated with this response. - /// - public BindingRequest Request { get; } - - /// - /// Gets the response payload. - /// - public ReadOnlyMemory Data { get; } - - /// - /// Gets the response metadata. - /// - public IReadOnlyDictionary Metadata { get; } - } + /// + /// Gets the response metadata. + /// + public IReadOnlyDictionary Metadata { get; } = + metadata ?? throw new ArgumentNullException(nameof(metadata)); } diff --git a/src/Dapr.Client/BulkPublishEntry.cs b/src/Dapr.Client/BulkPublishEntry.cs index d6b9a549..d4d9107f 100644 --- a/src/Dapr.Client/BulkPublishEntry.cs +++ b/src/Dapr.Client/BulkPublishEntry.cs @@ -11,51 +11,39 @@ // limitations under the License. // ------------------------------------------------------------------------ +#nullable enable using System.Collections.Generic; -namespace Dapr.Client +namespace Dapr.Client; + +/// +/// Class representing an entry in the BulkPublishRequest. +/// +/// The data type of the value. +/// A request scoped ID uniquely identifying this entry in the BulkPublishRequest. +/// Event to be published. +/// Content Type of the event to be published. +/// Metadata for the event. +public class BulkPublishEntry(string entryId, TValue eventData, string contentType, IReadOnlyDictionary? metadata = null) { /// - /// Class representing an entry in the BulkPublishRequest. + /// The ID uniquely identifying this particular request entry across the request and scoped for this request only. /// - /// The data type of the value. - public class BulkPublishEntry - { - /// - /// Initializes a new instance of the class. - /// - /// A request scoped ID uniquely identifying this entry in the BulkPublishRequest. - /// Event to be published. - /// Content Type of the event to be published. - /// Metadata for the event. - public BulkPublishEntry(string entryId, TValue eventData, string contentType, IReadOnlyDictionary metadata = default) - { - this.EntryId = entryId; - this.EventData = eventData; - this.ContentType = contentType; - this.Metadata = metadata; - } + public string EntryId { get; } = entryId; - /// - /// The ID uniquely identifying this particular request entry across the request and scoped for this request only. - /// - public string EntryId { get; } + /// + /// The event to be published. + /// + public TValue EventData { get; } = eventData; - /// - /// The event to be published. - /// - public TValue EventData { get; } - - /// - /// The content type of the event to be published. - /// - public string ContentType { get; } - - /// - /// The metadata set for this particular event. - /// Any particular values in this metadata overrides the request metadata present in BulkPublishRequest. - /// - public IReadOnlyDictionary Metadata { get; } + /// + /// The content type of the event to be published. + /// + public string ContentType { get; } = contentType; - } + /// + /// The metadata set for this particular event. + /// Any particular values in this metadata overrides the request metadata present in BulkPublishRequest. + /// + public IReadOnlyDictionary? Metadata { get; } = metadata; } diff --git a/src/Dapr.Client/BulkPublishResponse.cs b/src/Dapr.Client/BulkPublishResponse.cs index f37c35aa..2e227aaf 100644 --- a/src/Dapr.Client/BulkPublishResponse.cs +++ b/src/Dapr.Client/BulkPublishResponse.cs @@ -13,25 +13,16 @@ using System.Collections.Generic; -namespace Dapr.Client +namespace Dapr.Client; + +/// +/// Class representing the response returned on bulk publishing events. +/// +/// The List of BulkPublishResponseEntries representing the list of events that failed to be published. +public class BulkPublishResponse(List> failedEntries) { /// - /// Class representing the response returned on bulk publishing events. + /// The List of BulkPublishResponseFailedEntry objects that have failed to publish. /// - public class BulkPublishResponse - { - /// - /// Initializes a new instance of the class. - /// - /// The List of BulkPublishResponseEntries representing the list of events that failed to be published. - public BulkPublishResponse(List> failedEntries) - { - this.FailedEntries = failedEntries; - } - - /// - /// The List of BulkPublishResponseFailedEntry objects that have failed to publish. - /// - public List> FailedEntries { get; } - } + public List> FailedEntries { get; } = failedEntries; } diff --git a/src/Dapr.Client/BulkPublishResponseFailedEntry.cs b/src/Dapr.Client/BulkPublishResponseFailedEntry.cs index e8a46c9b..bc3d122e 100644 --- a/src/Dapr.Client/BulkPublishResponseFailedEntry.cs +++ b/src/Dapr.Client/BulkPublishResponseFailedEntry.cs @@ -11,32 +11,22 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Client +namespace Dapr.Client; + +/// +/// Class representing the status of each event that was published using BulkPublishRequest. +/// +/// The entry that failed to be published. +/// Error message as to why the entry failed to publish. +public class BulkPublishResponseFailedEntry(BulkPublishEntry entry, string errorMessage) { /// - /// Class representing the status of each event that was published using BulkPublishRequest. + /// The entry that has failed. /// - public class BulkPublishResponseFailedEntry - { - /// - /// Initializes a new instance of the class. - /// - /// The entry that failed to be published. - /// Error message as to why the entry failed to publish. - public BulkPublishResponseFailedEntry(BulkPublishEntry entry, string errorMessage) - { - this.Entry = entry; - this.ErrorMessage = errorMessage; - } - - /// - /// The entry that has failed. - /// - public BulkPublishEntry Entry { get; } + public BulkPublishEntry Entry { get; } = entry; - /// - /// Error message as to why the entry failed to publish. - /// - public string ErrorMessage { get; } - } + /// + /// Error message as to why the entry failed to publish. + /// + public string ErrorMessage { get; } = errorMessage; } diff --git a/src/Dapr.Client/BulkStateItem.cs b/src/Dapr.Client/BulkStateItem.cs index 5b30ddf2..d3b423f4 100644 --- a/src/Dapr.Client/BulkStateItem.cs +++ b/src/Dapr.Client/BulkStateItem.cs @@ -11,80 +11,59 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Client +namespace Dapr.Client; + +/// +/// Represents a state object returned from a bulk get state operation. +/// +/// The state key. +/// The value. +/// The ETag. +/// +/// Application code should not need to create instances of . +/// +public readonly struct BulkStateItem(string key, string value, string etag) { /// - /// Represents a state object returned from a bulk get state operation. + /// Gets the state key. /// - public readonly struct BulkStateItem - { - /// - /// Initializes a new instance of the class. - /// - /// The state key. - /// The value. - /// The ETag. - /// - /// Application code should not need to create instances of . - /// - public BulkStateItem(string key, string value, string etag) - { - this.Key = key; - this.Value = value; - this.ETag = etag; - } - - /// - /// Gets the state key. - /// - public string Key { get; } - - /// - /// Gets the value. - /// - public string Value { get; } - - /// - /// Get the ETag. - /// - public string ETag { get; } - } + public string Key { get; } = key; /// - /// Represents a state object returned from a bulk get state operation where the value has - /// been deserialized to the specified type. + /// Gets the value. /// - public readonly struct BulkStateItem - { - /// - /// Initializes a new instance of the class. - /// - /// The state key. - /// The typed value. - /// The ETag. - /// - /// Application code should not need to create instances of . - /// - public BulkStateItem(string key, TValue value, string etag) - { - this.Key = key; - this.Value = value; - this.ETag = etag; - } + public string Value { get; } = value; - /// - /// Gets the state key. - /// - public string Key { get; } - - /// - /// Gets the deserialized value of the indicated type. - /// - public TValue Value { get; } - - /// - /// Get the ETag. - /// - public string ETag { get; } - } + /// + /// Get the ETag. + /// + public string ETag { get; } = etag; +} + +/// +/// Represents a state object returned from a bulk get state operation where the value has +/// been deserialized to the specified type. +/// +/// The state key. +/// The typed value. +/// The ETag. +/// +/// Application code should not need to create instances of . +/// +public readonly struct BulkStateItem(string key, TValue value, string etag) +{ + /// + /// Gets the state key. + /// + public string Key { get; } = key; + + /// + /// Gets the deserialized value of the indicated type. + /// + public TValue Value { get; } = value; + + /// + /// Get the ETag. + /// + public string ETag { get; } = etag; } diff --git a/src/Dapr.Client/CloudEvent.cs b/src/Dapr.Client/CloudEvent.cs index bb0eaf46..485a615d 100644 --- a/src/Dapr.Client/CloudEvent.cs +++ b/src/Dapr.Client/CloudEvent.cs @@ -15,54 +15,45 @@ using System; using System.Text.Json.Serialization; using Dapr.Client; -namespace Dapr +namespace Dapr; + +/// +/// Represents a CloudEvent without data. +/// +public class CloudEvent { - /// - /// Represents a CloudEvent without data. - /// - public class CloudEvent - { - /// - /// CloudEvent 'source' attribute. - /// - [JsonPropertyName("source")] - public Uri Source { get; init; } + /// + /// CloudEvent 'source' attribute. + /// + [JsonPropertyName("source")] + public Uri Source { get; init; } - /// - /// CloudEvent 'type' attribute. - /// - [JsonPropertyName("type")] - public string Type { get; init; } + /// + /// CloudEvent 'type' attribute. + /// + [JsonPropertyName("type")] + public string Type { get; init; } - /// - /// CloudEvent 'subject' attribute. - /// - [JsonPropertyName("subject")] - public string Subject { get; init; } - } - - /// - /// Represents a CloudEvent with typed data. - /// - public class CloudEvent : CloudEvent - { - /// - /// Initialize a new instance of the class. - /// - public CloudEvent(TData data) - { - Data = data; - } - - /// - /// CloudEvent 'data' content. - /// - public TData Data { get; } - - /// - /// Gets event data. - /// - [JsonPropertyName("datacontenttype")] - public string DataContentType { get; } = Constants.ContentTypeApplicationJson; - } + /// + /// CloudEvent 'subject' attribute. + /// + [JsonPropertyName("subject")] + public string Subject { get; init; } +} + +/// +/// Represents a CloudEvent with typed data. +/// +public class CloudEvent(TData data) : CloudEvent +{ + /// + /// CloudEvent 'data' content. + /// + public TData Data { get; } = data; + + /// + /// Gets event data. + /// + [JsonPropertyName("datacontenttype")] + public string DataContentType { get; } = Constants.ContentTypeApplicationJson; } diff --git a/src/Dapr.Client/ConcurrencyMode.cs b/src/Dapr.Client/ConcurrencyMode.cs index 4efc48d8..4f2a2e8f 100644 --- a/src/Dapr.Client/ConcurrencyMode.cs +++ b/src/Dapr.Client/ConcurrencyMode.cs @@ -11,21 +11,20 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Client +namespace Dapr.Client; + +/// +/// Concurrency mode for state operations with Dapr. +/// +public enum ConcurrencyMode { /// - /// Concurrency mode for state operations with Dapr. + /// State operations will be handled in a first-write-wins fashion /// - public enum ConcurrencyMode - { - /// - /// State operations will be handled in a first-write-wins fashion - /// - FirstWrite, + FirstWrite, - /// - /// State operations will be handled in a last-write-wins fashion - /// - LastWrite, - } -} + /// + /// State operations will be handled in a last-write-wins fashion + /// + LastWrite, +} \ No newline at end of file diff --git a/src/Dapr.Client/ConfigurationItem.cs b/src/Dapr.Client/ConfigurationItem.cs index fde406db..9478462b 100644 --- a/src/Dapr.Client/ConfigurationItem.cs +++ b/src/Dapr.Client/ConfigurationItem.cs @@ -1,39 +1,28 @@ using System.Collections.Generic; -namespace Dapr.Client +namespace Dapr.Client; + +/// +/// Class that represents an item fetched from the Dapr Configuration API. +/// +/// The value of the configuration item. +/// The version of the fetched item. +/// The metadata associated with the request. +public class ConfigurationItem(string value, string version, IReadOnlyDictionary metadata) { /// - /// Class that represents an item fetched from the Dapr Configuration API. + /// The value of the configuration item. /// - public class ConfigurationItem - { - /// - /// Constructor for a ConfigurationItem. - /// - /// The value of the configuration item. - /// The version of the fetched item. - /// The metadata associated with the request. - public ConfigurationItem(string value, string version, IReadOnlyDictionary metadata) - { - Value = value; - Version = version; - Metadata = metadata; - } + public string Value { get; } = value; - /// - /// The value of the configuration item. - /// - public string Value { get; } + /// + /// The version of the item retrieved. This is only provided on responses and + /// the statestore is not expected to keep all versions available. + /// + public string Version { get; } = version; - /// - /// The version of the item retrieved. This is only provided on responses and - /// the statestore is not expected to keep all versions available. - /// - public string Version { get; } - - /// - /// The metadata that is passed to/from the statestore component. - /// - public IReadOnlyDictionary Metadata { get; } - } + /// + /// The metadata that is passed to/from the statestore component. + /// + public IReadOnlyDictionary Metadata { get; } = metadata; } diff --git a/src/Dapr.Client/ConfigurationSource.cs b/src/Dapr.Client/ConfigurationSource.cs index ca3f8d6d..2c8ee31b 100644 --- a/src/Dapr.Client/ConfigurationSource.cs +++ b/src/Dapr.Client/ConfigurationSource.cs @@ -14,19 +14,18 @@ using System.Collections.Generic; using System.Threading; -namespace Dapr.Client +namespace Dapr.Client; + +/// +/// Abstraction around a configuration source. +/// +public abstract class ConfigurationSource : IAsyncEnumerable> { /// - /// Abstraction around a configuration source. + /// The Id associated with this configuration source. /// - public abstract class ConfigurationSource : IAsyncEnumerable> - { - /// - /// The Id associated with this configuration source. - /// - public abstract string Id { get; } + public abstract string Id { get; } - /// - public abstract IAsyncEnumerator> GetAsyncEnumerator(CancellationToken cancellationToken = default); - } -} + /// + public abstract IAsyncEnumerator> GetAsyncEnumerator(CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/Dapr.Client/ConsistencyMode.cs b/src/Dapr.Client/ConsistencyMode.cs index 91f13ca5..5492d515 100644 --- a/src/Dapr.Client/ConsistencyMode.cs +++ b/src/Dapr.Client/ConsistencyMode.cs @@ -11,21 +11,20 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Client +namespace Dapr.Client; + +/// +/// Consistency mode for state operations with Dapr. +/// +public enum ConsistencyMode { /// - /// Consistency mode for state operations with Dapr. + /// Eventual consistency. /// - public enum ConsistencyMode - { - /// - /// Eventual consistency. - /// - Eventual, + Eventual, - /// - /// Strong consistency. - /// - Strong, - } -} + /// + /// Strong consistency. + /// + Strong, +} \ No newline at end of file diff --git a/src/Dapr.Client/Constants.cs b/src/Dapr.Client/Constants.cs index 37ebbb21..206fe1c2 100644 --- a/src/Dapr.Client/Constants.cs +++ b/src/Dapr.Client/Constants.cs @@ -13,12 +13,11 @@ using System.Net.Mime; -namespace Dapr.Client +namespace Dapr.Client; + +internal class Constants { - internal class Constants - { - public const string ContentTypeApplicationJson = MediaTypeNames.Application.Json; - public const string ContentTypeApplicationGrpc = "application/grpc"; - public const string ContentTypeCloudEvent = "application/cloudevents+json"; - } -} + public const string ContentTypeApplicationJson = MediaTypeNames.Application.Json; + public const string ContentTypeApplicationGrpc = "application/grpc"; + public const string ContentTypeCloudEvent = "application/cloudevents+json"; +} \ No newline at end of file diff --git a/src/Dapr.Client/CryptographyEnums.cs b/src/Dapr.Client/CryptographyEnums.cs index f5955b38..949de7de 100644 --- a/src/Dapr.Client/CryptographyEnums.cs +++ b/src/Dapr.Client/CryptographyEnums.cs @@ -13,64 +13,63 @@ using System.Runtime.Serialization; -namespace Dapr.Client +namespace Dapr.Client; + +/// +/// The cipher used for data encryption operations. +/// +public enum DataEncryptionCipher { /// - /// The cipher used for data encryption operations. + /// The default data encryption cipher used, this represents AES GCM. /// - public enum DataEncryptionCipher - { - /// - /// The default data encryption cipher used, this represents AES GCM. - /// - [EnumMember(Value = "aes-gcm")] - AesGcm, - /// - /// Represents the ChaCha20-Poly1305 data encryption cipher. - /// - [EnumMember(Value = "chacha20-poly1305")] - ChaCha20Poly1305 - }; - + [EnumMember(Value = "aes-gcm")] + AesGcm, /// - /// The algorithm used for key wrapping cryptographic operations. + /// Represents the ChaCha20-Poly1305 data encryption cipher. /// - public enum KeyWrapAlgorithm - { - /// - /// Represents the AES key wrap algorithm. - /// - [EnumMember(Value="A256KW")] - Aes, - /// - /// An alias for the AES key wrap algorithm. - /// - [EnumMember(Value="A256KW")] - A256kw, - /// - /// Represents the AES 128 CBC key wrap algorithm. - /// - [EnumMember(Value="A128CBC")] - A128cbc, - /// - /// Represents the AES 192 CBC key wrap algorithm. - /// - [EnumMember(Value="A192CBC")] - A192cbc, - /// - /// Represents the AES 256 CBC key wrap algorithm. - /// - [EnumMember(Value="A256CBC")] - A256cbc, - /// - /// Represents the RSA key wrap algorithm. - /// - [EnumMember(Value= "RSA-OAEP-256")] - Rsa, - /// - /// An alias for the RSA key wrap algorithm. - /// - [EnumMember(Value= "RSA-OAEP-256")] - RsaOaep256 //Alias for RSA - } -} + [EnumMember(Value = "chacha20-poly1305")] + ChaCha20Poly1305 +}; + +/// +/// The algorithm used for key wrapping cryptographic operations. +/// +public enum KeyWrapAlgorithm +{ + /// + /// Represents the AES key wrap algorithm. + /// + [EnumMember(Value="A256KW")] + Aes, + /// + /// An alias for the AES key wrap algorithm. + /// + [EnumMember(Value="A256KW")] + A256kw, + /// + /// Represents the AES 128 CBC key wrap algorithm. + /// + [EnumMember(Value="A128CBC")] + A128cbc, + /// + /// Represents the AES 192 CBC key wrap algorithm. + /// + [EnumMember(Value="A192CBC")] + A192cbc, + /// + /// Represents the AES 256 CBC key wrap algorithm. + /// + [EnumMember(Value="A256CBC")] + A256cbc, + /// + /// Represents the RSA key wrap algorithm. + /// + [EnumMember(Value= "RSA-OAEP-256")] + Rsa, + /// + /// An alias for the RSA key wrap algorithm. + /// + [EnumMember(Value= "RSA-OAEP-256")] + RsaOaep256 //Alias for RSA +} \ No newline at end of file diff --git a/src/Dapr.Client/CryptographyOptions.cs b/src/Dapr.Client/CryptographyOptions.cs index ae94a8f2..15f6ca7c 100644 --- a/src/Dapr.Client/CryptographyOptions.cs +++ b/src/Dapr.Client/CryptographyOptions.cs @@ -1,80 +1,63 @@ #nullable enable using System; -namespace Dapr.Client +namespace Dapr.Client; + +/// +/// A collection of options used to configure how encryption cryptographic operations are performed. +/// +/// +public class EncryptionOptions(KeyWrapAlgorithm keyWrapAlgorithm) { /// - /// A collection of options used to configure how encryption cryptographic operations are performed. + /// The name of the algorithm used to wrap the encryption key. /// - public class EncryptionOptions + public KeyWrapAlgorithm KeyWrapAlgorithm { get; set; } = keyWrapAlgorithm; + + private int streamingBlockSizeInBytes = 4 * 1024; // 4 KB + /// + /// The size of the block in bytes used to send data to the sidecar for cryptography operations. + /// + /// + /// This defaults to 4KB and generally should not exceed 64KB. + /// + public int StreamingBlockSizeInBytes { - /// - /// Creates a new instance of the . - /// - /// - public EncryptionOptions(KeyWrapAlgorithm keyWrapAlgorithm) + get => streamingBlockSizeInBytes; + set { - KeyWrapAlgorithm = keyWrapAlgorithm; + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(value); + streamingBlockSizeInBytes = value; } - - /// - /// The name of the algorithm used to wrap the encryption key. - /// - public KeyWrapAlgorithm KeyWrapAlgorithm { get; set; } - - private int streamingBlockSizeInBytes = 4 * 1024; // 4 KB - /// - /// The size of the block in bytes used to send data to the sidecar for cryptography operations. - /// - /// - /// This defaults to 4KB and generally should not exceed 64KB. - /// - public int StreamingBlockSizeInBytes - { - get => streamingBlockSizeInBytes; - set - { - if (value <= 0) - { - throw new ArgumentOutOfRangeException(nameof(value)); - } - - streamingBlockSizeInBytes = value; - } - } - - /// - /// The optional name (and optionally a version) of the key specified to use during decryption. - /// - public string? DecryptionKeyName { get; set; } = null; - - /// - /// The name of the cipher to use for the encryption operation. - /// - public DataEncryptionCipher EncryptionCipher { get; set; } = DataEncryptionCipher.AesGcm; } + + /// + /// The optional name (and optionally a version) of the key specified to use during decryption. + /// + public string? DecryptionKeyName { get; set; } = null; /// - /// A collection fo options used to configure how decryption cryptographic operations are performed. + /// The name of the cipher to use for the encryption operation. /// - public class DecryptionOptions + public DataEncryptionCipher EncryptionCipher { get; set; } = DataEncryptionCipher.AesGcm; +} + +/// +/// A collection fo options used to configure how decryption cryptographic operations are performed. +/// +public class DecryptionOptions +{ + private int streamingBlockSizeInBytes = 4 * 1024; // 4KB + /// + /// The size of the block in bytes used to send data to the sidecar for cryptography operations. + /// + public int StreamingBlockSizeInBytes { - private int streamingBlockSizeInBytes = 4 * 1024; // 4KB - /// - /// The size of the block in bytes used to send data to the sidecar for cryptography operations. - /// - public int StreamingBlockSizeInBytes + get => streamingBlockSizeInBytes; + set { - get => streamingBlockSizeInBytes; - set - { - if (value <= 0) - { - throw new ArgumentOutOfRangeException(nameof(value)); - } - - streamingBlockSizeInBytes = value; - } + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(value); + streamingBlockSizeInBytes = value; } } } diff --git a/src/Dapr.Client/DaprApiException.cs b/src/Dapr.Client/DaprApiException.cs index 75fc2cf7..adb3021a 100644 --- a/src/Dapr.Client/DaprApiException.cs +++ b/src/Dapr.Client/DaprApiException.cs @@ -11,125 +11,124 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr -{ - using System; - using System.Runtime.Serialization; +namespace Dapr; + +using System; +using System.Runtime.Serialization; + +/// +/// Exception for Dapr operations. +/// +[Serializable] +public class DaprApiException : DaprException +{ + /// + /// Initializes a new instance of the class with error code 'UNKNOWN'"/>. + /// + public DaprApiException() + : this(errorCode: null, false) + { + } /// - /// Exception for Dapr operations. + /// Initializes a new instance of the class with error code 'UNKNOWN' and a specified error message. /// - [Serializable] - public class DaprApiException : DaprException - { - /// - /// Initializes a new instance of the class with error code 'UNKNOWN'"/>. - /// - public DaprApiException() - : this(errorCode: null, false) - { - } + /// The error message that explains the reason for the exception. + public DaprApiException(string message) + : this(message, errorCode: null, false) + { + } - /// - /// Initializes a new instance of the class with error code 'UNKNOWN' and a specified error message. - /// - /// The error message that explains the reason for the exception. - public DaprApiException(string message) - : this(message, errorCode: null, false) - { - } + /// + /// Initializes a new instance of the class with a specified error + /// message and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception. + public DaprApiException(string message, Exception innerException) + : this(message, innerException, errorCode: null, false) + { + } - /// - /// Initializes a new instance of the class with a specified error - /// message and a reference to the inner exception that is the cause of this exception. - /// - /// The error message that explains the reason for the exception. - /// The exception that is the cause of the current exception. - public DaprApiException(string message, Exception innerException) - : this(message, innerException, errorCode: null, false) - { - } + /// + /// Initializes a new instance of the class with a specified error code. + /// + /// The error code associated with the exception. + /// True, if the exception is to be treated as an transient exception. + public DaprApiException(string errorCode, bool isTransient) + : this(string.Empty, errorCode, isTransient) + { + } - /// - /// Initializes a new instance of the class with a specified error code. - /// - /// The error code associated with the exception. - /// True, if the exception is to be treated as an transient exception. - public DaprApiException(string errorCode, bool isTransient) - : this(string.Empty, errorCode, isTransient) - { - } + /// + /// Initializes a new instance of the class with specified error message and error code. + /// + /// The error message that explains the reason for the exception. + /// The error code associated with the exception. + /// Indicating if its an transient exception. + public DaprApiException(string message, string errorCode, bool isTransient) + : this(message, null, errorCode, isTransient) + { + } - /// - /// Initializes a new instance of the class with specified error message and error code. - /// - /// The error message that explains the reason for the exception. - /// The error code associated with the exception. - /// Indicating if its an transient exception. - public DaprApiException(string message, string errorCode, bool isTransient) - : this(message, null, errorCode, isTransient) - { - } + /// + /// Initializes a new instance of the class. + /// Initializes a new instance of class + /// with a specified error message, a reference to the inner exception that is the cause of this exception, and a specified error code. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception or null if no inner exception is specified. The class provides more details about the inner exception.. + /// The error code associated with the exception. + /// Indicating if its an transient exception. + public DaprApiException(string message, Exception inner, string errorCode, bool isTransient) + : base(message, inner) + { + this.ErrorCode = errorCode ?? "UNKNOWN"; + this.IsTransient = isTransient; + } - /// - /// Initializes a new instance of the class. - /// Initializes a new instance of class - /// with a specified error message, a reference to the inner exception that is the cause of this exception, and a specified error code. - /// - /// The error message that explains the reason for the exception. - /// The exception that is the cause of the current exception or null if no inner exception is specified. The class provides more details about the inner exception.. - /// The error code associated with the exception. - /// Indicating if its an transient exception. - public DaprApiException(string message, Exception inner, string errorCode, bool isTransient) - : base(message, inner) - { - this.ErrorCode = errorCode ?? "UNKNOWN"; - this.IsTransient = isTransient; - } - - /// - /// Initializes a new instance of the class with a specified context. - /// - /// The object that contains serialized object data of the exception being thrown. - /// The object that contains contextual information about the source or destination. The context parameter is reserved for future use and can be null. + /// + /// Initializes a new instance of the class with a specified context. + /// + /// The object that contains serialized object data of the exception being thrown. + /// The object that contains contextual information about the source or destination. The context parameter is reserved for future use and can be null. #if NET8_0_OR_GREATER [Obsolete(DiagnosticId = "SYSLIB0051")] // add this attribute to the serialization ctor #endif - protected DaprApiException(SerializationInfo info, StreamingContext context) - : base(info, context) + protected DaprApiException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + if (info != null) { - if (info != null) - { - this.ErrorCode = (string)info.GetValue(nameof(this.ErrorCode), typeof(string)); - this.IsTransient = info.GetBoolean(nameof(this.IsTransient)); - } + this.ErrorCode = (string)info.GetValue(nameof(this.ErrorCode), typeof(string)); + this.IsTransient = info.GetBoolean(nameof(this.IsTransient)); } + } - /// - /// Gets the error code parameter. - /// - /// The error code associated with the exception. - public string ErrorCode { get; } + /// + /// Gets the error code parameter. + /// + /// The error code associated with the exception. + public string ErrorCode { get; } - /// - /// Gets a value indicating whether gets exception is Transient and operation can be retried. - /// - /// Value indicating whether the exception is transient or not. - public bool IsTransient { get; } = false; + /// + /// Gets a value indicating whether gets exception is Transient and operation can be retried. + /// + /// Value indicating whether the exception is transient or not. + public bool IsTransient { get; } = false; - /// + /// #if NET8_0_OR_GREATER [Obsolete(DiagnosticId = "SYSLIB0051")] // add this attribute to GetObjectData #endif - public override void GetObjectData(SerializationInfo info, StreamingContext context) - { - base.GetObjectData(info, context); + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); - if (info != null) - { - info.AddValue(nameof(this.ErrorCode), this.ErrorCode); - info.AddValue(nameof(this.IsTransient), this.IsTransient); - } + if (info != null) + { + info.AddValue(nameof(this.ErrorCode), this.ErrorCode); + info.AddValue(nameof(this.IsTransient), this.IsTransient); } } -} +} \ No newline at end of file diff --git a/src/Dapr.Client/DaprClient.cs b/src/Dapr.Client/DaprClient.cs index 6be31a64..96fe0cfd 100644 --- a/src/Dapr.Client/DaprClient.cs +++ b/src/Dapr.Client/DaprClient.cs @@ -11,6 +11,7 @@ // limitations under the License. // ------------------------------------------------------------------------ +#nullable enable using System; using System.Collections.Generic; using System.IO; @@ -26,1403 +27,1399 @@ using Grpc.Core; using Grpc.Core.Interceptors; using Grpc.Net.Client; -namespace Dapr.Client +namespace Dapr.Client; + +/// +/// +/// Defines client methods for interacting with Dapr endpoints. +/// Use to create . +/// +/// +/// Implementations of implement because the client +/// accesses network resources. For best performance, create a single long-lived client instance and share +/// it for the lifetime of the application. Avoid creating and disposing a client instance for each operation +/// that the application performs - this can lead to socket exhaustion and other problems. +/// +/// +public abstract class DaprClient : IDisposable { + private bool disposed; + + /// + /// Gets the used for JSON serialization operations. + /// + public abstract JsonSerializerOptions JsonSerializerOptions { get; } + /// /// - /// Defines client methods for interacting with Dapr endpoints. - /// Use to create . + /// Creates an that can be used to perform Dapr service + /// invocation using objects. /// /// - /// Implementations of implement because the client - /// accesses network resources. For best performance, create a single long-lived client instance and share - /// it for the lifetime of the application. Avoid creating and disposing a client instance for each operation - /// that the application performs - this can lead to socket exhaustion and other problems. + /// 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 + /// and the path portion of the URI rewritten to follow the format of a Dapr service invocation request. /// /// - public abstract class DaprClient : IDisposable + /// + /// 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. + /// + /// The HTTP endpoint of the Dapr process to use for service invocation calls. + /// The token to be added to all request headers to Dapr runtime. + /// An that can be used to perform service invocation requests. + /// + /// + /// The object is intended to be a long-lived and holds access to networking resources. + /// Since the value of will not change during the lifespan of the application, + /// a single client object can be reused for the life of the application. + /// + /// + public static HttpClient CreateInvokeHttpClient(string? appId = null, string? daprEndpoint = null, string? daprApiToken = null) { - private bool disposed; - - /// - /// Gets the used for JSON serialization operations. - /// - public abstract JsonSerializerOptions JsonSerializerOptions { get; } - - /// - /// - /// 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 - /// 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. - /// - /// The HTTP endpoint of the Dapr process to use for service invocation calls. - /// The token to be added to all request headers to Dapr runtime. - /// An that can be used to perform service invocation requests. - /// - /// - /// The object is intended to be a long-lived and holds access to networking resources. - /// Since the value of will not change during the lifespan of the application, - /// a single client object can be reused for the life of the application. - /// - /// - public static HttpClient CreateInvokeHttpClient(string appId = null, string daprEndpoint = null, string daprApiToken = null) + var handler = new InvocationHandler() { - var handler = new InvocationHandler() - { - InnerHandler = new HttpClientHandler(), - DaprApiToken = daprApiToken, - DefaultAppId = appId, - }; + InnerHandler = new HttpClientHandler(), + DaprApiToken = daprApiToken, + DefaultAppId = appId, + }; - if (daprEndpoint is string) + if (daprEndpoint is not null) + { + // DaprEndpoint performs validation. + handler.DaprEndpoint = daprEndpoint; + } + + var httpClient = new HttpClient(handler); + httpClient.DefaultRequestHeaders.UserAgent.Add(UserAgent()); + + if (appId is not null) + { + try { - // DaprEndpoint performs validation. - handler.DaprEndpoint = daprEndpoint; + httpClient.BaseAddress = new Uri($"http://{appId}"); } - - var httpClient = new HttpClient(handler); - httpClient.DefaultRequestHeaders.UserAgent.Add(UserAgent()); - - if (appId is string) + catch (UriFormatException inner) { - try - { - httpClient.BaseAddress = new Uri($"http://{appId}"); - } - catch (UriFormatException inner) - { - throw new ArgumentException("The appId must be a valid hostname.", nameof(appId), inner); - } + throw new ArgumentException("The appId must be a valid hostname.", nameof(appId), inner); } - - return httpClient; } - /// - /// - /// Creates an that can be used to perform locally defined gRPC calls - /// using the Dapr sidecar as a proxy. - /// - /// - /// The created is used to intercept a with an - /// . The interceptor inserts the and, if present, - /// the into the request's metadata. - /// - /// - /// - /// The appId that is targetted by Dapr for gRPC invocations. - /// - /// - /// Optional gRPC endpoint for calling Dapr, defaults to . - /// - /// - /// Optional token to be attached to all requests, defaults to . - /// - /// An to be used for proxied gRPC calls through Dapr. - /// - /// - /// As the will remain constant, a single instance of the - /// can be used throughout the lifetime of the application. - /// - /// - public static CallInvoker CreateInvocationInvoker(string appId, string daprEndpoint = null, string daprApiToken = null) - { - var channel = GrpcChannel.ForAddress(daprEndpoint ?? DaprDefaults.GetDefaultGrpcEndpoint()); - return channel.Intercept(new InvocationInterceptor(appId, daprApiToken ?? DaprDefaults.GetDefaultDaprApiToken(null))); - } + return httpClient; + } - internal static KeyValuePair? GetDaprApiTokenHeader(string apiToken) - { - return string.IsNullOrWhiteSpace(apiToken) - ? null - : new KeyValuePair("dapr-api-token", apiToken); - } + /// + /// + /// Creates an that can be used to perform locally defined gRPC calls + /// using the Dapr sidecar as a proxy. + /// + /// + /// The created is used to intercept a with an + /// . The interceptor inserts the and, if present, + /// the into the request's metadata. + /// + /// + /// + /// The appId that is targetted by Dapr for gRPC invocations. + /// + /// + /// Optional gRPC endpoint for calling Dapr, defaults to . + /// + /// + /// Optional token to be attached to all requests, defaults to . + /// + /// An to be used for proxied gRPC calls through Dapr. + /// + /// + /// As the will remain constant, a single instance of the + /// can be used throughout the lifetime of the application. + /// + /// + public static CallInvoker CreateInvocationInvoker(string appId, string? daprEndpoint = null, string? daprApiToken = null) + { + var channel = GrpcChannel.ForAddress(daprEndpoint ?? DaprDefaults.GetDefaultGrpcEndpoint()); + return channel.Intercept(new InvocationInterceptor(appId, daprApiToken ?? DaprDefaults.GetDefaultDaprApiToken(null))); + } - /// - /// Publishes an event to the specified topic. - /// - /// The name of the pubsub component to use. - /// The name of the topic the request should be published to. - /// The data that will be JSON serialized and provided as the event payload. - /// A that can be used to cancel the operation. - /// The type of the data that will be JSON serialized and provided as the event payload. - /// A that will complete when the operation has completed. - public abstract Task PublishEventAsync( - string pubsubName, - string topicName, - TData data, - CancellationToken cancellationToken = default); + internal static KeyValuePair? GetDaprApiTokenHeader(string apiToken) + { + return string.IsNullOrWhiteSpace(apiToken) + ? null + : new KeyValuePair("dapr-api-token", apiToken); + } - /// - /// Publishes an event to the specified topic. - /// - /// The name of the pubsub component to use. - /// The name of the topic the request should be published to. - /// The data that will be JSON serialized and provided as the event payload. - /// - /// A collection of metadata key-value pairs that will be provided to the pubsub. The valid metadata keys and values - /// are determined by the type of pubsub component used. - /// - /// A that can be used to cancel the operation. - /// The type of the data that will be JSON serialized and provided as the event payload. - /// A that will complete when the operation has completed. - public abstract Task PublishEventAsync( - string pubsubName, - string topicName, - TData data, - Dictionary metadata, - CancellationToken cancellationToken = default); + /// + /// Publishes an event to the specified topic. + /// + /// The name of the pubsub component to use. + /// The name of the topic the request should be published to. + /// The data that will be JSON serialized and provided as the event payload. + /// A that can be used to cancel the operation. + /// The type of the data that will be JSON serialized and provided as the event payload. + /// A that will complete when the operation has completed. + public abstract Task PublishEventAsync( + string pubsubName, + string topicName, + TData data, + CancellationToken cancellationToken = default); - /// - /// Publishes an event to the specified topic. - /// - /// The name of the pubsub component to use. - /// The name of the topic the request should be published to. - /// A that can be used to cancel the operation. - /// A that will complete when the operation has completed. - public abstract Task PublishEventAsync( - string pubsubName, - string topicName, - CancellationToken cancellationToken = default); + /// + /// Publishes an event to the specified topic. + /// + /// The name of the pubsub component to use. + /// The name of the topic the request should be published to. + /// The data that will be JSON serialized and provided as the event payload. + /// + /// A collection of metadata key-value pairs that will be provided to the pubsub. The valid metadata keys and values + /// are determined by the type of pubsub component used. + /// + /// A that can be used to cancel the operation. + /// The type of the data that will be JSON serialized and provided as the event payload. + /// A that will complete when the operation has completed. + public abstract Task PublishEventAsync( + string pubsubName, + string topicName, + TData data, + Dictionary metadata, + CancellationToken cancellationToken = default); - /// - /// Publishes an event to the specified topic. - /// - /// The name of the pubsub component to use. - /// The name of the topic the request should be published to. - /// A collection of metadata key-value pairs that will be provided to the pubsub. The valid metadata keys and values are determined by the type of binding used. - /// A that can be used to cancel the operation. - /// A that will complete when the operation has completed. - public abstract Task PublishEventAsync( - string pubsubName, - string topicName, - Dictionary metadata, - CancellationToken cancellationToken = default); + /// + /// Publishes an event to the specified topic. + /// + /// The name of the pubsub component to use. + /// The name of the topic the request should be published to. + /// A that can be used to cancel the operation. + /// A that will complete when the operation has completed. + public abstract Task PublishEventAsync( + string pubsubName, + string topicName, + CancellationToken cancellationToken = default); - /// - /// // Bulk Publishes multiple events to the specified topic. - /// - /// The name of the pubsub component to use. - /// The name of the topic the request should be published to. - /// The list of events to be published. - /// The metadata to be set at the request level for the request. - /// A that can be used to cancel the operation. - /// A that will complete when the operation has completed. - public abstract Task> BulkPublishEventAsync( - string pubsubName, - string topicName, - IReadOnlyList events, - Dictionary metadata = default, - CancellationToken cancellationToken = default); + /// + /// Publishes an event to the specified topic. + /// + /// The name of the pubsub component to use. + /// The name of the topic the request should be published to. + /// A collection of metadata key-value pairs that will be provided to the pubsub. The valid metadata keys and values are determined by the type of binding used. + /// A that can be used to cancel the operation. + /// A that will complete when the operation has completed. + public abstract Task PublishEventAsync( + string pubsubName, + string topicName, + Dictionary metadata, + CancellationToken cancellationToken = default); - /// - /// Publishes an event to the specified topic. - /// - /// The name of the pubsub component to use. - /// The name of the topic the request should be published to. - /// The raw byte payload to inlcude in the message. - /// The content type of the given bytes, defaults to application/json. - /// A collection of metadata key-value pairs that will be provided to the pubsub. The valid metadata keys and values are determined by the type of binding used. - /// A that can be used to cancel the operation. - /// A that will complete when the operation has completed. - public abstract Task PublishByteEventAsync( - string pubsubName, - string topicName, - ReadOnlyMemory data, - string dataContentType = Constants.ContentTypeApplicationJson, - Dictionary metadata = default, - CancellationToken cancellationToken = default); + /// + /// // Bulk Publishes multiple events to the specified topic. + /// + /// The name of the pubsub component to use. + /// The name of the topic the request should be published to. + /// The list of events to be published. + /// The metadata to be set at the request level for the request. + /// A that can be used to cancel the operation. + /// A that will complete when the operation has completed. + public abstract Task> BulkPublishEventAsync( + string pubsubName, + string topicName, + IReadOnlyList events, + Dictionary? metadata = null, + CancellationToken cancellationToken = default); - /// - /// Invokes an output binding. - /// - /// The type of the data that will be JSON serialized and provided as the binding payload. - /// The name of the binding to sent the event to. - /// The type of operation to perform on the binding. - /// The data that will be JSON serialized and provided as the binding payload. - /// A collection of metadata key-value pairs that will be provided to the binding. The valid metadata keys and values are determined by the type of binding used. - /// A that can be used to cancel the operation. - /// A that will complete when the operation has completed. - public abstract Task InvokeBindingAsync( - string bindingName, - string operation, - TRequest data, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default); + /// + /// Publishes an event to the specified topic. + /// + /// The name of the pubsub component to use. + /// The name of the topic the request should be published to. + /// The raw byte payload to inlcude in the message. + /// The content type of the given bytes, defaults to application/json. + /// A collection of metadata key-value pairs that will be provided to the pubsub. The valid metadata keys and values are determined by the type of binding used. + /// A that can be used to cancel the operation. + /// A that will complete when the operation has completed. + public abstract Task PublishByteEventAsync( + string pubsubName, + string topicName, + ReadOnlyMemory data, + string dataContentType = Constants.ContentTypeApplicationJson, + Dictionary? metadata = null, + CancellationToken cancellationToken = default); - /// - /// Invokes an output binding. - /// - /// The type of the data that will be JSON serialized and provided as the binding payload. - /// The type of the data that will be JSON deserialized from the binding response. - /// The name of the binding to sent the event to. - /// The type of operation to perform on the binding. - /// The data that will be JSON serialized and provided as the binding payload. - /// A collection of metadata key-value pairs that will be provided to the binding. The valid metadata keys and values are determined by the type of binding used. - /// A that can be used to cancel the operation. - /// A that will complete when the operation has completed. - public abstract Task InvokeBindingAsync( - string bindingName, - string operation, - TRequest data, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default); + /// + /// Invokes an output binding. + /// + /// The type of the data that will be JSON serialized and provided as the binding payload. + /// The name of the binding to sent the event to. + /// The type of operation to perform on the binding. + /// The data that will be JSON serialized and provided as the binding payload. + /// A collection of metadata key-value pairs that will be provided to the binding. The valid metadata keys and values are determined by the type of binding used. + /// A that can be used to cancel the operation. + /// A that will complete when the operation has completed. + public abstract Task InvokeBindingAsync( + string bindingName, + string operation, + TRequest data, + IReadOnlyDictionary? metadata = null, + CancellationToken cancellationToken = default); - /// - /// Invokes a binding with the provided . This method allows for control of the binding - /// input and output using raw bytes. - /// - /// The to send. - /// A that can be used to cancel the operation. - /// A that will complete when the operation has completed. - public abstract Task InvokeBindingAsync( - BindingRequest request, - CancellationToken cancellationToken = default); + /// + /// Invokes an output binding. + /// + /// The type of the data that will be JSON serialized and provided as the binding payload. + /// The type of the data that will be JSON deserialized from the binding response. + /// The name of the binding to sent the event to. + /// The type of operation to perform on the binding. + /// The data that will be JSON serialized and provided as the binding payload. + /// A collection of metadata key-value pairs that will be provided to the binding. The valid metadata keys and values are determined by the type of binding used. + /// A that can be used to cancel the operation. + /// A that will complete when the operation has completed. + public abstract Task InvokeBindingAsync( + string bindingName, + string operation, + TRequest data, + IReadOnlyDictionary? metadata = null, + CancellationToken cancellationToken = default); - /// - /// Creates an that can be used to perform service invocation for the - /// application identified by and invokes the method specified by - /// with the POST HTTP method. - /// - /// The Dapr application id to invoke the method on. - /// The name of the method to invoke. - /// An for use with SendInvokeMethodRequestAsync. - public HttpRequestMessage CreateInvokeMethodRequest(string appId, string methodName) - { - return CreateInvokeMethodRequest(HttpMethod.Post, appId, methodName); - } + /// + /// Invokes a binding with the provided . This method allows for control of the binding + /// input and output using raw bytes. + /// + /// The to send. + /// A that can be used to cancel the operation. + /// A that will complete when the operation has completed. + public abstract Task InvokeBindingAsync( + BindingRequest request, + CancellationToken cancellationToken = default); - /// - /// Creates an that can be used to perform service invocation for the - /// application identified by and invokes the method specified by - /// with the POST HTTP method. - /// - /// 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 HttpRequestMessage CreateInvokeMethodRequest(string appId, string methodName, IReadOnlyCollection> queryStringParameters) - { - return CreateInvokeMethodRequest(HttpMethod.Post, appId, methodName, queryStringParameters); - } + /// + /// Creates an that can be used to perform service invocation for the + /// application identified by and invokes the method specified by + /// with the POST HTTP method. + /// + /// The Dapr application id to invoke the method on. + /// The name of the method to invoke. + /// An for use with SendInvokeMethodRequestAsync. + public HttpRequestMessage CreateInvokeMethodRequest(string appId, string methodName) + { + return CreateInvokeMethodRequest(HttpMethod.Post, appId, methodName); + } + + /// + /// Creates an that can be used to perform service invocation for the + /// application identified by and invokes the method specified by + /// with the POST HTTP method. + /// + /// 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 HttpRequestMessage CreateInvokeMethodRequest(string appId, string methodName, IReadOnlyCollection> queryStringParameters) + { + return CreateInvokeMethodRequest(HttpMethod.Post, appId, methodName, queryStringParameters); + } - /// - /// 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 abstract HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMethod, string appId, string methodName); + /// + /// 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 abstract HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMethod, string appId, string methodName); - /// - /// 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 abstract HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMethod, string appId, - string methodName, IReadOnlyCollection> queryStringParameters); + /// + /// 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 abstract HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMethod, string appId, + string methodName, IReadOnlyCollection> queryStringParameters); - /// - /// Creates an that can be used to perform service invocation for the - /// application identified by and invokes the method specified by - /// with the POST HTTP method 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 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. - /// An for use with SendInvokeMethodRequestAsync. - public HttpRequestMessage CreateInvokeMethodRequest(string appId, string methodName, TRequest data) - { - return CreateInvokeMethodRequest(HttpMethod.Post, appId, methodName, new List>(), data); - } + /// + /// Creates an that can be used to perform service invocation for the + /// application identified by and invokes the method specified by + /// with the POST HTTP method 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 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. + /// An for use with SendInvokeMethodRequestAsync. + public HttpRequestMessage CreateInvokeMethodRequest(string appId, string methodName, TRequest data) + { + return CreateInvokeMethodRequest(HttpMethod.Post, appId, methodName, new List>(), data); + } - /// - /// 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 abstract HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMethod, string appId, string methodName, IReadOnlyCollection> queryStringParameters, TRequest data); + /// + /// 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 abstract HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMethod, string appId, string methodName, IReadOnlyCollection> queryStringParameters, TRequest data); - /// - /// Perform health-check of Dapr sidecar. Return 'true' if sidecar is healthy. Otherwise 'false'. - /// CheckHealthAsync handle and will return 'false' if error will occur on transport level - /// - /// A that can be used to cancel the operation. - /// A that will return the value when the operation has completed. - public abstract Task CheckHealthAsync(CancellationToken cancellationToken = default); + /// + /// Perform health-check of Dapr sidecar. Return 'true' if sidecar is healthy. Otherwise 'false'. + /// CheckHealthAsync handle and will return 'false' if error will occur on transport level + /// + /// A that can be used to cancel the operation. + /// A that will return the value when the operation has completed. + public abstract Task CheckHealthAsync(CancellationToken cancellationToken = default); - /// - /// Perform health-check of Dapr sidecar's outbound APIs. Return 'true' if the sidecar is healthy. Otherwise false. This method should - /// be used over when the health of Dapr is being checked before it starts. This - /// health endpoint indicates that Dapr has stood up its APIs and is currently waiting on this application to report fully healthy. - /// - /// A that can be used to cancel the operation. - /// A that will return the value when the operation has completed. - public abstract Task CheckOutboundHealthAsync(CancellationToken cancellationToken = default); + /// + /// Perform health-check of Dapr sidecar's outbound APIs. Return 'true' if the sidecar is healthy. Otherwise false. This method should + /// be used over when the health of Dapr is being checked before it starts. This + /// health endpoint indicates that Dapr has stood up its APIs and is currently waiting on this application to report fully healthy. + /// + /// A that can be used to cancel the operation. + /// A that will return the value when the operation has completed. + public abstract Task CheckOutboundHealthAsync(CancellationToken cancellationToken = default); - /// - /// Calls until the sidecar is reporting as healthy. If the sidecar - /// does not become healthy, an exception will be thrown. - /// - /// A that can be used to cancel the operation. - /// A that will return when the operation has completed. - public abstract Task WaitForSidecarAsync(CancellationToken cancellationToken = default); + /// + /// Calls until the sidecar is reporting as healthy. If the sidecar + /// does not become healthy, an exception will be thrown. + /// + /// A that can be used to cancel the operation. + /// A that will return when the operation has completed. + public abstract Task WaitForSidecarAsync(CancellationToken cancellationToken = default); - /// - /// Send a command to the Dapr Sidecar telling it to shutdown. - /// - /// A that can be used to cancel the operation. - /// A that will return when the operation has completed. - public abstract Task ShutdownSidecarAsync(CancellationToken cancellationToken = default); + /// + /// Send a command to the Dapr Sidecar telling it to shutdown. + /// + /// A that can be used to cancel the operation. + /// A that will return when the operation has completed. + public abstract Task ShutdownSidecarAsync(CancellationToken cancellationToken = default); - /// - /// Calls the sidecar's metadata endpoint which returns information including: - /// - /// - /// The sidecar's ID. - /// - /// - /// The registered/active actors if any. - /// - /// - /// Registered components including name, type, version, and information on capabilities if present. - /// - /// - /// Any extended metadata that has been set via - /// - /// - /// - /// A that can be used to cancel the operation. - /// A that will return the value when the operation has completed. - public abstract Task GetMetadataAsync(CancellationToken cancellationToken = default); + /// + /// Calls the sidecar's metadata endpoint which returns information including: + /// + /// + /// The sidecar's ID. + /// + /// + /// The registered/active actors if any. + /// + /// + /// Registered components including name, type, version, and information on capabilities if present. + /// + /// + /// Any extended metadata that has been set via + /// + /// + /// + /// A that can be used to cancel the operation. + /// A that will return the value when the operation has completed. + public abstract Task GetMetadataAsync(CancellationToken cancellationToken = default); - /// - /// Perform service add extended metadata to the Dapr sidecar. - /// - /// Custom attribute name - /// Custom attribute value - /// A that can be used to cancel the operation. - /// A that will return the value when the operation has completed. - public abstract Task SetMetadataAsync(string attributeName, string attributeValue, CancellationToken cancellationToken = default); + /// + /// Perform service add extended metadata to the Dapr sidecar. + /// + /// Custom attribute name + /// Custom attribute value + /// A that can be used to cancel the operation. + /// A that will return the value when the operation has completed. + public abstract Task SetMetadataAsync(string attributeName, string attributeValue, CancellationToken cancellationToken = default); - /// - /// Perform service invocation using the request provided by . The response will - /// be returned without performing any validation on the status code. - /// - /// - /// The to send. The request must be a conforming Dapr service invocation request. - /// Use the CreateInvokeMethodRequest to create service invocation requests. - /// - /// A that can be used to cancel the operation. - /// A that will return the value when the operation has completed. - public abstract Task InvokeMethodWithResponseAsync(HttpRequestMessage request, CancellationToken cancellationToken = default); + /// + /// Perform service invocation using the request provided by . The response will + /// be returned without performing any validation on the status code. + /// + /// + /// The to send. The request must be a conforming Dapr service invocation request. + /// Use the CreateInvokeMethodRequest to create service invocation requests. + /// + /// A that can be used to cancel the operation. + /// A that will return the value when the operation has completed. + public abstract Task InvokeMethodWithResponseAsync(HttpRequestMessage request, CancellationToken cancellationToken = default); + + /// + /// + /// 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 HTTP endpoint 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. + /// + /// + public abstract HttpClient CreateInvokableHttpClient(string? appId = null); -#nullable enable - /// - /// - /// 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 HTTP endpoint 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. - /// - /// - public abstract HttpClient CreateInvokableHttpClient(string? appId = null); -#nullable disable + /// + /// Perform service invocation using the request provided by . If the response has a non-success + /// status an exception will be thrown. + /// + /// + /// The to send. The request must be a conforming Dapr service invocation request. + /// Use the CreateInvokeMethodRequest to create service invocation requests. + /// + /// A that can be used to cancel the operation. + /// A that will return when the operation has completed. + public abstract Task InvokeMethodAsync(HttpRequestMessage request, CancellationToken cancellationToken = default); - /// - /// Perform service invocation using the request provided by . If the response has a non-success - /// status an exception will be thrown. - /// - /// - /// The to send. The request must be a conforming Dapr service invocation request. - /// Use the CreateInvokeMethodRequest to create service invocation requests. - /// - /// A that can be used to cancel the operation. - /// A that will return when the operation has completed. - public abstract Task InvokeMethodAsync(HttpRequestMessage request, CancellationToken cancellationToken = default); + /// + /// Perform service invocation using the request provided by . If the response has a success + /// status code the body will be deserialized using JSON to a value of type ; + /// otherwise an exception will be thrown. + /// + /// The type of the data that will be JSON deserialized from the response body. + /// + /// The to send. The request must be a conforming Dapr service invocation request. + /// Use the CreateInvokeMethodRequest to create service invocation requests. + /// + /// A that can be used to cancel the operation. + /// A that will return the value when the operation has completed. + public abstract Task InvokeMethodAsync(HttpRequestMessage request, CancellationToken cancellationToken = default); - /// - /// Perform service invocation using the request provided by . If the response has a success - /// status code the body will be deserialized using JSON to a value of type ; - /// otherwise an exception will be thrown. - /// - /// The type of the data that will be JSON deserialized from the response body. - /// - /// The to send. The request must be a conforming Dapr service invocation request. - /// Use the CreateInvokeMethodRequest to create service invocation requests. - /// - /// A that can be used to cancel the operation. - /// A that will return the value when the operation has completed. - public abstract Task InvokeMethodAsync(HttpRequestMessage request, CancellationToken cancellationToken = default); + /// + /// Perform service invocation for the application identified by and invokes the method + /// specified by with the POST HTTP method and an empty request body. + /// If the response has a non-success status code an exception will be thrown. + /// + /// The Dapr application id to invoke the method on. + /// The name of the method to invoke. + /// A that can be used to cancel the operation. + /// A that will return when the operation has completed. + public Task InvokeMethodAsync( + string appId, + string methodName, + CancellationToken cancellationToken = default) + { + var request = CreateInvokeMethodRequest(appId, methodName); + return InvokeMethodAsync(request, cancellationToken); + } - /// - /// Perform service invocation for the application identified by and invokes the method - /// specified by with the POST HTTP method and an empty request body. - /// If the response has a non-success status code an exception will be thrown. - /// - /// The Dapr application id to invoke the method on. - /// The name of the method to invoke. - /// A that can be used to cancel the operation. - /// A that will return when the operation has completed. - public Task InvokeMethodAsync( - string appId, - string methodName, - CancellationToken cancellationToken = default) - { - var request = CreateInvokeMethodRequest(appId, methodName); - return InvokeMethodAsync(request, cancellationToken); - } + /// + /// Perform service invocation for the application identified by and invokes the method + /// specified by with the HTTP method specified by + /// and an empty request body. If the response has a non-success status code an exception will be thrown. + /// + /// The to use for the invocation request. + /// The Dapr application id to invoke the method on. + /// The name of the method to invoke. + /// A that can be used to cancel the operation. + /// A that will return when the operation has completed. + public Task InvokeMethodAsync( + HttpMethod httpMethod, + string appId, + string methodName, + CancellationToken cancellationToken = default) + { + var request = CreateInvokeMethodRequest(httpMethod, appId, methodName); + return InvokeMethodAsync(request, cancellationToken); + } - /// - /// Perform service invocation for the application identified by and invokes the method - /// specified by with the HTTP method specified by - /// and an empty request body. If the response has a non-success status code an exception will be thrown. - /// - /// The to use for the invocation request. - /// The Dapr application id to invoke the method on. - /// The name of the method to invoke. - /// A that can be used to cancel the operation. - /// A that will return when the operation has completed. - public Task InvokeMethodAsync( - HttpMethod httpMethod, - string appId, - string methodName, - CancellationToken cancellationToken = default) - { - var request = CreateInvokeMethodRequest(httpMethod, appId, methodName); - return InvokeMethodAsync(request, cancellationToken); - } + /// + /// Perform service invocation for the application identified by and invokes the method + /// specified by with the POST HTTP method + /// and a JSON serialized request body specified by . If the response has a non-success + /// status code an exception will be thrown. + /// + /// The type of the data that will be JSON serialized and provided as the request body. + /// 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 that can be used to cancel the operation. + /// A that will return when the operation has completed. + public Task InvokeMethodAsync( + string appId, + string methodName, + TRequest data, + CancellationToken cancellationToken = default) + { + var request = CreateInvokeMethodRequest(appId, methodName, data); + return InvokeMethodAsync(request, cancellationToken); + } - /// - /// Perform service invocation for the application identified by and invokes the method - /// specified by with the POST HTTP method - /// and a JSON serialized request body specified by . If the response has a non-success - /// status code an exception will be thrown. - /// - /// The type of the data that will be JSON serialized and provided as the request body. - /// 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 that can be used to cancel the operation. - /// A that will return when the operation has completed. - public Task InvokeMethodAsync( - string appId, - string methodName, - TRequest data, - CancellationToken cancellationToken = default) - { - var request = CreateInvokeMethodRequest(appId, methodName, data); - return InvokeMethodAsync(request, cancellationToken); - } + /// + /// 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 . If the response has a non-success + /// status code an exception will be thrown. + /// + /// 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 that can be used to cancel the operation. + /// A that will return when the operation has completed. + public Task InvokeMethodAsync( + HttpMethod httpMethod, + string appId, + string methodName, + TRequest data, + CancellationToken cancellationToken = default) + { + var request = CreateInvokeMethodRequest(httpMethod, appId, methodName, new List>(), data); + return InvokeMethodAsync(request, cancellationToken); + } - /// - /// 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 . If the response has a non-success - /// status code an exception will be thrown. - /// - /// 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 that can be used to cancel the operation. - /// A that will return when the operation has completed. - public Task InvokeMethodAsync( - HttpMethod httpMethod, - string appId, - string methodName, - TRequest data, - CancellationToken cancellationToken = default) - { - var request = CreateInvokeMethodRequest(httpMethod, appId, methodName, new List>(), data); - return InvokeMethodAsync(request, cancellationToken); - } + /// + /// Perform service invocation for the application identified by and invokes the method + /// specified by with the POST HTTP method + /// and an empty request body. If the response has a success + /// status code the body will be deserialized using JSON to a value of type ; + /// otherwise an exception will be thrown. + /// + /// The type of the data that will be JSON deserialized from the response body. + /// The Dapr application id to invoke the method on. + /// The name of the method to invoke. + /// A that can be used to cancel the operation. + /// A that will return the value when the operation has completed. + public Task InvokeMethodAsync( + string appId, + string methodName, + CancellationToken cancellationToken = default) + { + var request = CreateInvokeMethodRequest(appId, methodName); + return InvokeMethodAsync(request, cancellationToken); + } - /// - /// Perform service invocation for the application identified by and invokes the method - /// specified by with the POST HTTP method - /// and an empty request body. If the response has a success - /// status code the body will be deserialized using JSON to a value of type ; - /// otherwise an exception will be thrown. - /// - /// The type of the data that will be JSON deserialized from the response body. - /// The Dapr application id to invoke the method on. - /// The name of the method to invoke. - /// A that can be used to cancel the operation. - /// A that will return the value when the operation has completed. - public Task InvokeMethodAsync( - string appId, - string methodName, - CancellationToken cancellationToken = default) - { - var request = CreateInvokeMethodRequest(appId, methodName); - return InvokeMethodAsync(request, cancellationToken); - } + /// + /// Perform service invocation for the application identified by and invokes the method + /// specified by with the HTTP method specified by + /// and an empty request body. If the response has a success + /// status code the body will be deserialized using JSON to a value of type ; + /// otherwise an exception will be thrown. + /// + /// The type of the data that will be JSON deserialized from the response body. + /// The to use for the invocation request. + /// The Dapr application id to invoke the method on. + /// The name of the method to invoke. + /// A that can be used to cancel the operation. + /// A that will return the value when the operation has completed. + public Task InvokeMethodAsync( + HttpMethod httpMethod, + string appId, + string methodName, + CancellationToken cancellationToken = default) + { + var request = CreateInvokeMethodRequest(httpMethod, appId, methodName); + return InvokeMethodAsync(request, cancellationToken); + } - /// - /// Perform service invocation for the application identified by and invokes the method - /// specified by with the HTTP method specified by - /// and an empty request body. If the response has a success - /// status code the body will be deserialized using JSON to a value of type ; - /// otherwise an exception will be thrown. - /// - /// The type of the data that will be JSON deserialized from the response body. - /// The to use for the invocation request. - /// The Dapr application id to invoke the method on. - /// The name of the method to invoke. - /// A that can be used to cancel the operation. - /// A that will return the value when the operation has completed. - public Task InvokeMethodAsync( - HttpMethod httpMethod, - string appId, - string methodName, - CancellationToken cancellationToken = default) - { - var request = CreateInvokeMethodRequest(httpMethod, appId, methodName); - return InvokeMethodAsync(request, cancellationToken); - } + /// + /// Perform service invocation for the application identified by and invokes the method + /// specified by with the POST HTTP method + /// and a JSON serialized request body specified by . If the response has a success + /// status code the body will be deserialized using JSON to a value of type ; + /// otherwise an exception will be thrown. + /// + /// The type of the data that will be JSON serialized and provided as the request body. + /// The type of the data that will be JSON deserialized from the response body. + /// 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 that can be used to cancel the operation. + /// A that will return the value when the operation has completed. + public Task InvokeMethodAsync( + string appId, + string methodName, + TRequest data, + CancellationToken cancellationToken = default) + { + var request = CreateInvokeMethodRequest(appId, methodName, data); + return InvokeMethodAsync(request, cancellationToken); + } - /// - /// Perform service invocation for the application identified by and invokes the method - /// specified by with the POST HTTP method - /// and a JSON serialized request body specified by . If the response has a success - /// status code the body will be deserialized using JSON to a value of type ; - /// otherwise an exception will be thrown. - /// - /// The type of the data that will be JSON serialized and provided as the request body. - /// The type of the data that will be JSON deserialized from the response body. - /// 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 that can be used to cancel the operation. - /// A that will return the value when the operation has completed. - public Task InvokeMethodAsync( - string appId, - string methodName, - TRequest data, - CancellationToken cancellationToken = default) - { - var request = CreateInvokeMethodRequest(appId, methodName, data); - return InvokeMethodAsync(request, cancellationToken); - } + /// + /// 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 . If the response has a success + /// status code the body will be deserialized using JSON to a value of type ; + /// otherwise an exception will be thrown. + /// + /// The type of the data that will be JSON serialized and provided as the request body. + /// The type of the data that will be JSON deserialized from the response 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 that can be used to cancel the operation. + /// A that will return the value when the operation has completed. + public Task InvokeMethodAsync( + HttpMethod httpMethod, + string appId, + string methodName, + TRequest data, + CancellationToken cancellationToken = default) + { + var request = CreateInvokeMethodRequest(httpMethod, appId, methodName, new List>(), data); + return InvokeMethodAsync(request, cancellationToken); + } - /// - /// 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 . If the response has a success - /// status code the body will be deserialized using JSON to a value of type ; - /// otherwise an exception will be thrown. - /// - /// The type of the data that will be JSON serialized and provided as the request body. - /// The type of the data that will be JSON deserialized from the response 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 that can be used to cancel the operation. - /// A that will return the value when the operation has completed. - public Task InvokeMethodAsync( - HttpMethod httpMethod, - string appId, - string methodName, - TRequest data, - CancellationToken cancellationToken = default) - { - var request = CreateInvokeMethodRequest(httpMethod, appId, methodName, new List>(), data); - return InvokeMethodAsync(request, cancellationToken); - } + /// + /// Perform service invocation using gRPC semantics for the application identified by and invokes the method + /// specified by with an empty request body. + /// If the response has a non-success status code an exception will be thrown. + /// + /// The Dapr application id to invoke the method on. + /// The name of the method to invoke. + /// A that can be used to cancel the operation. + /// A that will return when the operation has completed. + public abstract Task InvokeMethodGrpcAsync( + string appId, + string methodName, + CancellationToken cancellationToken = default); - /// - /// Perform service invocation using gRPC semantics for the application identified by and invokes the method - /// specified by with an empty request body. - /// If the response has a non-success status code an exception will be thrown. - /// - /// The Dapr application id to invoke the method on. - /// The name of the method to invoke. - /// A that can be used to cancel the operation. - /// A that will return when the operation has completed. - public abstract Task InvokeMethodGrpcAsync( - string appId, - string methodName, - CancellationToken cancellationToken = default); - - /// - /// Perform service invocation using gRPC semantics for the application identified by and invokes the method - /// specified by with a Protobuf serialized request body specified by . - /// If the response has a non-success status code an exception will be thrown. - /// - /// The type of the data that will be Protobuf serialized and provided as the request body. - /// The Dapr application id to invoke the method on. - /// The name of the method to invoke. - /// The data that will be Protobuf serialized and provided as the request body. - /// A that can be used to cancel the operation. - /// A that will return when the operation has completed. - public abstract Task InvokeMethodGrpcAsync( - string appId, - string methodName, - TRequest data, - CancellationToken cancellationToken = default) + /// + /// Perform service invocation using gRPC semantics for the application identified by and invokes the method + /// specified by with a Protobuf serialized request body specified by . + /// If the response has a non-success status code an exception will be thrown. + /// + /// The type of the data that will be Protobuf serialized and provided as the request body. + /// The Dapr application id to invoke the method on. + /// The name of the method to invoke. + /// The data that will be Protobuf serialized and provided as the request body. + /// A that can be used to cancel the operation. + /// A that will return when the operation has completed. + public abstract Task InvokeMethodGrpcAsync( + string appId, + string methodName, + TRequest data, + CancellationToken cancellationToken = default) where TRequest : IMessage; - /// - /// Perform service invocation using gRPC semantics for the application identified by and invokes the method - /// specified by with an empty request body. If the response has a success - /// status code the body will be deserialized using Protobuf to a value of type ; - /// otherwise an exception will be thrown. - /// - /// The type of the data that will be Protobuf deserialized from the response body. - /// The Dapr application id to invoke the method on. - /// The name of the method to invoke. - /// A that can be used to cancel the operation. - /// A that will return the value when the operation has completed. - public abstract Task InvokeMethodGrpcAsync( - string appId, - string methodName, - CancellationToken cancellationToken = default) + /// + /// Perform service invocation using gRPC semantics for the application identified by and invokes the method + /// specified by with an empty request body. If the response has a success + /// status code the body will be deserialized using Protobuf to a value of type ; + /// otherwise an exception will be thrown. + /// + /// The type of the data that will be Protobuf deserialized from the response body. + /// The Dapr application id to invoke the method on. + /// The name of the method to invoke. + /// A that can be used to cancel the operation. + /// A that will return the value when the operation has completed. + public abstract Task InvokeMethodGrpcAsync( + string appId, + string methodName, + CancellationToken cancellationToken = default) where TResponse : IMessage, new(); - /// - /// Perform service invocation using gRPC semantics for the application identified by and invokes the method - /// specified by with a Protobuf serialized request body specified by . If the response has a success - /// status code the body will be deserialized using Protobuf to a value of type ; - /// otherwise an exception will be thrown. - /// - /// The type of the data that will be Protobuf serialized and provided as the request body. - /// The type of the data that will be Protobuf deserialized from the response body. - /// The Dapr application id to invoke the method on. - /// The name of the method to invoke. - /// The data that will be Protobuf serialized and provided as the request body. - /// A that can be used to cancel the operation. - /// A that will return the value when the operation has completed. - public abstract Task InvokeMethodGrpcAsync( - string appId, - string methodName, - TRequest data, - CancellationToken cancellationToken = default) + /// + /// Perform service invocation using gRPC semantics for the application identified by and invokes the method + /// specified by with a Protobuf serialized request body specified by . If the response has a success + /// status code the body will be deserialized using Protobuf to a value of type ; + /// otherwise an exception will be thrown. + /// + /// The type of the data that will be Protobuf serialized and provided as the request body. + /// The type of the data that will be Protobuf deserialized from the response body. + /// The Dapr application id to invoke the method on. + /// The name of the method to invoke. + /// The data that will be Protobuf serialized and provided as the request body. + /// A that can be used to cancel the operation. + /// A that will return the value when the operation has completed. + public abstract Task InvokeMethodGrpcAsync( + string appId, + string methodName, + TRequest data, + CancellationToken cancellationToken = default) where TRequest : IMessage where TResponse : IMessage, new(); - /// - /// Gets the current 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. - /// The data type of the value to read. - /// A that will return the value when the operation has completed. - public abstract Task GetStateAsync(string storeName, string key, ConsistencyMode? consistencyMode = default, IReadOnlyDictionary metadata = default, CancellationToken cancellationToken = default); + /// + /// Gets the current 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. + /// The data type of the value to read. + /// A that will return the value when the operation has completed. + public abstract Task GetStateAsync(string storeName, string key, ConsistencyMode? consistencyMode = default, IReadOnlyDictionary? metadata = null, CancellationToken cancellationToken = default); - /// - /// Gets a list of values associated with the from the Dapr state store. - /// - /// The name of state store to read from. - /// The list of keys to get values for. - /// The number of concurrent get operations the Dapr runtime will issue to the state store. a value equal to or smaller than 0 means max parallelism. - /// 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 list of values when the operation has completed. - public abstract Task> GetBulkStateAsync(string storeName, IReadOnlyList keys, int? parallelism, IReadOnlyDictionary metadata = default, CancellationToken cancellationToken = default); + /// + /// Gets a list of values associated with the from the Dapr state store. + /// + /// The name of state store to read from. + /// The list of keys to get values for. + /// The number of concurrent get operations the Dapr runtime will issue to the state store. a value equal to or smaller than 0 means max parallelism. + /// 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 list of values when the operation has completed. + public abstract Task> GetBulkStateAsync(string storeName, IReadOnlyList keys, int? parallelism, IReadOnlyDictionary? metadata = null, CancellationToken cancellationToken = default); - /// - /// Gets a list of deserialized values associated with the from the Dapr state store. This overload should be used - /// if you expect the values of all the retrieved items to match the shape of the indicated . If you expect that - /// the values may differ in type from one another, do not specify the type parameter and instead use the original method - /// so the serialized string values will be returned instead. - /// - /// The name of state store to read from. - /// The list of keys to get values for. - /// The number of concurrent get operations the Dapr runtime will issue to the state store. a value equal to or smaller than 0 means max parallelism. - /// 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 list of deserialized values when the operation has completed. - public abstract Task>> GetBulkStateAsync(string storeName, IReadOnlyList keys, int? parallelism, IReadOnlyDictionary metadata = default, CancellationToken cancellationToken = default); + /// + /// Gets a list of deserialized values associated with the from the Dapr state store. This overload should be used + /// if you expect the values of all the retrieved items to match the shape of the indicated . If you expect that + /// the values may differ in type from one another, do not specify the type parameter and instead use the original method + /// so the serialized string values will be returned instead. + /// + /// The name of state store to read from. + /// The list of keys to get values for. + /// The number of concurrent get operations the Dapr runtime will issue to the state store. a value equal to or smaller than 0 means max parallelism. + /// 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 list of deserialized values when the operation has completed. + public abstract Task>> GetBulkStateAsync(string storeName, IReadOnlyList keys, int? parallelism, IReadOnlyDictionary? metadata = null, CancellationToken cancellationToken = default); - /// - /// Saves a list of to the Dapr state store. - /// - /// The name of state store. - /// The list of items to save. - /// A that can be used to cancel the operation. - /// A that will complete when the operation has completed. - public abstract Task SaveBulkStateAsync(string storeName, IReadOnlyList> items, CancellationToken cancellationToken = default); + /// + /// Saves a list of to the Dapr state store. + /// + /// The name of state store. + /// The list of items to save. + /// A that can be used to cancel the operation. + /// A that will complete when the operation has completed. + public abstract Task SaveBulkStateAsync(string storeName, IReadOnlyList> items, CancellationToken cancellationToken = default); - /// - /// Deletes a list of from the Dapr state store. - /// - /// The name of state store to delete from. - /// The list of items to delete - /// A that can be used to cancel the operation. - /// A that will complete when the operation has completed. - public abstract Task DeleteBulkStateAsync(string storeName, IReadOnlyList items, CancellationToken cancellationToken = default); + /// + /// Deletes a list of from the Dapr state store. + /// + /// The name of state store to delete from. + /// The list of items to delete + /// A that can be used to cancel the operation. + /// A that will complete when the operation has completed. + public abstract Task DeleteBulkStateAsync(string storeName, IReadOnlyList items, CancellationToken cancellationToken = default); - /// - /// Gets the current value associated with the from the Dapr state store and an ETag. - /// - /// The data type of the value to read. - /// 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<(TValue value, string etag)> GetStateAndETagAsync(string storeName, string key, ConsistencyMode? consistencyMode = default, IReadOnlyDictionary metadata = default, CancellationToken cancellationToken = default); + /// + /// Gets the current value associated with the from the Dapr state store and an ETag. + /// + /// The data type of the value to read. + /// 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<(TValue value, string etag)> GetStateAndETagAsync(string storeName, string key, ConsistencyMode? consistencyMode = null, IReadOnlyDictionary? metadata = null, CancellationToken cancellationToken = default); - /// - /// Gets a for the current value associated with the from - /// the Dapr state store. - /// - /// 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. - /// The type of the data that will be JSON deserialized from the state store response. - /// A that will return the when the operation has completed. - public async Task> GetStateEntryAsync(string storeName, string key, ConsistencyMode? consistencyMode = default, IReadOnlyDictionary metadata = default, CancellationToken cancellationToken = default) - { - ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); - ArgumentVerifier.ThrowIfNullOrEmpty(key, nameof(key)); + /// + /// Gets a for the current value associated with the from + /// the Dapr state store. + /// + /// 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. + /// The type of the data that will be JSON deserialized from the state store response. + /// A that will return the when the operation has completed. + public async Task> GetStateEntryAsync(string storeName, string key, ConsistencyMode? consistencyMode = null, IReadOnlyDictionary? metadata = null, CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); + ArgumentVerifier.ThrowIfNullOrEmpty(key, nameof(key)); - var (state, etag) = await this.GetStateAndETagAsync(storeName, key, consistencyMode, metadata, cancellationToken); - return new StateEntry(this, storeName, key, state, etag); - } + var (state, etag) = await this.GetStateAndETagAsync(storeName, key, consistencyMode, metadata, cancellationToken); + return new StateEntry(this, storeName, key, state, etag); + } - /// - /// Saves the provided associated with the provided to the Dapr state - /// store. - /// - /// The name of the state store. - /// The state key. - /// The data that will be JSON serialized and 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. - /// The type of the data that will be JSON serialized and stored in the state store. - /// A that will complete when the operation has completed. - public abstract Task SaveStateAsync( - string storeName, - string key, - TValue value, - StateOptions stateOptions = default, - 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 data that will be JSON serialized and 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. + /// The type of the data that will be JSON serialized and stored in the state store. + /// A that will complete when the operation has completed. + public abstract Task SaveStateAsync( + string storeName, + string key, + TValue value, + StateOptions? stateOptions = null, + IReadOnlyDictionary? metadata = null, + 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 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 = null, + IReadOnlyDictionary? metadata = null, + 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); + /// + ///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 = null, + IReadOnlyDictionary? metadata = null, + 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. + /// + /// 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 = null, + IReadOnlyDictionary? metadata = null, + 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); + /// + /// 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 = null, + IReadOnlyDictionary? metadata = null, + 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. - /// store. - /// - /// The name of the state store. - /// The state key. - /// The data that will be JSON serialized and 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. - /// The type of the data that will be JSON serialized and stored in the state store. - /// A that will complete when the operation has completed. If the wrapped value is true the operation succeeded. - public abstract Task TrySaveStateAsync( - string storeName, - string key, - TValue value, - string etag, - StateOptions stateOptions = 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. + /// store. + /// + /// The name of the state store. + /// The state key. + /// The data that will be JSON serialized and 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. + /// The type of the data that will be JSON serialized and stored in the state store. + /// A that will complete when the operation has completed. If the wrapped value is true the operation succeeded. + public abstract Task TrySaveStateAsync( + string storeName, + string key, + TValue value, + string etag, + StateOptions? stateOptions = null, + IReadOnlyDictionary? metadata = null, + CancellationToken cancellationToken = default); - /// - /// Saves the provided to the Dapr state - /// store. - /// - /// The name of the state store. - /// A list of StateTransactionRequests. - /// 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 ExecuteStateTransactionAsync( - string storeName, - IReadOnlyList operations, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default); + /// + /// Saves the provided to the Dapr state + /// store. + /// + /// The name of the state store. + /// A list of StateTransactionRequests. + /// 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 ExecuteStateTransactionAsync( + string storeName, + IReadOnlyList operations, + IReadOnlyDictionary? metadata = null, + CancellationToken cancellationToken = default); - /// - /// Deletes the value associated with the provided in the Dapr state store. - /// - /// The state store name. - /// The state key. - /// A . - /// 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 DeleteStateAsync( - string storeName, - string key, - StateOptions stateOptions = default, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default); + /// + /// Deletes the value associated with the provided in the Dapr state store. + /// + /// The state store name. + /// The state key. + /// A . + /// 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 DeleteStateAsync( + string storeName, + string key, + StateOptions? stateOptions = null, + IReadOnlyDictionary? metadata = null, + CancellationToken cancellationToken = default); - /// - /// Tries to delete the the state associated with the provided using the - /// from the Dapr state. State store implementation will allow the delete only if the attached ETag matches with the latest ETag in the state store. - /// - /// The state store name. - /// The state key. - /// An ETag. - /// A . - /// 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. If the wrapped value is true the operation suceeded. - public abstract Task TryDeleteStateAsync( - string storeName, - string key, - string etag, - StateOptions stateOptions = default, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default); + /// + /// Tries to delete the the state associated with the provided using the + /// from the Dapr state. State store implementation will allow the delete only if the attached ETag matches with the latest ETag in the state store. + /// + /// The state store name. + /// The state key. + /// An ETag. + /// A . + /// 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. If the wrapped value is true the operation suceeded. + public abstract Task TryDeleteStateAsync( + string storeName, + string key, + string etag, + StateOptions? stateOptions = null, + IReadOnlyDictionary? metadata = null, + CancellationToken cancellationToken = default); - /// - /// Queries the specified statestore with the given query. The query is a JSON representation of the query as described by the Dapr QueryState API. - /// Note that the underlying statestore must support queries. - /// - /// The name of the statestore. - /// A JSON formatted query string. - /// Metadata to send to the statestore. - /// A that can be used to cancel the operation. - /// The data type of the value to read. - /// A that may be paginated, use to continue the query. - public abstract Task> QueryStateAsync( - string storeName, - string jsonQuery, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default); + /// + /// Queries the specified statestore with the given query. The query is a JSON representation of the query as described by the Dapr QueryState API. + /// Note that the underlying statestore must support queries. + /// + /// The name of the statestore. + /// A JSON formatted query string. + /// Metadata to send to the statestore. + /// A that can be used to cancel the operation. + /// The data type of the value to read. + /// A that may be paginated, use to continue the query. + public abstract Task> QueryStateAsync( + string storeName, + string jsonQuery, + IReadOnlyDictionary? metadata = null, + CancellationToken cancellationToken = default); - /// - /// Gets the secret value from the secret store. - /// - /// Secret store name. - /// Key for the secret. - /// A collection of metadata key-value pairs that will be provided to the secret store. The valid metadata keys and values are determined by the type of secret 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> GetSecretAsync( - string storeName, - string key, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default); + /// + /// Gets the secret value from the secret store. + /// + /// Secret store name. + /// Key for the secret. + /// A collection of metadata key-value pairs that will be provided to the secret store. The valid metadata keys and values are determined by the type of secret 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> GetSecretAsync( + string storeName, + string key, + IReadOnlyDictionary? metadata = null, + CancellationToken cancellationToken = default); - /// - /// Gets all secret values that the application is allowed to access from the secret store. - /// - /// Secret store name. - /// A collection of metadata key-value pairs that will be provided to the secret store. The valid metadata keys and values are determined by the type of secret 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>> GetBulkSecretAsync( - string storeName, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default); + /// + /// Gets all secret values that the application is allowed to access from the secret store. + /// + /// Secret store name. + /// A collection of metadata key-value pairs that will be provided to the secret store. The valid metadata keys and values are determined by the type of secret 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>> GetBulkSecretAsync( + string storeName, + IReadOnlyDictionary? metadata = null, + CancellationToken cancellationToken = default); - /// - /// Get a list of configuration items based on keys from the given statestore. - /// - /// The name of the configuration store to be queried. - /// An optional list of keys to query for. If provided, the result will only contain those keys. An empty list indicates all keys should be fetched. - /// Optional metadata that will be sent to the configuration store being queried. - /// A that can be used to cancel the operation. - /// A containing a - public abstract Task GetConfiguration( - string storeName, - IReadOnlyList keys, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default); + /// + /// Get a list of configuration items based on keys from the given statestore. + /// + /// The name of the configuration store to be queried. + /// An optional list of keys to query for. If provided, the result will only contain those keys. An empty list indicates all keys should be fetched. + /// Optional metadata that will be sent to the configuration store being queried. + /// A that can be used to cancel the operation. + /// A containing a + public abstract Task GetConfiguration( + string storeName, + IReadOnlyList keys, + IReadOnlyDictionary? metadata = null, + CancellationToken cancellationToken = default); - /// - /// Subscribe to a configuration store for the specified keys and receive an updated value whenever the key is updated in the store. - /// - /// The name of the configuration store to be queried. - /// An optional list of keys to query for. If provided, the result will only contain those keys. An empty list indicates all keys should be fetched. - /// Optional metadata that will be sent to the configuration store being queried. - /// A that can be used to cancel the operation. - /// A which contains a reference to the stream. - public abstract Task SubscribeConfiguration( - string storeName, - IReadOnlyList keys, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default); + /// + /// Subscribe to a configuration store for the specified keys and receive an updated value whenever the key is updated in the store. + /// + /// The name of the configuration store to be queried. + /// An optional list of keys to query for. If provided, the result will only contain those keys. An empty list indicates all keys should be fetched. + /// Optional metadata that will be sent to the configuration store being queried. + /// A that can be used to cancel the operation. + /// A which contains a reference to the stream. + public abstract Task SubscribeConfiguration( + string storeName, + IReadOnlyList keys, + IReadOnlyDictionary? metadata = null, + CancellationToken cancellationToken = default); - /// - /// Unsubscribe from a configuration store using the specified Id. - /// - /// The name of the configuration store. - /// The Id of the subscription that should no longer be watched. - /// A that can be used to cancel the operation. - /// - public abstract Task UnsubscribeConfiguration( - string storeName, - string id, - CancellationToken cancellationToken = default); + /// + /// Unsubscribe from a configuration store using the specified Id. + /// + /// The name of the configuration store. + /// The Id of the subscription that should no longer be watched. + /// A that can be used to cancel the operation. + /// + public abstract Task UnsubscribeConfiguration( + string storeName, + string id, + CancellationToken cancellationToken = default); - #region Cryptography + #region Cryptography - /// - /// Encrypts an array of bytes using the Dapr Cryptography encryption functionality. - /// - /// The name of the vault resource used by the operation. - /// The bytes of the plaintext value to encrypt. - /// The name of the key to use from the Vault for the encryption operation. - /// Options informing how the encryption operation should be configured. - /// A that can be used to cancel the operation. - /// An array of encrypted bytes. - [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public abstract Task> EncryptAsync(string vaultResourceName, - ReadOnlyMemory plaintextBytes, string keyName, EncryptionOptions encryptionOptions, - CancellationToken cancellationToken = default); + /// + /// Encrypts an array of bytes using the Dapr Cryptography encryption functionality. + /// + /// The name of the vault resource used by the operation. + /// The bytes of the plaintext value to encrypt. + /// The name of the key to use from the Vault for the encryption operation. + /// Options informing how the encryption operation should be configured. + /// A that can be used to cancel the operation. + /// An array of encrypted bytes. + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public abstract Task> EncryptAsync(string vaultResourceName, + ReadOnlyMemory plaintextBytes, string keyName, EncryptionOptions encryptionOptions, + CancellationToken cancellationToken = default); - /// - /// Encrypts a stream using the Dapr Cryptography encryption functionality. - /// - /// The name of the vault resource used by the operation. - /// The stream containing the bytes of the plaintext value to encrypt. - /// The name of the key to use from the Vault for the encryption operation. - /// Options informing how the encryption operation should be configured. - /// A that can be used to cancel the operation. - /// An array of encrypted bytes. - [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public abstract Task>> EncryptAsync(string vaultResourceName, Stream plaintextStream, string keyName, - EncryptionOptions encryptionOptions, CancellationToken cancellationToken = default); + /// + /// Encrypts a stream using the Dapr Cryptography encryption functionality. + /// + /// The name of the vault resource used by the operation. + /// The stream containing the bytes of the plaintext value to encrypt. + /// The name of the key to use from the Vault for the encryption operation. + /// Options informing how the encryption operation should be configured. + /// A that can be used to cancel the operation. + /// An array of encrypted bytes. + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public abstract Task>> EncryptAsync(string vaultResourceName, Stream plaintextStream, string keyName, + EncryptionOptions encryptionOptions, CancellationToken cancellationToken = default); - /// - /// Decrypts the specified ciphertext bytes using the Dapr Cryptography encryption functionality. - /// - /// The name of the vault resource used by the operation. - /// The bytes of the ciphertext value to decrypt. - /// The name of the key to use from the Vault for the decryption operation. - /// Options informing how the decryption operation should be configured. - /// A that can be used to cancel the operation. - /// An array of decrypted bytes. - [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public abstract Task> DecryptAsync(string vaultResourceName, ReadOnlyMemory ciphertextBytes, string keyName, DecryptionOptions options, - CancellationToken cancellationToken = default); + /// + /// Decrypts the specified ciphertext bytes using the Dapr Cryptography encryption functionality. + /// + /// The name of the vault resource used by the operation. + /// The bytes of the ciphertext value to decrypt. + /// The name of the key to use from the Vault for the decryption operation. + /// Options informing how the decryption operation should be configured. + /// A that can be used to cancel the operation. + /// An array of decrypted bytes. + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public abstract Task> DecryptAsync(string vaultResourceName, ReadOnlyMemory ciphertextBytes, string keyName, DecryptionOptions options, + CancellationToken cancellationToken = default); - /// - /// Decrypts the specified ciphertext bytes using the Dapr Cryptography encryption functionality. - /// - /// The name of the vault resource used by the operation. - /// The bytes of the ciphertext value to decrypt. - /// The name of the key to use from the Vault for the decryption operation. - /// A that can be used to cancel the operation. - /// An array of decrypted bytes. - [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public abstract Task> DecryptAsync(string vaultResourceName, - ReadOnlyMemory ciphertextBytes, string keyName, CancellationToken cancellationToken = default); + /// + /// Decrypts the specified ciphertext bytes using the Dapr Cryptography encryption functionality. + /// + /// The name of the vault resource used by the operation. + /// The bytes of the ciphertext value to decrypt. + /// The name of the key to use from the Vault for the decryption operation. + /// A that can be used to cancel the operation. + /// An array of decrypted bytes. + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public abstract Task> DecryptAsync(string vaultResourceName, + ReadOnlyMemory ciphertextBytes, string keyName, CancellationToken cancellationToken = default); - /// - /// Decrypts the specified stream of ciphertext using the Dapr Cryptography encryption functionality. - /// - /// The name of the vault resource used by the operation. - /// The stream containing the bytes of the ciphertext value to decrypt. - /// The name of the key to use from the Vault for the decryption operation. - /// Options informing how the decryption operation should be configured. - /// A that can be used to cancel the operation. - /// An asynchronously enumerable array of decrypted bytes. - [Obsolete( - "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public abstract Task>> DecryptAsync(string vaultResourceName, Stream ciphertextStream, - string keyName, DecryptionOptions options, CancellationToken cancellationToken = default); + /// + /// Decrypts the specified stream of ciphertext using the Dapr Cryptography encryption functionality. + /// + /// The name of the vault resource used by the operation. + /// The stream containing the bytes of the ciphertext value to decrypt. + /// The name of the key to use from the Vault for the decryption operation. + /// Options informing how the decryption operation should be configured. + /// A that can be used to cancel the operation. + /// An asynchronously enumerable array of decrypted bytes. + [Obsolete( + "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public abstract Task>> DecryptAsync(string vaultResourceName, Stream ciphertextStream, + string keyName, DecryptionOptions options, CancellationToken cancellationToken = default); - /// - /// Decrypts the specified stream of ciphertext using the Dapr Cryptography encryption functionality. - /// - /// The name of the vault resource used by the operation. - /// The stream containing the bytes of the ciphertext value to decrypt. - /// The name of the key to use from the Vault for the decryption operation. - /// A that can be used to cancel the operation. - /// An asynchronously enumerable array of decrypted bytes. - [Obsolete( - "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public abstract Task>> DecryptAsync(string vaultResourceName, Stream ciphertextStream, - string keyName, CancellationToken cancellationToken = default); + /// + /// Decrypts the specified stream of ciphertext using the Dapr Cryptography encryption functionality. + /// + /// The name of the vault resource used by the operation. + /// The stream containing the bytes of the ciphertext value to decrypt. + /// The name of the key to use from the Vault for the decryption operation. + /// A that can be used to cancel the operation. + /// An asynchronously enumerable array of decrypted bytes. + [Obsolete( + "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public abstract Task>> DecryptAsync(string vaultResourceName, Stream ciphertextStream, + string keyName, CancellationToken cancellationToken = default); - #endregion + #endregion - #region Cryptography - Subtle API + #region Cryptography - Subtle API - ///// - ///// Retrieves the value of the specified key from the vault. - ///// - ///// The name of the vault resource used by the operation. - ///// The name of the key to retrieve the value of. - ///// The format to use for the key result. - ///// A that can be used to cancel the operation. - ///// The name (and possibly version as name/version) of the key and its public key. - //[Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - //public abstract Task<(string Name, string PublicKey)> GetKeyAsync(string vaultResourceName, string keyName, SubtleGetKeyRequest.Types.KeyFormat keyFormat, - // CancellationToken cancellationToken = default); + ///// + ///// Retrieves the value of the specified key from the vault. + ///// + ///// The name of the vault resource used by the operation. + ///// The name of the key to retrieve the value of. + ///// The format to use for the key result. + ///// A that can be used to cancel the operation. + ///// The name (and possibly version as name/version) of the key and its public key. + //[Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public abstract Task<(string Name, string PublicKey)> GetKeyAsync(string vaultResourceName, string keyName, SubtleGetKeyRequest.Types.KeyFormat keyFormat, + // CancellationToken cancellationToken = default); - ///// - ///// Encrypts an array of bytes using the Dapr Cryptography functionality. - ///// - ///// The name of the vault resource used by the operation. - ///// The bytes of the plaintext value to encrypt. - ///// The name of the algorithm that should be used to perform the encryption. - ///// The name of the key used to perform the encryption operation. - ///// The bytes comprising the nonce. - ///// Any associated data when using AEAD ciphers. - ///// A that can be used to cancel the operation. - ///// The array of encrypted bytes. - //[Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - //public abstract Task<(byte[] CipherTextBytes, byte[] AuthenticationTag)> EncryptAsync( - // string vaultResourceName, - // byte[] plainTextBytes, - // string algorithm, - // string keyName, - // byte[] nonce, - // byte[] associatedData, - // CancellationToken cancellationToken = default); + ///// + ///// Encrypts an array of bytes using the Dapr Cryptography functionality. + ///// + ///// The name of the vault resource used by the operation. + ///// The bytes of the plaintext value to encrypt. + ///// The name of the algorithm that should be used to perform the encryption. + ///// The name of the key used to perform the encryption operation. + ///// The bytes comprising the nonce. + ///// Any associated data when using AEAD ciphers. + ///// A that can be used to cancel the operation. + ///// The array of encrypted bytes. + //[Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public abstract Task<(byte[] CipherTextBytes, byte[] AuthenticationTag)> EncryptAsync( + // string vaultResourceName, + // byte[] plainTextBytes, + // string algorithm, + // string keyName, + // byte[] nonce, + // byte[] associatedData, + // CancellationToken cancellationToken = default); - ///// - ///// Encrypts an array of bytes using the Dapr Cryptography functionality. - ///// - ///// The name of the vault resource used by the operation. - ///// The bytes of the plaintext value to encrypt. - ///// The name of the algorithm that should be used to perform the encryption. - ///// The name of the key used to perform the encryption operation. - ///// The bytes comprising the nonce. - ///// A that can be used to cancel the operation. - ///// The array of encrypted bytes. - //[Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - //public async Task<(byte[] CipherTextBytes, byte[] AuthenticationTag)> EncryptAsync( - // string vaultResourceName, - // byte[] plainTextBytes, - // string algorithm, - // string keyName, - // byte[] nonce, - // CancellationToken cancellationToken = default) => - // await EncryptAsync(vaultResourceName, plainTextBytes, algorithm, keyName, nonce, Array.Empty(), - // cancellationToken); + ///// + ///// Encrypts an array of bytes using the Dapr Cryptography functionality. + ///// + ///// The name of the vault resource used by the operation. + ///// The bytes of the plaintext value to encrypt. + ///// The name of the algorithm that should be used to perform the encryption. + ///// The name of the key used to perform the encryption operation. + ///// The bytes comprising the nonce. + ///// A that can be used to cancel the operation. + ///// The array of encrypted bytes. + //[Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public async Task<(byte[] CipherTextBytes, byte[] AuthenticationTag)> EncryptAsync( + // string vaultResourceName, + // byte[] plainTextBytes, + // string algorithm, + // string keyName, + // byte[] nonce, + // CancellationToken cancellationToken = default) => + // await EncryptAsync(vaultResourceName, plainTextBytes, algorithm, keyName, nonce, Array.Empty(), + // cancellationToken); - ///// - ///// Decrypts an array of bytes using the Dapr Cryptography functionality. - ///// - ///// The name of the vault the key is retrieved from for the decryption operation. - ///// The array of bytes to decrypt. - ///// A that can be used to cancel the operation. - ///// The algorithm to use to perform the decryption operation. - ///// The name of the key used for the decryption. - ///// The nonce value used. - ///// - ///// Any associated data when using AEAD ciphers. - ///// The array of plaintext bytes. - //[Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - //public abstract Task DecryptAsync(string vaultResourceName, byte[] cipherTextBytes, - // string algorithm, string keyName, byte[] nonce, byte[] tag, byte[] associatedData, - // CancellationToken cancellationToken = default); + ///// + ///// Decrypts an array of bytes using the Dapr Cryptography functionality. + ///// + ///// The name of the vault the key is retrieved from for the decryption operation. + ///// The array of bytes to decrypt. + ///// A that can be used to cancel the operation. + ///// The algorithm to use to perform the decryption operation. + ///// The name of the key used for the decryption. + ///// The nonce value used. + ///// + ///// Any associated data when using AEAD ciphers. + ///// The array of plaintext bytes. + //[Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public abstract Task DecryptAsync(string vaultResourceName, byte[] cipherTextBytes, + // string algorithm, string keyName, byte[] nonce, byte[] tag, byte[] associatedData, + // CancellationToken cancellationToken = default); - ///// - ///// Decrypts an array of bytes using the Dapr Cryptography functionality. - ///// - ///// The name of the vault the key is retrieved from for the decryption operation. - ///// The array of bytes to decrypt. - ///// A that can be used to cancel the operation. - ///// The algorithm to use to perform the decryption operation. - ///// The name of the key used for the decryption. - ///// The nonce value used. - ///// - ///// The array of plaintext bytes. - //[Obsolete( - // "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - //public async Task DecryptAsync(string vaultResourceName, byte[] cipherTextBytes, - // string algorithm, string keyName, byte[] nonce, byte[] tag, CancellationToken cancellationToken = default) => - // await DecryptAsync(vaultResourceName, cipherTextBytes, algorithm, keyName, nonce, tag, Array.Empty(), cancellationToken); + ///// + ///// Decrypts an array of bytes using the Dapr Cryptography functionality. + ///// + ///// The name of the vault the key is retrieved from for the decryption operation. + ///// The array of bytes to decrypt. + ///// A that can be used to cancel the operation. + ///// The algorithm to use to perform the decryption operation. + ///// The name of the key used for the decryption. + ///// The nonce value used. + ///// + ///// The array of plaintext bytes. + //[Obsolete( + // "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public async Task DecryptAsync(string vaultResourceName, byte[] cipherTextBytes, + // string algorithm, string keyName, byte[] nonce, byte[] tag, CancellationToken cancellationToken = default) => + // await DecryptAsync(vaultResourceName, cipherTextBytes, algorithm, keyName, nonce, tag, Array.Empty(), cancellationToken); - ///// - ///// Wraps the plaintext key using another. - ///// - ///// The name of the vault to retrieve the key from. - ///// The plaintext bytes comprising the key to wrap. - ///// The name of the key used to wrap the value. - ///// The algorithm to use to perform the wrap operation. - ///// The none used. - ///// Any associated data when using AEAD ciphers. - ///// A that can be used to cancel the operation. - ///// The bytes comprising the wrapped plain-text key and the authentication tag, if applicable. - //[Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - //public abstract Task<(byte[] WrappedKey, byte[] AuthenticationTag)> WrapKeyAsync(string vaultResourceName, byte[] plainTextKey, string keyName, string algorithm, byte[] nonce, byte[] associatedData, - // CancellationToken cancellationToken = default); + ///// + ///// Wraps the plaintext key using another. + ///// + ///// The name of the vault to retrieve the key from. + ///// The plaintext bytes comprising the key to wrap. + ///// The name of the key used to wrap the value. + ///// The algorithm to use to perform the wrap operation. + ///// The none used. + ///// Any associated data when using AEAD ciphers. + ///// A that can be used to cancel the operation. + ///// The bytes comprising the wrapped plain-text key and the authentication tag, if applicable. + //[Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public abstract Task<(byte[] WrappedKey, byte[] AuthenticationTag)> WrapKeyAsync(string vaultResourceName, byte[] plainTextKey, string keyName, string algorithm, byte[] nonce, byte[] associatedData, + // CancellationToken cancellationToken = default); - ///// - ///// Wraps the plaintext key using another. - ///// - ///// The name of the vault to retrieve the key from. - ///// The plaintext bytes comprising the key to wrap. - ///// The name of the key used to wrap the value. - ///// The algorithm to use to perform the wrap operation. - ///// The none used. - ///// A that can be used to cancel the operation. - ///// The bytes comprising the unwrapped key and the authentication tag, if applicable. - //[Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - //public async Task<(byte[] WrappedKey, byte[] AuthenticationTag)> WrapKeyAsync(string vaultResourceName, byte[] plainTextKey, string keyName, string algorithm, - // byte[] nonce, CancellationToken cancellationToken = default) => await WrapKeyAsync(vaultResourceName, plainTextKey, - // keyName, algorithm, nonce, Array.Empty(), cancellationToken); + ///// + ///// Wraps the plaintext key using another. + ///// + ///// The name of the vault to retrieve the key from. + ///// The plaintext bytes comprising the key to wrap. + ///// The name of the key used to wrap the value. + ///// The algorithm to use to perform the wrap operation. + ///// The none used. + ///// A that can be used to cancel the operation. + ///// The bytes comprising the unwrapped key and the authentication tag, if applicable. + //[Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public async Task<(byte[] WrappedKey, byte[] AuthenticationTag)> WrapKeyAsync(string vaultResourceName, byte[] plainTextKey, string keyName, string algorithm, + // byte[] nonce, CancellationToken cancellationToken = default) => await WrapKeyAsync(vaultResourceName, plainTextKey, + // keyName, algorithm, nonce, Array.Empty(), cancellationToken); - ///// - ///// Used to unwrap the specified key. - ///// - ///// The name of the vault to retrieve the key from. - ///// The byte comprising the wrapped key. - ///// The algorithm to use in unwrapping the key. - ///// The name of the key used to unwrap the wrapped key bytes. - ///// The nonce value. - ///// The bytes comprising the authentication tag. - ///// Any associated data when using AEAD ciphers. - ///// A that can be used to cancel the operation. - ///// The bytes comprising the unwrapped key. - //[Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - //public abstract Task UnwrapKeyAsync(string vaultResourceName, byte[] wrappedKey, string algorithm, string keyName, byte[] nonce, byte[] tag, byte[] associatedData, - // CancellationToken cancellationToken = default); + ///// + ///// Used to unwrap the specified key. + ///// + ///// The name of the vault to retrieve the key from. + ///// The byte comprising the wrapped key. + ///// The algorithm to use in unwrapping the key. + ///// The name of the key used to unwrap the wrapped key bytes. + ///// The nonce value. + ///// The bytes comprising the authentication tag. + ///// Any associated data when using AEAD ciphers. + ///// A that can be used to cancel the operation. + ///// The bytes comprising the unwrapped key. + //[Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public abstract Task UnwrapKeyAsync(string vaultResourceName, byte[] wrappedKey, string algorithm, string keyName, byte[] nonce, byte[] tag, byte[] associatedData, + // CancellationToken cancellationToken = default); - ///// - ///// Used to unwrap the specified key. - ///// - ///// The name of the vault to retrieve the key from. - ///// The byte comprising the wrapped key. - ///// The algorithm to use in unwrapping the key. - ///// The name of the key used to unwrap the wrapped key bytes. - ///// The nonce value. - ///// The bytes comprising the authentication tag. - ///// A that can be used to cancel the operation. - ///// The bytes comprising the unwrapped key. - //[Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - //public async Task UnwrapKeyAsync(string vaultResourceName, byte[] wrappedKey, string algorithm, string keyName, - // byte[] nonce, byte[] tag, - // CancellationToken cancellationToken = default) => await UnwrapKeyAsync(vaultResourceName, - // wrappedKey, algorithm, keyName, nonce, Array.Empty(), Array.Empty(), cancellationToken); + ///// + ///// Used to unwrap the specified key. + ///// + ///// The name of the vault to retrieve the key from. + ///// The byte comprising the wrapped key. + ///// The algorithm to use in unwrapping the key. + ///// The name of the key used to unwrap the wrapped key bytes. + ///// The nonce value. + ///// The bytes comprising the authentication tag. + ///// A that can be used to cancel the operation. + ///// The bytes comprising the unwrapped key. + //[Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public async Task UnwrapKeyAsync(string vaultResourceName, byte[] wrappedKey, string algorithm, string keyName, + // byte[] nonce, byte[] tag, + // CancellationToken cancellationToken = default) => await UnwrapKeyAsync(vaultResourceName, + // wrappedKey, algorithm, keyName, nonce, Array.Empty(), Array.Empty(), cancellationToken); - ///// - ///// Used to unwrap the specified key. - ///// - ///// The name of the vault to retrieve the key from. - ///// The byte comprising the wrapped key. - ///// The algorithm to use in unwrapping the key. - ///// The name of the key used to unwrap the wrapped key bytes. - ///// The nonce value. - ///// A that can be used to cancel the operation. - ///// The bytes comprising the unwrapped key. - //[Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - //public async Task UnwrapKeyAsync(string vaultResourceName, byte[] wrappedKey, string algorithm, string keyName, - // byte[] nonce, CancellationToken cancellationToken = default) => await UnwrapKeyAsync(vaultResourceName, - // wrappedKey, algorithm, keyName, nonce, Array.Empty(), Array.Empty(), cancellationToken); + ///// + ///// Used to unwrap the specified key. + ///// + ///// The name of the vault to retrieve the key from. + ///// The byte comprising the wrapped key. + ///// The algorithm to use in unwrapping the key. + ///// The name of the key used to unwrap the wrapped key bytes. + ///// The nonce value. + ///// A that can be used to cancel the operation. + ///// The bytes comprising the unwrapped key. + //[Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public async Task UnwrapKeyAsync(string vaultResourceName, byte[] wrappedKey, string algorithm, string keyName, + // byte[] nonce, CancellationToken cancellationToken = default) => await UnwrapKeyAsync(vaultResourceName, + // wrappedKey, algorithm, keyName, nonce, Array.Empty(), Array.Empty(), cancellationToken); - ///// - ///// Creates a signature of a digest value. - ///// - ///// The name of the vault to retrieve the key from. - ///// The digest value to create the signature for. - ///// The algorithm used to create the signature. - ///// The name of the key used. - ///// A that can be used to cancel the operation. - ///// The bytes comprising the signature. - //[Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - //public abstract Task SignAsync(string vaultResourceName, byte[] digest, string algorithm, string keyName, - // CancellationToken cancellationToken = default); + ///// + ///// Creates a signature of a digest value. + ///// + ///// The name of the vault to retrieve the key from. + ///// The digest value to create the signature for. + ///// The algorithm used to create the signature. + ///// The name of the key used. + ///// A that can be used to cancel the operation. + ///// The bytes comprising the signature. + //[Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public abstract Task SignAsync(string vaultResourceName, byte[] digest, string algorithm, string keyName, + // CancellationToken cancellationToken = default); - ///// - ///// Validates a signature. - ///// - ///// The name of the vault to retrieve the key from. - ///// The digest to validate the signature with. - ///// The signature to validate. - ///// The algorithm to validate the signature with. - ///// The name of the key used. - ///// A that can be used to cancel the operation. - ///// True if the signature verification is successful; otherwise false. - //[Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - //public abstract Task VerifyAsync(string vaultResourceName, byte[] digest, byte[] signature, string algorithm, string keyName, - // CancellationToken cancellationToken = default); + ///// + ///// Validates a signature. + ///// + ///// The name of the vault to retrieve the key from. + ///// The digest to validate the signature with. + ///// The signature to validate. + ///// The algorithm to validate the signature with. + ///// The name of the key used. + ///// A that can be used to cancel the operation. + ///// True if the signature verification is successful; otherwise false. + //[Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public abstract Task VerifyAsync(string vaultResourceName, byte[] digest, byte[] signature, string algorithm, string keyName, + // CancellationToken cancellationToken = default); - #endregion + #endregion - /// - /// Attempt to lock the given resourceId with response indicating success. - /// - /// The name of the lock store to be queried. - /// Lock key that stands for which resource to protect. - /// Indicates the identifier of lock owner. - /// The time after which the lock gets expired. - /// A that can be used to cancel the operation. - /// A containing a - [Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public abstract Task Lock( - string storeName, - string resourceId, - string lockOwner, - Int32 expiryInSeconds, - CancellationToken cancellationToken = default); + /// + /// Attempt to lock the given resourceId with response indicating success. + /// + /// The name of the lock store to be queried. + /// Lock key that stands for which resource to protect. + /// Indicates the identifier of lock owner. + /// The time after which the lock gets expired. + /// A that can be used to cancel the operation. + /// A containing a + [Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public abstract Task Lock( + string storeName, + string resourceId, + string lockOwner, + Int32 expiryInSeconds, + CancellationToken cancellationToken = default); - /// - /// Attempt to unlock the given resourceId with response indicating success. - /// - /// The name of the lock store to be queried. - /// Lock key that stands for which resource to protect. - /// Indicates the identifier of lock owner. - /// A that can be used to cancel the operation. - /// A containing a - [Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public abstract Task Unlock( - string storeName, - string resourceId, - string lockOwner, - CancellationToken cancellationToken = default); + /// + /// Attempt to unlock the given resourceId with response indicating success. + /// + /// The name of the lock store to be queried. + /// Lock key that stands for which resource to protect. + /// Indicates the identifier of lock owner. + /// A that can be used to cancel the operation. + /// A containing a + [Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public abstract Task Unlock( + string storeName, + string resourceId, + string lockOwner, + CancellationToken cancellationToken = default); - /// - public void Dispose() + /// + public void Dispose() + { + if (!this.disposed) { - if (!this.disposed) - { - Dispose(disposing: true); - this.disposed = true; - } - } - - /// - /// Disposes the resources associated with the object. - /// - /// true if called by a call to the Dispose method; otherwise false. - protected virtual void Dispose(bool disposing) - { - } - - /// - /// Returns the value for the User-Agent. - /// - /// A containing the value to use for User-Agent. - protected static ProductInfoHeaderValue UserAgent() - { - var assembly = typeof(DaprClient).Assembly; - string assemblyVersion = assembly - .GetCustomAttributes() - .FirstOrDefault()? - .InformationalVersion; - - return new ProductInfoHeaderValue("dapr-sdk-dotnet", $"v{assemblyVersion}"); + Dispose(disposing: true); + this.disposed = true; } } + + /// + /// Disposes the resources associated with the object. + /// + /// true if called by a call to the Dispose method; otherwise false. + protected virtual void Dispose(bool disposing) + { + } + + /// + /// Returns the value for the User-Agent. + /// + /// A containing the value to use for User-Agent. + protected static ProductInfoHeaderValue UserAgent() + { + var assemblyVersion = typeof(DaprClient).Assembly + .GetCustomAttributes() + .FirstOrDefault()? + .InformationalVersion; + + return new ProductInfoHeaderValue("dapr-sdk-dotnet", $"v{assemblyVersion}"); + } } diff --git a/src/Dapr.Client/DaprClientBuilder.cs b/src/Dapr.Client/DaprClientBuilder.cs index 68315c45..ad3fe6b0 100644 --- a/src/Dapr.Client/DaprClientBuilder.cs +++ b/src/Dapr.Client/DaprClientBuilder.cs @@ -11,180 +11,179 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Client +namespace Dapr.Client; + +using System; +using System.Net.Http; +using System.Text.Json; +using Grpc.Net.Client; +using Autogenerated = Autogen.Grpc.v1; + +/// +/// Builder for building +/// +public sealed class DaprClientBuilder { - using System; - using System.Net.Http; - using System.Text.Json; - using Grpc.Net.Client; - using Autogenerated = Autogen.Grpc.v1; + /// + /// Initializes a new instance of the class. + /// + public DaprClientBuilder() + { + this.GrpcEndpoint = DaprDefaults.GetDefaultGrpcEndpoint(); + this.HttpEndpoint = DaprDefaults.GetDefaultHttpEndpoint(); + + this.GrpcChannelOptions = new GrpcChannelOptions() + { + // The gRPC client doesn't throw the right exception for cancellation + // by default, this switches that behavior on. + ThrowOperationCanceledOnCancellation = true, + }; + + this.JsonSerializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web); + this.DaprApiToken = DaprDefaults.GetDefaultDaprApiToken(null); + } + + // property exposed for testing purposes + internal string GrpcEndpoint { get; private set; } + + // property exposed for testing purposes + internal string HttpEndpoint { get; private set; } + + private Func HttpClientFactory { get; set; } + + // property exposed for testing purposes + internal JsonSerializerOptions JsonSerializerOptions { get; private set; } + + // property exposed for testing purposes + internal GrpcChannelOptions GrpcChannelOptions { get; private set; } + internal string DaprApiToken { get; private set; } + internal TimeSpan Timeout { get; private set; } /// - /// Builder for building + /// Overrides the HTTP endpoint used by for communicating with the Dapr runtime. /// - public sealed class DaprClientBuilder + /// + /// The URI endpoint to use for HTTP calls to the Dapr runtime. The default value will be + /// DAPR_HTTP_ENDPOINT first, or http://127.0.0.1:DAPR_HTTP_PORT as fallback + /// where DAPR_HTTP_ENDPOINT and DAPR_HTTP_PORT represents the value of the + /// corresponding environment variables. + /// + /// The instance. + public DaprClientBuilder UseHttpEndpoint(string httpEndpoint) { - /// - /// Initializes a new instance of the class. - /// - public DaprClientBuilder() - { - this.GrpcEndpoint = DaprDefaults.GetDefaultGrpcEndpoint(); - this.HttpEndpoint = DaprDefaults.GetDefaultHttpEndpoint(); - - this.GrpcChannelOptions = new GrpcChannelOptions() - { - // The gRPC client doesn't throw the right exception for cancellation - // by default, this switches that behavior on. - ThrowOperationCanceledOnCancellation = true, - }; - - this.JsonSerializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web); - this.DaprApiToken = DaprDefaults.GetDefaultDaprApiToken(null); - } - - // property exposed for testing purposes - internal string GrpcEndpoint { get; private set; } - - // property exposed for testing purposes - internal string HttpEndpoint { get; private set; } - - private Func HttpClientFactory { get; set; } - - // property exposed for testing purposes - internal JsonSerializerOptions JsonSerializerOptions { get; private set; } - - // property exposed for testing purposes - internal GrpcChannelOptions GrpcChannelOptions { get; private set; } - internal string DaprApiToken { get; private set; } - internal TimeSpan Timeout { get; private set; } - - /// - /// Overrides the HTTP endpoint used by for communicating with the Dapr runtime. - /// - /// - /// The URI endpoint to use for HTTP calls to the Dapr runtime. The default value will be - /// DAPR_HTTP_ENDPOINT first, or http://127.0.0.1:DAPR_HTTP_PORT as fallback - /// where DAPR_HTTP_ENDPOINT and DAPR_HTTP_PORT represents the value of the - /// corresponding environment variables. - /// - /// The instance. - public DaprClientBuilder UseHttpEndpoint(string httpEndpoint) - { - ArgumentVerifier.ThrowIfNullOrEmpty(httpEndpoint, nameof(httpEndpoint)); - this.HttpEndpoint = httpEndpoint; - return this; - } - - // Internal for testing of DaprClient - internal DaprClientBuilder UseHttpClientFactory(Func factory) - { - this.HttpClientFactory = factory; - return this; - } - - /// - /// Overrides the gRPC endpoint used by for communicating with the Dapr runtime. - /// - /// - /// The URI endpoint to use for gRPC calls to the Dapr runtime. The default value will be - /// http://127.0.0.1:DAPR_GRPC_PORT where DAPR_GRPC_PORT represents the value of the - /// DAPR_GRPC_PORT environment variable. - /// - /// The instance. - public DaprClientBuilder UseGrpcEndpoint(string grpcEndpoint) - { - ArgumentVerifier.ThrowIfNullOrEmpty(grpcEndpoint, nameof(grpcEndpoint)); - this.GrpcEndpoint = grpcEndpoint; - return this; - } - - /// - /// - /// Uses the specified when serializing or deserializing using . - /// - /// - /// The default value is created using . - /// - /// - /// Json serialization options. - /// The instance. - public DaprClientBuilder UseJsonSerializationOptions(JsonSerializerOptions options) - { - this.JsonSerializerOptions = options; - return this; - } - - /// - /// Uses the provided for creating the . - /// - /// The to use for creating the . - /// The instance. - public DaprClientBuilder UseGrpcChannelOptions(GrpcChannelOptions grpcChannelOptions) - { - this.GrpcChannelOptions = grpcChannelOptions; - return this; - } - - /// - /// Adds the provided on every request to the Dapr runtime. - /// - /// The token to be added to the request headers/>. - /// The instance. - public DaprClientBuilder UseDaprApiToken(string apiToken) - { - this.DaprApiToken = apiToken; - return this; - } - - /// - /// Sets the timeout for the HTTP client used by the . - /// - /// - /// - public DaprClientBuilder UseTimeout(TimeSpan timeout) - { - this.Timeout = timeout; - return this; - } - - /// - /// Builds a instance from the properties of the builder. - /// - /// The . - public DaprClient Build() - { - var grpcEndpoint = new Uri(this.GrpcEndpoint); - if (grpcEndpoint.Scheme != "http" && grpcEndpoint.Scheme != "https") - { - throw new InvalidOperationException("The gRPC endpoint must use http or https."); - } - - if (grpcEndpoint.Scheme.Equals(Uri.UriSchemeHttp)) - { - // Set correct switch to maksecure gRPC service calls. This switch must be set before creating the GrpcChannel. - AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); - } - - var httpEndpoint = new Uri(this.HttpEndpoint); - if (httpEndpoint.Scheme != "http" && httpEndpoint.Scheme != "https") - { - throw new InvalidOperationException("The HTTP endpoint must use http or https."); - } - - var channel = GrpcChannel.ForAddress(this.GrpcEndpoint, this.GrpcChannelOptions); - var client = new Autogenerated.Dapr.DaprClient(channel); - - - var apiTokenHeader = DaprClient.GetDaprApiTokenHeader(this.DaprApiToken); - var httpClient = HttpClientFactory is object ? HttpClientFactory() : new HttpClient(); - - if (this.Timeout > TimeSpan.Zero) - { - httpClient.Timeout = this.Timeout; - } - - return new DaprClientGrpc(channel, client, httpClient, httpEndpoint, this.JsonSerializerOptions, apiTokenHeader); - } + ArgumentVerifier.ThrowIfNullOrEmpty(httpEndpoint, nameof(httpEndpoint)); + this.HttpEndpoint = httpEndpoint; + return this; } -} + + // Internal for testing of DaprClient + internal DaprClientBuilder UseHttpClientFactory(Func factory) + { + this.HttpClientFactory = factory; + return this; + } + + /// + /// Overrides the gRPC endpoint used by for communicating with the Dapr runtime. + /// + /// + /// The URI endpoint to use for gRPC calls to the Dapr runtime. The default value will be + /// http://127.0.0.1:DAPR_GRPC_PORT where DAPR_GRPC_PORT represents the value of the + /// DAPR_GRPC_PORT environment variable. + /// + /// The instance. + public DaprClientBuilder UseGrpcEndpoint(string grpcEndpoint) + { + ArgumentVerifier.ThrowIfNullOrEmpty(grpcEndpoint, nameof(grpcEndpoint)); + this.GrpcEndpoint = grpcEndpoint; + return this; + } + + /// + /// + /// Uses the specified when serializing or deserializing using . + /// + /// + /// The default value is created using . + /// + /// + /// Json serialization options. + /// The instance. + public DaprClientBuilder UseJsonSerializationOptions(JsonSerializerOptions options) + { + this.JsonSerializerOptions = options; + return this; + } + + /// + /// Uses the provided for creating the . + /// + /// The to use for creating the . + /// The instance. + public DaprClientBuilder UseGrpcChannelOptions(GrpcChannelOptions grpcChannelOptions) + { + this.GrpcChannelOptions = grpcChannelOptions; + return this; + } + + /// + /// Adds the provided on every request to the Dapr runtime. + /// + /// The token to be added to the request headers/>. + /// The instance. + public DaprClientBuilder UseDaprApiToken(string apiToken) + { + this.DaprApiToken = apiToken; + return this; + } + + /// + /// Sets the timeout for the HTTP client used by the . + /// + /// + /// + public DaprClientBuilder UseTimeout(TimeSpan timeout) + { + this.Timeout = timeout; + return this; + } + + /// + /// Builds a instance from the properties of the builder. + /// + /// The . + public DaprClient Build() + { + var grpcEndpoint = new Uri(this.GrpcEndpoint); + if (grpcEndpoint.Scheme != "http" && grpcEndpoint.Scheme != "https") + { + throw new InvalidOperationException("The gRPC endpoint must use http or https."); + } + + if (grpcEndpoint.Scheme.Equals(Uri.UriSchemeHttp)) + { + // Set correct switch to maksecure gRPC service calls. This switch must be set before creating the GrpcChannel. + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); + } + + var httpEndpoint = new Uri(this.HttpEndpoint); + if (httpEndpoint.Scheme != "http" && httpEndpoint.Scheme != "https") + { + throw new InvalidOperationException("The HTTP endpoint must use http or https."); + } + + var channel = GrpcChannel.ForAddress(this.GrpcEndpoint, this.GrpcChannelOptions); + var client = new Autogenerated.Dapr.DaprClient(channel); + + + var apiTokenHeader = DaprClient.GetDaprApiTokenHeader(this.DaprApiToken); + var httpClient = HttpClientFactory is object ? HttpClientFactory() : new HttpClient(); + + if (this.Timeout > TimeSpan.Zero) + { + httpClient.Timeout = this.Timeout; + } + + return new DaprClientGrpc(channel, client, httpClient, httpEndpoint, this.JsonSerializerOptions, apiTokenHeader); + } +} \ No newline at end of file diff --git a/src/Dapr.Client/DaprClientGrpc.cs b/src/Dapr.Client/DaprClientGrpc.cs index 394b313e..9ee860cf 100644 --- a/src/Dapr.Client/DaprClientGrpc.cs +++ b/src/Dapr.Client/DaprClientGrpc.cs @@ -11,8 +11,6 @@ // limitations under the License. // ------------------------------------------------------------------------ -using Dapr.Common.Extensions; - namespace Dapr.Client; using System; @@ -23,7 +21,6 @@ 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; @@ -207,7 +204,7 @@ internal class DaprClientGrpc : DaprClient Dictionary> entryMap = new Dictionary>(); - for (int counter = 0; counter < events.Count; counter++) + for (var counter = 0; counter < events.Count; counter++) { var entry = new Autogenerated.BulkPublishRequestEntry() { @@ -217,7 +214,6 @@ internal class DaprClientGrpc : DaprClient events[counter] is CloudEvent ? Constants.ContentTypeCloudEvent : Constants.ContentTypeApplicationJson, - Metadata = { }, }; envelope.Entries.Add(entry); entryMap.Add(counter.ToString(), new BulkPublishEntry( @@ -274,7 +270,7 @@ internal class DaprClientGrpc : DaprClient ArgumentVerifier.ThrowIfNullOrEmpty(bindingName, nameof(bindingName)); ArgumentVerifier.ThrowIfNullOrEmpty(operation, nameof(operation)); - var bytes = TypeConverters.ToJsonByteString(data, this.jsonSerializerOptions); + var bytes = TypeConverters.ToJsonByteString(data, this.jsonSerializerOptions); _ = await MakeInvokeBindingRequestAsync(bindingName, operation, bytes, metadata, cancellationToken); } @@ -289,7 +285,7 @@ internal class DaprClientGrpc : DaprClient ArgumentVerifier.ThrowIfNullOrEmpty(bindingName, nameof(bindingName)); ArgumentVerifier.ThrowIfNullOrEmpty(operation, nameof(operation)); - var bytes = TypeConverters.ToJsonByteString(data, this.jsonSerializerOptions); + var bytes = TypeConverters.ToJsonByteString(data, this.jsonSerializerOptions); var response = await MakeInvokeBindingRequestAsync(bindingName, operation, bytes, metadata, cancellationToken); try @@ -423,7 +419,7 @@ internal class DaprClientGrpc : DaprClient ArgumentVerifier.ThrowIfNull(methodName, nameof(methodName)); var request = CreateInvokeMethodRequest(httpMethod, appId, methodName, queryStringParameters); - request.Content = JsonContent.Create(data, options: this.JsonSerializerOptions); + request.Content = JsonContent.Create(data, options: this.JsonSerializerOptions); return request; } @@ -432,7 +428,7 @@ internal class DaprClientGrpc : DaprClient { ArgumentVerifier.ThrowIfNull(request, nameof(request)); - if (!this.httpEndpoint.IsBaseOf(request.RequestUri)) + if (request.RequestUri != null && !this.httpEndpoint.IsBaseOf(request.RequestUri)) { throw new InvalidOperationException("The provided request URI is not a Dapr service invocation URI."); } @@ -451,8 +447,8 @@ internal class DaprClientGrpc : DaprClient request.Options.TryGetValue(new HttpRequestOptionsKey(MethodNameKey), out var methodName); throw new InvocationException( - appId: appId as string, - methodName: methodName as string, + appId: appId, + methodName: methodName, innerException: ex, response: null); } @@ -501,8 +497,8 @@ internal class DaprClientGrpc : DaprClient request.Options.TryGetValue(new HttpRequestOptionsKey(MethodNameKey), out var methodName); throw new InvocationException( - appId: appId as string, - methodName: methodName as string, + appId: appId, + methodName: methodName, innerException: ex, response: response); } @@ -526,8 +522,8 @@ internal class DaprClientGrpc : DaprClient request.Options.TryGetValue(new HttpRequestOptionsKey(MethodNameKey), out var methodName); throw new InvocationException( - appId: appId as string, - methodName: methodName as string, + appId: appId, + methodName: methodName, innerException: ex, response: response); } @@ -544,8 +540,8 @@ internal class DaprClientGrpc : DaprClient request.Options.TryGetValue(new HttpRequestOptionsKey(MethodNameKey), out var methodName); throw new InvocationException( - appId: appId as string, - methodName: methodName as string, + appId: appId, + methodName: methodName, innerException: ex, response: response); } @@ -555,8 +551,8 @@ internal class DaprClientGrpc : DaprClient request.Options.TryGetValue(new HttpRequestOptionsKey(MethodNameKey), out var methodName); throw new InvocationException( - appId: appId as string, - methodName: methodName as string, + appId: appId, + methodName: methodName, innerException: ex, response: response); } @@ -1099,7 +1095,7 @@ internal class DaprClientGrpc : DaprClient public override async Task<(TValue value, string etag)> GetStateAndETagAsync( string storeName, string key, - ConsistencyMode? consistencyMode = default, + ConsistencyMode? consistencyMode = null, IReadOnlyDictionary metadata = default, CancellationToken cancellationToken = default) { @@ -1283,7 +1279,7 @@ internal class DaprClientGrpc : DaprClient { var stateOperation = new Autogenerated.TransactionalStateOperation { - OperationType = state.OperationType.ToString().ToLower(), Request = ToAutogeneratedStateItem(state) + OperationType = state.OperationType.ToString()?.ToLower(), Request = ToAutogeneratedStateItem(state) }; envelope.Operations.Add(stateOperation); @@ -1579,7 +1575,7 @@ internal class DaprClientGrpc : DaprClient var request = new Autogenerated.GetConfigurationRequest() { StoreName = storeName }; - if (keys != null && keys.Count > 0) + if (keys is { Count: > 0 }) { request.Keys.AddRange(keys); } @@ -1593,7 +1589,7 @@ internal class DaprClientGrpc : DaprClient } var options = CreateCallOptions(headers: null, cancellationToken); - Autogenerated.GetConfigurationResponse response = new Autogenerated.GetConfigurationResponse(); + Autogenerated.GetConfigurationResponse response; try { response = await client.GetConfigurationAsync(request, options); @@ -1815,7 +1811,7 @@ internal class DaprClientGrpc : DaprClient //At the same time, retrieve the decrypted response from the sidecar receiveResult) //Return only the result of the `RetrieveEncryptedStreamAsync` method - .ContinueWith(t => receiveResult.Result, cancellationToken); + .ContinueWith(_ => receiveResult.Result, cancellationToken); } /// @@ -2220,7 +2216,7 @@ internal class DaprClientGrpc : DaprClient }; var options = CreateCallOptions(headers: null, cancellationToken); - Autogenerated.UnlockResponse response = new Autogenerated.UnlockResponse(); + Autogenerated.UnlockResponse response; try { response = await client.UnlockAlpha1Async(request, options); @@ -2368,12 +2364,15 @@ internal class DaprClientGrpc : DaprClient { var options = new CallOptions(headers: headers ?? new Metadata(), cancellationToken: cancellationToken); - options.Headers.Add("User-Agent", UserAgent().ToString()); - - // add token for dapr api token based authentication - if (this.apiTokenHeader is not null) + if (options.Headers != null) { - options.Headers.Add(this.apiTokenHeader.Value.Key, this.apiTokenHeader.Value.Value); + options.Headers.Add("User-Agent", UserAgent().ToString()); + + // 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); + } } return options; diff --git a/src/Dapr.Client/DaprError.cs b/src/Dapr.Client/DaprError.cs index df464f6c..05a280b1 100644 --- a/src/Dapr.Client/DaprError.cs +++ b/src/Dapr.Client/DaprError.cs @@ -11,22 +11,21 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr +namespace Dapr; + +/// +/// The REST API operations for Dapr runtime return standard HTTP status codes. This type defines the additional +/// information returned from the Service Fabric API operations that are not successful. +/// +public class DaprError { /// - /// The REST API operations for Dapr runtime return standard HTTP status codes. This type defines the additional - /// information returned from the Service Fabric API operations that are not successful. - /// - public class DaprError - { - /// - /// Gets ErrorCode. - /// - public string ErrorCode { get; set; } + /// Gets ErrorCode. + /// + public string ErrorCode { get; set; } - /// - /// Gets error message. - /// - public string Message { get; set; } - } -} + /// + /// Gets error message. + /// + public string Message { get; set; } +} \ No newline at end of file diff --git a/src/Dapr.Client/DaprMetadata.cs b/src/Dapr.Client/DaprMetadata.cs index 4cd812e0..2d12f1e8 100644 --- a/src/Dapr.Client/DaprMetadata.cs +++ b/src/Dapr.Client/DaprMetadata.cs @@ -13,114 +13,82 @@ using System.Collections.Generic; -namespace Dapr.Client +namespace Dapr.Client; + +/// +/// Represents a metadata object returned from dapr sidecar. +/// +/// The application id. +/// The registered actors metadata. +/// The list of custom attributes as key-value pairs, where key is the attribute name. +/// The loaded components metadata. +public sealed class DaprMetadata(string id, IReadOnlyList actors, IReadOnlyDictionary extended, IReadOnlyList components) { /// - /// Represents a metadata object returned from dapr sidecar. + /// Gets the application id. /// - public sealed class DaprMetadata - { - /// - /// Initializes a new instance of the class. - /// - /// The application id. - /// The registered actors metadata. - /// The list of custom attributes as key-value pairs, where key is the attribute name. - /// The loaded components metadata. - public DaprMetadata(string id, IReadOnlyList actors, IReadOnlyDictionary extended, IReadOnlyList components) - { - Id = id; - Actors = actors; - Extended = extended; - Components = components; - } - - /// - /// Gets the application id. - /// - public string Id { get; } - - /// - /// Gets the registered actors metadata. - /// - public IReadOnlyList Actors { get; } - - /// - /// Gets the list of custom attributes as key-value pairs, where key is the attribute name. - /// - public IReadOnlyDictionary Extended { get; } - - /// - /// Gets the loaded components metadata. - /// - public IReadOnlyList Components { get; } - } + public string Id { get; } = id; /// - /// Represents a actor metadata object returned from dapr sidecar. + /// Gets the registered actors metadata. /// - public sealed class DaprActorMetadata - { - /// - /// Initializes a new instance of the class. - /// - /// This registered actor type. - /// The number of actors running. - public DaprActorMetadata(string type, int count) - { - Type = type; - Count = count; - } - - /// - /// Gets the registered actor type. - /// - public string Type { get; } - - /// - /// Gets the number of actors running. - /// - public int Count { get; } - } + public IReadOnlyList Actors { get; } = actors; /// - /// Represents a components metadata object returned from dapr sidecar. + /// Gets the list of custom attributes as key-value pairs, where key is the attribute name. /// - public sealed class DaprComponentsMetadata - { - /// - /// Initializes a new instance of the class. - /// - /// The name of the component. - /// The component type. - /// The component version. - /// The supported capabilities for this component type and version. - public DaprComponentsMetadata(string name, string type, string version, string[] capabilities) - { - Name = name; - Type = type; - Version = version; - Capabilities = capabilities; - } + public IReadOnlyDictionary Extended { get; } = extended; - /// - /// Gets the name of the component. - /// - public string Name { get; } - - /// - /// Gets the component type. - /// - public string Type { get; } - - /// - /// Gets the component version. - /// - public string Version { get; } - - /// - /// Gets the supported capabilities for this component type and version. - /// - public string[] Capabilities { get; } - } + /// + /// Gets the loaded components metadata. + /// + public IReadOnlyList Components { get; } = components; +} + +/// +/// Represents a actor metadata object returned from dapr sidecar. +/// +/// This registered actor type. +/// The number of actors running. +public sealed class DaprActorMetadata(string type, int count) +{ + /// + /// Gets the registered actor type. + /// + public string Type { get; } = type; + + /// + /// Gets the number of actors running. + /// + public int Count { get; } = count; +} + +/// +/// Represents a components metadata object returned from dapr sidecar. +/// +/// The name of the component. +/// The component type. +/// The component version. +/// The supported capabilities for this component type and version. +public sealed class DaprComponentsMetadata(string name, string type, string version, string[] capabilities) +{ + /// + /// Gets the name of the component. + /// + public string Name { get; } = name; + + /// + /// Gets the component type. + /// + public string Type { get; } = type; + + /// + /// Gets the component version. + /// + public string Version { get; } = version; + + /// + /// Gets the supported capabilities for this component type and version. + /// + public string[] Capabilities { get; } = capabilities; } diff --git a/src/Dapr.Client/DaprSubscribeConfigurationSource.cs b/src/Dapr.Client/DaprSubscribeConfigurationSource.cs index 084e79ee..b5214c9a 100644 --- a/src/Dapr.Client/DaprSubscribeConfigurationSource.cs +++ b/src/Dapr.Client/DaprSubscribeConfigurationSource.cs @@ -19,81 +19,66 @@ using System.Threading.Tasks; using Grpc.Core; using Autogenerated = Dapr.Client.Autogen.Grpc.v1; -namespace Dapr.Client +namespace Dapr.Client; + +internal class DaprSubscribeConfigurationSource : ConfigurationSource { - internal class DaprSubscribeConfigurationSource : ConfigurationSource + private AsyncServerStreamingCall call; + private string id = string.Empty; + + /// + /// Constructor. + /// + /// The streaming call from the Dapr Subscribe Configuration API. + internal DaprSubscribeConfigurationSource(AsyncServerStreamingCall call) : base() { - private AsyncServerStreamingCall call; - private string id = string.Empty; - - /// - /// Constructor. - /// - /// The streaming call from the Dapr Subscribe Configuration API. - internal DaprSubscribeConfigurationSource(AsyncServerStreamingCall call) : base() - { - this.call = call; - } - - /// - public override string Id => id; - - public override IAsyncEnumerator> GetAsyncEnumerator(CancellationToken cancellationToken = default) - { - return new DaprSubscribeConfigurationEnumerator(call, streamId => setIdIfNullOrEmpty(streamId), cancellationToken); - } - - private void setIdIfNullOrEmpty(string id) - { - if (string.IsNullOrEmpty(this.id)) - { - this.id = id; - } - } + this.call = call; } - internal class DaprSubscribeConfigurationEnumerator : IAsyncEnumerator> + /// + public override string Id => id; + + public override IAsyncEnumerator> GetAsyncEnumerator(CancellationToken cancellationToken = default) { - private AsyncServerStreamingCall call; - private Action idCallback; - private CancellationToken cancellationToken; + return new DaprSubscribeConfigurationEnumerator(call, SetIdIfNullOrEmpty, cancellationToken); + } - internal DaprSubscribeConfigurationEnumerator( - AsyncServerStreamingCall call, - Action idCallback, - CancellationToken cancellationToken = default) + private void SetIdIfNullOrEmpty(string id) + { + if (string.IsNullOrEmpty(this.id)) { - this.call = call; - this.idCallback = idCallback; - this.cancellationToken = cancellationToken; - } - - /// - public IDictionary Current - { - get - { - var current = call.ResponseStream.Current; - if (current != null) - { - idCallback(current.Id); - return current.Items.ToDictionary(item => item.Key, item => new ConfigurationItem(item.Value.Value, item.Value.Version, item.Value.Metadata)); - } - return null; - } - } - - /// - public ValueTask DisposeAsync() - { - call.Dispose(); - return new ValueTask(); - } - - /// - public async ValueTask MoveNextAsync() - { - return await call.ResponseStream.MoveNext(cancellationToken); + this.id = id; } } } + +internal class DaprSubscribeConfigurationEnumerator(AsyncServerStreamingCall call, Action idCallback, CancellationToken cancellationToken = default) : IAsyncEnumerator> +{ + /// + public IDictionary Current + { + get + { + var current = call.ResponseStream.Current; + if (current != null) + { + idCallback(current.Id); + return current.Items.ToDictionary(item => item.Key, item => new ConfigurationItem(item.Value.Value, item.Value.Version, item.Value.Metadata)); + } + return null; + } + } + + /// + public ValueTask DisposeAsync() + { + call.Dispose(); + return new ValueTask(); + } + + /// + public async ValueTask MoveNextAsync() + { + return await call.ResponseStream.MoveNext(cancellationToken); + } +} diff --git a/src/Dapr.Client/DeleteBulkStateItem.cs b/src/Dapr.Client/DeleteBulkStateItem.cs index 13a4722d..8f005981 100644 --- a/src/Dapr.Client/DeleteBulkStateItem.cs +++ b/src/Dapr.Client/DeleteBulkStateItem.cs @@ -11,48 +11,37 @@ // limitations under the License. // ------------------------------------------------------------------------ +#nullable enable using System.Collections.Generic; -namespace Dapr.Client +namespace Dapr.Client; + +/// +/// Represents a state object used for bulk delete state operation +/// +/// The state key. +/// The ETag. +/// The stateOptions. +/// The metadata. +public readonly struct BulkDeleteStateItem(string key, string etag, StateOptions? stateOptions = null, IReadOnlyDictionary? metadata = null) { /// - /// Represents a state object used for bulk delete state operation + /// Gets the state key. /// - public readonly struct BulkDeleteStateItem - { - /// - /// Initializes a new instance of the class. - /// - /// The state key. - /// The ETag. - /// The stateOptions. - /// The metadata. - public BulkDeleteStateItem(string key, string etag, StateOptions stateOptions = default, IReadOnlyDictionary metadata = default) - { - this.Key = key; - this.ETag = etag; - StateOptions = stateOptions; - Metadata = metadata; - } + public string Key { get; } = key; - /// - /// Gets the state key. - /// - public string Key { get; } + /// + /// Get the ETag. + /// + public string ETag { get; } = etag; - /// - /// Get the ETag. - /// - public string ETag { get; } + /// + /// Gets the StateOptions. + /// + public StateOptions? StateOptions { get; } = stateOptions; - /// - /// Gets the StateOptions. - /// - public StateOptions StateOptions { get; } - - /// - /// Gets the Metadata. - /// - public IReadOnlyDictionary Metadata { get; } - } + /// + /// Gets the Metadata. + /// + public IReadOnlyDictionary? Metadata { get; } = metadata; } diff --git a/src/Dapr.Client/Extensions/EnumExtensions.cs b/src/Dapr.Client/Extensions/EnumExtensions.cs index df9c9ad3..a239583d 100644 --- a/src/Dapr.Client/Extensions/EnumExtensions.cs +++ b/src/Dapr.Client/Extensions/EnumExtensions.cs @@ -16,26 +16,25 @@ using System; using System.Reflection; using System.Runtime.Serialization; -namespace Dapr.Client +namespace Dapr.Client; + +internal static class EnumExtensions { - internal static class EnumExtensions + /// + /// Reads the value of an enum out of the attached attribute. + /// + /// The enum. + /// The value of the enum to pull the value for. + /// + public static string GetValueFromEnumMember(this T value) where T : Enum { - /// - /// Reads the value of an enum out of the attached attribute. - /// - /// The enum. - /// The value of the enum to pull the value for. - /// - public static string GetValueFromEnumMember(this T value) where T : Enum - { - ArgumentNullException.ThrowIfNull(value, nameof(value)); + ArgumentNullException.ThrowIfNull(value, nameof(value)); - var memberInfo = typeof(T).GetMember(value.ToString(), BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly); - if (memberInfo.Length <= 0) - return value.ToString(); + var memberInfo = typeof(T).GetMember(value.ToString(), BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly); + if (memberInfo.Length <= 0) + return value.ToString(); - var attributes = memberInfo[0].GetCustomAttributes(typeof(EnumMemberAttribute), false); - return (attributes.Length > 0 ? ((EnumMemberAttribute)attributes[0]).Value : value.ToString()) ?? value.ToString(); - } + var attributes = memberInfo[0].GetCustomAttributes(typeof(EnumMemberAttribute), false); + return (attributes.Length > 0 ? ((EnumMemberAttribute)attributes[0]).Value : value.ToString()) ?? value.ToString(); } -} +} \ No newline at end of file diff --git a/src/Dapr.Client/Extensions/HttpExtensions.cs b/src/Dapr.Client/Extensions/HttpExtensions.cs index 259d2747..6e8c1a33 100644 --- a/src/Dapr.Client/Extensions/HttpExtensions.cs +++ b/src/Dapr.Client/Extensions/HttpExtensions.cs @@ -16,36 +16,35 @@ using System; using System.Collections.Generic; using System.Text; -namespace Dapr.Client +namespace Dapr.Client; + +/// +/// Provides extensions specific to HTTP types. +/// +internal static class HttpExtensions { /// - /// Provides extensions specific to HTTP types. + /// Appends key/value pairs to the query string on an HttpRequestMessage. /// - internal static class HttpExtensions + /// The uri to append the query string parameters to. + /// The key/value pairs to populate the query string with. + public static Uri AddQueryParameters(this Uri? uri, + IReadOnlyCollection>? queryStringParameters) { - /// - /// Appends key/value pairs to the query string on an HttpRequestMessage. - /// - /// The uri to append the query string parameters to. - /// The key/value pairs to populate the query string with. - public static Uri AddQueryParameters(this Uri? uri, - IReadOnlyCollection>? queryStringParameters) + ArgumentNullException.ThrowIfNull(uri, nameof(uri)); + if (queryStringParameters is null) + return uri; + + var uriBuilder = new UriBuilder(uri); + var qsBuilder = new StringBuilder(uriBuilder.Query); + foreach (var kvParam in queryStringParameters) { - ArgumentNullException.ThrowIfNull(uri, nameof(uri)); - if (queryStringParameters is null) - return uri; - - var uriBuilder = new UriBuilder(uri); - var qsBuilder = new StringBuilder(uriBuilder.Query); - foreach (var kvParam in queryStringParameters) - { - if (qsBuilder.Length > 0) - qsBuilder.Append('&'); - qsBuilder.Append($"{Uri.EscapeDataString(kvParam.Key)}={Uri.EscapeDataString(kvParam.Value)}"); - } - - uriBuilder.Query = qsBuilder.ToString(); - return uriBuilder.Uri; + if (qsBuilder.Length > 0) + qsBuilder.Append('&'); + qsBuilder.Append($"{Uri.EscapeDataString(kvParam.Key)}={Uri.EscapeDataString(kvParam.Value)}"); } + + uriBuilder.Query = qsBuilder.ToString(); + return uriBuilder.Uri; } -} +} \ No newline at end of file diff --git a/src/Dapr.Client/GetConfigurationResponse.cs b/src/Dapr.Client/GetConfigurationResponse.cs index 0349d134..c707345d 100644 --- a/src/Dapr.Client/GetConfigurationResponse.cs +++ b/src/Dapr.Client/GetConfigurationResponse.cs @@ -13,33 +13,16 @@ using System.Collections.Generic; -namespace Dapr.Client +namespace Dapr.Client; + +/// +/// Class representing the response from a GetConfiguration API call. +/// +/// The map of keys to items that was returned in the GetConfiguration call. +public class GetConfigurationResponse(IReadOnlyDictionary configMap) { /// - /// Class representing the response from a GetConfiguration API call. + /// The map of key to items returned in a GetConfiguration call. /// - public class GetConfigurationResponse - { - private readonly IReadOnlyDictionary configMap; - - /// - /// Constructor for a GetConfigurationResponse. - /// - /// The map of keys to items that was returned in the GetConfiguration call. - public GetConfigurationResponse(IReadOnlyDictionary configMap) - { - this.configMap = configMap; - } - - /// - /// The map of key to items returned in a GetConfiguration call. - /// - public IReadOnlyDictionary Items - { - get - { - return configMap; - } - } - } + public IReadOnlyDictionary Items => configMap; } diff --git a/src/Dapr.Client/InvocationException.cs b/src/Dapr.Client/InvocationException.cs index 19183f9b..253cd5ae 100644 --- a/src/Dapr.Client/InvocationException.cs +++ b/src/Dapr.Client/InvocationException.cs @@ -15,53 +15,52 @@ using System; using System.Net.Http; using Grpc.Core; -namespace Dapr.Client +namespace Dapr.Client; + +/// +/// The exception type thrown when an exception is encountered using Dapr service invocation. +/// +public class InvocationException : DaprException { /// - /// The exception type thrown when an exception is encountered using Dapr service invocation. + /// Initializes a new for a non-successful HTTP request. /// - public class InvocationException : DaprException + public InvocationException(string appId, string methodName, Exception innerException, HttpResponseMessage response) + : base(FormatExceptionForFailedRequest(appId, methodName), innerException) { - /// - /// Initializes a new for a non-successful HTTP request. - /// - public InvocationException(string appId, string methodName, Exception innerException, HttpResponseMessage response) - : base(FormatExceptionForFailedRequest(appId, methodName), innerException) - { - this.AppId = appId ?? "unknown"; - this.MethodName = methodName ?? "unknown"; - this.Response = response; - } - - /// - /// Initializes a new for a non-successful gRPC request. - /// - public InvocationException(string appId, string methodName, RpcException innerException) - : base(FormatExceptionForFailedRequest(appId, methodName), innerException) - { - this.AppId = appId ?? "unknown"; - this.MethodName = methodName ?? "unknown"; - } - - /// - /// Gets the destination app-id of the invocation request that failed. - /// - public string AppId { get; } - - /// - /// Gets the destination method name of the invocation request that failed. - /// - public string MethodName { get; } - - /// - /// Gets the of the request that failed. Will be null if the - /// failure was not related to an HTTP request or preventing the response from being recieved. - /// - public HttpResponseMessage Response { get; } - - private static string FormatExceptionForFailedRequest(string appId, string methodName) - { - return $"An exception occurred while invoking method: '{methodName}' on app-id: '{appId}'"; - } + this.AppId = appId ?? "unknown"; + this.MethodName = methodName ?? "unknown"; + this.Response = response; } -} + + /// + /// Initializes a new for a non-successful gRPC request. + /// + public InvocationException(string appId, string methodName, RpcException innerException) + : base(FormatExceptionForFailedRequest(appId, methodName), innerException) + { + this.AppId = appId ?? "unknown"; + this.MethodName = methodName ?? "unknown"; + } + + /// + /// Gets the destination app-id of the invocation request that failed. + /// + public string AppId { get; } + + /// + /// Gets the destination method name of the invocation request that failed. + /// + public string MethodName { get; } + + /// + /// Gets the of the request that failed. Will be null if the + /// failure was not related to an HTTP request or preventing the response from being recieved. + /// + public HttpResponseMessage Response { get; } + + private static string FormatExceptionForFailedRequest(string appId, string methodName) + { + return $"An exception occurred while invoking method: '{methodName}' on app-id: '{appId}'"; + } +} \ No newline at end of file diff --git a/src/Dapr.Client/InvocationHandler.cs b/src/Dapr.Client/InvocationHandler.cs index 36fd6b77..34277b78 100644 --- a/src/Dapr.Client/InvocationHandler.cs +++ b/src/Dapr.Client/InvocationHandler.cs @@ -19,142 +19,134 @@ using System.Threading.Tasks; #nullable enable -namespace Dapr.Client +namespace Dapr.Client; + +/// +/// +/// A implementation that rewrites URIs of outgoing requests +/// to use the Dapr service invocation protocol. This handle allows code using +/// to use the client as-if it were communciating with the destination application directly. +/// +/// +/// The handler 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 +/// and the path portion of the URI rewitten to follow the format of a Dapr service invocation request. +/// +/// +/// +/// This message handler does not distinguish between requests destined for Dapr service invocation and general +/// HTTP traffic, and will attempt to forward all traffic to the Dapr endpoint. Do not attempt to set +/// to a publicly routable URI, this will result in leaking of traffic and the Dapr +/// security token. +/// +public class InvocationHandler : DelegatingHandler { + private Uri parsedEndpoint = new(DaprDefaults.GetDefaultHttpEndpoint(), UriKind.Absolute); + private string? apiToken = DaprDefaults.GetDefaultDaprApiToken(null); + /// - /// - /// A implementation that rewrites URIs of outgoing requests - /// to use the Dapr service invocation protocol. This handle allows code using - /// to use the client as-if it were communciating with the destination application directly. - /// - /// - /// The handler 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 - /// and the path portion of the URI rewitten to follow the format of a Dapr service invocation request. - /// + /// Gets or the sets the URI of the Dapr HTTP endpoint used for service invocation. /// - /// - /// This message handler does not distinguish between requests destined for Dapr service invocation and general - /// HTTP traffic, and will attempt to forward all traffic to the Dapr endpoint. Do not attempt to set - /// to a publicly routable URI, this will result in leaking of traffic and the Dapr - /// security token. - /// - public class InvocationHandler : DelegatingHandler + /// The URI of the Dapr HTTP endpoint used for service invocation. + public string DaprEndpoint { - private Uri parsedEndpoint; - private string? apiToken; - - /// - /// Initializes a new instance of . - /// - public InvocationHandler() + get => this.parsedEndpoint.OriginalString; + set { - this.parsedEndpoint = new Uri(DaprDefaults.GetDefaultHttpEndpoint(), UriKind.Absolute); - this.apiToken = DaprDefaults.GetDefaultDaprApiToken(null); - } - - /// - /// Gets or the sets the URI of the Dapr HTTP endpoint used for service invocation. - /// - /// The URI of the Dapr HTTP endpoint used for service invocation. - public string DaprEndpoint - { - get + if (value == null) { - return this.parsedEndpoint.OriginalString; - } - set - { - if (value == null) - { - throw new ArgumentNullException(nameof(value)); - } - - // This will throw a reasonable exception if the URI is invalid. - var uri = new Uri(value, UriKind.Absolute); - if (uri.Scheme != "http" && uri.Scheme != "https") - { - throw new ArgumentException("The URI scheme of the Dapr endpoint must be http or https.", "value"); - } - - this.parsedEndpoint = uri; - } - } - - /// - /// Gets or sets the default AppId used for service invocation - /// - /// The AppId used for service invocation - public string? DefaultAppId { get; set; } - - // Internal for testing - internal string? DaprApiToken - { - get => this.apiToken; - set => this.apiToken = value; - } - - /// - protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - var original = request.RequestUri; - if (!this.TryRewriteUri(request.RequestUri, out var rewritten)) - { - throw new ArgumentException($"The request URI '{original}' is not a valid Dapr service invocation destination.", nameof(request)); + throw new ArgumentNullException(nameof(value)); } - try + // This will throw a reasonable exception if the URI is invalid. + var uri = new Uri(value, UriKind.Absolute); + if (uri.Scheme != "http" && uri.Scheme != "https") { - var apiTokenHeader = DaprClient.GetDaprApiTokenHeader(this.apiToken); + throw new ArgumentException("The URI scheme of the Dapr endpoint must be http or https.", "value"); + } + + this.parsedEndpoint = uri; + } + } + + /// + /// Gets or sets the default AppId used for service invocation + /// + /// The AppId used for service invocation + public string? DefaultAppId { get; set; } + + // Internal for testing + internal string? DaprApiToken + { + get => this.apiToken; + set => this.apiToken = value; + } + + /// + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var original = request.RequestUri; + if (!this.TryRewriteUri(request.RequestUri, out var rewritten)) + { + throw new ArgumentException($"The request URI '{original}' is not a valid Dapr service invocation destination.", nameof(request)); + } + + try + { + var token = this.apiToken; + if (token != null) + { + var apiTokenHeader = DaprClient.GetDaprApiTokenHeader(token); if (apiTokenHeader is not null) { request.Headers.Add(apiTokenHeader.Value.Key, apiTokenHeader.Value.Value); } - request.RequestUri = rewritten; + } - return await base.SendAsync(request, cancellationToken); - } - finally - { - request.RequestUri = original; - request.Headers.Remove("dapr-api-token"); - } + request.RequestUri = rewritten; + + return await base.SendAsync(request, cancellationToken); } - - // Internal for testing - internal bool TryRewriteUri(Uri? uri, [NotNullWhen(true)] out Uri? rewritten) + finally { - // For now the only invalid cases are when the request URI is missing or just silly. - // We may support additional cases for validation in the future (like an allow-list of App-Ids). - if (uri is null || !uri.IsAbsoluteUri || (uri.Scheme != "http" && uri.Scheme != "https")) - { - // do nothing - rewritten = null; - return false; - } - - string host; - - if (this.DefaultAppId is not null && uri.Host.Equals(this.DefaultAppId, StringComparison.InvariantCultureIgnoreCase)) - { - host = this.DefaultAppId; - } - else - { - host = uri.Host; - } - - var builder = new UriBuilder(uri) - { - Scheme = this.parsedEndpoint.Scheme, - Host = this.parsedEndpoint.Host, - Port = this.parsedEndpoint.Port, - Path = $"/v1.0/invoke/{host}/method" + uri.AbsolutePath, - }; - - rewritten = builder.Uri; - return true; + request.RequestUri = original; + request.Headers.Remove("dapr-api-token"); } } + + // Internal for testing + internal bool TryRewriteUri(Uri? uri, [NotNullWhen(true)] out Uri? rewritten) + { + // For now the only invalid cases are when the request URI is missing or just silly. + // We may support additional cases for validation in the future (like an allow-list of App-Ids). + if (uri is null || !uri.IsAbsoluteUri || (uri.Scheme != "http" && uri.Scheme != "https")) + { + // do nothing + rewritten = null; + return false; + } + + string host; + + if (this.DefaultAppId is not null && uri.Host.Equals(this.DefaultAppId, StringComparison.InvariantCultureIgnoreCase)) + { + host = this.DefaultAppId; + } + else + { + host = uri.Host; + } + + var builder = new UriBuilder(uri) + { + Scheme = this.parsedEndpoint.Scheme, + Host = this.parsedEndpoint.Host, + Port = this.parsedEndpoint.Port, + Path = $"/v1.0/invoke/{host}/method" + uri.AbsolutePath, + }; + + rewritten = builder.Uri; + return true; + } } diff --git a/src/Dapr.Client/InvocationInterceptor.cs b/src/Dapr.Client/InvocationInterceptor.cs index b4ecc4b5..0d297b65 100644 --- a/src/Dapr.Client/InvocationInterceptor.cs +++ b/src/Dapr.Client/InvocationInterceptor.cs @@ -14,124 +14,111 @@ using Grpc.Core; using Grpc.Core.Interceptors; -namespace Dapr.Client +namespace Dapr.Client; + +/// +/// gRPC interceptor which adds the required headers for Dapr gRPC proxying. +/// +/// The Id of the Dapr Application. +/// The api token used for authentication, can be null. +public class InvocationInterceptor(string appId, string daprApiToken) : Interceptor { /// - /// gRPC interceptor which adds the required headers for Dapr gRPC proxying. + /// Intercept and add headers to a BlockingUnaryCall. /// - public class InvocationInterceptor : Interceptor + /// The request to intercept. + /// The client interceptor context to add headers to. + /// The continuation of the request after all headers have been added. + public override TResponse BlockingUnaryCall( + TRequest request, + ClientInterceptorContext context, + BlockingUnaryCallContinuation continuation) { - private string appId; - private string daprApiToken; + AddCallerMetadata(ref context); - /// - /// Constructor. - /// The Id of the Dapr Application. - /// The api token used for authentication, can be null. - /// - public InvocationInterceptor(string appId, string daprApiToken) - { - this.appId = appId; - this.daprApiToken = daprApiToken; - } - - /// - /// Intercept and add headers to a BlockingUnaryCall. - /// - /// The request to intercept. - /// The client interceptor context to add headers to. - /// The continuation of the request after all headers have been added. - public override TResponse BlockingUnaryCall( - TRequest request, - ClientInterceptorContext context, - BlockingUnaryCallContinuation continuation) - { - AddCallerMetadata(ref context); - - return continuation(request, context); - } - - /// - /// Intercept and add headers to a AsyncUnaryCall. - /// - /// The request to intercept. - /// The client interceptor context to add headers to. - /// The continuation of the request after all headers have been added. - public override AsyncUnaryCall AsyncUnaryCall( - TRequest request, - ClientInterceptorContext context, - AsyncUnaryCallContinuation continuation) - { - AddCallerMetadata(ref context); - - return continuation(request, context); - } - - /// - /// Intercept and add headers to a AsyncClientStreamingCall. - /// - /// The client interceptor context to add headers to. - /// The continuation of the request after all headers have been added. - public override AsyncClientStreamingCall AsyncClientStreamingCall( - ClientInterceptorContext context, - AsyncClientStreamingCallContinuation continuation) - { - AddCallerMetadata(ref context); - - return continuation(context); - } - - /// - /// Intercept and add headers to a AsyncServerStreamingCall. - /// - /// The request to intercept. - /// The client interceptor context to add headers to. - /// The continuation of the request after all headers have been added. - public override AsyncServerStreamingCall AsyncServerStreamingCall( - TRequest request, - ClientInterceptorContext context, - AsyncServerStreamingCallContinuation continuation) - { - AddCallerMetadata(ref context); - - return continuation(request, context); - } - - /// - /// Intercept and add headers to a AsyncDuplexStreamingCall. - /// - /// The client interceptor context to add headers to. - /// The continuation of the request after all headers have been added. - public override AsyncDuplexStreamingCall AsyncDuplexStreamingCall( - ClientInterceptorContext context, - AsyncDuplexStreamingCallContinuation continuation) - { - AddCallerMetadata(ref context); - - return continuation(context); - } - - private void AddCallerMetadata(ref ClientInterceptorContext context) - where TRequest : class - where TResponse : class - { - var headers = context.Options.Headers; - - // Call doesn't have a headers collection to add to. - // Need to create a new context with headers for the call. - if (headers == null) - { - headers = new Metadata(); - var options = context.Options.WithHeaders(headers); - context = new ClientInterceptorContext(context.Method, context.Host, options); - } - - // Add caller metadata to call headers - headers.Add("dapr-app-id", appId); - if (daprApiToken != null) - { - headers.Add("dapr-api-token", daprApiToken); - } - } + return continuation(request, context); } -} \ No newline at end of file + + /// + /// Intercept and add headers to a AsyncUnaryCall. + /// + /// The request to intercept. + /// The client interceptor context to add headers to. + /// The continuation of the request after all headers have been added. + public override AsyncUnaryCall AsyncUnaryCall( + TRequest request, + ClientInterceptorContext context, + AsyncUnaryCallContinuation continuation) + { + AddCallerMetadata(ref context); + + return continuation(request, context); + } + + /// + /// Intercept and add headers to a AsyncClientStreamingCall. + /// + /// The client interceptor context to add headers to. + /// The continuation of the request after all headers have been added. + public override AsyncClientStreamingCall AsyncClientStreamingCall( + ClientInterceptorContext context, + AsyncClientStreamingCallContinuation continuation) + { + AddCallerMetadata(ref context); + + return continuation(context); + } + + /// + /// Intercept and add headers to a AsyncServerStreamingCall. + /// + /// The request to intercept. + /// The client interceptor context to add headers to. + /// The continuation of the request after all headers have been added. + public override AsyncServerStreamingCall AsyncServerStreamingCall( + TRequest request, + ClientInterceptorContext context, + AsyncServerStreamingCallContinuation continuation) + { + AddCallerMetadata(ref context); + + return continuation(request, context); + } + + /// + /// Intercept and add headers to a AsyncDuplexStreamingCall. + /// + /// The client interceptor context to add headers to. + /// The continuation of the request after all headers have been added. + public override AsyncDuplexStreamingCall AsyncDuplexStreamingCall( + ClientInterceptorContext context, + AsyncDuplexStreamingCallContinuation continuation) + { + AddCallerMetadata(ref context); + + return continuation(context); + } + + private void AddCallerMetadata(ref ClientInterceptorContext context) + where TRequest : class + where TResponse : class + { + var headers = context.Options.Headers; + + // Call doesn't have a headers collection to add to. + // Need to create a new context with headers for the call. + if (headers == null) + { + headers = []; + var options = context.Options.WithHeaders(headers); + context = new ClientInterceptorContext(context.Method, context.Host, options); + } + + // Add caller metadata to call headers + headers.Add("dapr-app-id", appId); + if (daprApiToken != null) + { + headers.Add("dapr-api-token", daprApiToken); + } + } +} diff --git a/src/Dapr.Client/SaveStateItem.cs b/src/Dapr.Client/SaveStateItem.cs index 818bee20..4df89e20 100644 --- a/src/Dapr.Client/SaveStateItem.cs +++ b/src/Dapr.Client/SaveStateItem.cs @@ -11,55 +11,43 @@ // limitations under the License. // ------------------------------------------------------------------------ +#nullable enable using System.Collections.Generic; -namespace Dapr.Client +namespace Dapr.Client; + +/// +/// Represents a state object used for bulk delete state operation +/// +/// The state key. +/// The state value. +/// The ETag. +/// The stateOptions. +/// The metadata. +public readonly struct SaveStateItem(string key, TValue value, string etag, StateOptions? stateOptions = null, IReadOnlyDictionary? metadata = null) { /// - /// Represents a state object used for bulk delete state operation + /// Gets the state key. /// - public readonly struct SaveStateItem - { - /// - /// Initializes a new instance of the class. - /// - /// The state key. - /// The state value. - /// The ETag. - /// The stateOptions. - /// The metadata. - public SaveStateItem(string key, TValue value, string etag, StateOptions stateOptions = default, IReadOnlyDictionary metadata = default) - { - this.Key = key; - this.Value = value; - this.ETag = etag; - this.StateOptions = stateOptions; - this.Metadata = metadata; - } + public string Key { get; } = key; - /// - /// Gets the state key. - /// - public string Key { get; } - - /// - /// Gets the state value. - /// - public TValue Value { get; } + /// + /// Gets the state value. + /// + public TValue Value { get; } = value; - /// - /// Get the ETag. - /// - public string ETag { get; } + /// + /// Get the ETag. + /// + public string ETag { get; } = etag; - /// - /// Gets the StateOptions. - /// - public StateOptions StateOptions { get; } + /// + /// Gets the StateOptions. + /// + public StateOptions? StateOptions { get; } = stateOptions; - /// - /// Gets the Metadata. - /// - public IReadOnlyDictionary Metadata { get; } - } + /// + /// Gets the Metadata. + /// + public IReadOnlyDictionary? Metadata { get; } = metadata; } diff --git a/src/Dapr.Client/StartWorkflowResponse.cs b/src/Dapr.Client/StartWorkflowResponse.cs index a927116c..cc8d8ec6 100644 --- a/src/Dapr.Client/StartWorkflowResponse.cs +++ b/src/Dapr.Client/StartWorkflowResponse.cs @@ -11,11 +11,10 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Client -{ - /// - /// Initializes a new . - /// - /// The instance ID associated with this response. - public record StartWorkflowResponse(string InstanceId); -} +namespace Dapr.Client; + +/// +/// Initializes a new . +/// +/// The instance ID associated with this response. +public record StartWorkflowResponse(string InstanceId); \ No newline at end of file diff --git a/src/Dapr.Client/StateEntry.cs b/src/Dapr.Client/StateEntry.cs index 6d12d386..01c362fa 100644 --- a/src/Dapr.Client/StateEntry.cs +++ b/src/Dapr.Client/StateEntry.cs @@ -11,136 +11,119 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr +#nullable enable +using System; + +namespace Dapr; + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Dapr.Client; + +/// +/// Represents a value in the Dapr state store. +/// +/// The data type of the value. +/// The instance used to retrieve the value. +/// The name of the state store. +/// The state key. +/// The value. +/// The ETag. +/// +/// Application code should not need to create instances of . Use +/// to access +/// state entries. +/// +public sealed class StateEntry(DaprClient client, string storeName, string key, TValue value, string etag) { - using System.Collections.Generic; - using System.Threading; - using System.Threading.Tasks; - using Dapr.Client; + /// + /// Gets the State Store Name. + /// + public string StoreName { get; } = storeName ?? throw new ArgumentNullException(nameof(storeName)); /// - /// Represents a value in the Dapr state store. + /// Gets the state key. /// - /// The data type of the value. - public sealed class StateEntry + public string Key { get; } = key ?? throw new ArgumentNullException(nameof(key)); + + /// + /// Gets or sets the value locally. This is not sent to the state store until an API (e.g. is called. + /// + public TValue Value { get; set; } = value; + + /// + /// The ETag. + /// + public string ETag { get; } = etag; + + /// + /// Deletes the entry associated with in the state store. + /// + /// A object. + /// An key/value pair that may be consumed by the state store. This depends on the state store used. + /// A that can be used to cancel the operation. + /// A that will complete when the operation has completed. + public Task DeleteAsync(StateOptions? stateOptions = null, IReadOnlyDictionary? metadata = null, CancellationToken cancellationToken = default) { - private readonly DaprClient client; + // ETag is intentionally not specified + return client.DeleteStateAsync(this.StoreName, this.Key, stateOptions, metadata, cancellationToken); + } - /// - /// Initializes a new instance of the class. - /// - /// The instance used to retrieve the value. - /// The name of the state store. - /// The state key. - /// The value. - /// The ETag. - /// - /// Application code should not need to create instances of . Use - /// to access - /// state entries. - /// - public StateEntry(DaprClient client, string storeName, string key, TValue value, string etag) - { - ArgumentVerifier.ThrowIfNull(client, nameof(client)); - ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); - ArgumentVerifier.ThrowIfNullOrEmpty(key, nameof(key)); + /// + /// Saves the current value of to the state store. + /// + /// Options for Save state operation. + /// An key/value pair that may be consumed by the state store. This is dependent on 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 Task SaveAsync(StateOptions? stateOptions = null, IReadOnlyDictionary? metadata = null, CancellationToken cancellationToken = default) + { + // ETag is intentionally not specified + return client.SaveStateAsync( + storeName: this.StoreName, + key: this.Key, + value: this.Value, + metadata: metadata, + stateOptions: stateOptions, + cancellationToken: cancellationToken); + } - this.StoreName = storeName; - this.Key = key; - this.Value = value; - this.client = client; + /// + /// Tries to save the state using the etag 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. + /// + /// An key/value pair that may be consumed by the state store. This is dependent on the type of state store used. + /// Options for Save state operation. + /// A that can be used to cancel the operation. + /// A that will complete when the operation has completed. If the wrapped value is true the operation suceeded. + public Task TrySaveAsync(StateOptions? stateOptions = null, IReadOnlyDictionary? metadata = null, CancellationToken cancellationToken = default) + { + return client.TrySaveStateAsync( + this.StoreName, + this.Key, + this.Value, + this.ETag, + stateOptions, + metadata, + cancellationToken); + } - this.ETag = etag; - } - - /// - /// Gets the State Store Name. - /// - public string StoreName { get; } - - /// - /// Gets the state key. - /// - public string Key { get; } - - /// - /// Gets or sets the value locally. This is not sent to the state store until an API (e.g. is called. - /// - public TValue Value { get; set; } - - /// - /// The ETag. - /// - public string ETag { get; } - - /// - /// Deletes the entry associated with in the state store. - /// - /// A object. - /// An key/value pair that may be consumed by the state store. This depends on the state store used. - /// A that can be used to cancel the operation. - /// A that will complete when the operation has completed. - public Task DeleteAsync(StateOptions stateOptions = default, IReadOnlyDictionary metadata = default, CancellationToken cancellationToken = default) - { - // ETag is intentionally not specified - return this.client.DeleteStateAsync(this.StoreName, this.Key, stateOptions, metadata, cancellationToken); - } - - /// - /// Saves the current value of to the state store. - /// - /// Options for Save state operation. - /// An key/value pair that may be consumed by the state store. This is dependent on 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 Task SaveAsync(StateOptions stateOptions = default, IReadOnlyDictionary metadata = default, CancellationToken cancellationToken = default) - { - // ETag is intentionally not specified - return this.client.SaveStateAsync( - storeName: this.StoreName, - key: this.Key, - value: this.Value, - metadata: metadata, - stateOptions: stateOptions, - cancellationToken: cancellationToken); - } - - /// - /// Tries to save the state using the etag 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. - /// - /// An key/value pair that may be consumed by the state store. This is dependent on the type of state store used. - /// Options for Save state operation. - /// A that can be used to cancel the operation. - /// A that will complete when the operation has completed. If the wrapped value is true the operation suceeded. - public Task TrySaveAsync(StateOptions stateOptions = default, IReadOnlyDictionary metadata = default, CancellationToken cancellationToken = default) - { - return this.client.TrySaveStateAsync( - this.StoreName, - this.Key, - this.Value, - this.ETag, - stateOptions, - metadata, - cancellationToken); - } - - /// - /// Tries to delete the the state using the - /// from the Dapr state. State store implementation will allow the delete only if the attached ETag matches with the latest ETag in the state store. - /// - /// Options for Save state operation. - /// An key/value pair that may be consumed by the state store. This depends on the state store used. - /// A that can be used to cancel the operation. - /// A that will complete when the operation has completed. If the wrapped value is true the operation suceeded. - public Task TryDeleteAsync(StateOptions stateOptions = default, IReadOnlyDictionary metadata = default, CancellationToken cancellationToken = default) - { - return this.client.TryDeleteStateAsync( - this.StoreName, - this.Key, - this.ETag, - stateOptions, - metadata, - cancellationToken); - } + /// + /// Tries to delete the the state using the + /// from the Dapr state. State store implementation will allow the delete only if the attached ETag matches with the latest ETag in the state store. + /// + /// Options for Save state operation. + /// An key/value pair that may be consumed by the state store. This depends on the state store used. + /// A that can be used to cancel the operation. + /// A that will complete when the operation has completed. If the wrapped value is true the operation suceeded. + public Task TryDeleteAsync(StateOptions? stateOptions = null, IReadOnlyDictionary? metadata = null, CancellationToken cancellationToken = default) + { + return client.TryDeleteStateAsync( + this.StoreName, + this.Key, + this.ETag, + stateOptions, + metadata, + cancellationToken); } } diff --git a/src/Dapr.Client/StateOperationType.cs b/src/Dapr.Client/StateOperationType.cs index 396ab8af..a91910c4 100644 --- a/src/Dapr.Client/StateOperationType.cs +++ b/src/Dapr.Client/StateOperationType.cs @@ -11,21 +11,20 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Client +namespace Dapr.Client; + +/// +/// Operation type for state operations with Dapr. +/// +public enum StateOperationType { /// - /// Operation type for state operations with Dapr. + /// Upsert a new or existing state /// - public enum StateOperationType - { - /// - /// Upsert a new or existing state - /// - Upsert, + Upsert, - /// - /// Delete a state - /// - Delete, - } -} + /// + /// Delete a state + /// + Delete, +} \ No newline at end of file diff --git a/src/Dapr.Client/StateOptions.cs b/src/Dapr.Client/StateOptions.cs index fa6f2739..1817539b 100644 --- a/src/Dapr.Client/StateOptions.cs +++ b/src/Dapr.Client/StateOptions.cs @@ -11,21 +11,20 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Client +namespace Dapr.Client; + +/// +/// Options when perfroming state operations with Dapr. +/// +public class StateOptions { /// - /// Options when perfroming state operations with Dapr. + /// Consistency mode for state operations with Dapr. /// - public class StateOptions - { - /// - /// Consistency mode for state operations with Dapr. - /// - public ConsistencyMode? Consistency { get; set; } + public ConsistencyMode? Consistency { get; set; } - /// - /// Concurrency mode for state operations with Dapr. - /// - public ConcurrencyMode? Concurrency { get; set; } - } -} + /// + /// Concurrency mode for state operations with Dapr. + /// + public ConcurrencyMode? Concurrency { get; set; } +} \ No newline at end of file diff --git a/src/Dapr.Client/StateQueryException.cs b/src/Dapr.Client/StateQueryException.cs index 77be58a7..867ec545 100644 --- a/src/Dapr.Client/StateQueryException.cs +++ b/src/Dapr.Client/StateQueryException.cs @@ -1,34 +1,23 @@ using System.Collections.Generic; -namespace Dapr.Client +namespace Dapr.Client; + +/// +/// Exception that is thrown when an erorr is encountered during a call to the Query API. +/// This exception contains the partial results (if any) from that exception. +/// +/// The description of the exception from the source. +/// The response containing successful items, if any, in a response with errors. +/// The key(s) that encountered an error during the query. +public class StateQueryException(string message, StateQueryResponse response, IReadOnlyList failedKeys) : DaprApiException(message) { /// - /// Exception that is thrown when an erorr is encountered during a call to the Query API. - /// This exception contains the partial results (if any) from that exception. + /// The response containing successful items, if any, in a response with errors. /// - public class StateQueryException : DaprApiException - { - /// - /// Constructor. - /// - /// The description of the exception from the source. - /// The response containing successful items, if any, in a response with errors. - /// The key(s) that encountered an error during the query. - public StateQueryException(string message, StateQueryResponse response, IReadOnlyList failedKeys) - : base(message) - { - Response = response; - FailedKeys = failedKeys; - } + public StateQueryResponse Response { get; } = response; - /// - /// The response containing successful items, if any, in a response with errors. - /// - public StateQueryResponse Response { get; } - - /// - /// The key(s) that encountered an error during the query. - /// - public IReadOnlyList FailedKeys { get; } - } + /// + /// The key(s) that encountered an error during the query. + /// + public IReadOnlyList FailedKeys { get; } = failedKeys; } diff --git a/src/Dapr.Client/StateQueryResponse.cs b/src/Dapr.Client/StateQueryResponse.cs index c1cc8738..b3aaca5c 100644 --- a/src/Dapr.Client/StateQueryResponse.cs +++ b/src/Dapr.Client/StateQueryResponse.cs @@ -14,80 +14,58 @@ limitations under the License. #nullable enable using System.Collections.Generic; -namespace Dapr.Client +namespace Dapr.Client; + +/// +/// Represents the response from a state query. +/// +/// The results of the query. +/// The pagination token to continue the query. +/// The metadata to be passed back to the caller. +public class StateQueryResponse(IReadOnlyList> results, string token, IReadOnlyDictionary metadata) { /// - /// Represents the response from a state query. + /// The results of the query. /// - public class StateQueryResponse - { - /// - /// Constructor. - /// - /// The results of the query. - /// The pagination token to continue the query. - /// The metadata to be passed back to the caller. - public StateQueryResponse(IReadOnlyList> results, string token, IReadOnlyDictionary metadata) - { - Results = new List>(results); - Token = token; - Metadata = metadata; - } - - /// - /// The results of the query. - /// - public IReadOnlyList> Results { get; } - - /// - /// The pagination token to continue the query. - /// - public string Token { get; } - - /// - /// The metadata to be passed back to the caller. - /// - public IReadOnlyDictionary Metadata { get; } - } + public IReadOnlyList> Results { get; } = new List>(results); /// - /// Represents an individual item from the results of a state query. + /// The pagination token to continue the query. /// - public class StateQueryItem - { - /// - /// Constructor. - /// - /// The key of the returned item. - /// The value of the returned item. - /// The ETag of the returned item. - /// The error, if one occurred, of the returned item. - public StateQueryItem(string key, TValue data, string etag, string error) - { - Key = key; - Data = data; - ETag = etag; - Error = error; - } + public string Token { get; } = token; - /// - /// The key from the matched query. - /// - public string Key { get; } - - /// - /// The data of the key from the matched query. - /// - public TValue? Data { get; } - - /// - /// The ETag for the key from the matched query. - /// - public string ETag { get; } - - /// - /// The error from the query, if one occurred. - /// - public string Error { get; } - } + /// + /// The metadata to be passed back to the caller. + /// + public IReadOnlyDictionary Metadata { get; } = metadata; +} + +/// +/// Represents an individual item from the results of a state query. +/// +/// The key of the returned item. +/// The value of the returned item. +/// The ETag of the returned item. +/// The error, if one occurred, of the returned item. +public class StateQueryItem(string key, TValue data, string etag, string error) +{ + /// + /// The key from the matched query. + /// + public string Key { get; } = key; + + /// + /// The data of the key from the matched query. + /// + public TValue? Data { get; } = data; + + /// + /// The ETag for the key from the matched query. + /// + public string ETag { get; } = etag; + + /// + /// The error from the query, if one occurred. + /// + public string Error { get; } = error; } diff --git a/src/Dapr.Client/StateTransactionRequest.cs b/src/Dapr.Client/StateTransactionRequest.cs index 756f3069..150362c6 100644 --- a/src/Dapr.Client/StateTransactionRequest.cs +++ b/src/Dapr.Client/StateTransactionRequest.cs @@ -11,66 +11,51 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Client +#nullable enable +using System; + +namespace Dapr.Client; + +using System.Collections.Generic; + +/// +/// Represents a single request in in a StateTransaction. +/// +/// The state key. +/// The serialized state value. +/// The operation type. +/// The etag (optional). +/// Additional key value pairs for the state (optional). +/// State options (optional). +public sealed class StateTransactionRequest(string key, byte[]? value, StateOperationType operationType, string? etag = null, IReadOnlyDictionary? metadata = null, StateOptions? options = null) { - using System.Collections.Generic; + /// + /// Gets the state key. + /// + public string Key { get; } = key ?? throw new ArgumentNullException(nameof(key)); /// - /// Represents a single request in in a StateTransaction. + /// Gets or sets the value locally. /// - public sealed class StateTransactionRequest - { + public byte[]? Value { get; set; } = value; - /// - /// Initializes a new instance of the class. - /// - /// The state key. - /// The serialized state value. - /// The operation type. - /// The etag (optional). - /// Additional key value pairs for the state (optional). - /// State options (optional). - public StateTransactionRequest(string key, byte[] value, StateOperationType operationType, string etag = default, IReadOnlyDictionary metadata = default, StateOptions options = default) - { - ArgumentVerifier.ThrowIfNull(key, nameof(key)); + /// + /// The Operation type. + /// + public StateOperationType? OperationType { get; set; } = operationType; - this.Key = key; - this.Value = value; - this.OperationType = operationType; - this.ETag = etag; - this.Metadata = metadata; - this.Options = options; - } + /// + /// The ETag (optional). + /// + public string? ETag { get; set; } = etag; + /// + /// Additional key-value pairs to be passed to the state store (optional). + /// + public IReadOnlyDictionary? Metadata { get; set; } = metadata; - /// - /// Gets the state key. - /// - public string Key { get; } - - /// - /// Gets or sets the value locally. - /// - public byte[] Value { get; set; } - - /// - /// The Operation type. - /// - public StateOperationType OperationType { get; set; } - - /// - /// The ETag (optional). - /// - public string ETag { get; set; } - - /// - /// Additional key-value pairs to be passed to the state store (optional). - /// - public IReadOnlyDictionary Metadata { get; set; } - - /// - /// State Options (optional). - /// - public StateOptions Options; - } + /// + /// State Options (optional). + /// + public StateOptions? Options = options; } diff --git a/src/Dapr.Client/SubscribeConfigurationResponse.cs b/src/Dapr.Client/SubscribeConfigurationResponse.cs index bea99289..32694891 100644 --- a/src/Dapr.Client/SubscribeConfigurationResponse.cs +++ b/src/Dapr.Client/SubscribeConfigurationResponse.cs @@ -1,38 +1,21 @@ using System.Collections.Generic; -namespace Dapr.Client +namespace Dapr.Client; + +/// +/// Response for a Subscribe Configuration request. +/// +/// The that provides the actual data from the subscription. +public class SubscribeConfigurationResponse(ConfigurationSource source) { /// - /// Response for a Subscribe Configuration request. + /// The Id of the Subscription. This will be until the first result has been streamed. + /// After that time, the Id can be used to unsubscribe. /// - public class SubscribeConfigurationResponse - { - private ConfigurationSource source; + public string Id => source.Id; - /// - /// Constructor. - /// - /// The that provides the actual data from the subscription. - public SubscribeConfigurationResponse(ConfigurationSource source) : base() - { - this.source = source; - } - - /// - /// The Id of the Subscription. This will be until the first result has been streamed. - /// After that time, the Id can be used to unsubscribe. - /// - public string Id - { - get - { - return source.Id; - } - } - - /// - /// Get the that is used to read the actual subscribed configuration data. - /// - public IAsyncEnumerable> Source => source; - } + /// + /// Get the that is used to read the actual subscribed configuration data. + /// + public IAsyncEnumerable> Source => source; } diff --git a/src/Dapr.Client/TryLockResponse.cs b/src/Dapr.Client/TryLockResponse.cs index e9057fbb..086eafc1 100644 --- a/src/Dapr.Client/TryLockResponse.cs +++ b/src/Dapr.Client/TryLockResponse.cs @@ -14,48 +14,47 @@ using System; using System.Threading.Tasks; -namespace Dapr.Client +namespace Dapr.Client; + +/// +/// Class representing the response from a Lock API call. +/// +[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] +public sealed class TryLockResponse : IAsyncDisposable { /// - /// Class representing the response from a Lock API call. + /// The success value of the tryLock API call /// - [Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public sealed class TryLockResponse : IAsyncDisposable + public bool Success { get; init; } + + /// + /// The resourceId required to unlock the lock + /// + public string ResourceId { get; init; } + + /// + /// The LockOwner required to unlock the lock + /// + public string LockOwner { get; init; } + + /// + /// The StoreName required to unlock the lock + /// + public string StoreName { get; init; } + + /// + /// Constructor for a TryLockResponse. + /// + public TryLockResponse() { - /// - /// The success value of the tryLock API call - /// - public bool Success { get; init; } + } - /// - /// The resourceId required to unlock the lock - /// - public string ResourceId { get; init; } - - /// - /// The LockOwner required to unlock the lock - /// - public string LockOwner { get; init; } - - /// - /// The StoreName required to unlock the lock - /// - public string StoreName { get; init; } - - /// - /// Constructor for a TryLockResponse. - /// - public TryLockResponse() - { - } - - /// - public async ValueTask DisposeAsync() { - using (var client = new DaprClientBuilder().Build()) { - if(this.Success) { - await client.Unlock(StoreName, ResourceId, LockOwner); - } - } + /// + public async ValueTask DisposeAsync() + { + using var client = new DaprClientBuilder().Build(); + if(this.Success) { + await client.Unlock(StoreName, ResourceId, LockOwner); } } } diff --git a/src/Dapr.Client/TypeConverters.cs b/src/Dapr.Client/TypeConverters.cs index 13ab6920..a9f50b46 100644 --- a/src/Dapr.Client/TypeConverters.cs +++ b/src/Dapr.Client/TypeConverters.cs @@ -11,54 +11,48 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Client +namespace Dapr.Client; + +using System.Text.Json; +using Google.Protobuf; +using Google.Protobuf.WellKnownTypes; + +/// +/// Some type converters. +/// +internal static class TypeConverters { - using System.Text.Json; - using Google.Protobuf; - using Google.Protobuf.WellKnownTypes; - /// - /// Some type converters. + /// Converts an arbitrary type to a based . /// - internal static class TypeConverters + /// The data to convert. + /// The JSON serialization options. + /// The type of the given data. + /// The given data as JSON based byte string. + public static ByteString ToJsonByteString(T data, JsonSerializerOptions options) { - /// - /// Converts an arbitrary type to a based . - /// - /// The data to convert. - /// The JSON serialization options. - /// The type of the given data. - /// The given data as JSON based byte string. - public static ByteString ToJsonByteString(T data, JsonSerializerOptions options) - { - var bytes = JsonSerializer.SerializeToUtf8Bytes(data, options); - return ByteString.CopyFrom(bytes); - } + var bytes = JsonSerializer.SerializeToUtf8Bytes(data, options); + return ByteString.CopyFrom(bytes); + } - public static Any ToJsonAny(T data, JsonSerializerOptions options) - { - return new Any() - { - Value = ToJsonByteString(data, options), + public static Any ToJsonAny(T data, JsonSerializerOptions options) + { + return new Any() + { + Value = ToJsonByteString(data, options), - // This isn't really compliant protobuf, because we're not setting TypeUrl, but it's - // what Dapr understands. - }; - } + // This isn't really compliant protobuf, because we're not setting TypeUrl, but it's + // what Dapr understands. + }; + } - public static T FromJsonByteString(ByteString bytes, JsonSerializerOptions options) - { - if (bytes.Length == 0) - { - return default; - } - - return JsonSerializer.Deserialize(bytes.Span, options); - } + public static T FromJsonByteString(ByteString bytes, JsonSerializerOptions options) + { + return bytes.Length == 0 ? default : JsonSerializer.Deserialize(bytes.Span, options); + } - public static T FromJsonAny(Any any, JsonSerializerOptions options) - { - return FromJsonByteString(any.Value, options); - } + public static T FromJsonAny(Any any, JsonSerializerOptions options) + { + return FromJsonByteString(any.Value, options); } } diff --git a/src/Dapr.Client/UnlockResponse.cs b/src/Dapr.Client/UnlockResponse.cs index 68cc4067..09bf31a6 100644 --- a/src/Dapr.Client/UnlockResponse.cs +++ b/src/Dapr.Client/UnlockResponse.cs @@ -11,47 +11,46 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Client +namespace Dapr.Client; + +/// +/// Enum representing the response from a Unlock API call. +/// +public enum LockStatus { /// - /// Enum representing the response from a Unlock API call. + /// Succes stating the lock is released. /// - public enum LockStatus - { - /// - /// Succes stating the lock is released. - /// - Success, - /// - /// LockDoesNotExist stating the lock does not exist. - /// - LockDoesNotExist, - /// - /// LockBelongsToOthers stating the lock is acquired by a different process. - /// - LockBelongsToOthers, - /// - /// InternalError statign an error in unlocking. - /// - InternalError, - } + Success, + /// + /// LockDoesNotExist stating the lock does not exist. + /// + LockDoesNotExist, + /// + /// LockBelongsToOthers stating the lock is acquired by a different process. + /// + LockBelongsToOthers, + /// + /// InternalError statign an error in unlocking. + /// + InternalError, +} + +/// +/// Class representing the response from a Unlock API call. +/// +public class UnlockResponse +{ + /// + /// The status of unlock API call + /// + public LockStatus status { set; get; } /// - /// Class representing the response from a Unlock API call. + /// Constructor for a UnlockResponse. /// - public class UnlockResponse - { - /// - /// The status of unlock API call - /// - public LockStatus status { set; get; } - - /// - /// Constructor for a UnlockResponse. - /// - /// The status value that is returned in the UnLock call. - public UnlockResponse(LockStatus status) { - this.status = status; - } + /// The status value that is returned in the UnLock call. + public UnlockResponse(LockStatus status) { + this.status = status; } } \ No newline at end of file diff --git a/src/Dapr.Client/UnsubscribeConfigurationResponse.cs b/src/Dapr.Client/UnsubscribeConfigurationResponse.cs index 8208efb3..aaf651e9 100644 --- a/src/Dapr.Client/UnsubscribeConfigurationResponse.cs +++ b/src/Dapr.Client/UnsubscribeConfigurationResponse.cs @@ -1,29 +1,19 @@ -namespace Dapr.Client +namespace Dapr.Client; + +/// +/// Response from an Unsubscribe Configuration call. +/// +/// Boolean indicating success. +/// Message from the Configuration API. +public class UnsubscribeConfigurationResponse(bool ok, string message) { /// - /// Response from an Unsubscribe Configuration call. + /// Boolean representing if the request was successful or not. /// - public class UnsubscribeConfigurationResponse - { - /// - /// Boolean representing if the request was successful or not. - /// - public bool Ok { get; } + public bool Ok { get; } = ok; - /// - /// The message from the Configuration API. - /// - public string Message { get; } - - /// - /// Constructor. - /// - /// Boolean indicating success. - /// Message from the Configuration API. - public UnsubscribeConfigurationResponse(bool ok, string message) - { - Ok = ok; - Message = message; - } - } + /// + /// The message from the Configuration API. + /// + public string Message { get; } = message; } diff --git a/src/Dapr.Common/ArgumentVerifier.cs b/src/Dapr.Common/ArgumentVerifier.cs index 62ae98b5..b7f14213 100644 --- a/src/Dapr.Common/ArgumentVerifier.cs +++ b/src/Dapr.Common/ArgumentVerifier.cs @@ -14,47 +14,46 @@ // TODO: Remove this when every project that uses this file has nullable enabled. #nullable enable -namespace Dapr +namespace Dapr; + +using System; +using System.Diagnostics.CodeAnalysis; + +/// +/// A utility class to perform argument validations. +/// +internal static class ArgumentVerifier { - using System; - using System.Diagnostics.CodeAnalysis; + /// + /// Throws ArgumentNullException if argument is null. + /// + /// Argument value to check. + /// Name of Argument. + public static void ThrowIfNull([NotNull] object? value, string name) + { + if (value == null) + { + throw new ArgumentNullException(name); + } + } /// - /// A utility class to perform argument validations. + /// Validates string and throws: + /// ArgumentNullException if argument is null. + /// ArgumentException if argument is empty. /// - internal static class ArgumentVerifier + /// Argument value to check. + /// Name of Argument. + public static void ThrowIfNullOrEmpty([NotNull] string? value, string name) { - /// - /// Throws ArgumentNullException if argument is null. - /// - /// Argument value to check. - /// Name of Argument. - public static void ThrowIfNull([NotNull] object? value, string name) + if (value == null) { - if (value == null) - { - throw new ArgumentNullException(name); - } + throw new ArgumentNullException(name); } - /// - /// Validates string and throws: - /// ArgumentNullException if argument is null. - /// ArgumentException if argument is empty. - /// - /// Argument value to check. - /// Name of Argument. - public static void ThrowIfNullOrEmpty([NotNull] string? value, string name) + if (string.IsNullOrEmpty(value)) { - if (value == null) - { - throw new ArgumentNullException(name); - } - - if (string.IsNullOrEmpty(value)) - { - throw new ArgumentException("The value cannot be null or empty", name); - } + throw new ArgumentException("The value cannot be null or empty", name); } } } diff --git a/src/Dapr.Common/DaprDefaults.cs b/src/Dapr.Common/DaprDefaults.cs index 2dd8dd37..9a504986 100644 --- a/src/Dapr.Common/DaprDefaults.cs +++ b/src/Dapr.Common/DaprDefaults.cs @@ -13,122 +13,121 @@ using Microsoft.Extensions.Configuration; -namespace Dapr +namespace Dapr; + +internal static class DaprDefaults { - internal static class DaprDefaults + private static string httpEndpoint = string.Empty; + private static string grpcEndpoint = string.Empty; + private static string daprApiToken = string.Empty; + private static string appApiToken = string.Empty; + + public const string DaprApiTokenName = "DAPR_API_TOKEN"; + public const string AppApiTokenName = "APP_API_TOKEN"; + public const string DaprHttpEndpointName = "DAPR_HTTP_ENDPOINT"; + public const string DaprHttpPortName = "DAPR_HTTP_PORT"; + public const string DaprGrpcEndpointName = "DAPR_GRPC_ENDPOINT"; + public const string DaprGrpcPortName = "DAPR_GRPC_PORT"; + + public const string DefaultDaprScheme = "http"; + public const string DefaultDaprHost = "localhost"; + public const int DefaultHttpPort = 3500; + public const int DefaultGrpcPort = 50001; + + /// + /// Get the value of environment variable DAPR_API_TOKEN + /// + /// The optional to pull the value from. + /// The value of environment variable DAPR_API_TOKEN + public static string GetDefaultDaprApiToken(IConfiguration? configuration) => + GetResourceValue(configuration, DaprApiTokenName) ?? string.Empty; + + /// + /// Get the value of environment variable APP_API_TOKEN + /// + /// The optional to pull the value from. + /// The value of environment variable APP_API_TOKEN + public static string GetDefaultAppApiToken(IConfiguration? configuration) => + GetResourceValue(configuration, AppApiTokenName) ?? string.Empty; + + /// + /// Get the value of HTTP endpoint based off environment variables + /// + /// The optional to pull the value from. + /// The value of HTTP endpoint based off environment variables + public static string GetDefaultHttpEndpoint(IConfiguration? configuration = null) { - private static string httpEndpoint = string.Empty; - private static string grpcEndpoint = string.Empty; - private static string daprApiToken = string.Empty; - private static string appApiToken = string.Empty; - - public const string DaprApiTokenName = "DAPR_API_TOKEN"; - public const string AppApiTokenName = "APP_API_TOKEN"; - public const string DaprHttpEndpointName = "DAPR_HTTP_ENDPOINT"; - public const string DaprHttpPortName = "DAPR_HTTP_PORT"; - public const string DaprGrpcEndpointName = "DAPR_GRPC_ENDPOINT"; - public const string DaprGrpcPortName = "DAPR_GRPC_PORT"; - - public const string DefaultDaprScheme = "http"; - public const string DefaultDaprHost = "localhost"; - public const int DefaultHttpPort = 3500; - public const int DefaultGrpcPort = 50001; - - /// - /// Get the value of environment variable DAPR_API_TOKEN - /// - /// The optional to pull the value from. - /// The value of environment variable DAPR_API_TOKEN - public static string GetDefaultDaprApiToken(IConfiguration? configuration) => - GetResourceValue(configuration, DaprApiTokenName) ?? string.Empty; - - /// - /// Get the value of environment variable APP_API_TOKEN - /// - /// The optional to pull the value from. - /// The value of environment variable APP_API_TOKEN - public static string GetDefaultAppApiToken(IConfiguration? configuration) => - GetResourceValue(configuration, AppApiTokenName) ?? string.Empty; - - /// - /// Get the value of HTTP endpoint based off environment variables - /// - /// The optional to pull the value from. - /// The value of HTTP endpoint based off environment variables - public static string GetDefaultHttpEndpoint(IConfiguration? configuration = null) - { - //Prioritize pulling from the IConfiguration and fallback to the environment variable if not populated - var endpoint = GetResourceValue(configuration, DaprHttpEndpointName); - var port = GetResourceValue(configuration, DaprHttpPortName); + //Prioritize pulling from the IConfiguration and fallback to the environment variable if not populated + var endpoint = GetResourceValue(configuration, DaprHttpEndpointName); + var port = GetResourceValue(configuration, DaprHttpPortName); - //Use the default HTTP port if we're unable to retrieve/parse the provided port - int? parsedGrpcPort = string.IsNullOrWhiteSpace(port) ? DefaultHttpPort : int.Parse(port); + //Use the default HTTP port if we're unable to retrieve/parse the provided port + int? parsedGrpcPort = string.IsNullOrWhiteSpace(port) ? DefaultHttpPort : int.Parse(port); - return BuildEndpoint(endpoint, parsedGrpcPort.Value); - } + return BuildEndpoint(endpoint, parsedGrpcPort.Value); + } - /// - /// Get the value of gRPC endpoint based off environment variables - /// - /// The optional to pull the value from. - /// The value of gRPC endpoint based off environment variables - public static string GetDefaultGrpcEndpoint(IConfiguration? configuration = null) - { - //Prioritize pulling from the IConfiguration and fallback to the environment variable if not populated - var endpoint = GetResourceValue(configuration, DaprGrpcEndpointName); - var port = GetResourceValue(configuration, DaprGrpcPortName); + /// + /// Get the value of gRPC endpoint based off environment variables + /// + /// The optional to pull the value from. + /// The value of gRPC endpoint based off environment variables + public static string GetDefaultGrpcEndpoint(IConfiguration? configuration = null) + { + //Prioritize pulling from the IConfiguration and fallback to the environment variable if not populated + var endpoint = GetResourceValue(configuration, DaprGrpcEndpointName); + var port = GetResourceValue(configuration, DaprGrpcPortName); - //Use the default gRPC port if we're unable to retrieve/parse the provided port - int? parsedGrpcPort = string.IsNullOrWhiteSpace(port) ? DefaultGrpcPort : int.Parse(port); + //Use the default gRPC port if we're unable to retrieve/parse the provided port + int? parsedGrpcPort = string.IsNullOrWhiteSpace(port) ? DefaultGrpcPort : int.Parse(port); - return BuildEndpoint(endpoint, parsedGrpcPort.Value); - } + return BuildEndpoint(endpoint, parsedGrpcPort.Value); + } - /// - /// Builds the Dapr endpoint. - /// - /// The endpoint value. - /// The endpoint port value, whether pulled from configuration/envvar or the default. - /// A constructed endpoint value. - private static string BuildEndpoint(string? endpoint, int endpointPort) + /// + /// Builds the Dapr endpoint. + /// + /// The endpoint value. + /// The endpoint port value, whether pulled from configuration/envvar or the default. + /// A constructed endpoint value. + private static string BuildEndpoint(string? endpoint, int endpointPort) + { + var endpointBuilder = new UriBuilder { Scheme = DefaultDaprScheme, Host = DefaultDaprHost }; //Port depends on endpoint + + if (!string.IsNullOrWhiteSpace(endpoint)) //If the endpoint is set, it doesn't matter if the port is { - var endpointBuilder = new UriBuilder { Scheme = DefaultDaprScheme, Host = DefaultDaprHost }; //Port depends on endpoint - - if (!string.IsNullOrWhiteSpace(endpoint)) //If the endpoint is set, it doesn't matter if the port is - { - //Extract the scheme, host and port from the endpoint and replace defaults - var uri = new Uri(endpoint); - endpointBuilder.Scheme = uri.Scheme; - endpointBuilder.Host = uri.Host; - endpointBuilder.Port = uri.Port; - } - else - { - //Should only set the port if the endpoint isn't populated - endpointBuilder.Port = endpointPort; - } - - return endpointBuilder.ToString(); + //Extract the scheme, host and port from the endpoint and replace defaults + var uri = new Uri(endpoint); + endpointBuilder.Scheme = uri.Scheme; + endpointBuilder.Host = uri.Host; + endpointBuilder.Port = uri.Port; } + else + { + //Should only set the port if the endpoint isn't populated + endpointBuilder.Port = endpointPort; + } + + return endpointBuilder.ToString(); + } - /// - /// Retrieves the specified value prioritizing pulling it from , falling back - /// to an environment variable, and using an empty string as a default. - /// - /// An instance of an . - /// The name of the value to retrieve. - /// The value of the resource. - private static string? GetResourceValue(IConfiguration? configuration, string name) + /// + /// Retrieves the specified value prioritizing pulling it from , falling back + /// to an environment variable, and using an empty string as a default. + /// + /// An instance of an . + /// The name of the value to retrieve. + /// The value of the resource. + private static string? GetResourceValue(IConfiguration? configuration, string name) + { + //Attempt to retrieve first from the configuration + var configurationValue = configuration?[name]; + if (configurationValue is not null) { - //Attempt to retrieve first from the configuration - var configurationValue = configuration?[name]; - if (configurationValue is not null) - { - return configurationValue; - } - - //Fall back to the environment variable with the same name or default to an empty string - return Environment.GetEnvironmentVariable(name); + return configurationValue; } + + //Fall back to the environment variable with the same name or default to an empty string + return Environment.GetEnvironmentVariable(name); } } diff --git a/src/Dapr.Common/DaprException.cs b/src/Dapr.Common/DaprException.cs index 2b600ef3..d2cedea7 100644 --- a/src/Dapr.Common/DaprException.cs +++ b/src/Dapr.Common/DaprException.cs @@ -13,45 +13,44 @@ using System.Runtime.Serialization; -namespace Dapr +namespace Dapr; + +/// +/// The base type of exceptions thrown by the Dapr .NET SDK. +/// +[Serializable] +public class DaprException : Exception { /// - /// The base type of exceptions thrown by the Dapr .NET SDK. + /// Initializes a new instance of with the provided . /// - [Serializable] - public class DaprException : Exception + /// The exception message. + public DaprException(string message) + : base(message) { - /// - /// Initializes a new instance of with the provided . - /// - /// The exception message. - public DaprException(string message) - : base(message) - { - } + } - /// - /// Initializes a new instance of with the provided - /// and . - /// - /// The exception message. - /// The inner exception. - public DaprException(string message, Exception innerException) - : base(message, innerException) - { - } + /// + /// Initializes a new instance of with the provided + /// and . + /// + /// The exception message. + /// The inner exception. + public DaprException(string message, Exception innerException) + : base(message, innerException) + { + } - /// - /// Initializes a new instance of the class with a specified context. - /// - /// The object that contains serialized object data of the exception being thrown. - /// The object that contains contextual information about the source or destination. The context parameter is reserved for future use and can be null. + /// + /// Initializes a new instance of the class with a specified context. + /// + /// The object that contains serialized object data of the exception being thrown. + /// The object that contains contextual information about the source or destination. The context parameter is reserved for future use and can be null. #if NET8_0_OR_GREATER [Obsolete(DiagnosticId = "SYSLIB0051")] // add this attribute to GetObjectData #endif - protected DaprException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } + protected DaprException(SerializationInfo info, StreamingContext context) + : base(info, context) + { } } diff --git a/src/Dapr.Common/Exceptions/DaprExceptionExtensions.cs b/src/Dapr.Common/Exceptions/DaprExceptionExtensions.cs index 2f195d69..e1bd3d8c 100644 --- a/src/Dapr.Common/Exceptions/DaprExceptionExtensions.cs +++ b/src/Dapr.Common/Exceptions/DaprExceptionExtensions.cs @@ -14,42 +14,41 @@ using System.Diagnostics.CodeAnalysis; using Grpc.Core; -namespace Dapr.Common.Exceptions +namespace Dapr.Common.Exceptions; + +/// +/// Provides extension methods for . +/// +public static class DaprExceptionExtensions { /// - /// Provides extension methods for . + /// Attempt to retrieve from . /// - public static class DaprExceptionExtensions + /// A Dapr exception. . + /// out if parsable from inner exception, null otherwise. + /// True if extended info is available, false otherwise. + public static bool TryGetExtendedErrorInfo(this DaprException exception, [NotNullWhen(true)] out DaprExtendedErrorInfo? daprExtendedErrorInfo) { - /// - /// Attempt to retrieve from . - /// - /// A Dapr exception. . - /// out if parsable from inner exception, null otherwise. - /// True if extended info is available, false otherwise. - public static bool TryGetExtendedErrorInfo(this DaprException exception, [NotNullWhen(true)] out DaprExtendedErrorInfo? daprExtendedErrorInfo) + daprExtendedErrorInfo = null; + if (exception.InnerException is not RpcException rpcException) { - daprExtendedErrorInfo = null; - if (exception.InnerException is not RpcException rpcException) - { - return false; - } - - var metadata = rpcException.Trailers.Get(DaprExtendedErrorConstants.GrpcDetails); - - if (metadata is null) - { - return false; - } - - var status = Google.Rpc.Status.Parser.ParseFrom(metadata.ValueBytes); - - daprExtendedErrorInfo = new DaprExtendedErrorInfo(status.Code, status.Message) - { - Details = status.Details.Select(detail => ExtendedErrorDetailFactory.CreateErrorDetail(detail)).ToArray(), - }; - - return true; + return false; } + + var metadata = rpcException.Trailers.Get(DaprExtendedErrorConstants.GrpcDetails); + + if (metadata is null) + { + return false; + } + + var status = Google.Rpc.Status.Parser.ParseFrom(metadata.ValueBytes); + + daprExtendedErrorInfo = new DaprExtendedErrorInfo(status.Code, status.Message) + { + Details = status.Details.Select(detail => ExtendedErrorDetailFactory.CreateErrorDetail(detail)).ToArray(), + }; + + return true; } -} +} \ No newline at end of file diff --git a/src/Dapr.Common/Exceptions/DaprExtendedErrorConstants.cs b/src/Dapr.Common/Exceptions/DaprExtendedErrorConstants.cs index 9507103b..b846827d 100644 --- a/src/Dapr.Common/Exceptions/DaprExtendedErrorConstants.cs +++ b/src/Dapr.Common/Exceptions/DaprExtendedErrorConstants.cs @@ -11,24 +11,23 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Common.Exceptions +namespace Dapr.Common.Exceptions; + +/// +/// Definitions of expected types to be returned from the Dapr runtime. +/// +internal static class DaprExtendedErrorConstants { - /// - /// Definitions of expected types to be returned from the Dapr runtime. - /// - internal static class DaprExtendedErrorConstants - { - public const string ErrorDetailTypeUrl = "type.googleapis.com/"; - public const string GrpcDetails = "grpc-status-details-bin"; - public const string ErrorInfo = $"{ErrorDetailTypeUrl}Google.rpc.ErrorInfo"; - public const string RetryInfo = $"{ErrorDetailTypeUrl}Google.rpc.RetryInfo"; - public const string DebugInfo = $"{ErrorDetailTypeUrl}Google.rpc.DebugInfo"; - public const string QuotaFailure = $"{ErrorDetailTypeUrl}Google.rpc.QuotaFailure"; - public const string PreconditionFailure = $"{ErrorDetailTypeUrl}Google.rpc.PreconditionFailure"; - public const string BadRequest = $"{ErrorDetailTypeUrl}Google.rpc.BadRequest"; - public const string RequestInfo = $"{ErrorDetailTypeUrl}Google.rpc.RequestInfo"; - public const string ResourceInfo = $"{ErrorDetailTypeUrl}Google.rpc.ResourceInfo"; - public const string Help = $"{ErrorDetailTypeUrl}Google.rpc.Help"; - public const string LocalizedMessage = $"{ErrorDetailTypeUrl}Google.rpc.LocalizedMessage"; - } -} + public const string ErrorDetailTypeUrl = "type.googleapis.com/"; + public const string GrpcDetails = "grpc-status-details-bin"; + public const string ErrorInfo = $"{ErrorDetailTypeUrl}Google.rpc.ErrorInfo"; + public const string RetryInfo = $"{ErrorDetailTypeUrl}Google.rpc.RetryInfo"; + public const string DebugInfo = $"{ErrorDetailTypeUrl}Google.rpc.DebugInfo"; + public const string QuotaFailure = $"{ErrorDetailTypeUrl}Google.rpc.QuotaFailure"; + public const string PreconditionFailure = $"{ErrorDetailTypeUrl}Google.rpc.PreconditionFailure"; + public const string BadRequest = $"{ErrorDetailTypeUrl}Google.rpc.BadRequest"; + public const string RequestInfo = $"{ErrorDetailTypeUrl}Google.rpc.RequestInfo"; + public const string ResourceInfo = $"{ErrorDetailTypeUrl}Google.rpc.ResourceInfo"; + public const string Help = $"{ErrorDetailTypeUrl}Google.rpc.Help"; + public const string LocalizedMessage = $"{ErrorDetailTypeUrl}Google.rpc.LocalizedMessage"; +} \ No newline at end of file diff --git a/src/Dapr.Common/Exceptions/DaprExtendedErrorDetail.cs b/src/Dapr.Common/Exceptions/DaprExtendedErrorDetail.cs index 17c10c20..0ed738c2 100644 --- a/src/Dapr.Common/Exceptions/DaprExtendedErrorDetail.cs +++ b/src/Dapr.Common/Exceptions/DaprExtendedErrorDetail.cs @@ -11,147 +11,145 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Common.Exceptions +namespace Dapr.Common.Exceptions; + +/// +/// Abstract base class of the Dapr extended error detail. +/// +public abstract record DaprExtendedErrorDetail(DaprExtendedErrorType ErrorType); + +/// +/// Detail when the type url is unrecognized. +/// +/// The unrecognized type url. +public sealed record DaprUnknownDetail(string TypeUrl) : DaprExtendedErrorDetail(DaprExtendedErrorType.Unknown); + +/// +/// Detail proving debugging information. +/// +/// Stack trace entries relating to error. +/// Further related debugging information. +public sealed record DaprDebugInfoDetail(IReadOnlyCollection StackEntries, string Detail) : DaprExtendedErrorDetail(DaprExtendedErrorType.DebugInfo); + +/// +/// A precondtion violation. +/// +/// The type of the violation. +/// The subject that the violation relates to. +/// A description of how the precondition may have failed. +public sealed record DaprPreconditionFailureViolation(string Type, string Subject, string Description); + +/// +/// Detail relating to a failed precondition e.g user has not completed some required check. +/// +public sealed record DaprPreconditionFailureDetail() : DaprExtendedErrorDetail(DaprExtendedErrorType.PreconditionFailure) { /// - /// Abstract base class of the Dapr extended error detail. + /// Collection of . /// - public abstract record DaprExtendedErrorDetail(DaprExtendedErrorType ErrorType); + public IReadOnlyCollection Violations { get; init; } = Array.Empty(); +} +/// +/// Provides the time offset the client should use before retrying. +/// +/// Second offset. +/// Nano offset. +public sealed record DaprRetryDelay(long Seconds, int Nanos); + +/// +/// Detail containing retry information. Provides the minimum amount of the time the client should wait before retrying a request. +/// +public sealed record DaprRetryInfoDetail() : DaprExtendedErrorDetail(DaprExtendedErrorType.RetryInfo) +{ /// - /// Detail when the type url is unrecognized. + /// A . /// - /// The unrecognized type url. - public sealed record DaprUnknownDetail(string TypeUrl) : DaprExtendedErrorDetail(DaprExtendedErrorType.Unknown); + public DaprRetryDelay Delay = new(Seconds: 1, Nanos: default); +} +/// +/// Further details relating to a quota violation. +/// +/// The subject where the quota violation occured e.g and ip address or remote resource. +/// Further information relating to the quota violation. +public sealed record DaprQuotaFailureViolation(string Subject, string Description); + +/// +/// Detail relating to a quota failure e.g reaching API limit. +/// +public sealed record DaprQuotaFailureDetail() : DaprExtendedErrorDetail(DaprExtendedErrorType.QuotaFailure) +{ /// - /// Detail proving debugging information. + /// Collection of . /// - /// Stack trace entries relating to error. - /// Further related debugging information. - public sealed record DaprDebugInfoDetail(IReadOnlyCollection StackEntries, string Detail) : DaprExtendedErrorDetail(DaprExtendedErrorType.DebugInfo); + public IReadOnlyCollection Violations { get; init; } = Array.Empty(); +} +/// +/// Further infomation related to a bad request. +/// +/// The field that generated the bad request e.g 'NewAccountName||'. +/// Further description of the field error e.g 'Account name cannot contain '||'' +public sealed record DaprBadRequestDetailFieldViolation(string Field, string Description); + +/// +/// Detail containing information related to a bad request from the client. +/// +public sealed record DaprBadRequestDetail() : DaprExtendedErrorDetail(DaprExtendedErrorType.BadRequest) +{ /// - /// A precondtion violation. + /// Collection of . /// - /// The type of the violation. - /// The subject that the violation relates to. - /// A description of how the precondition may have failed. - public sealed record DaprPreconditionFailureViolation(string Type, string Subject, string Description); + public IReadOnlyCollection FieldViolations { get; init; } = Array.Empty(); +} +/// +/// Detail containing request info used by the client to provide back to the server in relation to filing bugs, providing feedback, or general debugging by the server. +/// +/// A string understandable by the server e.g an internal UID related a trace. +/// Any data that furthers server debugging. +public sealed record DaprRequestInfoDetail(string RequestId, string ServingData) : DaprExtendedErrorDetail(DaprExtendedErrorType.RequestInfo); + +/// +/// Detail containing a message that can be localized. +/// +/// The locale e.g 'en-US'. +/// A message to be localized. +public sealed record DaprLocalizedMessageDetail(string Locale, string Message) : DaprExtendedErrorDetail(DaprExtendedErrorType.LocalizedMessage); + + +/// +/// Contains a link to a help resource. +/// +/// Url to help resources or documentation e.g 'https://v1-15.docs.dapr.io/developing-applications/error-codes/error-codes-reference/'. +/// A description of the link. +public sealed record DaprHelpDetailLink(string Url, string Description); + +/// +/// Detail containing links to further help resources. +/// +public sealed record DaprHelpDetail() : DaprExtendedErrorDetail(DaprExtendedErrorType.Help) +{ /// - /// Detail relating to a failed precondition e.g user has not completed some required check. + /// Collection of . /// - public sealed record DaprPreconditionFailureDetail() : DaprExtendedErrorDetail(DaprExtendedErrorType.PreconditionFailure) - { - /// - /// Collection of . - /// - public IReadOnlyCollection Violations { get; init; } = Array.Empty(); - } + public IReadOnlyCollection Links { get; init; } = Array.Empty(); +} - /// - /// Provides the time offset the client should use before retrying. - /// - /// Second offset. - /// Nano offset. - public sealed record DaprRetryDelay(long Seconds, int Nanos); +/// +/// Detail containg resource information. +/// +/// The type of the resource e.g 'state'. +/// The name of the resource e.g 'statestore'. +/// The owner of the resource. +/// Further description of the resource. +public sealed record DaprResourceInfoDetail(string ResourceType, string ResourceName, string Owner, string Description) : DaprExtendedErrorDetail(DaprExtendedErrorType.ResourceInfo); - /// - /// Detail containing retry information. Provides the minimum amount of the time the client should wait before retrying a request. - /// - public sealed record DaprRetryInfoDetail() : DaprExtendedErrorDetail(DaprExtendedErrorType.RetryInfo) - { - /// - /// A . - /// - public DaprRetryDelay Delay = new(Seconds: 1, Nanos: default); - } - - /// - /// Further details relating to a quota violation. - /// - /// The subject where the quota violation occured e.g and ip address or remote resource. - /// Further information relating to the quota violation. - public sealed record DaprQuotaFailureViolation(string Subject, string Description); - - /// - /// Detail relating to a quota failure e.g reaching API limit. - /// - public sealed record DaprQuotaFailureDetail() : DaprExtendedErrorDetail(DaprExtendedErrorType.QuotaFailure) - { - /// - /// Collection of . - /// - public IReadOnlyCollection Violations { get; init; } = Array.Empty(); - } - - /// - /// Further infomation related to a bad request. - /// - /// The field that generated the bad request e.g 'NewAccountName||'. - /// Further description of the field error e.g 'Account name cannot contain '||'' - public sealed record DaprBadRequestDetailFieldViolation(string Field, string Description); - - /// - /// Detail containing information related to a bad request from the client. - /// - public sealed record DaprBadRequestDetail() : DaprExtendedErrorDetail(DaprExtendedErrorType.BadRequest) - { - /// - /// Collection of . - /// - public IReadOnlyCollection FieldViolations { get; init; } = Array.Empty(); - } - - /// - /// Detail containing request info used by the client to provide back to the server in relation to filing bugs, providing feedback, or general debugging by the server. - /// - /// A string understandable by the server e.g an internal UID related a trace. - /// Any data that furthers server debugging. - public sealed record DaprRequestInfoDetail(string RequestId, string ServingData) : DaprExtendedErrorDetail(DaprExtendedErrorType.RequestInfo); - - /// - /// Detail containing a message that can be localized. - /// - /// The locale e.g 'en-US'. - /// A message to be localized. - public sealed record DaprLocalizedMessageDetail(string Locale, string Message) : DaprExtendedErrorDetail(DaprExtendedErrorType.LocalizedMessage); - - - /// - /// Contains a link to a help resource. - /// - /// Url to help resources or documentation e.g 'https://v1-15.docs.dapr.io/developing-applications/error-codes/error-codes-reference/'. - /// A description of the link. - public sealed record DaprHelpDetailLink(string Url, string Description); - - /// - /// Detail containing links to further help resources. - /// - public sealed record DaprHelpDetail() : DaprExtendedErrorDetail(DaprExtendedErrorType.Help) - { - /// - /// Collection of . - /// - public IReadOnlyCollection Links { get; init; } = Array.Empty(); - } - - /// - /// Detail containg resource information. - /// - /// The type of the resource e.g 'state'. - /// The name of the resource e.g 'statestore'. - /// The owner of the resource. - /// Further description of the resource. - public sealed record DaprResourceInfoDetail(string ResourceType, string ResourceName, string Owner, string Description) : DaprExtendedErrorDetail(DaprExtendedErrorType.ResourceInfo); - - /// - /// Detail containing information related to a server error. - /// - /// The error reason e.g 'DAPR_STATE_ILLEGAL_KEY'. - /// The error domain e.g 'dapr.io'. - /// Further key / value based metadata. - public sealed record DaprErrorInfoDetail(string Reason, string Domain, IDictionary? Metadata) : DaprExtendedErrorDetail(DaprExtendedErrorType.ErrorInfo); - -} +/// +/// Detail containing information related to a server error. +/// +/// The error reason e.g 'DAPR_STATE_ILLEGAL_KEY'. +/// The error domain e.g 'dapr.io'. +/// Further key / value based metadata. +public sealed record DaprErrorInfoDetail(string Reason, string Domain, IDictionary? Metadata) : DaprExtendedErrorDetail(DaprExtendedErrorType.ErrorInfo); \ No newline at end of file diff --git a/src/Dapr.Common/Exceptions/DaprExtendedErrorInfo.cs b/src/Dapr.Common/Exceptions/DaprExtendedErrorInfo.cs index bcd337a3..30586b09 100644 --- a/src/Dapr.Common/Exceptions/DaprExtendedErrorInfo.cs +++ b/src/Dapr.Common/Exceptions/DaprExtendedErrorInfo.cs @@ -11,18 +11,17 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Common.Exceptions +namespace Dapr.Common.Exceptions; + +/// +/// Dapr implementation of the richer error model. +/// +/// A status code. +/// A message. +public sealed record DaprExtendedErrorInfo(int Code, string Message) { /// - /// Dapr implementation of the richer error model. + /// A collection of details that provide more information on the error. /// - /// A status code. - /// A message. - public sealed record DaprExtendedErrorInfo(int Code, string Message) - { - /// - /// A collection of details that provide more information on the error. - /// - public DaprExtendedErrorDetail[] Details { get; init; } = Array.Empty(); - } -} + public DaprExtendedErrorDetail[] Details { get; init; } = Array.Empty(); +} \ No newline at end of file diff --git a/src/Dapr.Common/Exceptions/DaprExtendedErrorType.cs b/src/Dapr.Common/Exceptions/DaprExtendedErrorType.cs index 664f6bee..109bd392 100644 --- a/src/Dapr.Common/Exceptions/DaprExtendedErrorType.cs +++ b/src/Dapr.Common/Exceptions/DaprExtendedErrorType.cs @@ -11,80 +11,79 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Common.Exceptions +namespace Dapr.Common.Exceptions; + +/// +/// Extended error detail types. +/// This is based on the Richer Error Model (see and +/// ) +/// and is implemented by the Dapr runtime (see ). +/// +public enum DaprExtendedErrorType { /// - /// Extended error detail types. - /// This is based on the Richer Error Model (see and - /// ) - /// and is implemented by the Dapr runtime (see ). + /// Unknown extended error type. + /// Implemented by . /// - public enum DaprExtendedErrorType - { - /// - /// Unknown extended error type. - /// Implemented by . - /// - Unknown, + Unknown, - /// - /// Retry info detail type. - /// See . - /// - RetryInfo, + /// + /// Retry info detail type. + /// See . + /// + RetryInfo, - /// - /// Debug info detail type. - /// See . - /// - DebugInfo, + /// + /// Debug info detail type. + /// See . + /// + DebugInfo, - /// - /// Quote failure detail type. - /// See . - /// - QuotaFailure, + /// + /// Quote failure detail type. + /// See . + /// + QuotaFailure, - /// - /// Precondition failure detail type. - /// See . - /// - PreconditionFailure, + /// + /// Precondition failure detail type. + /// See . + /// + PreconditionFailure, - /// - /// Request info detail type. - /// See . - /// - RequestInfo, + /// + /// Request info detail type. + /// See . + /// + RequestInfo, - /// - /// Localized message detail type. - /// See . - /// - LocalizedMessage, + /// + /// Localized message detail type. + /// See . + /// + LocalizedMessage, - /// - /// Bad request detail type. - /// See . - /// - BadRequest, + /// + /// Bad request detail type. + /// See . + /// + BadRequest, - /// - /// Error info detail type. - /// See . - /// - ErrorInfo, + /// + /// Error info detail type. + /// See . + /// + ErrorInfo, - /// - /// Help detail type. - /// See . - /// - Help, + /// + /// Help detail type. + /// See . + /// + Help, - /// - /// Resource info detail type. - /// See . - /// - ResourceInfo - } -} + /// + /// Resource info detail type. + /// See . + /// + ResourceInfo +} \ No newline at end of file diff --git a/src/Dapr.Common/Exceptions/ExtendedErrorDetailFactory.cs b/src/Dapr.Common/Exceptions/ExtendedErrorDetailFactory.cs index f81b6646..a29c5088 100644 --- a/src/Dapr.Common/Exceptions/ExtendedErrorDetailFactory.cs +++ b/src/Dapr.Common/Exceptions/ExtendedErrorDetailFactory.cs @@ -15,108 +15,107 @@ using Google.Protobuf; using Google.Protobuf.WellKnownTypes; using Google.Rpc; -namespace Dapr.Common.Exceptions +namespace Dapr.Common.Exceptions; + +/// +/// factory. +/// +internal static class ExtendedErrorDetailFactory { /// - /// factory. + /// Create a new from an instance of . /// - internal static class ExtendedErrorDetailFactory + /// The serialized detail message to create the error detail from. + /// A new instance of + internal static DaprExtendedErrorDetail CreateErrorDetail(Any message) { - /// - /// Create a new from an instance of . - /// - /// The serialized detail message to create the error detail from. - /// A new instance of - internal static DaprExtendedErrorDetail CreateErrorDetail(Any message) + var data = message.Value; + return message.TypeUrl switch { - var data = message.Value; - return message.TypeUrl switch - { - DaprExtendedErrorConstants.RetryInfo => ToDaprRetryInfoDetail(data), - DaprExtendedErrorConstants.ErrorInfo => ToDaprErrorInfoDetail(data), - DaprExtendedErrorConstants.DebugInfo => ToDaprDebugInfoDetail(data), - DaprExtendedErrorConstants.QuotaFailure => ToDaprQuotaFailureDetail(data), - DaprExtendedErrorConstants.PreconditionFailure => ToDaprPreconditionFailureDetail(data), - DaprExtendedErrorConstants.BadRequest => ToDaprBadRequestDetail(data), - DaprExtendedErrorConstants.RequestInfo => ToDaprRequestInfoDetail(data), - DaprExtendedErrorConstants.ResourceInfo => ToDaprResourceInfoDetail(data), - DaprExtendedErrorConstants.Help => ToDaprHelpDetail(data), - DaprExtendedErrorConstants.LocalizedMessage => ToDaprLocalizedMessageDetail(data), - _ => new DaprUnknownDetail(message.TypeUrl) - }; - } - - private static DaprRetryInfoDetail ToDaprRetryInfoDetail(ByteString data) - { - var retryInfo = RetryInfo.Parser.ParseFrom(data); - return new() { Delay = new DaprRetryDelay(Seconds: retryInfo.RetryDelay.Seconds, Nanos: retryInfo.RetryDelay.Nanos) } ; - } - - private static DaprLocalizedMessageDetail ToDaprLocalizedMessageDetail(ByteString data) - { - var localizedMessage = LocalizedMessage.Parser.ParseFrom(data); - return new(Locale: localizedMessage.Locale, Message: localizedMessage.Message); - } - - private static DaprDebugInfoDetail ToDaprDebugInfoDetail(ByteString data) - { - var debugInfo = DebugInfo.Parser.ParseFrom(data); - return new(StackEntries: debugInfo.StackEntries.ToArray(), Detail: debugInfo.Detail); - } - - private static DaprQuotaFailureDetail ToDaprQuotaFailureDetail(ByteString data) - { - var quotaFailure = QuotaFailure.Parser.ParseFrom(data); - return new() - { - Violations = quotaFailure.Violations.Select(violation => new DaprQuotaFailureViolation(Subject: violation.Subject, Description: violation.Description)).ToArray(), - }; - } - - private static DaprPreconditionFailureDetail ToDaprPreconditionFailureDetail(ByteString data) - { - var preconditionFailure = PreconditionFailure.Parser.ParseFrom(data); - return new() - { - Violations = preconditionFailure.Violations.Select(violation => new DaprPreconditionFailureViolation(Type: violation.Type, Subject: violation.Subject, Description: violation.Description)).ToArray() - }; - } - - private static DaprRequestInfoDetail ToDaprRequestInfoDetail(ByteString data) - { - var requestInfo = RequestInfo.Parser.ParseFrom(data); - return new(RequestId: requestInfo.RequestId, ServingData: requestInfo.ServingData); - } - - private static DaprResourceInfoDetail ToDaprResourceInfoDetail(ByteString data) - { - var resourceInfo = ResourceInfo.Parser.ParseFrom(data); - return new(ResourceType: resourceInfo.ResourceType, ResourceName: resourceInfo.ResourceName, Owner: resourceInfo.Owner, Description: resourceInfo.Description); - } - - private static DaprBadRequestDetail ToDaprBadRequestDetail(ByteString data) - { - var badRequest = BadRequest.Parser.ParseFrom(data); - return new() - { - FieldViolations = badRequest.FieldViolations.Select( - fieldViolation => new DaprBadRequestDetailFieldViolation(Field: fieldViolation.Field, Description: fieldViolation.Description)).ToArray() - }; - } - - private static DaprErrorInfoDetail ToDaprErrorInfoDetail(ByteString data) - { - var errorInfo = ErrorInfo.Parser.ParseFrom(data); - return new(Reason: errorInfo.Reason, Domain: errorInfo.Domain, Metadata: errorInfo.Metadata); - } - - private static DaprHelpDetail ToDaprHelpDetail(ByteString data) - { - var helpInfo = Help.Parser.ParseFrom(data); - return new() - { - Links = helpInfo.Links.Select(link => new DaprHelpDetailLink(Url: link.Url, Description: link.Description)).ToArray() - }; - } + DaprExtendedErrorConstants.RetryInfo => ToDaprRetryInfoDetail(data), + DaprExtendedErrorConstants.ErrorInfo => ToDaprErrorInfoDetail(data), + DaprExtendedErrorConstants.DebugInfo => ToDaprDebugInfoDetail(data), + DaprExtendedErrorConstants.QuotaFailure => ToDaprQuotaFailureDetail(data), + DaprExtendedErrorConstants.PreconditionFailure => ToDaprPreconditionFailureDetail(data), + DaprExtendedErrorConstants.BadRequest => ToDaprBadRequestDetail(data), + DaprExtendedErrorConstants.RequestInfo => ToDaprRequestInfoDetail(data), + DaprExtendedErrorConstants.ResourceInfo => ToDaprResourceInfoDetail(data), + DaprExtendedErrorConstants.Help => ToDaprHelpDetail(data), + DaprExtendedErrorConstants.LocalizedMessage => ToDaprLocalizedMessageDetail(data), + _ => new DaprUnknownDetail(message.TypeUrl) + }; } -} + + private static DaprRetryInfoDetail ToDaprRetryInfoDetail(ByteString data) + { + var retryInfo = RetryInfo.Parser.ParseFrom(data); + return new() { Delay = new DaprRetryDelay(Seconds: retryInfo.RetryDelay.Seconds, Nanos: retryInfo.RetryDelay.Nanos) } ; + } + + private static DaprLocalizedMessageDetail ToDaprLocalizedMessageDetail(ByteString data) + { + var localizedMessage = LocalizedMessage.Parser.ParseFrom(data); + return new(Locale: localizedMessage.Locale, Message: localizedMessage.Message); + } + + private static DaprDebugInfoDetail ToDaprDebugInfoDetail(ByteString data) + { + var debugInfo = DebugInfo.Parser.ParseFrom(data); + return new(StackEntries: debugInfo.StackEntries.ToArray(), Detail: debugInfo.Detail); + } + + private static DaprQuotaFailureDetail ToDaprQuotaFailureDetail(ByteString data) + { + var quotaFailure = QuotaFailure.Parser.ParseFrom(data); + return new() + { + Violations = quotaFailure.Violations.Select(violation => new DaprQuotaFailureViolation(Subject: violation.Subject, Description: violation.Description)).ToArray(), + }; + } + + private static DaprPreconditionFailureDetail ToDaprPreconditionFailureDetail(ByteString data) + { + var preconditionFailure = PreconditionFailure.Parser.ParseFrom(data); + return new() + { + Violations = preconditionFailure.Violations.Select(violation => new DaprPreconditionFailureViolation(Type: violation.Type, Subject: violation.Subject, Description: violation.Description)).ToArray() + }; + } + + private static DaprRequestInfoDetail ToDaprRequestInfoDetail(ByteString data) + { + var requestInfo = RequestInfo.Parser.ParseFrom(data); + return new(RequestId: requestInfo.RequestId, ServingData: requestInfo.ServingData); + } + + private static DaprResourceInfoDetail ToDaprResourceInfoDetail(ByteString data) + { + var resourceInfo = ResourceInfo.Parser.ParseFrom(data); + return new(ResourceType: resourceInfo.ResourceType, ResourceName: resourceInfo.ResourceName, Owner: resourceInfo.Owner, Description: resourceInfo.Description); + } + + private static DaprBadRequestDetail ToDaprBadRequestDetail(ByteString data) + { + var badRequest = BadRequest.Parser.ParseFrom(data); + return new() + { + FieldViolations = badRequest.FieldViolations.Select( + fieldViolation => new DaprBadRequestDetailFieldViolation(Field: fieldViolation.Field, Description: fieldViolation.Description)).ToArray() + }; + } + + private static DaprErrorInfoDetail ToDaprErrorInfoDetail(ByteString data) + { + var errorInfo = ErrorInfo.Parser.ParseFrom(data); + return new(Reason: errorInfo.Reason, Domain: errorInfo.Domain, Metadata: errorInfo.Metadata); + } + + private static DaprHelpDetail ToDaprHelpDetail(ByteString data) + { + var helpInfo = Help.Parser.ParseFrom(data); + return new() + { + Links = helpInfo.Links.Select(link => new DaprHelpDetailLink(Url: link.Url, Description: link.Description)).ToArray() + }; + } +} \ No newline at end of file diff --git a/src/Dapr.Extensions.Configuration/DaprConfigurationStoreExtension.cs b/src/Dapr.Extensions.Configuration/DaprConfigurationStoreExtension.cs index 190acacd..f0910ad0 100644 --- a/src/Dapr.Extensions.Configuration/DaprConfigurationStoreExtension.cs +++ b/src/Dapr.Extensions.Configuration/DaprConfigurationStoreExtension.cs @@ -17,84 +17,83 @@ using System.Threading; using Dapr.Client; using Microsoft.Extensions.Configuration; -namespace Dapr.Extensions.Configuration +namespace Dapr.Extensions.Configuration; + +/// +/// Extension used to call the Dapr Configuration API and store the values in a . +/// +public static class DaprConfigurationStoreExtension { /// - /// Extension used to call the Dapr Configuration API and store the values in a . + /// Register a constant configuration store. This will make one call to Dapr with the given keys to the + /// Configuration API. /// - public static class DaprConfigurationStoreExtension + /// The to add to. + /// The configuration store to query. + /// The keys, if any, to request. If empty, returns all configuration items. + /// The used for the request. + /// The used to configure the timeout waiting for Dapr. + /// Optional metadata sent to the configuration store. + /// The . + public static IConfigurationBuilder AddDaprConfigurationStore( + this IConfigurationBuilder configurationBuilder, + string store, + IReadOnlyList keys, + DaprClient client, + TimeSpan sidecarWaitTimeout, + IReadOnlyDictionary? metadata = default) { - /// - /// Register a constant configuration store. This will make one call to Dapr with the given keys to the - /// Configuration API. - /// - /// The to add to. - /// The configuration store to query. - /// The keys, if any, to request. If empty, returns all configuration items. - /// The used for the request. - /// The used to configure the timeout waiting for Dapr. - /// Optional metadata sent to the configuration store. - /// The . - public static IConfigurationBuilder AddDaprConfigurationStore( - this IConfigurationBuilder configurationBuilder, - string store, - IReadOnlyList keys, - DaprClient client, - TimeSpan sidecarWaitTimeout, - IReadOnlyDictionary? metadata = default) + ArgumentVerifier.ThrowIfNullOrEmpty(store, nameof(store)); + ArgumentVerifier.ThrowIfNull(keys, nameof(keys)); + ArgumentVerifier.ThrowIfNull(client, nameof(client)); + + configurationBuilder.Add(new DaprConfigurationStoreSource() { - ArgumentVerifier.ThrowIfNullOrEmpty(store, nameof(store)); - ArgumentVerifier.ThrowIfNull(keys, nameof(keys)); - ArgumentVerifier.ThrowIfNull(client, nameof(client)); + Store = store, + Keys = keys, + Client = client, + SidecarWaitTimeout = sidecarWaitTimeout, + IsStreaming = false, + Metadata = metadata + }); - configurationBuilder.Add(new DaprConfigurationStoreSource() - { - Store = store, - Keys = keys, - Client = client, - SidecarWaitTimeout = sidecarWaitTimeout, - IsStreaming = false, - Metadata = metadata - }); - - return configurationBuilder; - } - - /// - /// Register a streaming configuration store. This opens a stream to the Dapr Configuration API which - /// will get updates to keys should any occur. This stream will not be closed until canceled with the - /// or the configuration is unsubscribed in Dapr. - /// - /// The to add to. - /// The configuration store to query. - /// The keys, if any, to request. If empty, returns all configuration items. - /// The used for the request. - /// The used to configure the timeout waiting for Dapr. - /// Optional metadata sent to the configuration store. - /// The . - public static IConfigurationBuilder AddStreamingDaprConfigurationStore( - this IConfigurationBuilder configurationBuilder, - string store, - IReadOnlyList keys, - DaprClient client, - TimeSpan sidecarWaitTimeout, - IReadOnlyDictionary? metadata = default) - { - ArgumentVerifier.ThrowIfNullOrEmpty(store, nameof(store)); - ArgumentVerifier.ThrowIfNull(keys, nameof(keys)); - ArgumentVerifier.ThrowIfNull(client, nameof(client)); - - configurationBuilder.Add(new DaprConfigurationStoreSource() - { - Store = store, - Keys = keys, - Client = client, - SidecarWaitTimeout = sidecarWaitTimeout, - IsStreaming = true, - Metadata = metadata - }); - - return configurationBuilder; - } + return configurationBuilder; } -} + + /// + /// Register a streaming configuration store. This opens a stream to the Dapr Configuration API which + /// will get updates to keys should any occur. This stream will not be closed until canceled with the + /// or the configuration is unsubscribed in Dapr. + /// + /// The to add to. + /// The configuration store to query. + /// The keys, if any, to request. If empty, returns all configuration items. + /// The used for the request. + /// The used to configure the timeout waiting for Dapr. + /// Optional metadata sent to the configuration store. + /// The . + public static IConfigurationBuilder AddStreamingDaprConfigurationStore( + this IConfigurationBuilder configurationBuilder, + string store, + IReadOnlyList keys, + DaprClient client, + TimeSpan sidecarWaitTimeout, + IReadOnlyDictionary? metadata = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(store, nameof(store)); + ArgumentVerifier.ThrowIfNull(keys, nameof(keys)); + ArgumentVerifier.ThrowIfNull(client, nameof(client)); + + configurationBuilder.Add(new DaprConfigurationStoreSource() + { + Store = store, + Keys = keys, + Client = client, + SidecarWaitTimeout = sidecarWaitTimeout, + IsStreaming = true, + Metadata = metadata + }); + + return configurationBuilder; + } +} \ No newline at end of file diff --git a/src/Dapr.Extensions.Configuration/DaprConfigurationStoreProvider.cs b/src/Dapr.Extensions.Configuration/DaprConfigurationStoreProvider.cs index 461ee995..b5f2900d 100644 --- a/src/Dapr.Extensions.Configuration/DaprConfigurationStoreProvider.cs +++ b/src/Dapr.Extensions.Configuration/DaprConfigurationStoreProvider.cs @@ -18,108 +18,107 @@ using System.Threading.Tasks; using Dapr.Client; using Microsoft.Extensions.Configuration; -namespace Dapr.Extensions.Configuration +namespace Dapr.Extensions.Configuration; + +/// +/// A configuration provider that utilizes the Dapr Configuration API. It can either be a single, constant +/// call or a streaming call. +/// +internal class DaprConfigurationStoreProvider : ConfigurationProvider, IDisposable { + private string store; + private IReadOnlyList keys; + private DaprClient daprClient; + private TimeSpan sidecarWaitTimeout; + private bool isStreaming; + private IReadOnlyDictionary? metadata; + private CancellationTokenSource cts; + private Task subscribeTask = Task.CompletedTask; + /// - /// A configuration provider that utilizes the Dapr Configuration API. It can either be a single, constant - /// call or a streaming call. + /// Constructor. /// - internal class DaprConfigurationStoreProvider : ConfigurationProvider, IDisposable + /// The configuration store to query. + /// The keys, if any, to request. If empty, returns all configuration items. + /// The used for the request. + /// The used to configure the timeout waiting for Dapr. + /// Determines if the source is streaming or not. + /// Optional metadata sent to the configuration store. + public DaprConfigurationStoreProvider( + string store, + IReadOnlyList keys, + DaprClient daprClient, + TimeSpan sidecarWaitTimeout, + bool isStreaming = false, + IReadOnlyDictionary? metadata = default) { - private string store; - private IReadOnlyList keys; - private DaprClient daprClient; - private TimeSpan sidecarWaitTimeout; - private bool isStreaming; - private IReadOnlyDictionary? metadata; - private CancellationTokenSource cts; - private Task subscribeTask = Task.CompletedTask; + this.store = store; + this.keys = keys; + this.daprClient = daprClient; + this.sidecarWaitTimeout = sidecarWaitTimeout; + this.isStreaming = isStreaming; + this.metadata = metadata ?? new Dictionary(); + this.cts = new CancellationTokenSource(); + } - /// - /// Constructor. - /// - /// The configuration store to query. - /// The keys, if any, to request. If empty, returns all configuration items. - /// The used for the request. - /// The used to configure the timeout waiting for Dapr. - /// Determines if the source is streaming or not. - /// Optional metadata sent to the configuration store. - public DaprConfigurationStoreProvider( - string store, - IReadOnlyList keys, - DaprClient daprClient, - TimeSpan sidecarWaitTimeout, - bool isStreaming = false, - IReadOnlyDictionary? metadata = default) + public void Dispose() + { + cts.Cancel(); + } + + /// + public override void Load() => LoadAsync().ConfigureAwait(false).GetAwaiter().GetResult(); + + private async Task LoadAsync() + { + // Wait for the sidecar to become available. + using (var tokenSource = new CancellationTokenSource(sidecarWaitTimeout)) { - this.store = store; - this.keys = keys; - this.daprClient = daprClient; - this.sidecarWaitTimeout = sidecarWaitTimeout; - this.isStreaming = isStreaming; - this.metadata = metadata ?? new Dictionary(); - this.cts = new CancellationTokenSource(); + await daprClient.WaitForSidecarAsync(tokenSource.Token); } - public void Dispose() + if (isStreaming) { - cts.Cancel(); - } - - /// - public override void Load() => LoadAsync().ConfigureAwait(false).GetAwaiter().GetResult(); - - private async Task LoadAsync() - { - // Wait for the sidecar to become available. - using (var tokenSource = new CancellationTokenSource(sidecarWaitTimeout)) + subscribeTask = Task.Run(async () => { - await daprClient.WaitForSidecarAsync(tokenSource.Token); - } - - if (isStreaming) - { - subscribeTask = Task.Run(async () => + while (!cts.Token.IsCancellationRequested) { - while (!cts.Token.IsCancellationRequested) + var id = string.Empty; + try { - var id = string.Empty; - try + var subscribeConfigurationResponse = await daprClient.SubscribeConfiguration(store, keys, metadata, cts.Token); + await foreach (var items in subscribeConfigurationResponse.Source.WithCancellation(cts.Token)) { - var subscribeConfigurationResponse = await daprClient.SubscribeConfiguration(store, keys, metadata, cts.Token); - await foreach (var items in subscribeConfigurationResponse.Source.WithCancellation(cts.Token)) + var data = new Dictionary(Data, StringComparer.OrdinalIgnoreCase); + foreach (var item in items) { - var data = new Dictionary(Data, StringComparer.OrdinalIgnoreCase); - foreach (var item in items) - { - id = subscribeConfigurationResponse.Id; - data[item.Key] = item.Value.Value; - } - Data = data; - // Whenever we get an update, make sure to update the reloadToken. - OnReload(); - } - } - catch (Exception) - { - // If we catch an exception, try and cancel the subscription so we can connect again. - if (!string.IsNullOrEmpty(id)) - { - await daprClient.UnsubscribeConfiguration(store, id); + id = subscribeConfigurationResponse.Id; + data[item.Key] = item.Value.Value; } + Data = data; + // Whenever we get an update, make sure to update the reloadToken. + OnReload(); + } + } + catch (Exception) + { + // If we catch an exception, try and cancel the subscription so we can connect again. + if (!string.IsNullOrEmpty(id)) + { + await daprClient.UnsubscribeConfiguration(store, id); } } - }); - } - else - { - // We don't need to worry about ReloadTokens here because it is a constant response. - var getConfigurationResponse = await daprClient.GetConfiguration(store, keys, metadata, cts.Token); - foreach (var item in getConfigurationResponse.Items) - { - Set(item.Key, item.Value.Value); } + }); + } + else + { + // We don't need to worry about ReloadTokens here because it is a constant response. + var getConfigurationResponse = await daprClient.GetConfiguration(store, keys, metadata, cts.Token); + foreach (var item in getConfigurationResponse.Items) + { + Set(item.Key, item.Value.Value); } } } -} +} \ No newline at end of file diff --git a/src/Dapr.Extensions.Configuration/DaprConfigurationStoreSource.cs b/src/Dapr.Extensions.Configuration/DaprConfigurationStoreSource.cs index e7c2bf34..513e156b 100644 --- a/src/Dapr.Extensions.Configuration/DaprConfigurationStoreSource.cs +++ b/src/Dapr.Extensions.Configuration/DaprConfigurationStoreSource.cs @@ -16,47 +16,46 @@ using System.Collections.Generic; using Dapr.Client; using Microsoft.Extensions.Configuration; -namespace Dapr.Extensions.Configuration +namespace Dapr.Extensions.Configuration; + +/// +/// Configuration source that provides a . +/// +public class DaprConfigurationStoreSource : IConfigurationSource { /// - /// Configuration source that provides a . + /// The Configuration Store to query. /// - public class DaprConfigurationStoreSource : IConfigurationSource + public string Store { get; set; } = default!; + + /// + /// The list of keys to request from the configuration. If empty, request all keys. + /// + public IReadOnlyList Keys { get; set; } = default!; + + /// + /// The used to query the configuration. + /// + public DaprClient Client { get; set; } = default!; + + /// + /// Boolean stating if this is a streaming call or not. + /// + public bool IsStreaming { get; set; } = false; + + /// + /// Gets or sets the that is used to control the timeout waiting for the Dapr sidecar to become healthly. + /// + public TimeSpan SidecarWaitTimeout { get; set; } + + /// + /// The optional metadata to be sent to the configuration store. + /// + public IReadOnlyDictionary? Metadata { get; set; } = default; + + /// + public IConfigurationProvider Build(IConfigurationBuilder builder) { - /// - /// The Configuration Store to query. - /// - public string Store { get; set; } = default!; - - /// - /// The list of keys to request from the configuration. If empty, request all keys. - /// - public IReadOnlyList Keys { get; set; } = default!; - - /// - /// The used to query the configuration. - /// - public DaprClient Client { get; set; } = default!; - - /// - /// Boolean stating if this is a streaming call or not. - /// - public bool IsStreaming { get; set; } = false; - - /// - /// Gets or sets the that is used to control the timeout waiting for the Dapr sidecar to become healthly. - /// - public TimeSpan SidecarWaitTimeout { get; set; } - - /// - /// The optional metadata to be sent to the configuration store. - /// - public IReadOnlyDictionary? Metadata { get; set; } = default; - - /// - public IConfigurationProvider Build(IConfigurationBuilder builder) - { - return new DaprConfigurationStoreProvider(Store, Keys, Client, SidecarWaitTimeout, IsStreaming, Metadata); - } + return new DaprConfigurationStoreProvider(Store, Keys, Client, SidecarWaitTimeout, IsStreaming, Metadata); } -} +} \ No newline at end of file diff --git a/src/Dapr.Extensions.Configuration/DaprSecretDescriptor.cs b/src/Dapr.Extensions.Configuration/DaprSecretDescriptor.cs index 6d86dc04..493c40bb 100644 --- a/src/Dapr.Extensions.Configuration/DaprSecretDescriptor.cs +++ b/src/Dapr.Extensions.Configuration/DaprSecretDescriptor.cs @@ -13,60 +13,59 @@ using System.Collections.Generic; -namespace Dapr.Extensions.Configuration +namespace Dapr.Extensions.Configuration; + +/// +/// Represents the name and metadata for a Secret. +/// +public class DaprSecretDescriptor { /// - /// Represents the name and metadata for a Secret. + /// The name of the secret to retrieve from the Dapr secret store. /// - public class DaprSecretDescriptor + /// + /// If the is not specified, this value will also be used as the key to retrieve the secret from the associated source secret store. + /// + public string SecretName { get; } + + /// + /// A collection of metadata key-value pairs that will be provided to the secret store. The valid metadata keys and values are determined by the type of secret store used. + /// + public IReadOnlyDictionary Metadata { get; } + + /// + /// A value indicating whether to throw an exception if the secret is not found in the source secret store. + /// + /// + /// Setting this value to will suppress the exception; otherwise, will not. + /// + public bool IsRequired { get; } + + /// + /// The secret key that maps to the to retrieve from the source secret store. + /// + /// + /// Use this property when the does not match the key used to retrieve the secret from the source secret store. + /// + public string SecretKey { get; } + + /// + /// Secret Descriptor Constructor + /// + public DaprSecretDescriptor(string secretName, bool isRequired = true, string secretKey = "") + : this(secretName, new Dictionary(), isRequired, secretKey) { - /// - /// The name of the secret to retrieve from the Dapr secret store. - /// - /// - /// If the is not specified, this value will also be used as the key to retrieve the secret from the associated source secret store. - /// - public string SecretName { get; } - /// - /// A collection of metadata key-value pairs that will be provided to the secret store. The valid metadata keys and values are determined by the type of secret store used. - /// - public IReadOnlyDictionary Metadata { get; } - - /// - /// A value indicating whether to throw an exception if the secret is not found in the source secret store. - /// - /// - /// Setting this value to will suppress the exception; otherwise, will not. - /// - public bool IsRequired { get; } - - /// - /// The secret key that maps to the to retrieve from the source secret store. - /// - /// - /// Use this property when the does not match the key used to retrieve the secret from the source secret store. - /// - public string SecretKey { get; } - - /// - /// Secret Descriptor Constructor - /// - public DaprSecretDescriptor(string secretName, bool isRequired = true, string secretKey = "") - : this(secretName, new Dictionary(), isRequired, secretKey) - { - - } - - /// - /// Secret Descriptor Constructor - /// - public DaprSecretDescriptor(string secretName, IReadOnlyDictionary metadata, bool isRequired = true, string secretKey = "") - { - SecretName = secretName; - Metadata = metadata; - IsRequired = isRequired; - SecretKey = string.IsNullOrEmpty(secretKey) ? secretName : secretKey; - } } -} + + /// + /// Secret Descriptor Constructor + /// + public DaprSecretDescriptor(string secretName, IReadOnlyDictionary metadata, bool isRequired = true, string secretKey = "") + { + SecretName = secretName; + Metadata = metadata; + IsRequired = isRequired; + SecretKey = string.IsNullOrEmpty(secretKey) ? secretName : secretKey; + } +} \ No newline at end of file diff --git a/src/Dapr.Extensions.Configuration/DaprSecretStoreConfigurationExtensions.cs b/src/Dapr.Extensions.Configuration/DaprSecretStoreConfigurationExtensions.cs index 15ae6ab8..7df531e3 100644 --- a/src/Dapr.Extensions.Configuration/DaprSecretStoreConfigurationExtensions.cs +++ b/src/Dapr.Extensions.Configuration/DaprSecretStoreConfigurationExtensions.cs @@ -18,205 +18,204 @@ using Microsoft.Extensions.Configuration; using Dapr.Extensions.Configuration.DaprSecretStore; using System.Linq; -namespace Dapr.Extensions.Configuration +namespace Dapr.Extensions.Configuration; + +/// +/// Extension methods for registering with . +/// +public static class DaprSecretStoreConfigurationExtensions { /// - /// Extension methods for registering with . + /// Adds an that reads configuration values from the Dapr Secret Store. /// - public static class DaprSecretStoreConfigurationExtensions + /// The to add to. + /// Dapr secret store name. + /// The secrets to retrieve. + /// The Dapr client + /// The . + public static IConfigurationBuilder AddDaprSecretStore( + this IConfigurationBuilder configurationBuilder, + string store, + IEnumerable secretDescriptors, + DaprClient client) { - /// - /// Adds an that reads configuration values from the Dapr Secret Store. - /// - /// The to add to. - /// Dapr secret store name. - /// The secrets to retrieve. - /// The Dapr client - /// The . - public static IConfigurationBuilder AddDaprSecretStore( - this IConfigurationBuilder configurationBuilder, - string store, - IEnumerable secretDescriptors, - DaprClient client) + ArgumentVerifier.ThrowIfNullOrEmpty(store, nameof(store)); + ArgumentVerifier.ThrowIfNull(secretDescriptors, nameof(secretDescriptors)); + ArgumentVerifier.ThrowIfNull(client, nameof(client)); + + configurationBuilder.Add(new DaprSecretStoreConfigurationSource() { - ArgumentVerifier.ThrowIfNullOrEmpty(store, nameof(store)); - ArgumentVerifier.ThrowIfNull(secretDescriptors, nameof(secretDescriptors)); - ArgumentVerifier.ThrowIfNull(client, nameof(client)); + Store = store, + SecretDescriptors = secretDescriptors, + Client = client + }); - configurationBuilder.Add(new DaprSecretStoreConfigurationSource() - { - Store = store, - SecretDescriptors = secretDescriptors, - Client = client - }); - - return configurationBuilder; - } - - /// - /// Adds an that reads configuration values from the Dapr Secret Store. - /// - /// The to add to. - /// Dapr secret store name. - /// The secrets to retrieve. - /// The Dapr client. - /// The used to configure the timeout waiting for Dapr. - /// The . - public static IConfigurationBuilder AddDaprSecretStore( - this IConfigurationBuilder configurationBuilder, - string store, - IEnumerable secretDescriptors, - DaprClient client, - TimeSpan sidecarWaitTimeout) - { - ArgumentVerifier.ThrowIfNullOrEmpty(store, nameof(store)); - ArgumentVerifier.ThrowIfNull(secretDescriptors, nameof(secretDescriptors)); - ArgumentVerifier.ThrowIfNull(client, nameof(client)); - - configurationBuilder.Add(new DaprSecretStoreConfigurationSource() - { - Store = store, - SecretDescriptors = secretDescriptors, - Client = client, - SidecarWaitTimeout = sidecarWaitTimeout - }); - - return configurationBuilder; - } - - /// - /// Adds an that reads configuration values from the Dapr Secret Store. - /// - /// The to add to. - /// Dapr secret store name. - /// A collection of metadata key-value pairs that will be provided to the secret store. The valid metadata keys and values are determined by the type of secret store used. - /// The Dapr client - /// The . - public static IConfigurationBuilder AddDaprSecretStore( - this IConfigurationBuilder configurationBuilder, - string store, - DaprClient client, - IReadOnlyDictionary? metadata = null) - { - ArgumentVerifier.ThrowIfNullOrEmpty(store, nameof(store)); - ArgumentVerifier.ThrowIfNull(client, nameof(client)); - - configurationBuilder.Add(new DaprSecretStoreConfigurationSource() - { - Store = store, - Metadata = metadata, - Client = client - }); - - return configurationBuilder; - } - - /// - /// Adds an that reads configuration values from the Dapr Secret Store. - /// - /// The to add to. - /// Dapr secret store name. - /// A collection of metadata key-value pairs that will be provided to the secret store. The valid metadata keys and values are determined by the type of secret store used. - /// The Dapr client - /// The used to configure the timeout waiting for Dapr. - /// The . - public static IConfigurationBuilder AddDaprSecretStore( - this IConfigurationBuilder configurationBuilder, - string store, - DaprClient client, - TimeSpan sidecarWaitTimeout, - IReadOnlyDictionary? metadata = null) - { - ArgumentVerifier.ThrowIfNullOrEmpty(store, nameof(store)); - ArgumentVerifier.ThrowIfNull(client, nameof(client)); - - configurationBuilder.Add(new DaprSecretStoreConfigurationSource() - { - Store = store, - Metadata = metadata, - Client = client, - SidecarWaitTimeout = sidecarWaitTimeout - }); - - return configurationBuilder; - } - - /// - /// Adds an that reads configuration values from the Dapr Secret Store. - /// - /// The to add to. - /// Dapr secret store name. - /// A collection of delimiters that will be replaced by ':' in the key of every secret. - /// The Dapr client - /// The . - public static IConfigurationBuilder AddDaprSecretStore( - this IConfigurationBuilder configurationBuilder, - string store, - DaprClient client, - IEnumerable? keyDelimiters) - { - ArgumentVerifier.ThrowIfNullOrEmpty(store, nameof(store)); - ArgumentVerifier.ThrowIfNull(client, nameof(client)); - - var source = new DaprSecretStoreConfigurationSource - { - Store = store, - Client = client - }; - - if (keyDelimiters != null) - { - source.KeyDelimiters = keyDelimiters.ToList(); - } - - configurationBuilder.Add(source); - - return configurationBuilder; - } - - /// - /// Adds an that reads configuration values from the Dapr Secret Store. - /// - /// The to add to. - /// Dapr secret store name. - /// A collection of delimiters that will be replaced by ':' in the key of every secret. - /// The Dapr client - /// The used to configure the timeout waiting for Dapr. - /// The . - public static IConfigurationBuilder AddDaprSecretStore( - this IConfigurationBuilder configurationBuilder, - string store, - DaprClient client, - IEnumerable? keyDelimiters, - TimeSpan sidecarWaitTimeout) - { - ArgumentVerifier.ThrowIfNullOrEmpty(store, nameof(store)); - ArgumentVerifier.ThrowIfNull(client, nameof(client)); - - var source = new DaprSecretStoreConfigurationSource - { - Store = store, - Client = client, - SidecarWaitTimeout = sidecarWaitTimeout - }; - - if (keyDelimiters != null) - { - source.KeyDelimiters = keyDelimiters.ToList(); - } - - configurationBuilder.Add(source); - - return configurationBuilder; - } - - /// - /// Adds an that reads configuration values from the command line. - /// - /// The to add to. - /// Configures the source. - /// The . - public static IConfigurationBuilder AddDaprSecretStore(this IConfigurationBuilder configurationBuilder, Action configureSource) - => configurationBuilder.Add(configureSource); + return configurationBuilder; } -} + + /// + /// Adds an that reads configuration values from the Dapr Secret Store. + /// + /// The to add to. + /// Dapr secret store name. + /// The secrets to retrieve. + /// The Dapr client. + /// The used to configure the timeout waiting for Dapr. + /// The . + public static IConfigurationBuilder AddDaprSecretStore( + this IConfigurationBuilder configurationBuilder, + string store, + IEnumerable secretDescriptors, + DaprClient client, + TimeSpan sidecarWaitTimeout) + { + ArgumentVerifier.ThrowIfNullOrEmpty(store, nameof(store)); + ArgumentVerifier.ThrowIfNull(secretDescriptors, nameof(secretDescriptors)); + ArgumentVerifier.ThrowIfNull(client, nameof(client)); + + configurationBuilder.Add(new DaprSecretStoreConfigurationSource() + { + Store = store, + SecretDescriptors = secretDescriptors, + Client = client, + SidecarWaitTimeout = sidecarWaitTimeout + }); + + return configurationBuilder; + } + + /// + /// Adds an that reads configuration values from the Dapr Secret Store. + /// + /// The to add to. + /// Dapr secret store name. + /// A collection of metadata key-value pairs that will be provided to the secret store. The valid metadata keys and values are determined by the type of secret store used. + /// The Dapr client + /// The . + public static IConfigurationBuilder AddDaprSecretStore( + this IConfigurationBuilder configurationBuilder, + string store, + DaprClient client, + IReadOnlyDictionary? metadata = null) + { + ArgumentVerifier.ThrowIfNullOrEmpty(store, nameof(store)); + ArgumentVerifier.ThrowIfNull(client, nameof(client)); + + configurationBuilder.Add(new DaprSecretStoreConfigurationSource() + { + Store = store, + Metadata = metadata, + Client = client + }); + + return configurationBuilder; + } + + /// + /// Adds an that reads configuration values from the Dapr Secret Store. + /// + /// The to add to. + /// Dapr secret store name. + /// A collection of metadata key-value pairs that will be provided to the secret store. The valid metadata keys and values are determined by the type of secret store used. + /// The Dapr client + /// The used to configure the timeout waiting for Dapr. + /// The . + public static IConfigurationBuilder AddDaprSecretStore( + this IConfigurationBuilder configurationBuilder, + string store, + DaprClient client, + TimeSpan sidecarWaitTimeout, + IReadOnlyDictionary? metadata = null) + { + ArgumentVerifier.ThrowIfNullOrEmpty(store, nameof(store)); + ArgumentVerifier.ThrowIfNull(client, nameof(client)); + + configurationBuilder.Add(new DaprSecretStoreConfigurationSource() + { + Store = store, + Metadata = metadata, + Client = client, + SidecarWaitTimeout = sidecarWaitTimeout + }); + + return configurationBuilder; + } + + /// + /// Adds an that reads configuration values from the Dapr Secret Store. + /// + /// The to add to. + /// Dapr secret store name. + /// A collection of delimiters that will be replaced by ':' in the key of every secret. + /// The Dapr client + /// The . + public static IConfigurationBuilder AddDaprSecretStore( + this IConfigurationBuilder configurationBuilder, + string store, + DaprClient client, + IEnumerable? keyDelimiters) + { + ArgumentVerifier.ThrowIfNullOrEmpty(store, nameof(store)); + ArgumentVerifier.ThrowIfNull(client, nameof(client)); + + var source = new DaprSecretStoreConfigurationSource + { + Store = store, + Client = client + }; + + if (keyDelimiters != null) + { + source.KeyDelimiters = keyDelimiters.ToList(); + } + + configurationBuilder.Add(source); + + return configurationBuilder; + } + + /// + /// Adds an that reads configuration values from the Dapr Secret Store. + /// + /// The to add to. + /// Dapr secret store name. + /// A collection of delimiters that will be replaced by ':' in the key of every secret. + /// The Dapr client + /// The used to configure the timeout waiting for Dapr. + /// The . + public static IConfigurationBuilder AddDaprSecretStore( + this IConfigurationBuilder configurationBuilder, + string store, + DaprClient client, + IEnumerable? keyDelimiters, + TimeSpan sidecarWaitTimeout) + { + ArgumentVerifier.ThrowIfNullOrEmpty(store, nameof(store)); + ArgumentVerifier.ThrowIfNull(client, nameof(client)); + + var source = new DaprSecretStoreConfigurationSource + { + Store = store, + Client = client, + SidecarWaitTimeout = sidecarWaitTimeout + }; + + if (keyDelimiters != null) + { + source.KeyDelimiters = keyDelimiters.ToList(); + } + + configurationBuilder.Add(source); + + return configurationBuilder; + } + + /// + /// Adds an that reads configuration values from the command line. + /// + /// The to add to. + /// Configures the source. + /// The . + public static IConfigurationBuilder AddDaprSecretStore(this IConfigurationBuilder configurationBuilder, Action configureSource) + => configurationBuilder.Add(configureSource); +} \ No newline at end of file diff --git a/src/Dapr.Extensions.Configuration/DaprSecretStoreConfigurationProvider.cs b/src/Dapr.Extensions.Configuration/DaprSecretStoreConfigurationProvider.cs index ecd0ac91..b2d4dc41 100644 --- a/src/Dapr.Extensions.Configuration/DaprSecretStoreConfigurationProvider.cs +++ b/src/Dapr.Extensions.Configuration/DaprSecretStoreConfigurationProvider.cs @@ -19,245 +19,244 @@ using System.Threading.Tasks; using Dapr.Client; using Microsoft.Extensions.Configuration; -namespace Dapr.Extensions.Configuration.DaprSecretStore +namespace Dapr.Extensions.Configuration.DaprSecretStore; + +/// +/// A Dapr Secret Store based . +/// +internal class DaprSecretStoreConfigurationProvider : ConfigurationProvider { + internal static readonly TimeSpan DefaultSidecarWaitTimeout = TimeSpan.FromSeconds(5); + + private readonly string store; + + private readonly bool normalizeKey; + + private readonly IList? keyDelimiters; + + private readonly IEnumerable? secretDescriptors; + + private readonly IReadOnlyDictionary? metadata; + + private readonly DaprClient client; + + private readonly TimeSpan sidecarWaitTimeout; + /// - /// A Dapr Secret Store based . + /// Creates a new instance of . /// - internal class DaprSecretStoreConfigurationProvider : ConfigurationProvider + /// Dapr Secret Store name. + /// Indicates whether any key delimiters should be replaced with the delimiter ":". + /// The secrets to retrieve. + /// Dapr client used to retrieve Secrets + public DaprSecretStoreConfigurationProvider( + string store, + bool normalizeKey, + IEnumerable secretDescriptors, + DaprClient client) : this(store, normalizeKey, null, secretDescriptors, client, DefaultSidecarWaitTimeout) { - internal static readonly TimeSpan DefaultSidecarWaitTimeout = TimeSpan.FromSeconds(5); + } - private readonly string store; + /// + /// Creates a new instance of . + /// + /// Dapr Secret Store name. + /// Indicates whether any key delimiters should be replaced with the delimiter ":". + /// A collection of delimiters that will be replaced by ':' in the key of every secret. + /// The secrets to retrieve. + /// Dapr client used to retrieve Secrets + public DaprSecretStoreConfigurationProvider( + string store, + bool normalizeKey, + IList? keyDelimiters, + IEnumerable secretDescriptors, + DaprClient client) : this(store, normalizeKey, keyDelimiters, secretDescriptors, client, DefaultSidecarWaitTimeout) + { + } - private readonly bool normalizeKey; + /// + /// Creates a new instance of . + /// + /// Dapr Secret Store name. + /// Indicates whether any key delimiters should be replaced with the delimiter ":". + /// A collection of delimiters that will be replaced by ':' in the key of every secret. + /// The secrets to retrieve. + /// Dapr client used to retrieve Secrets + /// The used to configure the timeout waiting for Dapr. + public DaprSecretStoreConfigurationProvider( + string store, + bool normalizeKey, + IList? keyDelimiters, + IEnumerable secretDescriptors, + DaprClient client, + TimeSpan sidecarWaitTimeout) + { + ArgumentVerifier.ThrowIfNullOrEmpty(store, nameof(store)); + ArgumentVerifier.ThrowIfNull(secretDescriptors, nameof(secretDescriptors)); + ArgumentVerifier.ThrowIfNull(client, nameof(client)); - private readonly IList? keyDelimiters; - - private readonly IEnumerable? secretDescriptors; - - private readonly IReadOnlyDictionary? metadata; - - private readonly DaprClient client; - - private readonly TimeSpan sidecarWaitTimeout; - - /// - /// Creates a new instance of . - /// - /// Dapr Secret Store name. - /// Indicates whether any key delimiters should be replaced with the delimiter ":". - /// The secrets to retrieve. - /// Dapr client used to retrieve Secrets - public DaprSecretStoreConfigurationProvider( - string store, - bool normalizeKey, - IEnumerable secretDescriptors, - DaprClient client) : this(store, normalizeKey, null, secretDescriptors, client, DefaultSidecarWaitTimeout) + if (secretDescriptors.Count() == 0) { + throw new ArgumentException("No secret descriptor was provided", nameof(secretDescriptors)); } - /// - /// Creates a new instance of . - /// - /// Dapr Secret Store name. - /// Indicates whether any key delimiters should be replaced with the delimiter ":". - /// A collection of delimiters that will be replaced by ':' in the key of every secret. - /// The secrets to retrieve. - /// Dapr client used to retrieve Secrets - public DaprSecretStoreConfigurationProvider( - string store, - bool normalizeKey, - IList? keyDelimiters, - IEnumerable secretDescriptors, - DaprClient client) : this(store, normalizeKey, keyDelimiters, secretDescriptors, client, DefaultSidecarWaitTimeout) - { - } + this.store = store; + this.normalizeKey = normalizeKey; + this.keyDelimiters = keyDelimiters; + this.secretDescriptors = secretDescriptors; + this.client = client; + this.sidecarWaitTimeout = sidecarWaitTimeout; + } - /// - /// Creates a new instance of . - /// - /// Dapr Secret Store name. - /// Indicates whether any key delimiters should be replaced with the delimiter ":". - /// A collection of delimiters that will be replaced by ':' in the key of every secret. - /// The secrets to retrieve. - /// Dapr client used to retrieve Secrets - /// The used to configure the timeout waiting for Dapr. - public DaprSecretStoreConfigurationProvider( - string store, - bool normalizeKey, - IList? keyDelimiters, - IEnumerable secretDescriptors, - DaprClient client, - TimeSpan sidecarWaitTimeout) - { - ArgumentVerifier.ThrowIfNullOrEmpty(store, nameof(store)); - ArgumentVerifier.ThrowIfNull(secretDescriptors, nameof(secretDescriptors)); - ArgumentVerifier.ThrowIfNull(client, nameof(client)); + /// + /// Creates a new instance of . + /// + /// Dapr Secret Store name. + /// Indicates whether any key delimiters should be replaced with the delimiter ":". + /// A collection of metadata key-value pairs that will be provided to the secret store. The valid metadata keys and values are determined by the type of secret store used. + /// Dapr client used to retrieve Secrets + public DaprSecretStoreConfigurationProvider( + string store, + bool normalizeKey, + IReadOnlyDictionary? metadata, + DaprClient client) : this(store, normalizeKey, null, metadata, client, DefaultSidecarWaitTimeout) + { + } - if (secretDescriptors.Count() == 0) + /// + /// Creates a new instance of . + /// + /// Dapr Secret Store name. + /// Indicates whether any key delimiters should be replaced with the delimiter ":". + /// A collection of delimiters that will be replaced by ':' in the key of every secret. + /// A collection of metadata key-value pairs that will be provided to the secret store. The valid metadata keys and values are determined by the type of secret store used. + /// Dapr client used to retrieve Secrets + public DaprSecretStoreConfigurationProvider( + string store, + bool normalizeKey, + IList? keyDelimiters, + IReadOnlyDictionary? metadata, + DaprClient client) : this(store, normalizeKey, keyDelimiters, metadata, client, DefaultSidecarWaitTimeout) + { + } + + /// + /// Creates a new instance of . + /// + /// Dapr Secret Store name. + /// Indicates whether any key delimiters should be replaced with the delimiter ":". + /// A collection of delimiters that will be replaced by ':' in the key of every secret. + /// A collection of metadata key-value pairs that will be provided to the secret store. The valid metadata keys and values are determined by the type of secret store used. + /// Dapr client used to retrieve Secrets + /// The used to configure the timeout waiting for Dapr. + public DaprSecretStoreConfigurationProvider( + string store, + bool normalizeKey, + IList? keyDelimiters, + IReadOnlyDictionary? metadata, + DaprClient client, + TimeSpan sidecarWaitTimeout) + { + ArgumentVerifier.ThrowIfNullOrEmpty(store, nameof(store)); + ArgumentVerifier.ThrowIfNull(client, nameof(client)); + + this.store = store; + this.normalizeKey = normalizeKey; + this.keyDelimiters = keyDelimiters; + this.metadata = metadata; + this.client = client; + this.sidecarWaitTimeout = sidecarWaitTimeout; + } + + private string NormalizeKey(string key) + { + if (this.keyDelimiters?.Count > 0) + { + foreach (var keyDelimiter in this.keyDelimiters) { - throw new ArgumentException("No secret descriptor was provided", nameof(secretDescriptors)); + key = key.Replace(keyDelimiter, ConfigurationPath.KeyDelimiter); } - - this.store = store; - this.normalizeKey = normalizeKey; - this.keyDelimiters = keyDelimiters; - this.secretDescriptors = secretDescriptors; - this.client = client; - this.sidecarWaitTimeout = sidecarWaitTimeout; } - /// - /// Creates a new instance of . - /// - /// Dapr Secret Store name. - /// Indicates whether any key delimiters should be replaced with the delimiter ":". - /// A collection of metadata key-value pairs that will be provided to the secret store. The valid metadata keys and values are determined by the type of secret store used. - /// Dapr client used to retrieve Secrets - public DaprSecretStoreConfigurationProvider( - string store, - bool normalizeKey, - IReadOnlyDictionary? metadata, - DaprClient client) : this(store, normalizeKey, null, metadata, client, DefaultSidecarWaitTimeout) + return key; + } + + /// + /// Loads the configuration by calling the asynchronous LoadAsync method and blocking the calling + /// thread until the operation is completed. + /// + public override void Load() => LoadAsync().ConfigureAwait(false).GetAwaiter().GetResult(); + + private async Task LoadAsync() + { + var data = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + + // Wait for the Dapr Sidecar to report healthy before attempting to fetch secrets. + using (var tokenSource = new CancellationTokenSource(sidecarWaitTimeout)) { + await client.WaitForSidecarAsync(tokenSource.Token); } - /// - /// Creates a new instance of . - /// - /// Dapr Secret Store name. - /// Indicates whether any key delimiters should be replaced with the delimiter ":". - /// A collection of delimiters that will be replaced by ':' in the key of every secret. - /// A collection of metadata key-value pairs that will be provided to the secret store. The valid metadata keys and values are determined by the type of secret store used. - /// Dapr client used to retrieve Secrets - public DaprSecretStoreConfigurationProvider( - string store, - bool normalizeKey, - IList? keyDelimiters, - IReadOnlyDictionary? metadata, - DaprClient client) : this(store, normalizeKey, keyDelimiters, metadata, client, DefaultSidecarWaitTimeout) + if (secretDescriptors != null) { - } - - /// - /// Creates a new instance of . - /// - /// Dapr Secret Store name. - /// Indicates whether any key delimiters should be replaced with the delimiter ":". - /// A collection of delimiters that will be replaced by ':' in the key of every secret. - /// A collection of metadata key-value pairs that will be provided to the secret store. The valid metadata keys and values are determined by the type of secret store used. - /// Dapr client used to retrieve Secrets - /// The used to configure the timeout waiting for Dapr. - public DaprSecretStoreConfigurationProvider( - string store, - bool normalizeKey, - IList? keyDelimiters, - IReadOnlyDictionary? metadata, - DaprClient client, - TimeSpan sidecarWaitTimeout) - { - ArgumentVerifier.ThrowIfNullOrEmpty(store, nameof(store)); - ArgumentVerifier.ThrowIfNull(client, nameof(client)); - - this.store = store; - this.normalizeKey = normalizeKey; - this.keyDelimiters = keyDelimiters; - this.metadata = metadata; - this.client = client; - this.sidecarWaitTimeout = sidecarWaitTimeout; - } - - private string NormalizeKey(string key) - { - if (this.keyDelimiters?.Count > 0) + foreach (var secretDescriptor in secretDescriptors) { - foreach (var keyDelimiter in this.keyDelimiters) + + Dictionary result; + + try { - key = key.Replace(keyDelimiter, ConfigurationPath.KeyDelimiter); + result = await client + .GetSecretAsync(store, secretDescriptor.SecretKey, secretDescriptor.Metadata) + .ConfigureAwait(false); } - } - - return key; - } - - /// - /// Loads the configuration by calling the asynchronous LoadAsync method and blocking the calling - /// thread until the operation is completed. - /// - public override void Load() => LoadAsync().ConfigureAwait(false).GetAwaiter().GetResult(); - - private async Task LoadAsync() - { - var data = new Dictionary(StringComparer.InvariantCultureIgnoreCase); - - // Wait for the Dapr Sidecar to report healthy before attempting to fetch secrets. - using (var tokenSource = new CancellationTokenSource(sidecarWaitTimeout)) - { - await client.WaitForSidecarAsync(tokenSource.Token); - } - - if (secretDescriptors != null) - { - foreach (var secretDescriptor in secretDescriptors) + catch (DaprException) { - - Dictionary result; - - try + if (secretDescriptor.IsRequired) { - result = await client - .GetSecretAsync(store, secretDescriptor.SecretKey, secretDescriptor.Metadata) - .ConfigureAwait(false); - } - catch (DaprException) - { - if (secretDescriptor.IsRequired) - { - throw; - } - result = new Dictionary(); - } - - foreach (var key in result.Keys) - { - if (data.ContainsKey(key)) - { - throw new InvalidOperationException( - $"A duplicate key '{key}' was found in the secret store '{store}'. Please remove any duplicates from your secret store."); - } - - // The name of the key "as desired" by the user based on the descriptor. - // - // NOTE: This should vary only if a single secret of the same name is returned. - string desiredKey = StringComparer.Ordinal.Equals(key, secretDescriptor.SecretKey) ? secretDescriptor.SecretName : key; - - // The name of the key normalized based on the configured delimiters. - string normalizedKey = normalizeKey ? NormalizeKey(desiredKey) : desiredKey; - - data.Add(normalizedKey, result[key]); + throw; } + result = new Dictionary(); } - Data = data; - } - else - { - var result = await client.GetBulkSecretAsync(store, metadata).ConfigureAwait(false); foreach (var key in result.Keys) { - foreach (var secret in result[key]) + if (data.ContainsKey(key)) { - if (data.ContainsKey(secret.Key)) - { - throw new InvalidOperationException($"A duplicate key '{secret.Key}' was found in the secret store '{store}'. Please remove any duplicates from your secret store."); - } - - data.Add(normalizeKey ? NormalizeKey(secret.Key) : secret.Key, secret.Value); + throw new InvalidOperationException( + $"A duplicate key '{key}' was found in the secret store '{store}'. Please remove any duplicates from your secret store."); } + + // The name of the key "as desired" by the user based on the descriptor. + // + // NOTE: This should vary only if a single secret of the same name is returned. + string desiredKey = StringComparer.Ordinal.Equals(key, secretDescriptor.SecretKey) ? secretDescriptor.SecretName : key; + + // The name of the key normalized based on the configured delimiters. + string normalizedKey = normalizeKey ? NormalizeKey(desiredKey) : desiredKey; + + data.Add(normalizedKey, result[key]); } - Data = data; } + + Data = data; + } + else + { + var result = await client.GetBulkSecretAsync(store, metadata).ConfigureAwait(false); + foreach (var key in result.Keys) + { + foreach (var secret in result[key]) + { + if (data.ContainsKey(secret.Key)) + { + throw new InvalidOperationException($"A duplicate key '{secret.Key}' was found in the secret store '{store}'. Please remove any duplicates from your secret store."); + } + + data.Add(normalizeKey ? NormalizeKey(secret.Key) : secret.Key, secret.Value); + } + } + Data = data; } } -} +} \ No newline at end of file diff --git a/src/Dapr.Extensions.Configuration/DaprSecretStoreConfigurationSource.cs b/src/Dapr.Extensions.Configuration/DaprSecretStoreConfigurationSource.cs index eee3d0b2..de73c851 100644 --- a/src/Dapr.Extensions.Configuration/DaprSecretStoreConfigurationSource.cs +++ b/src/Dapr.Extensions.Configuration/DaprSecretStoreConfigurationSource.cs @@ -16,65 +16,64 @@ using System.Collections.Generic; using Dapr.Client; using Microsoft.Extensions.Configuration; -namespace Dapr.Extensions.Configuration.DaprSecretStore +namespace Dapr.Extensions.Configuration.DaprSecretStore; + +/// +/// Represents Dapr Secret Store as an . +/// +public class DaprSecretStoreConfigurationSource : IConfigurationSource { /// - /// Represents Dapr Secret Store as an . + /// Gets or sets the store name. /// - public class DaprSecretStoreConfigurationSource : IConfigurationSource + public string Store { get; set; } = default!; + + /// + /// Gets or sets a value indicating whether any key delimiters should be replaced with the delimiter ":". + /// Default value true. + /// + public bool NormalizeKey { get; set; } = true; + + /// + /// Gets or sets the custom key delimiters. Contains the '__' delimiter by default. + /// + public IList KeyDelimiters { get; set; } = new List { "__" }; + + /// + /// Gets or sets the secret descriptors. + /// + public IEnumerable? SecretDescriptors { get; set; } + + /// + /// A collection of metadata key-value pairs that will be provided to the secret store. The valid metadata keys and values are determined by the type of secret store used. + /// + public IReadOnlyDictionary? Metadata { get; set; } + + /// + /// Gets or sets the http client. + /// + public DaprClient Client { get; set; } = default!; + + /// + /// Gets or sets the that is used to control the timeout waiting for the Dapr sidecar to become healthly. + /// + public TimeSpan? SidecarWaitTimeout { get; set; } + + /// + public IConfigurationProvider Build(IConfigurationBuilder builder) { - /// - /// Gets or sets the store name. - /// - public string Store { get; set; } = default!; - - /// - /// Gets or sets a value indicating whether any key delimiters should be replaced with the delimiter ":". - /// Default value true. - /// - public bool NormalizeKey { get; set; } = true; - - /// - /// Gets or sets the custom key delimiters. Contains the '__' delimiter by default. - /// - public IList KeyDelimiters { get; set; } = new List { "__" }; - - /// - /// Gets or sets the secret descriptors. - /// - public IEnumerable? SecretDescriptors { get; set; } - - /// - /// A collection of metadata key-value pairs that will be provided to the secret store. The valid metadata keys and values are determined by the type of secret store used. - /// - public IReadOnlyDictionary? Metadata { get; set; } - - /// - /// Gets or sets the http client. - /// - public DaprClient Client { get; set; } = default!; - - /// - /// Gets or sets the that is used to control the timeout waiting for the Dapr sidecar to become healthly. - /// - public TimeSpan? SidecarWaitTimeout { get; set; } - - /// - public IConfigurationProvider Build(IConfigurationBuilder builder) + if (SecretDescriptors != null) { - if (SecretDescriptors != null) + if (Metadata != null) { - if (Metadata != null) - { - throw new ArgumentException($"{nameof(Metadata)} must be null when {nameof(SecretDescriptors)} is set", nameof(Metadata)); - } + throw new ArgumentException($"{nameof(Metadata)} must be null when {nameof(SecretDescriptors)} is set", nameof(Metadata)); + } - return new DaprSecretStoreConfigurationProvider(Store, NormalizeKey, KeyDelimiters, SecretDescriptors, Client, SidecarWaitTimeout ?? DaprSecretStoreConfigurationProvider.DefaultSidecarWaitTimeout); - } - else - { - return new DaprSecretStoreConfigurationProvider(Store, NormalizeKey, KeyDelimiters, Metadata, Client, SidecarWaitTimeout ?? DaprSecretStoreConfigurationProvider.DefaultSidecarWaitTimeout); - } + return new DaprSecretStoreConfigurationProvider(Store, NormalizeKey, KeyDelimiters, SecretDescriptors, Client, SidecarWaitTimeout ?? DaprSecretStoreConfigurationProvider.DefaultSidecarWaitTimeout); + } + else + { + return new DaprSecretStoreConfigurationProvider(Store, NormalizeKey, KeyDelimiters, Metadata, Client, SidecarWaitTimeout ?? DaprSecretStoreConfigurationProvider.DefaultSidecarWaitTimeout); } } -} +} \ No newline at end of file diff --git a/src/Dapr.Jobs/DaprJobsClientBuilder.cs b/src/Dapr.Jobs/DaprJobsClientBuilder.cs index 509486a1..12a66a89 100644 --- a/src/Dapr.Jobs/DaprJobsClientBuilder.cs +++ b/src/Dapr.Jobs/DaprJobsClientBuilder.cs @@ -20,16 +20,9 @@ namespace Dapr.Jobs; /// /// Builds a . /// -public sealed class DaprJobsClientBuilder : DaprGenericClientBuilder +/// An optional instance of . +public sealed class DaprJobsClientBuilder(IConfiguration? configuration = null) : DaprGenericClientBuilder(configuration) { - /// - /// 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. /// diff --git a/src/Dapr.Jobs/Extensions/DaprSerializationExtensions.cs b/src/Dapr.Jobs/Extensions/DaprSerializationExtensions.cs index 1f02a32c..0a612e3a 100644 --- a/src/Dapr.Jobs/Extensions/DaprSerializationExtensions.cs +++ b/src/Dapr.Jobs/Extensions/DaprSerializationExtensions.cs @@ -25,7 +25,7 @@ public static class DaprJobsSerializationExtensions /// /// Default JSON serializer options. /// - private static readonly JsonSerializerOptions defaultOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web); + private static readonly JsonSerializerOptions defaultOptions = new(JsonSerializerDefaults.Web); /// /// Schedules a job with Dapr. diff --git a/src/Dapr.Jobs/JsonConverters/Iso8601DateTimeJsonConverter.cs b/src/Dapr.Jobs/JsonConverters/Iso8601DateTimeJsonConverter.cs index 90ffd3d4..d0198d8f 100644 --- a/src/Dapr.Jobs/JsonConverters/Iso8601DateTimeJsonConverter.cs +++ b/src/Dapr.Jobs/JsonConverters/Iso8601DateTimeJsonConverter.cs @@ -17,8 +17,7 @@ using System.Text.Json; namespace Dapr.Jobs.JsonConverters; /// -/// Converts from an ISO 8601 DateTime to a string and back. This is primarily used to serialize -/// dates for use with CosmosDB. +/// Converts from an ISO 8601 DateTime to a string and back. /// public sealed class Iso8601DateTimeJsonConverter : JsonConverter { diff --git a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClientBuilder.cs b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClientBuilder.cs index 829bab75..691ff9d3 100644 --- a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClientBuilder.cs +++ b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClientBuilder.cs @@ -20,16 +20,9 @@ namespace Dapr.Messaging.PublishSubscribe; /// /// Builds a . /// -public sealed class DaprPublishSubscribeClientBuilder : DaprGenericClientBuilder +/// An optional instance of . +public sealed class DaprPublishSubscribeClientBuilder(IConfiguration? configuration = null) : DaprGenericClientBuilder(configuration) { - /// - /// Used to initialize a new instance of the . - /// - /// An optional instance of . - public DaprPublishSubscribeClientBuilder(IConfiguration? configuration = null) : base(configuration) - { - } - /// /// Builds the client instance from the properties of the builder. /// diff --git a/src/Dapr.Workflow/DaprWorkflowActivityContext.cs b/src/Dapr.Workflow/DaprWorkflowActivityContext.cs index cf902dea..27aab253 100644 --- a/src/Dapr.Workflow/DaprWorkflowActivityContext.cs +++ b/src/Dapr.Workflow/DaprWorkflowActivityContext.cs @@ -11,27 +11,26 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Workflow +namespace Dapr.Workflow; + +using System; +using Microsoft.DurableTask; + +/// +/// Defines properties and methods for task activity context objects. +/// +public class DaprWorkflowActivityContext : WorkflowActivityContext { - using System; - using Microsoft.DurableTask; + readonly TaskActivityContext innerContext; - /// - /// Defines properties and methods for task activity context objects. - /// - public class DaprWorkflowActivityContext : WorkflowActivityContext + internal DaprWorkflowActivityContext(TaskActivityContext innerContext) { - readonly TaskActivityContext innerContext; - - internal DaprWorkflowActivityContext(TaskActivityContext innerContext) - { - this.innerContext = innerContext ?? throw new ArgumentNullException(nameof(innerContext)); - } - - /// - public override TaskName Name => this.innerContext.Name; - - /// - public override string InstanceId => this.innerContext.InstanceId; + this.innerContext = innerContext ?? throw new ArgumentNullException(nameof(innerContext)); } + + /// + public override TaskName Name => this.innerContext.Name; + + /// + public override string InstanceId => this.innerContext.InstanceId; } diff --git a/src/Dapr.Workflow/DaprWorkflowClient.cs b/src/Dapr.Workflow/DaprWorkflowClient.cs index e4c88f0e..add9bbac 100644 --- a/src/Dapr.Workflow/DaprWorkflowClient.cs +++ b/src/Dapr.Workflow/DaprWorkflowClient.cs @@ -13,291 +13,281 @@ using System; using System.Threading; using System.Threading.Tasks; -using Dapr.Client; using Microsoft.DurableTask; using Microsoft.DurableTask.Client; -namespace Dapr.Workflow +namespace Dapr.Workflow; + +/// +/// Defines client operations for managing Dapr Workflow instances. +/// +/// +/// This is an alternative to the general purpose Dapr client. It uses a gRPC connection to send +/// commands directly to the workflow engine, bypassing the Dapr API layer. +/// +/// The Durable Task client used to communicate with the Dapr sidecar. +/// Thrown if is null. +public class DaprWorkflowClient(DurableTaskClient innerClient) : IAsyncDisposable { + readonly DurableTaskClient innerClient = innerClient ?? throw new ArgumentNullException(nameof(innerClient)); + /// - /// Defines client operations for managing Dapr Workflow instances. + /// Schedules a new workflow instance for execution. + /// + /// The name of the workflow to schedule. + /// + /// The unique ID of the workflow instance to schedule. If not specified, a new GUID value is used. + /// + /// + /// The time when the workflow instance should start executing. If not specified or if a date-time in the past + /// is specified, the workflow instance will be scheduled immediately. + /// + /// + /// The optional input to pass to the scheduled workflow instance. This must be a serializable value. + /// + public Task ScheduleNewWorkflowAsync( + string name, + string? instanceId = null, + object? input = null, + DateTime? startTime = null) + { + StartOrchestrationOptions options = new(instanceId, startTime); + return this.innerClient.ScheduleNewOrchestrationInstanceAsync(name, input, options); + } + + /// + /// Fetches runtime state for the specified workflow instance. + /// + /// The unique ID of the workflow instance to fetch. + /// + /// Specify true to fetch the workflow instance's inputs, outputs, and custom status, or false to + /// omit them. Defaults to true. + /// + public async Task GetWorkflowStateAsync(string instanceId, bool getInputsAndOutputs = true) + { + OrchestrationMetadata? metadata = await this.innerClient.GetInstancesAsync( + instanceId, + getInputsAndOutputs); + return new WorkflowState(metadata); + } + + /// + /// Waits for a workflow to start running and returns a object that contains metadata + /// about the started workflow. /// /// - /// This is an alternative to the general purpose Dapr client. It uses a gRPC connection to send - /// commands directly to the workflow engine, bypassing the Dapr API layer. + /// + /// A "started" workflow instance is any instance not in the state. + /// + /// This method will return a completed task if the workflow has already started running or has already completed. + /// /// - public class DaprWorkflowClient : IAsyncDisposable + /// The unique ID of the workflow instance to wait for. + /// + /// Specify true to fetch the workflow instance's inputs, outputs, and custom status, or false to + /// omit them. Setting this value to false can help minimize the network bandwidth, serialization, and memory costs + /// associated with fetching the instance metadata. + /// + /// A that can be used to cancel the wait operation. + /// + /// Returns a record that describes the workflow instance and its execution + /// status. If the specified workflow isn't found, the value will be false. + /// + public async Task WaitForWorkflowStartAsync( + string instanceId, + bool getInputsAndOutputs = true, + CancellationToken cancellation = default) { - readonly DurableTaskClient innerClient; + OrchestrationMetadata metadata = await this.innerClient.WaitForInstanceStartAsync( + instanceId, + getInputsAndOutputs, + cancellation); + return new WorkflowState(metadata); + } - /// - /// Initializes a new instance of the class. - /// - /// The Durable Task client used to communicate with the Dapr sidecar. - /// Thrown if is null. - public DaprWorkflowClient(DurableTaskClient innerClient) - { - this.innerClient = innerClient ?? throw new ArgumentNullException(nameof(innerClient)); - } + /// + /// Waits for a workflow to complete and returns a + /// object that contains metadata about the started instance. + /// + /// + /// + /// A "completed" workflow instance is any instance in one of the terminal states. For example, the + /// , , or + /// states. + /// + /// Workflows are long-running and could take hours, days, or months before completing. + /// Workflows can also be eternal, in which case they'll never complete unless terminated. + /// In such cases, this call may block indefinitely, so care must be taken to ensure appropriate timeouts are + /// enforced using the parameter. + /// + /// If a workflow instance is already complete when this method is called, the method will return immediately. + /// + /// + /// + public async Task WaitForWorkflowCompletionAsync( + string instanceId, + bool getInputsAndOutputs = true, + CancellationToken cancellation = default) + { + OrchestrationMetadata metadata = await this.innerClient.WaitForInstanceCompletionAsync( + instanceId, + getInputsAndOutputs, + cancellation); + return new WorkflowState(metadata); + } - /// - /// Schedules a new workflow instance for execution. - /// - /// The name of the workflow to schedule. - /// - /// The unique ID of the workflow instance to schedule. If not specified, a new GUID value is used. - /// - /// - /// The time when the workflow instance should start executing. If not specified or if a date-time in the past - /// is specified, the workflow instance will be scheduled immediately. - /// - /// - /// The optional input to pass to the scheduled workflow instance. This must be a serializable value. - /// - public Task ScheduleNewWorkflowAsync( - string name, - string? instanceId = null, - object? input = null, - DateTime? startTime = null) - { - StartOrchestrationOptions options = new(instanceId, startTime); - return this.innerClient.ScheduleNewOrchestrationInstanceAsync(name, input, options); - } + /// + /// Terminates a running workflow instance and updates its runtime status to + /// . + /// + /// + /// + /// This method internally enqueues a "terminate" message in the task hub. When the task hub worker processes + /// this message, it will update the runtime status of the target instance to + /// . You can use the + /// to wait for the instance to reach + /// the terminated state. + /// + /// + /// Terminating a workflow terminates all of the child workflow instances that were created by the target. But it + /// has no effect on any in-flight activity function executions + /// that were started by the terminated instance. Those actions will continue to run + /// without interruption. However, their results will be discarded. + /// + /// At the time of writing, there is no way to terminate an in-flight activity execution. + /// + /// + /// The ID of the workflow instance to terminate. + /// The optional output to set for the terminated workflow instance. + /// + /// The cancellation token. This only cancels enqueueing the termination request to the backend. Does not abort + /// termination of the workflow once enqueued. + /// + /// A task that completes when the terminate message is enqueued. + public Task TerminateWorkflowAsync( + string instanceId, + string? output = null, + CancellationToken cancellation = default) + { + TerminateInstanceOptions options = new TerminateInstanceOptions { + Output = output, + Recursive = true, + }; + return this.innerClient.TerminateInstanceAsync(instanceId, options, cancellation); + } - /// - /// Fetches runtime state for the specified workflow instance. - /// - /// The unique ID of the workflow instance to fetch. - /// - /// Specify true to fetch the workflow instance's inputs, outputs, and custom status, or false to - /// omit them. Defaults to true. - /// - public async Task GetWorkflowStateAsync(string instanceId, bool getInputsAndOutputs = true) - { - OrchestrationMetadata? metadata = await this.innerClient.GetInstancesAsync( - instanceId, - getInputsAndOutputs); - return new WorkflowState(metadata); - } + /// + /// Sends an event notification message to a waiting workflow instance. + /// + /// + /// + /// In order to handle the event, the target workflow instance must be waiting for an + /// event named using the + /// API. + /// If the target workflow instance is not yet waiting for an event named , + /// then the event will be saved in the workflow instance state and dispatched immediately when the + /// workflow calls . + /// This event saving occurs even if the workflow has canceled its wait operation before the event was received. + /// + /// Workflows can wait for the same event name multiple times, so sending multiple events with the same name is + /// allowed. Each external event received by an workflow will complete just one task returned by the + /// method. + /// + /// Raised events for a completed or non-existent workflow instance will be silently discarded. + /// + /// + /// The ID of the workflow instance that will handle the event. + /// The name of the event. Event names are case-insensitive. + /// The serializable data payload to include with the event. + /// + /// The cancellation token. This only cancels enqueueing the event to the backend. Does not abort sending the event + /// once enqueued. + /// + /// A task that completes when the event notification message has been enqueued. + /// + /// Thrown if or is null or empty. + /// + public Task RaiseEventAsync( + string instanceId, + string eventName, + object? eventPayload = null, + CancellationToken cancellation = default) + { + return this.innerClient.RaiseEventAsync(instanceId, eventName, eventPayload, cancellation); + } - /// - /// Waits for a workflow to start running and returns a object that contains metadata - /// about the started workflow. - /// - /// - /// - /// A "started" workflow instance is any instance not in the state. - /// - /// This method will return a completed task if the workflow has already started running or has already completed. - /// - /// - /// The unique ID of the workflow instance to wait for. - /// - /// Specify true to fetch the workflow instance's inputs, outputs, and custom status, or false to - /// omit them. Setting this value to false can help minimize the network bandwidth, serialization, and memory costs - /// associated with fetching the instance metadata. - /// - /// A that can be used to cancel the wait operation. - /// - /// Returns a record that describes the workflow instance and its execution - /// status. If the specified workflow isn't found, the value will be false. - /// - public async Task WaitForWorkflowStartAsync( - string instanceId, - bool getInputsAndOutputs = true, - CancellationToken cancellation = default) - { - OrchestrationMetadata metadata = await this.innerClient.WaitForInstanceStartAsync( - instanceId, - getInputsAndOutputs, - cancellation); - return new WorkflowState(metadata); - } + /// + /// Suspends a workflow instance, halting processing of it until + /// is used to resume the workflow. + /// + /// The instance ID of the workflow to suspend. + /// The optional suspension reason. + /// + /// A that can be used to cancel the suspend operation. Note, cancelling this token + /// does not resume the workflow if suspend was successful. + /// + /// A task that completes when the suspend has been committed to the backend. + public Task SuspendWorkflowAsync( + string instanceId, + string? reason = null, + CancellationToken cancellation = default) + { + return this.innerClient.SuspendInstanceAsync(instanceId, reason, cancellation); + } - /// - /// Waits for a workflow to complete and returns a - /// object that contains metadata about the started instance. - /// - /// - /// - /// A "completed" workflow instance is any instance in one of the terminal states. For example, the - /// , , or - /// states. - /// - /// Workflows are long-running and could take hours, days, or months before completing. - /// Workflows can also be eternal, in which case they'll never complete unless terminated. - /// In such cases, this call may block indefinitely, so care must be taken to ensure appropriate timeouts are - /// enforced using the parameter. - /// - /// If a workflow instance is already complete when this method is called, the method will return immediately. - /// - /// - /// - public async Task WaitForWorkflowCompletionAsync( - string instanceId, - bool getInputsAndOutputs = true, - CancellationToken cancellation = default) - { - OrchestrationMetadata metadata = await this.innerClient.WaitForInstanceCompletionAsync( - instanceId, - getInputsAndOutputs, - cancellation); - return new WorkflowState(metadata); - } + /// + /// Resumes a workflow instance that was suspended via . + /// + /// The instance ID of the workflow to resume. + /// The optional resume reason. + /// + /// A that can be used to cancel the resume operation. Note, cancelling this token + /// does not re-suspend the workflow if resume was successful. + /// + /// A task that completes when the resume has been committed to the backend. + public Task ResumeWorkflowAsync( + string instanceId, + string? reason = null, + CancellationToken cancellation = default) + { + return this.innerClient.ResumeInstanceAsync(instanceId, reason, cancellation); + } - /// - /// Terminates a running workflow instance and updates its runtime status to - /// . - /// - /// - /// - /// This method internally enqueues a "terminate" message in the task hub. When the task hub worker processes - /// this message, it will update the runtime status of the target instance to - /// . You can use the - /// to wait for the instance to reach - /// the terminated state. - /// - /// - /// Terminating a workflow terminates all of the child workflow instances that were created by the target. But it - /// has no effect on any in-flight activity function executions - /// that were started by the terminated instance. Those actions will continue to run - /// without interruption. However, their results will be discarded. - /// - /// At the time of writing, there is no way to terminate an in-flight activity execution. - /// - /// - /// The ID of the workflow instance to terminate. - /// The optional output to set for the terminated workflow instance. - /// - /// The cancellation token. This only cancels enqueueing the termination request to the backend. Does not abort - /// termination of the workflow once enqueued. - /// - /// A task that completes when the terminate message is enqueued. - public Task TerminateWorkflowAsync( - string instanceId, - string? output = null, - CancellationToken cancellation = default) - { - TerminateInstanceOptions options = new TerminateInstanceOptions { - Output = output, - Recursive = true, - }; - return this.innerClient.TerminateInstanceAsync(instanceId, options, cancellation); - } + /// + /// Purges workflow instance state from the workflow state store. + /// + /// + /// + /// This method can be used to permanently delete workflow metadata from the underlying state store, + /// including any stored inputs, outputs, and workflow history records. This is often useful for implementing + /// data retention policies and for keeping storage costs minimal. Only workflow instances in the + /// , , or + /// state can be purged. + /// + /// + /// Purging a workflow purges all of the child workflows that were created by the target. + /// + /// + /// The unique ID of the workflow instance to purge. + /// + /// A that can be used to cancel the purge operation. + /// + /// + /// Returns a task that completes when the purge operation has completed. The value of this task will be + /// true if the workflow state was found and purged successfully; false otherwise. + /// + public async Task PurgeInstanceAsync(string instanceId, CancellationToken cancellation = default) + { + PurgeInstanceOptions options = new PurgeInstanceOptions {Recursive = true}; + PurgeResult result = await this.innerClient.PurgeInstanceAsync(instanceId, options, cancellation); + return result.PurgedInstanceCount > 0; + } - /// - /// Sends an event notification message to a waiting workflow instance. - /// - /// - /// - /// In order to handle the event, the target workflow instance must be waiting for an - /// event named using the - /// API. - /// If the target workflow instance is not yet waiting for an event named , - /// then the event will be saved in the workflow instance state and dispatched immediately when the - /// workflow calls . - /// This event saving occurs even if the workflow has canceled its wait operation before the event was received. - /// - /// Workflows can wait for the same event name multiple times, so sending multiple events with the same name is - /// allowed. Each external event received by an workflow will complete just one task returned by the - /// method. - /// - /// Raised events for a completed or non-existent workflow instance will be silently discarded. - /// - /// - /// The ID of the workflow instance that will handle the event. - /// The name of the event. Event names are case-insensitive. - /// The serializable data payload to include with the event. - /// - /// The cancellation token. This only cancels enqueueing the event to the backend. Does not abort sending the event - /// once enqueued. - /// - /// A task that completes when the event notification message has been enqueued. - /// - /// Thrown if or is null or empty. - /// - public Task RaiseEventAsync( - string instanceId, - string eventName, - object? eventPayload = null, - CancellationToken cancellation = default) - { - return this.innerClient.RaiseEventAsync(instanceId, eventName, eventPayload, cancellation); - } - - /// - /// Suspends a workflow instance, halting processing of it until - /// is used to resume the workflow. - /// - /// The instance ID of the workflow to suspend. - /// The optional suspension reason. - /// - /// A that can be used to cancel the suspend operation. Note, cancelling this token - /// does not resume the workflow if suspend was successful. - /// - /// A task that completes when the suspend has been committed to the backend. - public Task SuspendWorkflowAsync( - string instanceId, - string? reason = null, - CancellationToken cancellation = default) - { - return this.innerClient.SuspendInstanceAsync(instanceId, reason, cancellation); - } - - /// - /// Resumes a workflow instance that was suspended via . - /// - /// The instance ID of the workflow to resume. - /// The optional resume reason. - /// - /// A that can be used to cancel the resume operation. Note, cancelling this token - /// does not re-suspend the workflow if resume was successful. - /// - /// A task that completes when the resume has been committed to the backend. - public Task ResumeWorkflowAsync( - string instanceId, - string? reason = null, - CancellationToken cancellation = default) - { - return this.innerClient.ResumeInstanceAsync(instanceId, reason, cancellation); - } - - /// - /// Purges workflow instance state from the workflow state store. - /// - /// - /// - /// This method can be used to permanently delete workflow metadata from the underlying state store, - /// including any stored inputs, outputs, and workflow history records. This is often useful for implementing - /// data retention policies and for keeping storage costs minimal. Only workflow instances in the - /// , , or - /// state can be purged. - /// - /// - /// Purging a workflow purges all of the child workflows that were created by the target. - /// - /// - /// The unique ID of the workflow instance to purge. - /// - /// A that can be used to cancel the purge operation. - /// - /// - /// Returns a task that completes when the purge operation has completed. The value of this task will be - /// true if the workflow state was found and purged successfully; false otherwise. - /// - public async Task PurgeInstanceAsync(string instanceId, CancellationToken cancellation = default) - { - PurgeInstanceOptions options = new PurgeInstanceOptions {Recursive = true}; - PurgeResult result = await this.innerClient.PurgeInstanceAsync(instanceId, options, cancellation); - return result.PurgedInstanceCount > 0; - } - - /// - /// Disposes any unmanaged resources associated with this client. - /// - public ValueTask DisposeAsync() - { - return ((IAsyncDisposable)this.innerClient).DisposeAsync(); - } + /// + /// Disposes any unmanaged resources associated with this client. + /// + public ValueTask DisposeAsync() + { + return ((IAsyncDisposable)this.innerClient).DisposeAsync(); } } diff --git a/src/Dapr.Workflow/DaprWorkflowClientBuilderFactory.cs b/src/Dapr.Workflow/DaprWorkflowClientBuilderFactory.cs index 760dadd8..32d70907 100644 --- a/src/Dapr.Workflow/DaprWorkflowClientBuilderFactory.cs +++ b/src/Dapr.Workflow/DaprWorkflowClientBuilderFactory.cs @@ -24,20 +24,8 @@ namespace Dapr.Workflow; /// /// A factory for building a . /// -internal sealed class DaprWorkflowClientBuilderFactory +internal sealed class DaprWorkflowClientBuilderFactory(IConfiguration? configuration, IHttpClientFactory httpClientFactory) { - private readonly IConfiguration? configuration; - private readonly IHttpClientFactory httpClientFactory; - - /// - /// Constructor used to inject the required types into the factory. - /// - public DaprWorkflowClientBuilderFactory(IConfiguration? configuration, IHttpClientFactory httpClientFactory) - { - this.configuration = configuration; - this.httpClientFactory = httpClientFactory; - } - /// /// Responsible for building the client itself. /// diff --git a/src/Dapr.Workflow/DaprWorkflowContext.cs b/src/Dapr.Workflow/DaprWorkflowContext.cs index 55c96595..4e51a55f 100644 --- a/src/Dapr.Workflow/DaprWorkflowContext.cs +++ b/src/Dapr.Workflow/DaprWorkflowContext.cs @@ -13,133 +13,132 @@ using Microsoft.Extensions.Logging; -namespace Dapr.Workflow +namespace Dapr.Workflow; + +using System; +using Microsoft.DurableTask; +using System.Threading.Tasks; +using System.Threading; + +class DaprWorkflowContext : WorkflowContext { - using System; - using Microsoft.DurableTask; - using System.Threading.Tasks; - using System.Threading; + readonly TaskOrchestrationContext innerContext; - class DaprWorkflowContext : WorkflowContext + internal DaprWorkflowContext(TaskOrchestrationContext innerContext) { - readonly TaskOrchestrationContext innerContext; + this.innerContext = innerContext ?? throw new ArgumentNullException(nameof(innerContext)); + } - internal DaprWorkflowContext(TaskOrchestrationContext innerContext) - { - this.innerContext = innerContext ?? throw new ArgumentNullException(nameof(innerContext)); - } + public override string Name => this.innerContext.Name; - public override string Name => this.innerContext.Name; + public override string InstanceId => this.innerContext.InstanceId; - public override string InstanceId => this.innerContext.InstanceId; + public override DateTime CurrentUtcDateTime => this.innerContext.CurrentUtcDateTime; - public override DateTime CurrentUtcDateTime => this.innerContext.CurrentUtcDateTime; - - public override bool IsReplaying => this.innerContext.IsReplaying; + public override bool IsReplaying => this.innerContext.IsReplaying; - public override Task CallActivityAsync(string name, object? input = null, WorkflowTaskOptions? options = null) + public override Task CallActivityAsync(string name, object? input = null, WorkflowTaskOptions? options = null) + { + return WrapExceptions(this.innerContext.CallActivityAsync(name, input, options?.ToDurableTaskOptions())); + } + + public override Task CallActivityAsync(string name, object? input = null, WorkflowTaskOptions? options = null) + { + return WrapExceptions(this.innerContext.CallActivityAsync(name, input, options?.ToDurableTaskOptions())); + } + + public override Task CreateTimer(TimeSpan delay, CancellationToken cancellationToken = default) + { + return this.innerContext.CreateTimer(delay, cancellationToken); + } + + public override Task CreateTimer(DateTime fireAt, CancellationToken cancellationToken) + { + return this.innerContext.CreateTimer(fireAt, cancellationToken); + } + + public override Task WaitForExternalEventAsync(string eventName, CancellationToken cancellationToken = default) + { + return this.innerContext.WaitForExternalEvent(eventName, cancellationToken); + } + + public override Task WaitForExternalEventAsync(string eventName, TimeSpan timeout) + { + return this.innerContext.WaitForExternalEvent(eventName, timeout); + } + + public override void SendEvent(string instanceId, string eventName, object payload) + { + this.innerContext.SendEvent(instanceId, eventName, payload); + } + + public override void SetCustomStatus(object? customStatus) + { + this.innerContext.SetCustomStatus(customStatus); + } + + public override Task CallChildWorkflowAsync(string workflowName, object? input = null, ChildWorkflowTaskOptions? options = null) + { + return WrapExceptions(this.innerContext.CallSubOrchestratorAsync(workflowName, input, options?.ToDurableTaskOptions())); + } + + public override Task CallChildWorkflowAsync(string workflowName, object? input = null, ChildWorkflowTaskOptions? options = null) + { + return WrapExceptions(this.innerContext.CallSubOrchestratorAsync(workflowName, input, options?.ToDurableTaskOptions())); + } + + public override void ContinueAsNew(object? newInput = null, bool preserveUnprocessedEvents = true) + { + this.innerContext.ContinueAsNew(newInput!, preserveUnprocessedEvents); + } + + 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 { - return WrapExceptions(this.innerContext.CallActivityAsync(name, input, options?.ToDurableTaskOptions())); + await task; } - - public override Task CallActivityAsync(string name, object? input = null, WorkflowTaskOptions? options = null) + catch (TaskFailedException ex) { - return WrapExceptions(this.innerContext.CallActivityAsync(name, input, options?.ToDurableTaskOptions())); - } - - public override Task CreateTimer(TimeSpan delay, CancellationToken cancellationToken = default) - { - return this.innerContext.CreateTimer(delay, cancellationToken); - } - - public override Task CreateTimer(DateTime fireAt, CancellationToken cancellationToken) - { - return this.innerContext.CreateTimer(fireAt, cancellationToken); - } - - public override Task WaitForExternalEventAsync(string eventName, CancellationToken cancellationToken = default) - { - return this.innerContext.WaitForExternalEvent(eventName, cancellationToken); - } - - public override Task WaitForExternalEventAsync(string eventName, TimeSpan timeout) - { - return this.innerContext.WaitForExternalEvent(eventName, timeout); - } - - public override void SendEvent(string instanceId, string eventName, object payload) - { - this.innerContext.SendEvent(instanceId, eventName, payload); - } - - public override void SetCustomStatus(object? customStatus) - { - this.innerContext.SetCustomStatus(customStatus); - } - - public override Task CallChildWorkflowAsync(string workflowName, object? input = null, ChildWorkflowTaskOptions? options = null) - { - return WrapExceptions(this.innerContext.CallSubOrchestratorAsync(workflowName, input, options?.ToDurableTaskOptions())); - } - - public override Task CallChildWorkflowAsync(string workflowName, object? input = null, ChildWorkflowTaskOptions? options = null) - { - return WrapExceptions(this.innerContext.CallSubOrchestratorAsync(workflowName, input, options?.ToDurableTaskOptions())); - } - - public override void ContinueAsNew(object? newInput = null, bool preserveUnprocessedEvents = true) - { - this.innerContext.ContinueAsNew(newInput!, preserveUnprocessedEvents); - } - - 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 - { - await task; - } - catch (TaskFailedException ex) - { - var details = new WorkflowTaskFailureDetails(ex.FailureDetails); - throw new WorkflowTaskFailedException(ex.Message, details); - } - } - - static async Task WrapExceptions(Task task) - { - try - { - return await task; - } - catch (TaskFailedException ex) - { - var details = new WorkflowTaskFailureDetails(ex.FailureDetails); - throw new WorkflowTaskFailedException(ex.Message, details); - } + var details = new WorkflowTaskFailureDetails(ex.FailureDetails); + throw new WorkflowTaskFailedException(ex.Message, details); } } -} + + static async Task WrapExceptions(Task task) + { + try + { + return await task; + } + catch (TaskFailedException ex) + { + var details = new WorkflowTaskFailureDetails(ex.FailureDetails); + throw new WorkflowTaskFailedException(ex.Message, details); + } + } +} \ No newline at end of file diff --git a/src/Dapr.Workflow/Workflow.cs b/src/Dapr.Workflow/Workflow.cs index 6510414b..b8d43b35 100644 --- a/src/Dapr.Workflow/Workflow.cs +++ b/src/Dapr.Workflow/Workflow.cs @@ -11,127 +11,126 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Workflow +namespace Dapr.Workflow; + +using System; +using System.Threading.Tasks; + +/// +/// Common interface for workflow implementations. +/// +/// +/// Users should not implement workflows using this interface, directly. +/// Instead, should be used to implement workflows. +/// +public interface IWorkflow { - using System; - using System.Threading.Tasks; + /// + /// Gets the type of the input parameter that this workflow accepts. + /// + Type InputType { get; } /// - /// Common interface for workflow implementations. + /// Gets the type of the return value that this workflow produces. /// - /// - /// Users should not implement workflows using this interface, directly. - /// Instead, should be used to implement workflows. - /// - public interface IWorkflow - { - /// - /// Gets the type of the input parameter that this workflow accepts. - /// - Type InputType { get; } - - /// - /// Gets the type of the return value that this workflow produces. - /// - Type OutputType { get; } - - /// - /// Invokes the workflow with the specified context and input. - /// - /// The workflow's context. - /// The workflow's input. - /// Returns the workflow output as the result of a . - Task RunAsync(WorkflowContext context, object? input); - } + Type OutputType { get; } /// - /// Represents the base class for workflows. + /// Invokes the workflow with the specified context and input. /// - /// - /// - /// Workflows describe how actions are executed and the order in which actions are executed. Workflows - /// don't call into external services or do complex computation directly. Rather, they delegate these tasks to - /// activities, which perform the actual work. - /// - /// - /// Workflows can be scheduled using the Dapr client or by other workflows as child-workflows using the - /// method. - /// - /// - /// Workflows may be replayed multiple times to rebuild their local state after being reloaded into memory. - /// workflow code must therefore be deterministic to ensure no unexpected side effects from execution - /// replay. To account for this behavior, there are several coding constraints to be aware of: - /// - /// - /// A workflow must not generate random numbers or random GUIDs, get the current date, read environment - /// variables, or do anything else that might result in a different value if the code is replayed in the future. - /// Activities and built-in properties and methods on the parameter, like - /// and , - /// can be used to work around these restrictions. - /// - /// - /// Workflow logic must be executed on the workflow thread (the thread that calls . - /// Creating new threads, scheduling callbacks on worker pool threads, or awaiting non-workflow tasks is forbidden - /// and may result in failures or other unexpected behavior. Blocking the workflow thread may also result in unexpected - /// performance degredation. The use of await should be restricted to workflow tasks - i.e. tasks returned from - /// methods on the parameter object or tasks that wrap these workflow tasks, like - /// and . - /// - /// - /// Avoid infinite loops as they could cause the application to run out of memory. Instead, ensure that loops are - /// bounded or use to restart the workflow with a new input. - /// - /// - /// Avoid logging normally in the workflow code because log messages will be duplicated on each replay. - /// Instead, write log statements when is false. - /// - /// - /// - /// - /// Workflow code is tightly coupled with its execution history so special care must be taken when making changes - /// to workflow code. For example, adding or removing activity tasks to a workflow's code may cause a - /// mismatch between code and history for in-flight workflows. To avoid potential issues related to workflow - /// versioning, consider applying the following code update strategies: - /// - /// - /// Deploy multiple versions of applications side-by-side allowing new code to run independently of old code. - /// - /// - /// Rather than changing existing workflows, create new workflows that implement the modified behavior. - /// - /// - /// Ensure all in-flight workflows are complete before applying code changes to existing workflow code. - /// - /// - /// If possible, only make changes to workflow code that won't impact its history or execution path. For - /// example, renaming variables or adding log statements have no impact on a workflow's execution path and - /// are safe to apply to existing workflows. - /// - /// - /// - /// - /// The type of the input parameter that this workflow accepts. This type must be JSON-serializable. - /// The type of the return value that this workflow produces. This type must be JSON-serializable. - public abstract class Workflow : IWorkflow - { - /// - Type IWorkflow.InputType => typeof(TInput); - - /// - Type IWorkflow.OutputType => typeof(TOutput); - - /// - async Task IWorkflow.RunAsync(WorkflowContext context, object? input) - { - return await this.RunAsync(context, (TInput)input!); - } - - /// - /// Override to implement workflow logic. - /// - /// The workflow context. - /// The deserialized workflow input. - /// The output of the workflow as a task. - public abstract Task RunAsync(WorkflowContext context, TInput input); - } + /// The workflow's context. + /// The workflow's input. + /// Returns the workflow output as the result of a . + Task RunAsync(WorkflowContext context, object? input); } + +/// +/// Represents the base class for workflows. +/// +/// +/// +/// Workflows describe how actions are executed and the order in which actions are executed. Workflows +/// don't call into external services or do complex computation directly. Rather, they delegate these tasks to +/// activities, which perform the actual work. +/// +/// +/// Workflows can be scheduled using the Dapr client or by other workflows as child-workflows using the +/// method. +/// +/// +/// Workflows may be replayed multiple times to rebuild their local state after being reloaded into memory. +/// workflow code must therefore be deterministic to ensure no unexpected side effects from execution +/// replay. To account for this behavior, there are several coding constraints to be aware of: +/// +/// +/// A workflow must not generate random numbers or random GUIDs, get the current date, read environment +/// variables, or do anything else that might result in a different value if the code is replayed in the future. +/// Activities and built-in properties and methods on the parameter, like +/// and , +/// can be used to work around these restrictions. +/// +/// +/// Workflow logic must be executed on the workflow thread (the thread that calls . +/// Creating new threads, scheduling callbacks on worker pool threads, or awaiting non-workflow tasks is forbidden +/// and may result in failures or other unexpected behavior. Blocking the workflow thread may also result in unexpected +/// performance degredation. The use of await should be restricted to workflow tasks - i.e. tasks returned from +/// methods on the parameter object or tasks that wrap these workflow tasks, like +/// and . +/// +/// +/// Avoid infinite loops as they could cause the application to run out of memory. Instead, ensure that loops are +/// bounded or use to restart the workflow with a new input. +/// +/// +/// Avoid logging normally in the workflow code because log messages will be duplicated on each replay. +/// Instead, write log statements when is false. +/// +/// +/// +/// +/// Workflow code is tightly coupled with its execution history so special care must be taken when making changes +/// to workflow code. For example, adding or removing activity tasks to a workflow's code may cause a +/// mismatch between code and history for in-flight workflows. To avoid potential issues related to workflow +/// versioning, consider applying the following code update strategies: +/// +/// +/// Deploy multiple versions of applications side-by-side allowing new code to run independently of old code. +/// +/// +/// Rather than changing existing workflows, create new workflows that implement the modified behavior. +/// +/// +/// Ensure all in-flight workflows are complete before applying code changes to existing workflow code. +/// +/// +/// If possible, only make changes to workflow code that won't impact its history or execution path. For +/// example, renaming variables or adding log statements have no impact on a workflow's execution path and +/// are safe to apply to existing workflows. +/// +/// +/// +/// +/// The type of the input parameter that this workflow accepts. This type must be JSON-serializable. +/// The type of the return value that this workflow produces. This type must be JSON-serializable. +public abstract class Workflow : IWorkflow +{ + /// + Type IWorkflow.InputType => typeof(TInput); + + /// + Type IWorkflow.OutputType => typeof(TOutput); + + /// + async Task IWorkflow.RunAsync(WorkflowContext context, object? input) + { + return await this.RunAsync(context, (TInput)input!); + } + + /// + /// Override to implement workflow logic. + /// + /// The workflow context. + /// The deserialized workflow input. + /// The output of the workflow as a task. + public abstract Task RunAsync(WorkflowContext context, TInput input); +} \ No newline at end of file diff --git a/src/Dapr.Workflow/WorkflowActivity.cs b/src/Dapr.Workflow/WorkflowActivity.cs index 4daffe57..ff13de1c 100644 --- a/src/Dapr.Workflow/WorkflowActivity.cs +++ b/src/Dapr.Workflow/WorkflowActivity.cs @@ -11,83 +11,82 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Workflow +namespace Dapr.Workflow; + +using System; +using System.Threading.Tasks; + +/// +/// Common interface for workflow activity implementations. +/// +/// +/// Users should not implement workflow activities using this interface, directly. +/// Instead, should be used to implement workflow activities. +/// +public interface IWorkflowActivity { - using System; - using System.Threading.Tasks; + /// + /// Gets the type of the input parameter that this activity accepts. + /// + Type InputType { get; } /// - /// Common interface for workflow activity implementations. + /// Gets the type of the return value that this activity produces. /// - /// - /// Users should not implement workflow activities using this interface, directly. - /// Instead, should be used to implement workflow activities. - /// - public interface IWorkflowActivity - { - /// - /// Gets the type of the input parameter that this activity accepts. - /// - Type InputType { get; } - - /// - /// Gets the type of the return value that this activity produces. - /// - Type OutputType { get; } - - /// - /// Invokes the workflow activity with the specified context and input. - /// - /// The workflow activity's context. - /// The workflow activity's input. - /// Returns the workflow activity output as the result of a . - Task RunAsync(WorkflowActivityContext context, object? input); - } + Type OutputType { get; } /// - /// Base class for workflow activities. + /// Invokes the workflow activity with the specified context and input. /// - /// - /// - /// Workflow activities are the basic unit of work in a workflow. Activities are the tasks that are - /// orchestrated in the business process. For example, you might create a workflow to process an order. The tasks - /// may involve checking the inventory, charging the customer, and creating a shipment. Each task would be a separate - /// activity. These activities may be executed serially, in parallel, or some combination of both. - /// - /// Unlike workflows, activities aren't restricted in the type of work you can do in them. Activities - /// are frequently used to make network calls or run CPU intensive operations. An activity can also return data back to - /// the workflow. The Dapr workflow engine guarantees that each called activity will be executed - /// at least once as part of a workflow's execution. - /// - /// Because activities only guarantee at least once execution, it's recommended that activity logic be implemented as - /// idempotent whenever possible. - /// - /// Activities are invoked by workflows using one of the - /// method overloads. - /// - /// - /// The type of the input parameter that this activity accepts. - /// The type of the return value that this activity produces. - public abstract class WorkflowActivity : IWorkflowActivity - { - /// - Type IWorkflowActivity.InputType => typeof(TInput); - - /// - Type IWorkflowActivity.OutputType => typeof(TOutput); - - /// - async Task IWorkflowActivity.RunAsync(WorkflowActivityContext context, object? input) - { - return await this.RunAsync(context, (TInput)input!); - } - - /// - /// Override to implement async (non-blocking) workflow activity logic. - /// - /// Provides access to additional context for the current activity execution. - /// The deserialized activity input. - /// The output of the activity as a task. - public abstract Task RunAsync(WorkflowActivityContext context, TInput input); - } + /// The workflow activity's context. + /// The workflow activity's input. + /// Returns the workflow activity output as the result of a . + Task RunAsync(WorkflowActivityContext context, object? input); } + +/// +/// Base class for workflow activities. +/// +/// +/// +/// Workflow activities are the basic unit of work in a workflow. Activities are the tasks that are +/// orchestrated in the business process. For example, you might create a workflow to process an order. The tasks +/// may involve checking the inventory, charging the customer, and creating a shipment. Each task would be a separate +/// activity. These activities may be executed serially, in parallel, or some combination of both. +/// +/// Unlike workflows, activities aren't restricted in the type of work you can do in them. Activities +/// are frequently used to make network calls or run CPU intensive operations. An activity can also return data back to +/// the workflow. The Dapr workflow engine guarantees that each called activity will be executed +/// at least once as part of a workflow's execution. +/// +/// Because activities only guarantee at least once execution, it's recommended that activity logic be implemented as +/// idempotent whenever possible. +/// +/// Activities are invoked by workflows using one of the +/// method overloads. +/// +/// +/// The type of the input parameter that this activity accepts. +/// The type of the return value that this activity produces. +public abstract class WorkflowActivity : IWorkflowActivity +{ + /// + Type IWorkflowActivity.InputType => typeof(TInput); + + /// + Type IWorkflowActivity.OutputType => typeof(TOutput); + + /// + async Task IWorkflowActivity.RunAsync(WorkflowActivityContext context, object? input) + { + return await this.RunAsync(context, (TInput)input!); + } + + /// + /// Override to implement async (non-blocking) workflow activity logic. + /// + /// Provides access to additional context for the current activity execution. + /// The deserialized activity input. + /// The output of the activity as a task. + public abstract Task RunAsync(WorkflowActivityContext context, TInput input); +} \ No newline at end of file diff --git a/src/Dapr.Workflow/WorkflowActivityContext.cs b/src/Dapr.Workflow/WorkflowActivityContext.cs index a77c3ef9..ba3c3ee8 100644 --- a/src/Dapr.Workflow/WorkflowActivityContext.cs +++ b/src/Dapr.Workflow/WorkflowActivityContext.cs @@ -11,23 +11,22 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Workflow +namespace Dapr.Workflow; + +using Microsoft.DurableTask; + +/// +/// Defines properties and methods for task activity context objects. +/// +public abstract class WorkflowActivityContext { - using Microsoft.DurableTask; + /// + /// Gets the name of the activity. + /// + public abstract TaskName Name { get; } /// - /// Defines properties and methods for task activity context objects. + /// Gets the unique ID of the current workflow instance. /// - public abstract class WorkflowActivityContext - { - /// - /// Gets the name of the activity. - /// - public abstract TaskName Name { get; } - - /// - /// Gets the unique ID of the current workflow instance. - /// - public abstract string InstanceId { get; } - } -} + public abstract string InstanceId { get; } +} \ No newline at end of file diff --git a/src/Dapr.Workflow/WorkflowContext.cs b/src/Dapr.Workflow/WorkflowContext.cs index afc544ed..4c25c9af 100644 --- a/src/Dapr.Workflow/WorkflowContext.cs +++ b/src/Dapr.Workflow/WorkflowContext.cs @@ -13,325 +13,324 @@ using Microsoft.Extensions.Logging; -namespace Dapr.Workflow +namespace Dapr.Workflow; + +using System; +using System.Threading; +using System.Threading.Tasks; + +/// +/// 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 : IWorkflowContext { - using System; - using System.Threading; - using System.Threading.Tasks; + /// + /// Gets the name of the current workflow. + /// + public abstract string Name { get; } + + /// + /// Gets the instance ID of the current workflow. + /// + public abstract string InstanceId { get; } /// - /// 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. + /// Gets the current workflow time in UTC. /// - public abstract class WorkflowContext : IWorkflowContext + /// + /// The current workflow time is stored in the workflow history and this API will + /// return the same value each time it is called from a particular point in the workflow's + /// execution. It is a deterministic, replay-safe replacement for existing .NET APIs for getting + /// the current time, such as and + /// (which should not be used). + /// + public abstract DateTime CurrentUtcDateTime { get; } + + /// + /// Gets a value indicating whether the workflow is currently replaying a previous execution. + /// + /// + /// + /// Workflow functions are "replayed" after being unloaded from memory to reconstruct local variable state. + /// During a replay, previously executed tasks will be completed automatically with previously seen values + /// that are stored in the workflow history. One the workflow reaches the point in the workflow logic + /// where it's no longer replaying existing history, the property will return false. + /// + /// You can use this property if you have 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 workflow is currently replaying a previous execution; otherwise false. + /// + public abstract bool IsReplaying { get; } + + /// + /// Asynchronously invokes an activity by name and with the specified input value. + /// + /// + /// + /// Activities are the basic unit of work in a workflow. Unlike workflows, which are not + /// allowed to do any I/O or call non-deterministic APIs, activities have no implementation restrictions. + /// + /// An activity may execute in the local machine or a remote machine. The exact behavior depends on the underlying + /// workflow engine, which is responsible for distributing tasks across machines. In general, you should never make + /// any assumptions about where an activity will run. You should also assume at-least-once execution guarantees for + /// activities, meaning that an activity may be executed twice if, for example, there is a process failure before + /// the activities result is saved into storage. + /// + /// Both the inputs and outputs of activities are serialized and stored in durable storage. It's highly recommended + /// to not include any sensitive data in activity inputs or outputs. It's also recommended to not use large payloads + /// for activity inputs and outputs, which can result in expensive serialization and network utilization. For data + /// that cannot be cheaply or safely persisted to storage, it's recommended to instead pass references + /// (for example, a URL to a storage blob/bucket) to the data and have activities fetch the data directly as part of their + /// implementation. + /// + /// + /// The name of the activity to call. + /// The serializable input to pass to the activity. + /// Additional options that control the execution and processing of the activity. + /// A task that completes when the activity completes or fails. + /// The specified activity does not exist. + /// + /// Thrown if the calling thread is not the workflow dispatch thread. + /// + /// + /// The activity failed with an unhandled exception. The details of the failure can be found in the + /// property. + /// + public virtual Task CallActivityAsync(string name, object? input = null, WorkflowTaskOptions? options = null) { - /// - /// Gets the name of the current workflow. - /// - public abstract string Name { get; } - - /// - /// Gets the instance ID of the current workflow. - /// - public abstract string InstanceId { get; } - - /// - /// Gets the current workflow time in UTC. - /// - /// - /// The current workflow time is stored in the workflow history and this API will - /// return the same value each time it is called from a particular point in the workflow's - /// execution. It is a deterministic, replay-safe replacement for existing .NET APIs for getting - /// the current time, such as and - /// (which should not be used). - /// - public abstract DateTime CurrentUtcDateTime { get; } - - /// - /// Gets a value indicating whether the workflow is currently replaying a previous execution. - /// - /// - /// - /// Workflow functions are "replayed" after being unloaded from memory to reconstruct local variable state. - /// During a replay, previously executed tasks will be completed automatically with previously seen values - /// that are stored in the workflow history. One the workflow reaches the point in the workflow logic - /// where it's no longer replaying existing history, the property will return false. - /// - /// You can use this property if you have 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 workflow is currently replaying a previous execution; otherwise false. - /// - public abstract bool IsReplaying { get; } - - /// - /// Asynchronously invokes an activity by name and with the specified input value. - /// - /// - /// - /// Activities are the basic unit of work in a workflow. Unlike workflows, which are not - /// allowed to do any I/O or call non-deterministic APIs, activities have no implementation restrictions. - /// - /// An activity may execute in the local machine or a remote machine. The exact behavior depends on the underlying - /// workflow engine, which is responsible for distributing tasks across machines. In general, you should never make - /// any assumptions about where an activity will run. You should also assume at-least-once execution guarantees for - /// activities, meaning that an activity may be executed twice if, for example, there is a process failure before - /// the activities result is saved into storage. - /// - /// Both the inputs and outputs of activities are serialized and stored in durable storage. It's highly recommended - /// to not include any sensitive data in activity inputs or outputs. It's also recommended to not use large payloads - /// for activity inputs and outputs, which can result in expensive serialization and network utilization. For data - /// that cannot be cheaply or safely persisted to storage, it's recommended to instead pass references - /// (for example, a URL to a storage blob/bucket) to the data and have activities fetch the data directly as part of their - /// implementation. - /// - /// - /// The name of the activity to call. - /// The serializable input to pass to the activity. - /// Additional options that control the execution and processing of the activity. - /// A task that completes when the activity completes or fails. - /// The specified activity does not exist. - /// - /// Thrown if the calling thread is not the workflow dispatch thread. - /// - /// - /// The activity failed with an unhandled exception. The details of the failure can be found in the - /// property. - /// - public virtual Task CallActivityAsync(string name, object? input = null, WorkflowTaskOptions? options = null) - { - return this.CallActivityAsync(name, input, options); - } - - /// - /// A task that completes when the activity completes or fails. The result of the task is the activity's return value. - /// - /// - public abstract Task CallActivityAsync(string name, object? input = null, WorkflowTaskOptions? options = null); - - /// - /// Creates a durable timer that expires after the specified delay. - /// - /// The amount of time before the timer should expire. - /// Used to cancel the durable timer. - /// A task that completes when the durable timer expires. - /// - /// Thrown if the calling thread is not the workflow dispatch thread. - /// - public virtual Task CreateTimer(TimeSpan delay, CancellationToken cancellationToken = default) - { - return this.CreateTimer(this.CurrentUtcDateTime.Add(delay), cancellationToken); - } - - /// - /// Creates a durable timer that expires at a set date and time. - /// - /// The time at which the timer should expire. - /// Used to cancel the durable timer. - /// - public abstract Task CreateTimer(DateTime fireAt, CancellationToken cancellationToken); - - /// - /// Waits for an event to be raised with name and returns the event data. - /// - /// - /// - /// External clients can raise events to a waiting workflow instance. Similarly, workflows can raise - /// events to other workflows using the method. - /// - /// If the current workflow instance is not yet waiting for an event named , - /// then the event will be saved in the workflow instance state and dispatched immediately when this method is - /// called. This event saving occurs even if the current workflow cancels the wait operation before the event is - /// received. - /// - /// Workflows can wait for the same event name multiple times, so waiting for multiple events with the same name - /// is allowed. Each external event received by a workflow will complete just one task returned by this method. - /// - /// - /// - /// The name of the event to wait for. Event names are case-insensitive. External event names can be reused any - /// number of times; they are not required to be unique. - /// - /// A CancellationToken to use to abort waiting for the event. - /// Any serializable type that represents the event payload. - /// - /// A task that completes when the external event is received. The value of the task is the deserialized event payload. - /// - /// - /// Thrown if is cancelled before the external event is received. - /// - /// - /// Thrown if the calling thread is not the workflow dispatch thread. - /// - public abstract Task WaitForExternalEventAsync(string eventName, CancellationToken cancellationToken = default); - - /// - /// Waits for an event to be raised with name and returns the event data. - /// - /// - /// The name of the event to wait for. Event names are case-insensitive. External event names can be reused any - /// number of times; they are not required to be unique. - /// - /// The amount of time to wait before cancelling the external event task. - /// - /// Thrown if elapses before the external event is received. - /// - /// - public abstract Task WaitForExternalEventAsync(string eventName, TimeSpan timeout); - - /// - /// Raises an external event for the specified workflow instance. - /// - /// - /// The target workflow can handle the sent event using the - /// method. - /// - /// If the target workflow doesn't exist, the event will be silently dropped. - /// - /// - /// The ID of the workflow instance to send the event to. - /// The name of the event to wait for. Event names are case-insensitive. - /// The serializable payload of the external event. - public abstract void SendEvent(string instanceId, string eventName, object payload); - - /// - /// Assigns a custom status value to the current workflow. - /// - /// - /// The value is serialized and stored in workflow state and will - /// be made available to the workflow status query APIs. - /// - /// - /// A serializable value to assign as the custom status value or null to clear the custom status. - /// - /// - /// Thrown if the calling thread is not the workflow dispatch thread. - /// - public abstract void SetCustomStatus(object? customStatus); - - /// - /// Executes the specified workflow as a child workflow and returns the result. - /// - /// - /// The type into which to deserialize the child workflow's output. - /// - /// - public abstract Task CallChildWorkflowAsync( - string workflowName, - object? input = null, - ChildWorkflowTaskOptions? options = null); - - /// - /// Executes the specified workflow as a child workflow. - /// - /// - /// - /// In addition to activities, workflows can schedule other workflows as child workflows. - /// A child workflow has its own instance ID, history, and status that is independent of the parent workflow - /// that started it. You can use to specify an instance ID - /// for the child workflow. Otherwise, the instance ID will be randomly generated. - /// - /// Child workflows have many benefits: - /// - /// You can split large workflows into a series of smaller child workflows, making your code more maintainable. - /// You can distribute workflow logic across multiple compute nodes concurrently, which is useful if - /// your workflow logic otherwise needs to coordinate a lot of tasks. - /// You can reduce memory usage and CPU overhead by keeping the history of parent workflow smaller. - /// - /// - /// The return value of a child workflow is its output. If a child workflow fails with an exception, then that - /// exception will be surfaced to the parent workflow, just like it is when an activity task fails with an - /// exception. Child workflows also support automatic retry policies. - /// - /// Terminating a parent workflow terminates all the child workflows created by the workflow instance. See the documentation at - /// https://docs.dapr.io/developing-applications/building-blocks/workflow/workflow-features-concepts/#child-workflows regarding - /// the terminate workflow API for more information. - /// - /// - /// The name of the workflow to call. - /// The serializable input to pass to the child workflow. - /// - /// Additional options that control the execution and processing of the child workflow. - /// - /// A task that completes when the child workflow completes or fails. - /// The specified workflow does not exist. - /// - /// Thrown if the calling thread is not the workflow dispatch thread. - /// - /// - /// The child workflow failed with an unhandled exception. The details of the failure can be found in the - /// property. - /// - public virtual Task CallChildWorkflowAsync( - string workflowName, - object? input = null, - ChildWorkflowTaskOptions? options = null) - { - 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. - /// - /// - /// - /// This method is primarily designed for eternal workflows, which are workflows that - /// may not ever complete. It works by restarting the workflow, providing it with a new input, - /// and truncating the existing workflow history. It allows the workflow to continue - /// running indefinitely without having its history grow unbounded. The benefits of periodically - /// truncating history include decreased memory usage, decreased storage volumes, and shorter workflow - /// replays when rebuilding state. - /// - /// The results of any incomplete tasks will be discarded when a workflow calls - /// . For example, if a timer is scheduled and then - /// is called before the timer fires, the timer event will be discarded. The only exception to this - /// is external events. By default, if an external event is received by an workflow but not yet - /// processed, the event is saved in the workflow state unit it is received by a call to - /// . These events will continue to remain in memory - /// even after an workflow restarts using . You can disable this behavior and - /// remove any saved external events by specifying false for the - /// parameter value. - /// - /// Workflow implementations should complete immediately after calling the method. - /// - /// - /// The JSON-serializable input data to re-initialize the instance with. - /// - /// If set to true, re-adds any unprocessed external events into the new execution - /// history when the workflow instance restarts. If false, any unprocessed - /// external events will be discarded when the workflow instance restarts. - /// - public abstract void ContinueAsNew(object? newInput = null, bool preserveUnprocessedEvents = true); - - /// - /// Creates a new GUID that is safe for replay within a workflow. - /// - /// - /// The default implementation of this method creates a name-based UUID V5 using the algorithm from RFC 4122 §4.3. - /// The name input used to generate this value is a combination of the workflow instance ID, the current time, - /// and an internally managed sequence number. - /// - /// The new value. - public abstract Guid NewGuid(); + return this.CallActivityAsync(name, input, options); } -} + + /// + /// A task that completes when the activity completes or fails. The result of the task is the activity's return value. + /// + /// + public abstract Task CallActivityAsync(string name, object? input = null, WorkflowTaskOptions? options = null); + + /// + /// Creates a durable timer that expires after the specified delay. + /// + /// The amount of time before the timer should expire. + /// Used to cancel the durable timer. + /// A task that completes when the durable timer expires. + /// + /// Thrown if the calling thread is not the workflow dispatch thread. + /// + public virtual Task CreateTimer(TimeSpan delay, CancellationToken cancellationToken = default) + { + return this.CreateTimer(this.CurrentUtcDateTime.Add(delay), cancellationToken); + } + + /// + /// Creates a durable timer that expires at a set date and time. + /// + /// The time at which the timer should expire. + /// Used to cancel the durable timer. + /// + public abstract Task CreateTimer(DateTime fireAt, CancellationToken cancellationToken); + + /// + /// Waits for an event to be raised with name and returns the event data. + /// + /// + /// + /// External clients can raise events to a waiting workflow instance. Similarly, workflows can raise + /// events to other workflows using the method. + /// + /// If the current workflow instance is not yet waiting for an event named , + /// then the event will be saved in the workflow instance state and dispatched immediately when this method is + /// called. This event saving occurs even if the current workflow cancels the wait operation before the event is + /// received. + /// + /// Workflows can wait for the same event name multiple times, so waiting for multiple events with the same name + /// is allowed. Each external event received by a workflow will complete just one task returned by this method. + /// + /// + /// + /// The name of the event to wait for. Event names are case-insensitive. External event names can be reused any + /// number of times; they are not required to be unique. + /// + /// A CancellationToken to use to abort waiting for the event. + /// Any serializable type that represents the event payload. + /// + /// A task that completes when the external event is received. The value of the task is the deserialized event payload. + /// + /// + /// Thrown if is cancelled before the external event is received. + /// + /// + /// Thrown if the calling thread is not the workflow dispatch thread. + /// + public abstract Task WaitForExternalEventAsync(string eventName, CancellationToken cancellationToken = default); + + /// + /// Waits for an event to be raised with name and returns the event data. + /// + /// + /// The name of the event to wait for. Event names are case-insensitive. External event names can be reused any + /// number of times; they are not required to be unique. + /// + /// The amount of time to wait before cancelling the external event task. + /// + /// Thrown if elapses before the external event is received. + /// + /// + public abstract Task WaitForExternalEventAsync(string eventName, TimeSpan timeout); + + /// + /// Raises an external event for the specified workflow instance. + /// + /// + /// The target workflow can handle the sent event using the + /// method. + /// + /// If the target workflow doesn't exist, the event will be silently dropped. + /// + /// + /// The ID of the workflow instance to send the event to. + /// The name of the event to wait for. Event names are case-insensitive. + /// The serializable payload of the external event. + public abstract void SendEvent(string instanceId, string eventName, object payload); + + /// + /// Assigns a custom status value to the current workflow. + /// + /// + /// The value is serialized and stored in workflow state and will + /// be made available to the workflow status query APIs. + /// + /// + /// A serializable value to assign as the custom status value or null to clear the custom status. + /// + /// + /// Thrown if the calling thread is not the workflow dispatch thread. + /// + public abstract void SetCustomStatus(object? customStatus); + + /// + /// Executes the specified workflow as a child workflow and returns the result. + /// + /// + /// The type into which to deserialize the child workflow's output. + /// + /// + public abstract Task CallChildWorkflowAsync( + string workflowName, + object? input = null, + ChildWorkflowTaskOptions? options = null); + + /// + /// Executes the specified workflow as a child workflow. + /// + /// + /// + /// In addition to activities, workflows can schedule other workflows as child workflows. + /// A child workflow has its own instance ID, history, and status that is independent of the parent workflow + /// that started it. You can use to specify an instance ID + /// for the child workflow. Otherwise, the instance ID will be randomly generated. + /// + /// Child workflows have many benefits: + /// + /// You can split large workflows into a series of smaller child workflows, making your code more maintainable. + /// You can distribute workflow logic across multiple compute nodes concurrently, which is useful if + /// your workflow logic otherwise needs to coordinate a lot of tasks. + /// You can reduce memory usage and CPU overhead by keeping the history of parent workflow smaller. + /// + /// + /// The return value of a child workflow is its output. If a child workflow fails with an exception, then that + /// exception will be surfaced to the parent workflow, just like it is when an activity task fails with an + /// exception. Child workflows also support automatic retry policies. + /// + /// Terminating a parent workflow terminates all the child workflows created by the workflow instance. See the documentation at + /// https://docs.dapr.io/developing-applications/building-blocks/workflow/workflow-features-concepts/#child-workflows regarding + /// the terminate workflow API for more information. + /// + /// + /// The name of the workflow to call. + /// The serializable input to pass to the child workflow. + /// + /// Additional options that control the execution and processing of the child workflow. + /// + /// A task that completes when the child workflow completes or fails. + /// The specified workflow does not exist. + /// + /// Thrown if the calling thread is not the workflow dispatch thread. + /// + /// + /// The child workflow failed with an unhandled exception. The details of the failure can be found in the + /// property. + /// + public virtual Task CallChildWorkflowAsync( + string workflowName, + object? input = null, + ChildWorkflowTaskOptions? options = null) + { + 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. + /// + /// + /// + /// This method is primarily designed for eternal workflows, which are workflows that + /// may not ever complete. It works by restarting the workflow, providing it with a new input, + /// and truncating the existing workflow history. It allows the workflow to continue + /// running indefinitely without having its history grow unbounded. The benefits of periodically + /// truncating history include decreased memory usage, decreased storage volumes, and shorter workflow + /// replays when rebuilding state. + /// + /// The results of any incomplete tasks will be discarded when a workflow calls + /// . For example, if a timer is scheduled and then + /// is called before the timer fires, the timer event will be discarded. The only exception to this + /// is external events. By default, if an external event is received by an workflow but not yet + /// processed, the event is saved in the workflow state unit it is received by a call to + /// . These events will continue to remain in memory + /// even after an workflow restarts using . You can disable this behavior and + /// remove any saved external events by specifying false for the + /// parameter value. + /// + /// Workflow implementations should complete immediately after calling the method. + /// + /// + /// The JSON-serializable input data to re-initialize the instance with. + /// + /// If set to true, re-adds any unprocessed external events into the new execution + /// history when the workflow instance restarts. If false, any unprocessed + /// external events will be discarded when the workflow instance restarts. + /// + public abstract void ContinueAsNew(object? newInput = null, bool preserveUnprocessedEvents = true); + + /// + /// Creates a new GUID that is safe for replay within a workflow. + /// + /// + /// The default implementation of this method creates a name-based UUID V5 using the algorithm from RFC 4122 §4.3. + /// The name input used to generate this value is a combination of the workflow instance ID, the current time, + /// and an internally managed sequence number. + /// + /// The new value. + public abstract Guid NewGuid(); +} \ No newline at end of file diff --git a/src/Dapr.Workflow/WorkflowLoggingService.cs b/src/Dapr.Workflow/WorkflowLoggingService.cs index 115db817..a4c4b3e7 100644 --- a/src/Dapr.Workflow/WorkflowLoggingService.cs +++ b/src/Dapr.Workflow/WorkflowLoggingService.cs @@ -11,63 +11,57 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Workflow +namespace Dapr.Workflow; + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Configuration; + +/// +/// Defines runtime options for workflows. +/// +internal sealed class WorkflowLoggingService(ILogger logger) : IHostedService { - using System.Collections.Generic; - using System.Threading; - using System.Threading.Tasks; - using Microsoft.Extensions.Hosting; - using Microsoft.Extensions.Logging; - using Microsoft.Extensions.Configuration; + private static readonly HashSet registeredWorkflows = []; + private static readonly HashSet registeredActivities = []; - /// - /// Defines runtime options for workflows. - /// - internal sealed class WorkflowLoggingService : IHostedService + public Task StartAsync(CancellationToken cancellationToken) { - private readonly ILogger logger; - private static readonly HashSet registeredWorkflows = new(); - private static readonly HashSet registeredActivities = new(); + logger.Log(LogLevel.Information, "WorkflowLoggingService started"); - public WorkflowLoggingService(ILogger logger) + logger.Log(LogLevel.Information, "List of registered workflows"); + foreach (string item in registeredWorkflows) { - this.logger = logger; - } - public Task StartAsync(CancellationToken cancellationToken) - { - this.logger.Log(LogLevel.Information, "WorkflowLoggingService started"); - - this.logger.Log(LogLevel.Information, "List of registered workflows"); - foreach (string item in registeredWorkflows) - { - this.logger.Log(LogLevel.Information, item); - } - - this.logger.Log(LogLevel.Information, "List of registered activities:"); - foreach (string item in registeredActivities) - { - this.logger.Log(LogLevel.Information, item); - } - - return Task.CompletedTask; + logger.Log(LogLevel.Information, item); } - public Task StopAsync(CancellationToken cancellationToken) + logger.Log(LogLevel.Information, "List of registered activities:"); + foreach (string item in registeredActivities) { - this.logger.Log(LogLevel.Information, "WorkflowLoggingService stopped"); - - return Task.CompletedTask; - } - - public static void LogWorkflowName(string workflowName) - { - registeredWorkflows.Add(workflowName); - } - - public static void LogActivityName(string activityName) - { - registeredActivities.Add(activityName); + logger.Log(LogLevel.Information, item); } + return Task.CompletedTask; } + + public Task StopAsync(CancellationToken cancellationToken) + { + logger.Log(LogLevel.Information, "WorkflowLoggingService stopped"); + + return Task.CompletedTask; + } + + public static void LogWorkflowName(string workflowName) + { + registeredWorkflows.Add(workflowName); + } + + public static void LogActivityName(string activityName) + { + registeredActivities.Add(activityName); + } + } diff --git a/src/Dapr.Workflow/WorkflowRetryPolicy.cs b/src/Dapr.Workflow/WorkflowRetryPolicy.cs index 35f0f910..d69fe0fa 100644 --- a/src/Dapr.Workflow/WorkflowRetryPolicy.cs +++ b/src/Dapr.Workflow/WorkflowRetryPolicy.cs @@ -10,95 +10,87 @@ // See the License for the specific language governing permissions and // limitations under the License. // ------------------------------------------------------------------------ + using System; using System.Threading; using Microsoft.DurableTask; -namespace Dapr.Workflow +namespace Dapr.Workflow; + +/// +/// A declarative retry policy that can be configured for activity or child workflow calls. +/// +/// The maximum number of task invocation attempts. Must be 1 or greater. +/// The amount of time to delay between the first and second attempt. +/// +/// The exponential back-off coefficient used to determine the delay between subsequent retries. Must be 1.0 or greater. +/// +/// +/// The maximum time to delay between attempts, regardless of. +/// +/// The overall timeout for retries. +/// +/// The value can be used to specify an unlimited timeout for +/// or . +/// +/// +/// Thrown if any of the following are true: +/// +/// The value for is less than or equal to zero. +/// The value for is less than or equal to . +/// The value for is less than 1.0. +/// The value for is less than . +/// The value for is less than . +/// +/// +public class WorkflowRetryPolicy( + int maxNumberOfAttempts, + TimeSpan firstRetryInterval, + double backoffCoefficient = 1.0, + TimeSpan? maxRetryInterval = null, + TimeSpan? retryTimeout = null) { + private readonly RetryPolicy durableRetryPolicy = new( + maxNumberOfAttempts, + firstRetryInterval, + backoffCoefficient, + maxRetryInterval, + retryTimeout); + /// - /// A declarative retry policy that can be configured for activity or child workflow calls. + /// Gets the max number of attempts for executing a given task. /// - public class WorkflowRetryPolicy - { - readonly RetryPolicy durableRetryPolicy; + public int MaxNumberOfAttempts => this.durableRetryPolicy.MaxNumberOfAttempts; - /// - /// Initializes a new instance of the class. - /// - /// The maximum number of task invocation attempts. Must be 1 or greater. - /// The amount of time to delay between the first and second attempt. - /// - /// The exponential back-off coefficient used to determine the delay between subsequent retries. Must be 1.0 or greater. - /// - /// - /// The maximum time to delay between attempts, regardless of. - /// - /// The overall timeout for retries. - /// - /// The value can be used to specify an unlimited timeout for - /// or . - /// - /// - /// Thrown if any of the following are true: - /// - /// The value for is less than or equal to zero. - /// The value for is less than or equal to . - /// The value for is less than 1.0. - /// The value for is less than . - /// The value for is less than . - /// - /// - public WorkflowRetryPolicy( - int maxNumberOfAttempts, - TimeSpan firstRetryInterval, - double backoffCoefficient = 1.0, - TimeSpan? maxRetryInterval = null, - TimeSpan? retryTimeout = null) - { - this.durableRetryPolicy = new RetryPolicy( - maxNumberOfAttempts, - firstRetryInterval, - backoffCoefficient, - maxRetryInterval, - retryTimeout); - } + /// + /// Gets the amount of time to delay between the first and second attempt. + /// + public TimeSpan FirstRetryInterval => this.durableRetryPolicy.FirstRetryInterval; - /// - /// Gets the max number of attempts for executing a given task. - /// - public int MaxNumberOfAttempts => this.durableRetryPolicy.MaxNumberOfAttempts; + /// + /// Gets the exponential back-off coefficient used to determine the delay between subsequent retries. + /// + /// + /// Defaults to 1.0 for no back-off. + /// + public double BackoffCoefficient => this.durableRetryPolicy.BackoffCoefficient; - /// - /// Gets the amount of time to delay between the first and second attempt. - /// - public TimeSpan FirstRetryInterval => this.durableRetryPolicy.FirstRetryInterval; + /// + /// Gets the maximum time to delay between attempts. + /// + /// + /// Defaults to 1 hour. + /// + public TimeSpan MaxRetryInterval => this.durableRetryPolicy.MaxRetryInterval; - /// - /// Gets the exponential back-off coefficient used to determine the delay between subsequent retries. - /// - /// - /// Defaults to 1.0 for no back-off. - /// - public double BackoffCoefficient => this.durableRetryPolicy.BackoffCoefficient; + /// + /// Gets the overall timeout for retries. No further attempts will be made at executing a task after this retry + /// timeout expires. + /// + /// + /// Defaults to . + /// + public TimeSpan RetryTimeout => this.durableRetryPolicy.RetryTimeout; - /// - /// Gets the maximum time to delay between attempts. - /// - /// - /// Defaults to 1 hour. - /// - public TimeSpan MaxRetryInterval => this.durableRetryPolicy.MaxRetryInterval; - - /// - /// Gets the overall timeout for retries. No further attempts will be made at executing a task after this retry - /// timeout expires. - /// - /// - /// Defaults to . - /// - public TimeSpan RetryTimeout => this.durableRetryPolicy.RetryTimeout; - - internal RetryPolicy GetDurableRetryPolicy() => this.durableRetryPolicy; - } + internal RetryPolicy GetDurableRetryPolicy() => this.durableRetryPolicy; } diff --git a/src/Dapr.Workflow/WorkflowRuntimeOptions.cs b/src/Dapr.Workflow/WorkflowRuntimeOptions.cs index 9afdfb5e..c030d0b1 100644 --- a/src/Dapr.Workflow/WorkflowRuntimeOptions.cs +++ b/src/Dapr.Workflow/WorkflowRuntimeOptions.cs @@ -13,179 +13,177 @@ using Grpc.Net.Client; -namespace Dapr.Workflow +namespace Dapr.Workflow; + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.DurableTask; +using Microsoft.Extensions.DependencyInjection; + +/// +/// Defines runtime options for workflows. +/// +public sealed class WorkflowRuntimeOptions { - using System; - using System.Collections.Generic; - using System.Threading.Tasks; - using Microsoft.DurableTask; - using Microsoft.Extensions.DependencyInjection; + /// + /// Dictionary to name and register a workflow. + /// + readonly Dictionary> factories = new(); /// - /// Defines runtime options for workflows. + /// Override GrpcChannelOptions. /// - public sealed class WorkflowRuntimeOptions + internal GrpcChannelOptions? GrpcChannelOptions { get; private set; } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Instances of this type are expected to be instantiated from a dependency injection container. + /// + public WorkflowRuntimeOptions() { - /// - /// Dictionary to name and register a workflow. - /// - readonly Dictionary> factories = new(); + } - /// - /// Override GrpcChannelOptions. - /// - internal GrpcChannelOptions? GrpcChannelOptions { get; private set; } + /// + /// Registers a workflow as a function that takes a specified input type and returns a specified output type. + /// + /// Workflow name + /// Function implementing the workflow definition + public void RegisterWorkflow(string name, Func> implementation) + { + // Dapr workflows are implemented as specialized Durable Task orchestrations + this.factories.Add(name, (DurableTaskRegistry registry) => + { + registry.AddOrchestratorFunc(name, (innerContext, input) => + { + WorkflowContext workflowContext = new DaprWorkflowContext(innerContext); + return implementation(workflowContext, input); + }); + WorkflowLoggingService.LogWorkflowName(name); + }); + } + + /// + /// Registers a workflow class that derives from . + /// + /// The type to register. + public void RegisterWorkflow() where TWorkflow : class, IWorkflow, new() + { + string name = typeof(TWorkflow).Name; + + // Dapr workflows are implemented as specialized Durable Task orchestrations + this.factories.Add(name, (DurableTaskRegistry registry) => + { + registry.AddOrchestrator(name, () => + { + TWorkflow workflow = Activator.CreateInstance(); + return new OrchestratorWrapper(workflow); + }); + WorkflowLoggingService.LogWorkflowName(name); + }); + } + + /// + /// Registers a workflow activity as a function that takes a specified input type and returns a specified output type. + /// + /// Activity name + /// Activity implemetation + public void RegisterActivity(string name, Func> implementation) + { + // Dapr activities are implemented as specialized Durable Task activities + this.factories.Add(name, (DurableTaskRegistry registry) => + { + registry.AddActivityFunc(name, (innerContext, input) => + { + WorkflowActivityContext activityContext = new DaprWorkflowActivityContext(innerContext); + return implementation(activityContext, input); + }); + WorkflowLoggingService.LogActivityName(name); + }); + } + + /// + /// Registers a workflow activity class that derives from . + /// + /// The type to register. + public void RegisterActivity() where TActivity : class, IWorkflowActivity + { + string name = typeof(TActivity).Name; + + // Dapr workflows are implemented as specialized Durable Task orchestrations + this.factories.Add(name, (DurableTaskRegistry registry) => + { + registry.AddActivity(name, serviceProvider => + { + // Workflow activity classes support dependency injection. + TActivity activity = ActivatorUtilities.CreateInstance(serviceProvider); + return new ActivityWrapper(activity); + }); + WorkflowLoggingService.LogActivityName(name); + }); + } - /// - /// Initializes a new instance of the class. - /// - /// - /// Instances of this type are expected to be instanciated from a dependency injection container. - /// - public WorkflowRuntimeOptions() + /// + /// Uses the provided for creating the . + /// + /// The to use for creating the . + public void UseGrpcChannelOptions(GrpcChannelOptions grpcChannelOptions) + { + this.GrpcChannelOptions = grpcChannelOptions; + } + + /// + /// Method to add workflows and activities to the registry. + /// + /// The registry we will add workflows and activities to + internal void AddWorkflowsAndActivitiesToRegistry(DurableTaskRegistry registry) + { + foreach (Action factory in this.factories.Values) { + factory.Invoke(registry); // This adds workflows to the registry indirectly. + } + } + + /// + /// Helper class that provides a Durable Task orchestrator wrapper for a workflow. + /// + class OrchestratorWrapper : ITaskOrchestrator + { + readonly IWorkflow workflow; + + public OrchestratorWrapper(IWorkflow workflow) + { + this.workflow = workflow; } - /// - /// Registers a workflow as a function that takes a specified input type and returns a specified output type. - /// - /// Workflow name - /// Function implementing the workflow definition - public void RegisterWorkflow(string name, Func> implementation) + public Type InputType => this.workflow.InputType; + + public Type OutputType => this.workflow.OutputType; + + public Task RunAsync(TaskOrchestrationContext context, object? input) { - // Dapr workflows are implemented as specialized Durable Task orchestrations - this.factories.Add(name, (DurableTaskRegistry registry) => - { - registry.AddOrchestratorFunc(name, (innerContext, input) => - { - WorkflowContext workflowContext = new DaprWorkflowContext(innerContext); - return implementation(workflowContext, input); - }); - WorkflowLoggingService.LogWorkflowName(name); - }); + return this.workflow.RunAsync(new DaprWorkflowContext(context), input); + } + } + + class ActivityWrapper : ITaskActivity + { + readonly IWorkflowActivity activity; + + public ActivityWrapper(IWorkflowActivity activity) + { + this.activity = activity; } - /// - /// Registers a workflow class that derives from . - /// - /// The type to register. - public void RegisterWorkflow() where TWorkflow : class, IWorkflow, new() + public Type InputType => this.activity.InputType; + + public Type OutputType => this.activity.OutputType; + + public Task RunAsync(TaskActivityContext context, object? input) { - string name = typeof(TWorkflow).Name; - - // Dapr workflows are implemented as specialized Durable Task orchestrations - this.factories.Add(name, (DurableTaskRegistry registry) => - { - registry.AddOrchestrator(name, () => - { - TWorkflow workflow = Activator.CreateInstance(); - return new OrchestratorWrapper(workflow); - }); - WorkflowLoggingService.LogWorkflowName(name); - }); - } - - /// - /// Registers a workflow activity as a function that takes a specified input type and returns a specified output type. - /// - /// Activity name - /// Activity implemetation - public void RegisterActivity(string name, Func> implementation) - { - // Dapr activities are implemented as specialized Durable Task activities - this.factories.Add(name, (DurableTaskRegistry registry) => - { - registry.AddActivityFunc(name, (innerContext, input) => - { - WorkflowActivityContext activityContext = new DaprWorkflowActivityContext(innerContext); - return implementation(activityContext, input); - }); - WorkflowLoggingService.LogActivityName(name); - }); - } - - /// - /// Registers a workflow activity class that derives from . - /// - /// The type to register. - public void RegisterActivity() where TActivity : class, IWorkflowActivity - { - string name = typeof(TActivity).Name; - - // Dapr workflows are implemented as specialized Durable Task orchestrations - this.factories.Add(name, (DurableTaskRegistry registry) => - { - registry.AddActivity(name, serviceProvider => - { - // Workflow activity classes support dependency injection. - TActivity activity = ActivatorUtilities.CreateInstance(serviceProvider); - return new ActivityWrapper(activity); - }); - WorkflowLoggingService.LogActivityName(name); - }); - } - - /// - /// Uses the provided for creating the . - /// - /// The to use for creating the . - public void UseGrpcChannelOptions(GrpcChannelOptions grpcChannelOptions) - { - this.GrpcChannelOptions = grpcChannelOptions; - } - - /// - /// Method to add workflows and activities to the registry. - /// - /// The registry we will add workflows and activities to - internal void AddWorkflowsAndActivitiesToRegistry(DurableTaskRegistry registry) - { - foreach (Action factory in this.factories.Values) - { - factory.Invoke(registry); // This adds workflows to the registry indirectly. - } - } - - /// - /// Helper class that provides a Durable Task orchestrator wrapper for a workflow. - /// - class OrchestratorWrapper : ITaskOrchestrator - { - readonly IWorkflow workflow; - - public OrchestratorWrapper(IWorkflow workflow) - { - this.workflow = workflow; - } - - public Type InputType => this.workflow.InputType; - - public Type OutputType => this.workflow.OutputType; - - public Task RunAsync(TaskOrchestrationContext context, object? input) - { - return this.workflow.RunAsync(new DaprWorkflowContext(context), input); - } - } - - class ActivityWrapper : ITaskActivity - { - readonly IWorkflowActivity activity; - - public ActivityWrapper(IWorkflowActivity activity) - { - this.activity = activity; - } - - public Type InputType => this.activity.InputType; - - public Type OutputType => this.activity.OutputType; - - public Task RunAsync(TaskActivityContext context, object? input) - { - return this.activity.RunAsync(new DaprWorkflowActivityContext(context), input); - } + return this.activity.RunAsync(new DaprWorkflowActivityContext(context), input); } } } - diff --git a/src/Dapr.Workflow/WorkflowRuntimeStatus.cs b/src/Dapr.Workflow/WorkflowRuntimeStatus.cs index 24024cd6..9cc0942f 100644 --- a/src/Dapr.Workflow/WorkflowRuntimeStatus.cs +++ b/src/Dapr.Workflow/WorkflowRuntimeStatus.cs @@ -11,46 +11,45 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Workflow +namespace Dapr.Workflow; + +/// +/// Enum describing the runtime status of a workflow. +/// +public enum WorkflowRuntimeStatus { /// - /// Enum describing the runtime status of a workflow. + /// The status of the workflow is unknown. /// - public enum WorkflowRuntimeStatus - { - /// - /// The status of the workflow is unknown. - /// - Unknown = -1, + Unknown = -1, - /// - /// The workflow started running. - /// - Running, + /// + /// The workflow started running. + /// + Running, - /// - /// The workflow completed normally. - /// - Completed, + /// + /// The workflow completed normally. + /// + Completed, - /// - /// The workflow completed with an unhandled exception. - /// - Failed, + /// + /// The workflow completed with an unhandled exception. + /// + Failed, - /// - /// The workflow was abruptly terminated via a management API call. - /// - Terminated, + /// + /// The workflow was abruptly terminated via a management API call. + /// + Terminated, - /// - /// The workflow was scheduled but hasn't started running. - /// - Pending, + /// + /// The workflow was scheduled but hasn't started running. + /// + Pending, - /// - /// The workflow was suspended. - /// - Suspended, - } -} + /// + /// The workflow was suspended. + /// + Suspended, +} \ No newline at end of file diff --git a/src/Dapr.Workflow/WorkflowState.cs b/src/Dapr.Workflow/WorkflowState.cs index ea1ffae2..caaba0f9 100644 --- a/src/Dapr.Workflow/WorkflowState.cs +++ b/src/Dapr.Workflow/WorkflowState.cs @@ -11,155 +11,154 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Workflow +namespace Dapr.Workflow; + +using System; +using Dapr.Client; +using Microsoft.DurableTask.Client; + +/// +/// Represents a snapshot of a workflow instance's current state, including runtime status. +/// +public class WorkflowState { - using System; - using Dapr.Client; - using Microsoft.DurableTask.Client; + readonly OrchestrationMetadata? workflowState; + readonly WorkflowTaskFailureDetails? failureDetails; - /// - /// Represents a snapshot of a workflow instance's current state, including runtime status. - /// - public class WorkflowState + internal WorkflowState(OrchestrationMetadata? orchestrationMetadata) { - readonly OrchestrationMetadata? workflowState; - readonly WorkflowTaskFailureDetails? failureDetails; - - internal WorkflowState(OrchestrationMetadata? orchestrationMetadata) + // This value will be null if the workflow doesn't exist. + this.workflowState = orchestrationMetadata; + if (orchestrationMetadata?.FailureDetails != null) { - // This value will be null if the workflow doesn't exist. - this.workflowState = orchestrationMetadata; - if (orchestrationMetadata?.FailureDetails != null) - { - this.failureDetails = new WorkflowTaskFailureDetails(orchestrationMetadata.FailureDetails); - } - } - - /// - /// Gets a value indicating whether the requested workflow instance exists. - /// - public bool Exists => this.workflowState != null; - - /// - /// Gets a value indicating whether the requested workflow is in a running state. - /// - public bool IsWorkflowRunning => this.workflowState?.RuntimeStatus == OrchestrationRuntimeStatus.Running; - - /// - /// Gets a value indicating whether the requested workflow is in a terminal state. - /// - public bool IsWorkflowCompleted => this.workflowState?.IsCompleted == true; - - /// - /// Gets the time at which this workflow instance was created. - /// - public DateTimeOffset CreatedAt => this.workflowState?.CreatedAt ?? default; - - /// - /// Gets the time at which this workflow instance last had its state updated. - /// - public DateTimeOffset LastUpdatedAt => this.workflowState?.LastUpdatedAt ?? default; - - /// - /// Gets the execution status of the workflow. - /// - public WorkflowRuntimeStatus RuntimeStatus - { - get - { - if (this.workflowState == null) - { - return WorkflowRuntimeStatus.Unknown; - } - - switch (this.workflowState.RuntimeStatus) - { - case OrchestrationRuntimeStatus.Running: - return WorkflowRuntimeStatus.Running; - case OrchestrationRuntimeStatus.Completed: - return WorkflowRuntimeStatus.Completed; - case OrchestrationRuntimeStatus.Failed: - return WorkflowRuntimeStatus.Failed; - case OrchestrationRuntimeStatus.Terminated: - return WorkflowRuntimeStatus.Terminated; - case OrchestrationRuntimeStatus.Pending: - return WorkflowRuntimeStatus.Pending; - case OrchestrationRuntimeStatus.Suspended: - return WorkflowRuntimeStatus.Suspended; - default: - return WorkflowRuntimeStatus.Unknown; - } - } - } - - /// - /// Gets the failure details, if any, for the workflow instance. - /// - /// - /// This property contains data only if the workflow is in the - /// state, and only if this instance metadata was fetched with the option to include output data. - /// - /// The failure details if the workflow was in a failed state; null otherwise. - public WorkflowTaskFailureDetails? FailureDetails => this.failureDetails; - - /// - /// Deserializes the workflow input into . - /// - /// The type to deserialize the workflow input into. - /// Returns the input as , or returns a default value if the workflow doesn't exist. - public T? ReadInputAs() - { - if (this.workflowState == null) - { - return default; - } - - if (string.IsNullOrEmpty(this.workflowState.SerializedInput)) - { - return default; - } - - return this.workflowState.ReadInputAs(); - } - - /// - /// Deserializes the workflow output into . - /// - /// The type to deserialize the workflow output into. - /// Returns the output as , or returns a default value if the workflow doesn't exist. - public T? ReadOutputAs() - { - if (this.workflowState == null) - { - return default; - } - - if (string.IsNullOrEmpty(this.workflowState.SerializedOutput)) - { - return default; - } - - return this.workflowState.ReadOutputAs(); - } - - /// - /// Deserializes the workflow's custom status into . - /// - /// The type to deserialize the workflow's custom status into. - /// Returns the custom status as , or returns a default value if the workflow doesn't exist. - public T? ReadCustomStatusAs() - { - if (this.workflowState == null) - { - return default; - } - - if (string.IsNullOrEmpty(this.workflowState.SerializedCustomStatus)) - { - return default; - } - - return this.workflowState.ReadCustomStatusAs(); + this.failureDetails = new WorkflowTaskFailureDetails(orchestrationMetadata.FailureDetails); } } -} + + /// + /// Gets a value indicating whether the requested workflow instance exists. + /// + public bool Exists => this.workflowState != null; + + /// + /// Gets a value indicating whether the requested workflow is in a running state. + /// + public bool IsWorkflowRunning => this.workflowState?.RuntimeStatus == OrchestrationRuntimeStatus.Running; + + /// + /// Gets a value indicating whether the requested workflow is in a terminal state. + /// + public bool IsWorkflowCompleted => this.workflowState?.IsCompleted == true; + + /// + /// Gets the time at which this workflow instance was created. + /// + public DateTimeOffset CreatedAt => this.workflowState?.CreatedAt ?? default; + + /// + /// Gets the time at which this workflow instance last had its state updated. + /// + public DateTimeOffset LastUpdatedAt => this.workflowState?.LastUpdatedAt ?? default; + + /// + /// Gets the execution status of the workflow. + /// + public WorkflowRuntimeStatus RuntimeStatus + { + get + { + if (this.workflowState == null) + { + return WorkflowRuntimeStatus.Unknown; + } + + switch (this.workflowState.RuntimeStatus) + { + case OrchestrationRuntimeStatus.Running: + return WorkflowRuntimeStatus.Running; + case OrchestrationRuntimeStatus.Completed: + return WorkflowRuntimeStatus.Completed; + case OrchestrationRuntimeStatus.Failed: + return WorkflowRuntimeStatus.Failed; + case OrchestrationRuntimeStatus.Terminated: + return WorkflowRuntimeStatus.Terminated; + case OrchestrationRuntimeStatus.Pending: + return WorkflowRuntimeStatus.Pending; + case OrchestrationRuntimeStatus.Suspended: + return WorkflowRuntimeStatus.Suspended; + default: + return WorkflowRuntimeStatus.Unknown; + } + } + } + + /// + /// Gets the failure details, if any, for the workflow instance. + /// + /// + /// This property contains data only if the workflow is in the + /// state, and only if this instance metadata was fetched with the option to include output data. + /// + /// The failure details if the workflow was in a failed state; null otherwise. + public WorkflowTaskFailureDetails? FailureDetails => this.failureDetails; + + /// + /// Deserializes the workflow input into . + /// + /// The type to deserialize the workflow input into. + /// Returns the input as , or returns a default value if the workflow doesn't exist. + public T? ReadInputAs() + { + if (this.workflowState == null) + { + return default; + } + + if (string.IsNullOrEmpty(this.workflowState.SerializedInput)) + { + return default; + } + + return this.workflowState.ReadInputAs(); + } + + /// + /// Deserializes the workflow output into . + /// + /// The type to deserialize the workflow output into. + /// Returns the output as , or returns a default value if the workflow doesn't exist. + public T? ReadOutputAs() + { + if (this.workflowState == null) + { + return default; + } + + if (string.IsNullOrEmpty(this.workflowState.SerializedOutput)) + { + return default; + } + + return this.workflowState.ReadOutputAs(); + } + + /// + /// Deserializes the workflow's custom status into . + /// + /// The type to deserialize the workflow's custom status into. + /// Returns the custom status as , or returns a default value if the workflow doesn't exist. + public T? ReadCustomStatusAs() + { + if (this.workflowState == null) + { + return default; + } + + if (string.IsNullOrEmpty(this.workflowState.SerializedCustomStatus)) + { + return default; + } + + return this.workflowState.ReadCustomStatusAs(); + } +} \ No newline at end of file diff --git a/src/Dapr.Workflow/WorkflowTaskFailedException.cs b/src/Dapr.Workflow/WorkflowTaskFailedException.cs index 575947a3..cd7600f9 100644 --- a/src/Dapr.Workflow/WorkflowTaskFailedException.cs +++ b/src/Dapr.Workflow/WorkflowTaskFailedException.cs @@ -11,29 +11,20 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Workflow +namespace Dapr.Workflow; + +using System; + +/// +/// Exception type for Dapr Workflow task failures. +/// +/// The exception message. +/// Details about the failure. +public class WorkflowTaskFailedException(string message, WorkflowTaskFailureDetails failureDetails) + : Exception(message) { - using System; - /// - /// Exception type for Dapr Workflow task failures. + /// Gets more information about the underlying workflow task failure. /// - public class WorkflowTaskFailedException : Exception - { - /// - /// Initializes a new instance of the class. - /// - /// The exception message. - /// Details about the failure. - public WorkflowTaskFailedException(string message, WorkflowTaskFailureDetails failureDetails) - : base(message) - { - this.FailureDetails = failureDetails ?? throw new ArgumentNullException(nameof(failureDetails)); - } - - /// - /// Gets more information about the underlying workflow task failure. - /// - public WorkflowTaskFailureDetails FailureDetails { get; } - } + public WorkflowTaskFailureDetails FailureDetails { get; } = failureDetails ?? throw new ArgumentNullException(nameof(failureDetails)); } diff --git a/src/Dapr.Workflow/WorkflowTaskFailureDetails.cs b/src/Dapr.Workflow/WorkflowTaskFailureDetails.cs index 167ba210..35733b22 100644 --- a/src/Dapr.Workflow/WorkflowTaskFailureDetails.cs +++ b/src/Dapr.Workflow/WorkflowTaskFailureDetails.cs @@ -11,62 +11,61 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Workflow +namespace Dapr.Workflow; + +using System; +using Microsoft.DurableTask; + +/// +/// Represents workflow task failure details. +/// +public class WorkflowTaskFailureDetails { - using System; - using Microsoft.DurableTask; + readonly TaskFailureDetails details; + + internal WorkflowTaskFailureDetails(TaskFailureDetails details) + { + this.details = details ?? throw new ArgumentNullException(nameof(details)); + } /// - /// Represents workflow task failure details. + /// Gets the error type, which is the namespace-qualified exception type name. /// - public class WorkflowTaskFailureDetails + public string ErrorType => this.details.ErrorType; + + /// + /// Gets a summary description of the failure, which is typically an exception message. + /// + public string ErrorMessage => this.details.ErrorMessage; + + /// + /// Gets the stack trace of the failure. + /// + public string? StackTrace => this.details.StackTrace; + + /// + /// Returns true if the failure was caused by the specified exception type. + /// + /// + /// This method allows checking if a workflow task failed due to an exception of a specific type by attempting + /// to load the type specified in . If the exception type cannot be loaded + /// for any reason, this method will return false. Base types are supported. + /// + /// The type of exception to test against. + /// + /// Returns true if the value matches ; false otherwise. + /// + public bool IsCausedBy() where T : Exception { - readonly TaskFailureDetails details; - - internal WorkflowTaskFailureDetails(TaskFailureDetails details) - { - this.details = details ?? throw new ArgumentNullException(nameof(details)); - } - - /// - /// Gets the error type, which is the namespace-qualified exception type name. - /// - public string ErrorType => this.details.ErrorType; - - /// - /// Gets a summary description of the failure, which is typically an exception message. - /// - public string ErrorMessage => this.details.ErrorMessage; - - /// - /// Gets the stack trace of the failure. - /// - public string? StackTrace => this.details.StackTrace; - - /// - /// Returns true if the failure was caused by the specified exception type. - /// - /// - /// This method allows checking if a workflow task failed due to an exception of a specific type by attempting - /// to load the type specified in . If the exception type cannot be loaded - /// for any reason, this method will return false. Base types are supported. - /// - /// The type of exception to test against. - /// - /// Returns true if the value matches ; false otherwise. - /// - public bool IsCausedBy() where T : Exception - { - return this.details.IsCausedBy(); - } - - /// - /// Gets a debug-friendly description of the failure information. - /// - /// A debugger friendly display string. - public override string ToString() - { - return $"{this.ErrorType}: {this.ErrorMessage}"; - } + return this.details.IsCausedBy(); } -} + + /// + /// Gets a debug-friendly description of the failure information. + /// + /// A debugger friendly display string. + public override string ToString() + { + return $"{this.ErrorType}: {this.ErrorMessage}"; + } +} \ No newline at end of file diff --git a/src/Dapr.Workflow/WorkflowTaskOptions.cs b/src/Dapr.Workflow/WorkflowTaskOptions.cs index d93dbc55..7ab201a0 100644 --- a/src/Dapr.Workflow/WorkflowTaskOptions.cs +++ b/src/Dapr.Workflow/WorkflowTaskOptions.cs @@ -13,56 +13,41 @@ using Microsoft.DurableTask; -namespace Dapr.Workflow +namespace Dapr.Workflow; + +/// +/// Options that can be used to control the behavior of workflow task execution. +/// +/// The workflow retry policy. +public record WorkflowTaskOptions(WorkflowRetryPolicy? RetryPolicy = null) { - /// - /// Options that can be used to control the behavior of workflow task execution. - /// - /// The workflow retry policy. - public record WorkflowTaskOptions(WorkflowRetryPolicy? RetryPolicy = null) + internal TaskOptions ToDurableTaskOptions() { - internal TaskOptions ToDurableTaskOptions() + TaskRetryOptions? retryOptions = null; + if (this.RetryPolicy is not null) { - TaskRetryOptions? retryOptions = null; - if (this.RetryPolicy is not null) - { - retryOptions = this.RetryPolicy.GetDurableRetryPolicy(); - } - - return new TaskOptions(retryOptions); - } - } - - /// - /// Options for controlling the behavior of child workflow execution. - /// - public record ChildWorkflowTaskOptions : WorkflowTaskOptions - { - /// - /// Initializes a new instance of the record. - /// - /// The instance ID to use for the child workflow. - /// The child workflow's retry policy. - public ChildWorkflowTaskOptions(string? instanceId = null, WorkflowRetryPolicy ? retryPolicy = null) - : base(retryPolicy) - { - this.InstanceId = instanceId; + retryOptions = this.RetryPolicy.GetDurableRetryPolicy(); } - /// - /// Gets the instance ID to use when creating a child workflow. - /// - public string? InstanceId { get; init; } - - internal new SubOrchestrationOptions ToDurableTaskOptions() - { - TaskRetryOptions? retryOptions = null; - if (this.RetryPolicy is not null) - { - retryOptions = this.RetryPolicy.GetDurableRetryPolicy(); - } - - return new SubOrchestrationOptions(retryOptions, this.InstanceId); - } + return new TaskOptions(retryOptions); + } +} + +/// +/// Options for controlling the behavior of child workflow execution. +/// +/// The instance ID to use for the child workflow. +/// The child workflow's retry policy. +public record ChildWorkflowTaskOptions(string? InstanceId = null, WorkflowRetryPolicy? RetryPolicy = null) : WorkflowTaskOptions(RetryPolicy) +{ + internal new SubOrchestrationOptions ToDurableTaskOptions() + { + TaskRetryOptions? retryOptions = null; + if (this.RetryPolicy is not null) + { + retryOptions = this.RetryPolicy.GetDurableRetryPolicy(); + } + + return new SubOrchestrationOptions(retryOptions, this.InstanceId); } } diff --git a/test/Dapr.Actors.AspNetCore.IntegrationTest.App/ActivationTests/DependencyInjectionActor.cs b/test/Dapr.Actors.AspNetCore.IntegrationTest.App/ActivationTests/DependencyInjectionActor.cs index 0182e023..b40c3f5a 100644 --- a/test/Dapr.Actors.AspNetCore.IntegrationTest.App/ActivationTests/DependencyInjectionActor.cs +++ b/test/Dapr.Actors.AspNetCore.IntegrationTest.App/ActivationTests/DependencyInjectionActor.cs @@ -14,31 +14,30 @@ using System.Threading.Tasks; using Dapr.Actors.Runtime; -namespace Dapr.Actors.AspNetCore.IntegrationTest.App.ActivationTests +namespace Dapr.Actors.AspNetCore.IntegrationTest.App.ActivationTests; + +public class DependencyInjectionActor : Actor, IDependencyInjectionActor { - public class DependencyInjectionActor : Actor, IDependencyInjectionActor - { - private readonly CounterService counter; + private readonly CounterService counter; - public DependencyInjectionActor(ActorHost host, CounterService counter) - : base(host) - { - this.counter = counter; - } - - public Task IncrementAsync() - { - return Task.FromResult(this.counter.Value++); - } + public DependencyInjectionActor(ActorHost host, CounterService counter) + : base(host) + { + this.counter = counter; } - public interface IDependencyInjectionActor : IActor + public Task IncrementAsync() { - Task IncrementAsync(); - } - - public class CounterService - { - public int Value { get; set; } + return Task.FromResult(this.counter.Value++); } } + +public interface IDependencyInjectionActor : IActor +{ + Task IncrementAsync(); +} + +public class CounterService +{ + public int Value { get; set; } +} \ No newline at end of file diff --git a/test/Dapr.Actors.AspNetCore.IntegrationTest.App/Program.cs b/test/Dapr.Actors.AspNetCore.IntegrationTest.App/Program.cs index a6b47145..5572b46b 100644 --- a/test/Dapr.Actors.AspNetCore.IntegrationTest.App/Program.cs +++ b/test/Dapr.Actors.AspNetCore.IntegrationTest.App/Program.cs @@ -1,33 +1,32 @@ -// ------------------------------------------------------------------------ -// 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. -// 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. -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ +// 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. +// 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.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; -namespace Dapr.Actors.AspNetCore.IntegrationTest.App -{ - public class Program - { - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } +namespace Dapr.Actors.AspNetCore.IntegrationTest.App; - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }); +public class Program +{ + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); } -} + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); +} \ No newline at end of file diff --git a/test/Dapr.Actors.AspNetCore.IntegrationTest.App/Startup.cs b/test/Dapr.Actors.AspNetCore.IntegrationTest.App/Startup.cs index 6f010c15..5baae44b 100644 --- a/test/Dapr.Actors.AspNetCore.IntegrationTest.App/Startup.cs +++ b/test/Dapr.Actors.AspNetCore.IntegrationTest.App/Startup.cs @@ -1,15 +1,15 @@ -// ------------------------------------------------------------------------ -// 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. -// 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. -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ +// 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. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ using Dapr.Actors.AspNetCore.IntegrationTest.App.ActivationTests; using Microsoft.AspNetCore.Builder; @@ -17,32 +17,31 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -namespace Dapr.Actors.AspNetCore.IntegrationTest.App +namespace Dapr.Actors.AspNetCore.IntegrationTest.App; + +public class Startup { - public class Startup + public void ConfigureServices(IServiceCollection services) { - public void ConfigureServices(IServiceCollection services) + services.AddScoped(); + services.AddActors(options => { - services.AddScoped(); - services.AddActors(options => - { - options.Actors.RegisterActor(); - }); - } - - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.UseRouting(); - - app.UseEndpoints(endpoints => - { - endpoints.MapActorsHandlers(); - }); - } + options.Actors.RegisterActor(); + }); } -} + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseRouting(); + + app.UseEndpoints(endpoints => + { + endpoints.MapActorsHandlers(); + }); + } +} \ No newline at end of file diff --git a/test/Dapr.Actors.AspNetCore.IntegrationTest/ActivationTests.cs b/test/Dapr.Actors.AspNetCore.IntegrationTest/ActivationTests.cs index 2fa61a6a..83e66e9d 100644 --- a/test/Dapr.Actors.AspNetCore.IntegrationTest/ActivationTests.cs +++ b/test/Dapr.Actors.AspNetCore.IntegrationTest/ActivationTests.cs @@ -19,68 +19,67 @@ using Dapr.Actors.AspNetCore.IntegrationTest.App.ActivationTests; using Xunit; using Xunit.Sdk; -namespace Dapr.Actors.AspNetCore.IntegrationTest +namespace Dapr.Actors.AspNetCore.IntegrationTest; + +public class ActivationTests { - public class ActivationTests + private readonly JsonSerializerOptions options = new JsonSerializerOptions() { - private readonly JsonSerializerOptions options = new JsonSerializerOptions() - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - PropertyNameCaseInsensitive = true, - }; + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = true, + }; - [Fact] - public async Task CanActivateActorWithDependencyInjection() - { - using var factory = new AppWebApplicationFactory(); - var httpClient = factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions { HandleCookies = false }); + [Fact] + public async Task CanActivateActorWithDependencyInjection() + { + using var factory = new AppWebApplicationFactory(); + var httpClient = factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions { HandleCookies = false }); - // Doing this twice verifies that the Actor stays active and retains state using DI. - var text = await IncrementCounterAsync(httpClient, "A"); - Assert.Equal("0", text); + // Doing this twice verifies that the Actor stays active and retains state using DI. + var text = await IncrementCounterAsync(httpClient, "A"); + Assert.Equal("0", text); - text = await IncrementCounterAsync(httpClient, "A"); - Assert.Equal("1", text); + text = await IncrementCounterAsync(httpClient, "A"); + Assert.Equal("1", text); - await DeactivateActor(httpClient, "A"); - } - - private async Task IncrementCounterAsync(HttpClient httpClient, string actorId) - { - var actorTypeName = nameof(DependencyInjectionActor); - var methodName = nameof(DependencyInjectionActor.IncrementAsync); - - var request = new HttpRequestMessage(HttpMethod.Put, $"http://localhost/actors/{actorTypeName}/{actorId}/method/{methodName}"); - var response = await httpClient.SendAsync(request); - await Assert2XXStatusAsync(response); - - return await response.Content.ReadAsStringAsync(); - } - - private async Task DeactivateActor(HttpClient httpClient, string actorId) - { - var actorTypeName = nameof(DependencyInjectionActor); - - var request = new HttpRequestMessage(HttpMethod.Delete, $"http://localhost/actors/{actorTypeName}/{actorId}"); - var response = await httpClient.SendAsync(request); - await Assert2XXStatusAsync(response); - } - - private async Task Assert2XXStatusAsync(HttpResponseMessage response) - { - if (response.IsSuccessStatusCode) - { - return; - } - - if (response.Content == null) - { - throw new XunitException($"The response failed with a {response.StatusCode} and no body."); - } - - // We assume a textual response. #YOLO - var text = await response.Content.ReadAsStringAsync(); - throw new XunitException($"The response failed with a {response.StatusCode} and body:" + Environment.NewLine + text); - } + await DeactivateActor(httpClient, "A"); } -} + + private async Task IncrementCounterAsync(HttpClient httpClient, string actorId) + { + var actorTypeName = nameof(DependencyInjectionActor); + var methodName = nameof(DependencyInjectionActor.IncrementAsync); + + var request = new HttpRequestMessage(HttpMethod.Put, $"http://localhost/actors/{actorTypeName}/{actorId}/method/{methodName}"); + var response = await httpClient.SendAsync(request); + await Assert2XXStatusAsync(response); + + return await response.Content.ReadAsStringAsync(); + } + + private async Task DeactivateActor(HttpClient httpClient, string actorId) + { + var actorTypeName = nameof(DependencyInjectionActor); + + var request = new HttpRequestMessage(HttpMethod.Delete, $"http://localhost/actors/{actorTypeName}/{actorId}"); + var response = await httpClient.SendAsync(request); + await Assert2XXStatusAsync(response); + } + + private async Task Assert2XXStatusAsync(HttpResponseMessage response) + { + if (response.IsSuccessStatusCode) + { + return; + } + + if (response.Content == null) + { + throw new XunitException($"The response failed with a {response.StatusCode} and no body."); + } + + // We assume a textual response. #YOLO + var text = await response.Content.ReadAsStringAsync(); + throw new XunitException($"The response failed with a {response.StatusCode} and body:" + Environment.NewLine + text); + } +} \ No newline at end of file diff --git a/test/Dapr.Actors.AspNetCore.IntegrationTest/AppWebApplicationFactory.cs b/test/Dapr.Actors.AspNetCore.IntegrationTest/AppWebApplicationFactory.cs index de27c930..3362eb10 100644 --- a/test/Dapr.Actors.AspNetCore.IntegrationTest/AppWebApplicationFactory.cs +++ b/test/Dapr.Actors.AspNetCore.IntegrationTest/AppWebApplicationFactory.cs @@ -11,30 +11,29 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.AspNetCore.IntegrationTest +namespace Dapr.Actors.AspNetCore.IntegrationTest; + +using Dapr.Actors.AspNetCore.IntegrationTest.App; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +// The customizations here suppress logging from showing up in the console when +// running at the command line. +public class AppWebApplicationFactory : WebApplicationFactory { - using Dapr.Actors.AspNetCore.IntegrationTest.App; - using Microsoft.AspNetCore.Mvc.Testing; - using Microsoft.Extensions.Hosting; - using Microsoft.Extensions.Logging; - - // The customizations here suppress logging from showing up in the console when - // running at the command line. - public class AppWebApplicationFactory : WebApplicationFactory + protected override IHostBuilder CreateHostBuilder() { - protected override IHostBuilder CreateHostBuilder() + var builder = base.CreateHostBuilder(); + if (builder == null) { - var builder = base.CreateHostBuilder(); - if (builder == null) - { - return null; - } - - builder.ConfigureLogging(b => - { - b.SetMinimumLevel(LogLevel.None); - }); - return builder; + return null; } + + builder.ConfigureLogging(b => + { + b.SetMinimumLevel(LogLevel.None); + }); + return builder; } -} +} \ No newline at end of file diff --git a/test/Dapr.Actors.AspNetCore.IntegrationTest/HostingTests.cs b/test/Dapr.Actors.AspNetCore.IntegrationTest/HostingTests.cs index bf3757ce..0460b47c 100644 --- a/test/Dapr.Actors.AspNetCore.IntegrationTest/HostingTests.cs +++ b/test/Dapr.Actors.AspNetCore.IntegrationTest/HostingTests.cs @@ -28,214 +28,213 @@ using Microsoft.Extensions.Logging; using Xunit; using Xunit.Sdk; -namespace Dapr.Actors.AspNetCore.IntegrationTest +namespace Dapr.Actors.AspNetCore.IntegrationTest; + +public class HostingTests { - public class HostingTests + [Fact] + public void MapActorsHandlers_WithoutAddActors_Throws() { - [Fact] - public void MapActorsHandlers_WithoutAddActors_Throws() + var exception = Assert.Throws(() => { - var exception = Assert.Throws(() => + // Initializes web pipeline which will trigger the exception we throw. + // + // NOTE: in 3.1 TestServer.CreateClient triggers the failure, in 5.0 it's Host.Start + using var host = CreateHost(); + var server = host.GetTestServer(); + server.CreateClient(); + }); + + Assert.Equal( + "The ActorRuntime service is not registered with the dependency injection container. " + + "Call AddActors() inside ConfigureServices() to register the actor runtime and actor types.", + exception.Message); + } + + [Fact] + public async Task MapActorsHandlers_IncludesHealthChecks() + { + using var factory = new AppWebApplicationFactory(); + + var httpClient = factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions { HandleCookies = false }); + var response = await httpClient.GetAsync("/healthz"); + await Assert2XXStatusAsync(response); + } + + [Fact] + public async Task ActorsHealthz_ShouldNotRequireAuthorization() + { + using var host = CreateHost(); + var server = host.GetTestServer(); + + var httpClient = server.CreateClient(); + var response = await httpClient.GetAsync("/healthz"); + await Assert2XXStatusAsync(response); + } + + // We add our own health check on /healthz with worse priority than one + // that would be added by a user. Make sure this works and the if the user + // adds their own health check it will win. + [Fact] + public async Task MapActorsHandlers_ActorHealthCheckDoesNotConflict() + { + using var host = CreateHost(); + var server = host.GetTestServer(); + + var httpClient = server.CreateClient(); + var response = await httpClient.GetAsync("/healthz"); + await Assert2XXStatusAsync(response); + + var text = await response.Content.ReadAsStringAsync(); + Assert.Equal("Ice Cold, Solid Gold!", text); + } + + // Regression test for #434 + [Fact] + public async Task MapActorsHandlers_WorksWithFallbackRoute() + { + using var host = CreateHost(); + var server = host.GetTestServer(); + + var httpClient = server.CreateClient(); + var response = await httpClient.GetAsync("/dapr/config"); + await Assert2XXStatusAsync(response); + } + + private static IHost CreateHost() where TStartup : class + { + var builder = Host + .CreateDefaultBuilder() + .ConfigureLogging(b => { - // Initializes web pipeline which will trigger the exception we throw. - // - // NOTE: in 3.1 TestServer.CreateClient triggers the failure, in 5.0 it's Host.Start - using var host = CreateHost(); - var server = host.GetTestServer(); - server.CreateClient(); + // shhhh + b.SetMinimumLevel(LogLevel.None); + }) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + webBuilder.UseTestServer(); }); - - Assert.Equal( - "The ActorRuntime service is not registered with the dependency injection container. " + - "Call AddActors() inside ConfigureServices() to register the actor runtime and actor types.", - exception.Message); + var host = builder.Build(); + try + { + host.Start(); + } + catch + { + host.Dispose(); + throw; } - [Fact] - public async Task MapActorsHandlers_IncludesHealthChecks() - { - using var factory = new AppWebApplicationFactory(); + return host; + } - var httpClient = factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions { HandleCookies = false }); - var response = await httpClient.GetAsync("/healthz"); - await Assert2XXStatusAsync(response); + private class BadStartup + { + public void ConfigureServices(IServiceCollection services) + { + // no call to AddActors here. That's bad! + services.AddRouting(); + services.AddHealthChecks(); } - [Fact] - public async Task ActorsHealthz_ShouldNotRequireAuthorization() + public void Configure(IApplicationBuilder app) { - using var host = CreateHost(); - var server = host.GetTestServer(); - - var httpClient = server.CreateClient(); - var response = await httpClient.GetAsync("/healthz"); - await Assert2XXStatusAsync(response); - } - - // We add our own health check on /healthz with worse priority than one - // that would be added by a user. Make sure this works and the if the user - // adds their own health check it will win. - [Fact] - public async Task MapActorsHandlers_ActorHealthCheckDoesNotConflict() - { - using var host = CreateHost(); - var server = host.GetTestServer(); - - var httpClient = server.CreateClient(); - var response = await httpClient.GetAsync("/healthz"); - await Assert2XXStatusAsync(response); - - var text = await response.Content.ReadAsStringAsync(); - Assert.Equal("Ice Cold, Solid Gold!", text); - } - - // Regression test for #434 - [Fact] - public async Task MapActorsHandlers_WorksWithFallbackRoute() - { - using var host = CreateHost(); - var server = host.GetTestServer(); - - var httpClient = server.CreateClient(); - var response = await httpClient.GetAsync("/dapr/config"); - await Assert2XXStatusAsync(response); - } - - private static IHost CreateHost() where TStartup : class - { - var builder = Host - .CreateDefaultBuilder() - .ConfigureLogging(b => - { - // shhhh - b.SetMinimumLevel(LogLevel.None); - }) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - webBuilder.UseTestServer(); - }); - var host = builder.Build(); - try + app.UseRouting(); + app.UseEndpoints(endpoints => { - host.Start(); - } - catch - { - host.Dispose(); - throw; - } - - return host; - } - - private class BadStartup - { - public void ConfigureServices(IServiceCollection services) - { - // no call to AddActors here. That's bad! - services.AddRouting(); - services.AddHealthChecks(); - } - - public void Configure(IApplicationBuilder app) - { - app.UseRouting(); - app.UseEndpoints(endpoints => - { - endpoints.MapActorsHandlers(); - }); - } - } - - private class AuthorizedRoutesStartup - { - public void ConfigureServices(IServiceCollection services) - { - services.AddActors(default); - services.AddAuthentication().AddDapr(options => options.Token = "abcdefg"); - - services.AddAuthorization(o => o.AddDapr()); - } - - public void Configure(IApplicationBuilder app) - { - app.UseRouting(); - app.UseAuthentication(); - app.UseAuthorization(); - app.UseEndpoints(endpoints => - { - endpoints.MapActorsHandlers().RequireAuthorization("Dapr"); - }); - } - } - - private class FallbackRouteStartup - { - public void ConfigureServices(IServiceCollection services) - { - services.AddActors(default); - } - - public void Configure(IApplicationBuilder app) - { - app.UseRouting(); - app.UseEndpoints(endpoints => - { - // This routing feature registers a "route of last resort" which is what - // was tripping out Actors prior to changing how they are registered. - endpoints.MapFallback(context => - { - throw new InvalidTimeZoneException("This should not be called!"); - }); - endpoints.MapActorsHandlers(); - }); - } - } - - private class HealthCheckStartup - { - public void ConfigureServices(IServiceCollection services) - { - services.AddActors(default); - } - - public void Configure(IApplicationBuilder app) - { - app.UseRouting(); - app.UseEndpoints(endpoints => - { - endpoints.MapHealthChecks("/healthz", new HealthCheckOptions() - { - // Write something different so we know this one is called. - ResponseWriter = async (httpContext, report) => - { - await httpContext.Response.WriteAsync( - report.Status == HealthStatus.Healthy ? - "Ice Cold, Solid Gold!" : - "Oh Noes!"); - }, - }); - endpoints.MapActorsHandlers(); - }); - } - } - - private async Task Assert2XXStatusAsync(HttpResponseMessage response) - { - if (response.IsSuccessStatusCode) - { - return; - } - - if (response.Content == null) - { - throw new XunitException($"The response failed with a {response.StatusCode} and no body."); - } - - // We assume a textual response. #YOLO - var text = await response.Content.ReadAsStringAsync(); - throw new XunitException($"The response failed with a {response.StatusCode} and body:" + Environment.NewLine + text); + endpoints.MapActorsHandlers(); + }); } } -} + + private class AuthorizedRoutesStartup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddActors(default); + services.AddAuthentication().AddDapr(options => options.Token = "abcdefg"); + + services.AddAuthorization(o => o.AddDapr()); + } + + public void Configure(IApplicationBuilder app) + { + app.UseRouting(); + app.UseAuthentication(); + app.UseAuthorization(); + app.UseEndpoints(endpoints => + { + endpoints.MapActorsHandlers().RequireAuthorization("Dapr"); + }); + } + } + + private class FallbackRouteStartup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddActors(default); + } + + public void Configure(IApplicationBuilder app) + { + app.UseRouting(); + app.UseEndpoints(endpoints => + { + // This routing feature registers a "route of last resort" which is what + // was tripping out Actors prior to changing how they are registered. + endpoints.MapFallback(context => + { + throw new InvalidTimeZoneException("This should not be called!"); + }); + endpoints.MapActorsHandlers(); + }); + } + } + + private class HealthCheckStartup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddActors(default); + } + + public void Configure(IApplicationBuilder app) + { + app.UseRouting(); + app.UseEndpoints(endpoints => + { + endpoints.MapHealthChecks("/healthz", new HealthCheckOptions() + { + // Write something different so we know this one is called. + ResponseWriter = async (httpContext, report) => + { + await httpContext.Response.WriteAsync( + report.Status == HealthStatus.Healthy ? + "Ice Cold, Solid Gold!" : + "Oh Noes!"); + }, + }); + endpoints.MapActorsHandlers(); + }); + } + } + + private async Task Assert2XXStatusAsync(HttpResponseMessage response) + { + if (response.IsSuccessStatusCode) + { + return; + } + + if (response.Content == null) + { + throw new XunitException($"The response failed with a {response.StatusCode} and no body."); + } + + // We assume a textual response. #YOLO + var text = await response.Content.ReadAsStringAsync(); + throw new XunitException($"The response failed with a {response.StatusCode} and body:" + Environment.NewLine + text); + } +} \ No newline at end of file diff --git a/test/Dapr.Actors.AspNetCore.Test/ActorHostingTest.cs b/test/Dapr.Actors.AspNetCore.Test/ActorHostingTest.cs index 8e34eaff..53c8ec9e 100644 --- a/test/Dapr.Actors.AspNetCore.Test/ActorHostingTest.cs +++ b/test/Dapr.Actors.AspNetCore.Test/ActorHostingTest.cs @@ -18,94 +18,93 @@ using Dapr.Actors.Runtime; using Microsoft.Extensions.DependencyInjection; using Xunit; -namespace Dapr.Actors.AspNetCore +namespace Dapr.Actors.AspNetCore; + +public class ActorHostingTest { - public class ActorHostingTest + [Fact] + public void CanRegisterActorsInSingleCalls() { - [Fact] - public void CanRegisterActorsInSingleCalls() + var services = new ServiceCollection(); + services.AddLogging(); + services.AddOptions(); + services.AddActors(options => { - var services = new ServiceCollection(); - services.AddLogging(); - services.AddOptions(); - services.AddActors(options => - { - options.Actors.RegisterActor(); - options.Actors.RegisterActor(); - }); + options.Actors.RegisterActor(); + options.Actors.RegisterActor(); + }); - var runtime = services.BuildServiceProvider().GetRequiredService(); + var runtime = services.BuildServiceProvider().GetRequiredService(); - Assert.Collection( - runtime.RegisteredActors.Select(r => r.Type.ActorTypeName).OrderBy(t => t), - t => Assert.Equal(ActorTypeInformation.Get(typeof(TestActor1), actorTypeName: null).ActorTypeName, t), - t => Assert.Equal(ActorTypeInformation.Get(typeof(TestActor2), actorTypeName: null).ActorTypeName, t)); - } + Assert.Collection( + runtime.RegisteredActors.Select(r => r.Type.ActorTypeName).OrderBy(t => t), + t => Assert.Equal(ActorTypeInformation.Get(typeof(TestActor1), actorTypeName: null).ActorTypeName, t), + t => Assert.Equal(ActorTypeInformation.Get(typeof(TestActor2), actorTypeName: null).ActorTypeName, t)); + } - [Fact] - public void CanRegisterActorsInMultipleCalls() + [Fact] + public void CanRegisterActorsInMultipleCalls() + { + var services = new ServiceCollection(); + services.AddLogging(); + services.AddOptions(); + services.AddActors(options => { - var services = new ServiceCollection(); - services.AddLogging(); - services.AddOptions(); - services.AddActors(options => - { - options.Actors.RegisterActor(); - }); + options.Actors.RegisterActor(); + }); - services.AddActors(options => - { - options.Actors.RegisterActor(); - }); - - var runtime = services.BuildServiceProvider().GetRequiredService(); - - Assert.Collection( - runtime.RegisteredActors.Select(r => r.Type.ActorTypeName).OrderBy(t => t), - t => Assert.Equal(ActorTypeInformation.Get(typeof(TestActor1), actorTypeName: null).ActorTypeName, t), - t => Assert.Equal(ActorTypeInformation.Get(typeof(TestActor2), actorTypeName: null).ActorTypeName, t)); - } - - [Fact] - public void CanAccessProxyFactoryWithCustomJsonOptions() + services.AddActors(options => { - var jsonOptions = new JsonSerializerOptions(); + options.Actors.RegisterActor(); + }); - var services = new ServiceCollection(); - services.AddLogging(); - services.AddOptions(); - services.AddActors(options => - { - options.JsonSerializerOptions = jsonOptions; - }); + var runtime = services.BuildServiceProvider().GetRequiredService(); + + Assert.Collection( + runtime.RegisteredActors.Select(r => r.Type.ActorTypeName).OrderBy(t => t), + t => Assert.Equal(ActorTypeInformation.Get(typeof(TestActor1), actorTypeName: null).ActorTypeName, t), + t => Assert.Equal(ActorTypeInformation.Get(typeof(TestActor2), actorTypeName: null).ActorTypeName, t)); + } + + [Fact] + public void CanAccessProxyFactoryWithCustomJsonOptions() + { + var jsonOptions = new JsonSerializerOptions(); + + var services = new ServiceCollection(); + services.AddLogging(); + services.AddOptions(); + services.AddActors(options => + { + options.JsonSerializerOptions = jsonOptions; + }); - services.AddActors(options => - { - options.Actors.RegisterActor(); - }); - - var factory = (ActorProxyFactory)services.BuildServiceProvider().GetRequiredService(); - Assert.Same(jsonOptions, factory.DefaultOptions.JsonSerializerOptions); - } - - private interface ITestActor : IActor + services.AddActors(options => { - } + options.Actors.RegisterActor(); + }); - private class TestActor1 : Actor, ITestActor - { - public TestActor1(ActorHost host) - : base(host) - { - } - } + var factory = (ActorProxyFactory)services.BuildServiceProvider().GetRequiredService(); + Assert.Same(jsonOptions, factory.DefaultOptions.JsonSerializerOptions); + } - private class TestActor2 : Actor, ITestActor + private interface ITestActor : IActor + { + } + + private class TestActor1 : Actor, ITestActor + { + public TestActor1(ActorHost host) + : base(host) { - public TestActor2(ActorHost host) - : base(host) - { - } } } -} + + private class TestActor2 : Actor, ITestActor + { + public TestActor2(ActorHost host) + : base(host) + { + } + } +} \ No newline at end of file diff --git a/test/Dapr.Actors.AspNetCore.Test/Runtime/ActorsEndpointRouteBuilderExtensionsTests.cs b/test/Dapr.Actors.AspNetCore.Test/Runtime/ActorsEndpointRouteBuilderExtensionsTests.cs index e02ba39b..9fc67760 100644 --- a/test/Dapr.Actors.AspNetCore.Test/Runtime/ActorsEndpointRouteBuilderExtensionsTests.cs +++ b/test/Dapr.Actors.AspNetCore.Test/Runtime/ActorsEndpointRouteBuilderExtensionsTests.cs @@ -21,82 +21,81 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Xunit; -namespace Dapr.Actors.Runtime +namespace Dapr.Actors.Runtime; + +public class ActorsEndpointRouteBuilderExtensionsTests { - public class ActorsEndpointRouteBuilderExtensionsTests + [Fact] + public async Task MapActorsHandlers_MapDaprConfigEndpoint() { - [Fact] - public async Task MapActorsHandlers_MapDaprConfigEndpoint() + using var host = CreateHost(options => { - using var host = CreateHost(options => + options.Actors.RegisterActor(); + }); + var server = host.GetTestServer(); + + var httpClient = server.CreateClient(); + var response = await httpClient.GetAsync("/dapr/config"); + + var text = await response.Content.ReadAsStringAsync(); + Assert.Equal(@"{""entities"":[""TestActor""],""reentrancy"":{""enabled"":false}}", text); + } + + private static IHost CreateHost(Action configure) where TStartup : class + { + var builder = Host + .CreateDefaultBuilder() + .ConfigureLogging(b => { - options.Actors.RegisterActor(); + // shhhh + b.SetMinimumLevel(LogLevel.None); + }) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + webBuilder.UseTestServer(); + }) + .ConfigureServices(services => + { + services.AddActors(configure); }); - var server = host.GetTestServer(); - - var httpClient = server.CreateClient(); - var response = await httpClient.GetAsync("/dapr/config"); - - var text = await response.Content.ReadAsStringAsync(); - Assert.Equal(@"{""entities"":[""TestActor""],""reentrancy"":{""enabled"":false}}", text); + var host = builder.Build(); + try + { + host.Start(); + } + catch + { + host.Dispose(); + throw; } - private static IHost CreateHost(Action configure) where TStartup : class - { - var builder = Host - .CreateDefaultBuilder() - .ConfigureLogging(b => - { - // shhhh - b.SetMinimumLevel(LogLevel.None); - }) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - webBuilder.UseTestServer(); - }) - .ConfigureServices(services => - { - services.AddActors(configure); - }); - var host = builder.Build(); - try - { - host.Start(); - } - catch - { - host.Dispose(); - throw; - } + return host; + } - return host; + private class ActorsStartup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddActors(default); } - private class ActorsStartup + public void Configure(IApplicationBuilder app) { - public void ConfigureServices(IServiceCollection services) + app.UseRouting(); + app.UseEndpoints(endpoints => { - services.AddActors(default); - } - - public void Configure(IApplicationBuilder app) - { - app.UseRouting(); - app.UseEndpoints(endpoints => - { - endpoints.MapActorsHandlers(); - }); - } - } - - private interface ITestActor : IActor - { - } - - private class TestActor : Actor, ITestActor - { - public TestActor(ActorHost host) : base(host) { } + endpoints.MapActorsHandlers(); + }); } } -} + + private interface ITestActor : IActor + { + } + + private class TestActor : Actor, ITestActor + { + public TestActor(ActorHost host) : base(host) { } + } +} \ No newline at end of file diff --git a/test/Dapr.Actors.AspNetCore.Test/Runtime/DependencyInjectionActorActivatorTests.cs b/test/Dapr.Actors.AspNetCore.Test/Runtime/DependencyInjectionActorActivatorTests.cs index 9c3b5536..0ebb8d59 100644 --- a/test/Dapr.Actors.AspNetCore.Test/Runtime/DependencyInjectionActorActivatorTests.cs +++ b/test/Dapr.Actors.AspNetCore.Test/Runtime/DependencyInjectionActorActivatorTests.cs @@ -16,169 +16,168 @@ using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Xunit; -namespace Dapr.Actors.Runtime +namespace Dapr.Actors.Runtime; + +public class DependencyInjectionActorActivatorTests { - public class DependencyInjectionActorActivatorTests + private IServiceProvider CreateServices() { - private IServiceProvider CreateServices() + var services = new ServiceCollection(); + services.AddScoped(); + services.AddSingleton(); + return services.BuildServiceProvider(new ServiceProviderOptions() { ValidateScopes = true, }); + } + + private DependencyInjectionActorActivator CreateActivator(Type type) + { + return new DependencyInjectionActorActivator(CreateServices(), ActorTypeInformation.Get(type, actorTypeName: null)); + } + + [Fact] + public async Task CreateAsync_CanActivateWithDI() + { + var activator = CreateActivator(typeof(TestActor)); + + var host = ActorHost.CreateForTest(); + var state = await activator.CreateAsync(host); + var actor = Assert.IsType(state.Actor); + + Assert.NotNull(actor.SingletonService); + Assert.NotNull(actor.ScopedService); + } + + [Fact] + public async Task CreateAsync_CreatesNewScope() + { + var activator = CreateActivator(typeof(TestActor)); + + var host1 = ActorHost.CreateForTest(); + var state1 = await activator.CreateAsync(host1); + var actor1 = Assert.IsType(state1.Actor); + + var host2 = ActorHost.CreateForTest(); + var state2 = await activator.CreateAsync(host2); + var actor2 = Assert.IsType(state2.Actor); + + Assert.Same(actor1.SingletonService, actor2.SingletonService); + Assert.NotSame(actor1.ScopedService, actor2.ScopedService); + } + + [Fact] + public async Task CreateAsync_CustomJsonOptions() + { + var jsonOptions = new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = false }; + var activator = CreateActivator(typeof(TestActor)); + + var host = ActorHost.CreateForTest(new ActorTestOptions { JsonSerializerOptions = jsonOptions }); + var state = await activator.CreateAsync(host); + + Assert.Same(jsonOptions, state.Actor.Host.JsonSerializerOptions); + } + + [Fact] + public async Task DeleteAsync_DisposesScope() + { + var activator = CreateActivator(typeof(TestActor)); + + var host = ActorHost.CreateForTest(); + var state = await activator.CreateAsync(host); + var actor = Assert.IsType(state.Actor); + + Assert.False(actor.ScopedService.IsDisposed); + + await activator.DeleteAsync(state); + + Assert.True(actor.ScopedService.IsDisposed); + } + + [Fact] + public async Task DeleteAsync_Disposable() + { + var activator = CreateActivator(typeof(DisposableActor)); + + var host = ActorHost.CreateForTest(); + var state = await activator.CreateAsync(host); + var actor = Assert.IsType(state.Actor); + + await activator.DeleteAsync(state); // does not throw + + Assert.True(actor.IsDisposed); + } + + [Fact] + public async Task DeleteAsync_AsyncDisposable() + { + var activator = CreateActivator(typeof(AsyncDisposableActor)); + + var host = ActorHost.CreateForTest(); + var state = await activator.CreateAsync(host); + var actor = Assert.IsType(state.Actor); + + await activator.DeleteAsync(state); + + Assert.True(actor.IsDisposed); + } + + private class TestSingletonService + { + } + + private class TestScopedService : IDisposable + { + public bool IsDisposed { get; set; } + + public void Dispose() { - var services = new ServiceCollection(); - services.AddScoped(); - services.AddSingleton(); - return services.BuildServiceProvider(new ServiceProviderOptions() { ValidateScopes = true, }); - } - - private DependencyInjectionActorActivator CreateActivator(Type type) - { - return new DependencyInjectionActorActivator(CreateServices(), ActorTypeInformation.Get(type, actorTypeName: null)); - } - - [Fact] - public async Task CreateAsync_CanActivateWithDI() - { - var activator = CreateActivator(typeof(TestActor)); - - var host = ActorHost.CreateForTest(); - var state = await activator.CreateAsync(host); - var actor = Assert.IsType(state.Actor); - - Assert.NotNull(actor.SingletonService); - Assert.NotNull(actor.ScopedService); - } - - [Fact] - public async Task CreateAsync_CreatesNewScope() - { - var activator = CreateActivator(typeof(TestActor)); - - var host1 = ActorHost.CreateForTest(); - var state1 = await activator.CreateAsync(host1); - var actor1 = Assert.IsType(state1.Actor); - - var host2 = ActorHost.CreateForTest(); - var state2 = await activator.CreateAsync(host2); - var actor2 = Assert.IsType(state2.Actor); - - Assert.Same(actor1.SingletonService, actor2.SingletonService); - Assert.NotSame(actor1.ScopedService, actor2.ScopedService); - } - - [Fact] - public async Task CreateAsync_CustomJsonOptions() - { - var jsonOptions = new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = false }; - var activator = CreateActivator(typeof(TestActor)); - - var host = ActorHost.CreateForTest(new ActorTestOptions { JsonSerializerOptions = jsonOptions }); - var state = await activator.CreateAsync(host); - - Assert.Same(jsonOptions, state.Actor.Host.JsonSerializerOptions); - } - - [Fact] - public async Task DeleteAsync_DisposesScope() - { - var activator = CreateActivator(typeof(TestActor)); - - var host = ActorHost.CreateForTest(); - var state = await activator.CreateAsync(host); - var actor = Assert.IsType(state.Actor); - - Assert.False(actor.ScopedService.IsDisposed); - - await activator.DeleteAsync(state); - - Assert.True(actor.ScopedService.IsDisposed); - } - - [Fact] - public async Task DeleteAsync_Disposable() - { - var activator = CreateActivator(typeof(DisposableActor)); - - var host = ActorHost.CreateForTest(); - var state = await activator.CreateAsync(host); - var actor = Assert.IsType(state.Actor); - - await activator.DeleteAsync(state); // does not throw - - Assert.True(actor.IsDisposed); - } - - [Fact] - public async Task DeleteAsync_AsyncDisposable() - { - var activator = CreateActivator(typeof(AsyncDisposableActor)); - - var host = ActorHost.CreateForTest(); - var state = await activator.CreateAsync(host); - var actor = Assert.IsType(state.Actor); - - await activator.DeleteAsync(state); - - Assert.True(actor.IsDisposed); - } - - private class TestSingletonService - { - } - - private class TestScopedService : IDisposable - { - public bool IsDisposed { get; set; } - - public void Dispose() - { - IsDisposed = true; - } - } - - private interface ITestActor : IActor - { - } - - private class TestActor : Actor, ITestActor - { - public TestActor(ActorHost host, TestSingletonService singletonService, TestScopedService scopedService) - : base(host) - { - this.SingletonService = singletonService; - this.ScopedService = scopedService; - } - - public TestSingletonService SingletonService { get; } - public TestScopedService ScopedService { get; } - } - - private class DisposableActor : Actor, ITestActor, IDisposable - { - public DisposableActor(ActorHost host) - : base(host) - { - } - - public bool IsDisposed { get; set; } - - public void Dispose() - { - IsDisposed = true; - } - } - - private class AsyncDisposableActor : Actor, ITestActor, IAsyncDisposable - { - public AsyncDisposableActor(ActorHost host) - : base(host) - { - } - - public bool IsDisposed { get; set; } - - public ValueTask DisposeAsync() - { - IsDisposed = true; - return new ValueTask(); - } + IsDisposed = true; } } -} + + private interface ITestActor : IActor + { + } + + private class TestActor : Actor, ITestActor + { + public TestActor(ActorHost host, TestSingletonService singletonService, TestScopedService scopedService) + : base(host) + { + this.SingletonService = singletonService; + this.ScopedService = scopedService; + } + + public TestSingletonService SingletonService { get; } + public TestScopedService ScopedService { get; } + } + + private class DisposableActor : Actor, ITestActor, IDisposable + { + public DisposableActor(ActorHost host) + : base(host) + { + } + + public bool IsDisposed { get; set; } + + public void Dispose() + { + IsDisposed = true; + } + } + + private class AsyncDisposableActor : Actor, ITestActor, IAsyncDisposable + { + public AsyncDisposableActor(ActorHost host) + : base(host) + { + } + + public bool IsDisposed { get; set; } + + public ValueTask DisposeAsync() + { + IsDisposed = true; + return new ValueTask(); + } + } +} \ No newline at end of file diff --git a/test/Dapr.Actors.Generators.Test/Extensions/IEnumerableExtensionsTests.cs b/test/Dapr.Actors.Generators.Test/Extensions/IEnumerableExtensionsTests.cs index 97dbcfe1..5b73fb95 100644 --- a/test/Dapr.Actors.Generators.Test/Extensions/IEnumerableExtensionsTests.cs +++ b/test/Dapr.Actors.Generators.Test/Extensions/IEnumerableExtensionsTests.cs @@ -1,52 +1,51 @@ using Dapr.Actors.Generators.Extensions; -namespace Dapr.Actors.Generators.Test.Extensions +namespace Dapr.Actors.Generators.Test.Extensions; + +public class IEnumerableExtensionsTests { - public class IEnumerableExtensionsTests + [Fact] + public void IndexOf_WhenPredicateIsNull_ThrowsArgumentNullException() { - [Fact] - public void IndexOf_WhenPredicateIsNull_ThrowsArgumentNullException() - { - // Arrange - var source = new[] { 1, 2, 3, 4, 5 }; - Func predicate = null!; + // Arrange + var source = new[] { 1, 2, 3, 4, 5 }; + Func predicate = null!; - // Act - Action act = () => source.IndexOf(predicate); + // Act + Action act = () => source.IndexOf(predicate); - // Assert - Assert.Throws(act); - } - - [Theory] - [InlineData(new int[] { }, 3, -1)] - [InlineData(new[] { 1, 2, 3, 4, 5 }, 6, -1)] - public void IndexOf_WhenItemDoesNotExist_ReturnsMinusOne(int[] source, int item, int expected) - { - // Arrange - Func predicate = (x) => x == item; - - // Act - var index = source.IndexOf(predicate); - - // Assert - Assert.Equal(expected, index); - } - - [Theory] - [InlineData(new[] { 1, 2, 3, 4, 5 }, 3, 2)] - [InlineData(new[] { 1, 2, 3, 4, 5 }, 1, 0)] - [InlineData(new[] { 1, 2, 3, 4, 5 }, 5, 4)] - public void IndexOf_WhenItemExists_ReturnsIndexOfItem(int[] source, int item, int expected) - { - // Arrange - Func predicate = (x) => x == item; - - // Act - var index = source.IndexOf(predicate); - - // Assert - Assert.Equal(expected, index); - } + // Assert + Assert.Throws(act); } -} + + [Theory] + [InlineData(new int[] { }, 3, -1)] + [InlineData(new[] { 1, 2, 3, 4, 5 }, 6, -1)] + public void IndexOf_WhenItemDoesNotExist_ReturnsMinusOne(int[] source, int item, int expected) + { + // Arrange + Func predicate = (x) => x == item; + + // Act + var index = source.IndexOf(predicate); + + // Assert + Assert.Equal(expected, index); + } + + [Theory] + [InlineData(new[] { 1, 2, 3, 4, 5 }, 3, 2)] + [InlineData(new[] { 1, 2, 3, 4, 5 }, 1, 0)] + [InlineData(new[] { 1, 2, 3, 4, 5 }, 5, 4)] + public void IndexOf_WhenItemExists_ReturnsIndexOfItem(int[] source, int item, int expected) + { + // Arrange + Func predicate = (x) => x == item; + + // Act + var index = source.IndexOf(predicate); + + // Assert + Assert.Equal(expected, index); + } +} \ No newline at end of file diff --git a/test/Dapr.Actors.Generators.Test/Helpers/SyntaxFactoryHelpersTests.cs b/test/Dapr.Actors.Generators.Test/Helpers/SyntaxFactoryHelpersTests.cs index 807bd746..6d36cc12 100644 --- a/test/Dapr.Actors.Generators.Test/Helpers/SyntaxFactoryHelpersTests.cs +++ b/test/Dapr.Actors.Generators.Test/Helpers/SyntaxFactoryHelpersTests.cs @@ -2,132 +2,131 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; -namespace Dapr.Actors.Generators.Test.Helpers +namespace Dapr.Actors.Generators.Test.Helpers; + +public class SyntaxFactoryHelpersTests { - public class SyntaxFactoryHelpersTests + [Fact] + public void ThrowArgumentNullException_GenerateThrowArgumentNullExceptionSyntaxWithGivenArgumentName() { - [Fact] - public void ThrowArgumentNullException_GenerateThrowArgumentNullExceptionSyntaxWithGivenArgumentName() - { - // Arrange - var argumentName = "arg0"; - var expectedSource = $@"throw new System.ArgumentNullException(nameof(arg0));"; - var expectedSourceNormalized = SyntaxFactory.ParseSyntaxTree(expectedSource) - .GetRoot() - .NormalizeWhitespace() - .ToFullString(); + // Arrange + var argumentName = "arg0"; + var expectedSource = $@"throw new System.ArgumentNullException(nameof(arg0));"; + var expectedSourceNormalized = SyntaxFactory.ParseSyntaxTree(expectedSource) + .GetRoot() + .NormalizeWhitespace() + .ToFullString(); - // Act - var generatedSource = SyntaxFactory.ExpressionStatement(SyntaxFactoryHelpers.ThrowArgumentNullException(argumentName)) - .SyntaxTree - .GetRoot() - .NormalizeWhitespace() - .ToFullString(); + // Act + var generatedSource = SyntaxFactory.ExpressionStatement(SyntaxFactoryHelpers.ThrowArgumentNullException(argumentName)) + .SyntaxTree + .GetRoot() + .NormalizeWhitespace() + .ToFullString(); - // Assert - Assert.Equal(expectedSourceNormalized, generatedSource); - } + // Assert + Assert.Equal(expectedSourceNormalized, generatedSource); + } - [Fact] - public void ThrowIfArgumentNullException_GivesNullCheckSyntaxWithGivenArgumentName() - { - // Arrange - var argumentName = "arg0"; - var expectedSource = $@"if (arg0 is null) + [Fact] + public void ThrowIfArgumentNullException_GivesNullCheckSyntaxWithGivenArgumentName() + { + // Arrange + var argumentName = "arg0"; + var expectedSource = $@"if (arg0 is null) {{ throw new System.ArgumentNullException(nameof(arg0)); }}"; - var expectedSourceNormalized = SyntaxFactory.ParseSyntaxTree(expectedSource) - .GetRoot() - .NormalizeWhitespace() - .ToFullString(); + var expectedSourceNormalized = SyntaxFactory.ParseSyntaxTree(expectedSource) + .GetRoot() + .NormalizeWhitespace() + .ToFullString(); - // Act - var generatedSource = SyntaxFactoryHelpers.ThrowIfArgumentNull(argumentName) - .SyntaxTree - .GetRoot() - .NormalizeWhitespace() - .ToFullString(); + // Act + var generatedSource = SyntaxFactoryHelpers.ThrowIfArgumentNull(argumentName) + .SyntaxTree + .GetRoot() + .NormalizeWhitespace() + .ToFullString(); - // Assert - Assert.Equal(expectedSourceNormalized, generatedSource); - } + // Assert + Assert.Equal(expectedSourceNormalized, generatedSource); + } - [Fact] - public void ActorProxyInvokeMethodAsync_WithoutReturnTypeAndParamters_ReturnNonGenericInvokeMethodAsync() - { - // Arrange - var remoteMethodName = "RemoteMethodToCall"; - var remoteMethodParameters = Array.Empty(); - var remoteMethodReturnTypes = Array.Empty(); - var actorProxMemberAccessSyntax = SyntaxFactory.MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - SyntaxFactory.ThisExpression(), - SyntaxFactory.IdentifierName("actorProxy") - ); - var expectedSource = $@"this.actorProxy.InvokeMethodAsync(""RemoteMethodToCall"")"; - var expectedSourceNormalized = SyntaxFactory.ParseSyntaxTree(expectedSource) - .GetRoot() - .NormalizeWhitespace() - .ToFullString(); + [Fact] + public void ActorProxyInvokeMethodAsync_WithoutReturnTypeAndParamters_ReturnNonGenericInvokeMethodAsync() + { + // Arrange + var remoteMethodName = "RemoteMethodToCall"; + var remoteMethodParameters = Array.Empty(); + var remoteMethodReturnTypes = Array.Empty(); + var actorProxMemberAccessSyntax = SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.ThisExpression(), + SyntaxFactory.IdentifierName("actorProxy") + ); + var expectedSource = $@"this.actorProxy.InvokeMethodAsync(""RemoteMethodToCall"")"; + var expectedSourceNormalized = SyntaxFactory.ParseSyntaxTree(expectedSource) + .GetRoot() + .NormalizeWhitespace() + .ToFullString(); - // Act - var generatedSource = SyntaxFactoryHelpers.ActorProxyInvokeMethodAsync( + // Act + var generatedSource = SyntaxFactoryHelpers.ActorProxyInvokeMethodAsync( actorProxMemberAccessSyntax, remoteMethodName, remoteMethodParameters, remoteMethodReturnTypes) - .SyntaxTree - .GetRoot() - .NormalizeWhitespace() - .ToFullString(); ; + .SyntaxTree + .GetRoot() + .NormalizeWhitespace() + .ToFullString(); ; - // Assert - Assert.Equal(expectedSourceNormalized, generatedSource); - } - - [Fact] - public void NameOfExpression() - { - // Arrange - var argumentName = "arg0"; - var expectedSource = $@"nameof(arg0)"; - var expectedSourceNormalized = SyntaxFactory.ParseSyntaxTree(expectedSource) - .GetRoot() - .NormalizeWhitespace() - .ToFullString(); - - // Act - var generatedSource = SyntaxFactoryHelpers.NameOfExpression(argumentName) - .SyntaxTree - .GetRoot() - .NormalizeWhitespace() - .ToFullString(); - - // Assert - Assert.Equal(expectedSourceNormalized, generatedSource); - } - - [Theory] - [InlineData(Accessibility.Public, new[] { SyntaxKind.PublicKeyword })] - [InlineData(Accessibility.Internal, new[] { SyntaxKind.InternalKeyword })] - [InlineData(Accessibility.Private, new[] { SyntaxKind.PrivateKeyword })] - [InlineData(Accessibility.Protected, new[] { SyntaxKind.ProtectedKeyword })] - [InlineData(Accessibility.ProtectedAndInternal, new[] { SyntaxKind.ProtectedKeyword, SyntaxKind.InternalKeyword })] - public void GetSyntaxKinds_GenerateSyntaxForGivenAccessibility(Accessibility accessibility, ICollection expectedSyntaxKinds) - { - // Arrange - - // Act - var generatedSyntaxKinds = SyntaxFactoryHelpers.GetSyntaxKinds(accessibility); - - // Assert - foreach (var expectedSyntaxKind in expectedSyntaxKinds) - { - Assert.Contains(expectedSyntaxKind, generatedSyntaxKinds); - } - - Assert.Equal(expectedSyntaxKinds.Count, generatedSyntaxKinds.Count); - } + // Assert + Assert.Equal(expectedSourceNormalized, generatedSource); } -} + + [Fact] + public void NameOfExpression() + { + // Arrange + var argumentName = "arg0"; + var expectedSource = $@"nameof(arg0)"; + var expectedSourceNormalized = SyntaxFactory.ParseSyntaxTree(expectedSource) + .GetRoot() + .NormalizeWhitespace() + .ToFullString(); + + // Act + var generatedSource = SyntaxFactoryHelpers.NameOfExpression(argumentName) + .SyntaxTree + .GetRoot() + .NormalizeWhitespace() + .ToFullString(); + + // Assert + Assert.Equal(expectedSourceNormalized, generatedSource); + } + + [Theory] + [InlineData(Accessibility.Public, new[] { SyntaxKind.PublicKeyword })] + [InlineData(Accessibility.Internal, new[] { SyntaxKind.InternalKeyword })] + [InlineData(Accessibility.Private, new[] { SyntaxKind.PrivateKeyword })] + [InlineData(Accessibility.Protected, new[] { SyntaxKind.ProtectedKeyword })] + [InlineData(Accessibility.ProtectedAndInternal, new[] { SyntaxKind.ProtectedKeyword, SyntaxKind.InternalKeyword })] + public void GetSyntaxKinds_GenerateSyntaxForGivenAccessibility(Accessibility accessibility, ICollection expectedSyntaxKinds) + { + // Arrange + + // Act + var generatedSyntaxKinds = SyntaxFactoryHelpers.GetSyntaxKinds(accessibility); + + // Assert + foreach (var expectedSyntaxKind in expectedSyntaxKinds) + { + Assert.Contains(expectedSyntaxKind, generatedSyntaxKinds); + } + + Assert.Equal(expectedSyntaxKinds.Count, generatedSyntaxKinds.Count); + } +} \ No newline at end of file diff --git a/test/Dapr.Actors.Test/ActorStateManagerTest.cs b/test/Dapr.Actors.Test/ActorStateManagerTest.cs index a4e0e414..ae15e89c 100644 --- a/test/Dapr.Actors.Test/ActorStateManagerTest.cs +++ b/test/Dapr.Actors.Test/ActorStateManagerTest.cs @@ -11,182 +11,181 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Test +namespace Dapr.Actors.Test; + +using System; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; +using Xunit; +using Dapr.Actors.Communication; +using Dapr.Actors.Runtime; +using Moq; + +/// +/// Contains tests for ActorStateManager. +/// +public class ActorStateManagerTest { - using System; - using System.Text.Json; - using System.Threading; - using System.Threading.Tasks; - using System.Collections.Generic; - using Xunit; - using Dapr.Actors.Communication; - using Dapr.Actors.Runtime; - using Moq; - - /// - /// Contains tests for ActorStateManager. - /// - public class ActorStateManagerTest + [Fact] + public async Task SetGet() { - [Fact] - public async Task SetGet() - { - var interactor = new Mock(); - var host = ActorHost.CreateForTest(); - host.StateProvider = new DaprStateProvider(interactor.Object, new JsonSerializerOptions()); - var mngr = new ActorStateManager(new TestActor(host)); - var token = new CancellationToken(); + var interactor = new Mock(); + var host = ActorHost.CreateForTest(); + host.StateProvider = new DaprStateProvider(interactor.Object, new JsonSerializerOptions()); + var mngr = new ActorStateManager(new TestActor(host)); + var token = new CancellationToken(); - interactor - .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(new ActorStateResponse("", null))); + interactor + .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(new ActorStateResponse("", null))); - await mngr.AddStateAsync("key1", "value1", token); - await mngr.AddStateAsync("key2", "value2", token); - Assert.Equal("value1", await mngr.GetStateAsync("key1", token)); - Assert.Equal("value2", await mngr.GetStateAsync("key2", token)); + await mngr.AddStateAsync("key1", "value1", token); + await mngr.AddStateAsync("key2", "value2", token); + Assert.Equal("value1", await mngr.GetStateAsync("key1", token)); + Assert.Equal("value2", await mngr.GetStateAsync("key2", token)); - await Assert.ThrowsAsync(() => mngr.AddStateAsync("key1", "value3", token)); - await Assert.ThrowsAsync(() => mngr.AddStateAsync("key2", "value4", token)); + await Assert.ThrowsAsync(() => mngr.AddStateAsync("key1", "value3", token)); + await Assert.ThrowsAsync(() => mngr.AddStateAsync("key2", "value4", token)); - await mngr.SetStateAsync("key1", "value5", token); - await mngr.SetStateAsync("key2", "value6", token); - Assert.Equal("value5", await mngr.GetStateAsync("key1", token)); - Assert.Equal("value6", await mngr.GetStateAsync("key2", token)); - } - - [Fact] - public async Task StateWithTTL() - { - var interactor = new Mock(); - var host = ActorHost.CreateForTest(); - host.StateProvider = new DaprStateProvider(interactor.Object, new JsonSerializerOptions()); - var mngr = new ActorStateManager(new TestActor(host)); - var token = new CancellationToken(); - - interactor - .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(new ActorStateResponse("", null))); - - await mngr.AddStateAsync("key1", "value1", TimeSpan.FromSeconds(1), token); - await mngr.AddStateAsync("key2", "value2", TimeSpan.FromSeconds(1), token); - Assert.Equal("value1", await mngr.GetStateAsync("key1", token)); - Assert.Equal("value2", await mngr.GetStateAsync("key2", token)); - - await Task.Delay(TimeSpan.FromSeconds(1.5)); - - await Assert.ThrowsAsync(() => mngr.GetStateAsync("key1", token)); - await Assert.ThrowsAsync(() => mngr.GetStateAsync("key2", token)); - - // Should be able to add state again after expiry and should not expire. - await mngr.AddStateAsync("key1", "value1", token); - await mngr.AddStateAsync("key2", "value2", token); - Assert.Equal("value1", await mngr.GetStateAsync("key1", token)); - Assert.Equal("value2", await mngr.GetStateAsync("key2", token)); - await Task.Delay(TimeSpan.FromSeconds(1.5)); - Assert.Equal("value1", await mngr.GetStateAsync("key1", token)); - Assert.Equal("value2", await mngr.GetStateAsync("key2", token)); - } - - [Fact] - public async Task StateRemoveAddTTL() - { - var interactor = new Mock(); - var host = ActorHost.CreateForTest(); - host.StateProvider = new DaprStateProvider(interactor.Object, new JsonSerializerOptions()); - var mngr = new ActorStateManager(new TestActor(host)); - var token = new CancellationToken(); - - interactor - .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(new ActorStateResponse("", null))); - - await mngr.AddStateAsync("key1", "value1", TimeSpan.FromSeconds(1), token); - await mngr.AddStateAsync("key2", "value2", TimeSpan.FromSeconds(1), token); - Assert.Equal("value1", await mngr.GetStateAsync("key1", token)); - Assert.Equal("value2", await mngr.GetStateAsync("key2", token)); - - await mngr.SetStateAsync("key1", "value1", token); - await mngr.SetStateAsync("key2", "value2", token); - Assert.Equal("value1", await mngr.GetStateAsync("key1", token)); - Assert.Equal("value2", await mngr.GetStateAsync("key2", token)); - - // TTL is removed so state should not expire. - await Task.Delay(TimeSpan.FromSeconds(1.5)); - Assert.Equal("value1", await mngr.GetStateAsync("key1", token)); - Assert.Equal("value2", await mngr.GetStateAsync("key2", token)); - - // Adding TTL back should expire state. - await mngr.SetStateAsync("key1", "value1", TimeSpan.FromSeconds(1), token); - await mngr.SetStateAsync("key2", "value2", TimeSpan.FromSeconds(1), token); - Assert.Equal("value1", await mngr.GetStateAsync("key1", token)); - Assert.Equal("value2", await mngr.GetStateAsync("key2", token)); - await Task.Delay(TimeSpan.FromSeconds(1.5)); - await Assert.ThrowsAsync(() => mngr.GetStateAsync("key1", token)); - await Assert.ThrowsAsync(() => mngr.GetStateAsync("key2", token)); - } - - [Fact] - public async Task StateDaprdExpireTime() - { - var interactor = new Mock(); - var host = ActorHost.CreateForTest(); - host.StateProvider = new DaprStateProvider(interactor.Object, new JsonSerializerOptions()); - var mngr = new ActorStateManager(new TestActor(host)); - var token = new CancellationToken(); - - // Existing key which has an expiry time. - interactor - .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(new ActorStateResponse("\"value1\"", DateTime.UtcNow.AddSeconds(1)))); - - await Assert.ThrowsAsync(() => mngr.AddStateAsync("key1", "value3", token)); - Assert.Equal("value1", await mngr.GetStateAsync("key1", token)); - - // No longer return the value from the state provider. - interactor - .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(new ActorStateResponse("", null))); - - // Key should be expired after 1 seconds. - await Task.Delay(TimeSpan.FromSeconds(1.5)); - await Assert.ThrowsAsync(() => mngr.GetStateAsync("key1", token)); - await Assert.ThrowsAsync(() => mngr.RemoveStateAsync("key1", token)); - await mngr.AddStateAsync("key1", "value2", TimeSpan.FromSeconds(1), token); - Assert.Equal("value2", await mngr.GetStateAsync("key1", token)); - } - - [Fact] - public async Task RemoveState() - { - var interactor = new Mock(); - var host = ActorHost.CreateForTest(); - host.StateProvider = new DaprStateProvider(interactor.Object, new JsonSerializerOptions()); - var mngr = new ActorStateManager(new TestActor(host)); - var token = new CancellationToken(); - - interactor - .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(new ActorStateResponse("", null))); - - await Assert.ThrowsAsync(() => mngr.RemoveStateAsync("key1", token)); - - await mngr.AddStateAsync("key1", "value1", token); - await mngr.AddStateAsync("key2", "value2", token); - Assert.Equal("value1", await mngr.GetStateAsync("key1", token)); - Assert.Equal("value2", await mngr.GetStateAsync("key2", token)); - - await mngr.RemoveStateAsync("key1", token); - await mngr.RemoveStateAsync("key2", token); - - await Assert.ThrowsAsync(() => mngr.GetStateAsync("key1", token)); - await Assert.ThrowsAsync(() => mngr.GetStateAsync("key2", token)); - - // Should be able to add state again after removal. - await mngr.AddStateAsync("key1", "value1", TimeSpan.FromSeconds(1), token); - await mngr.AddStateAsync("key2", "value2", TimeSpan.FromSeconds(1), token); - Assert.Equal("value1", await mngr.GetStateAsync("key1", token)); - Assert.Equal("value2", await mngr.GetStateAsync("key2", token)); - } + await mngr.SetStateAsync("key1", "value5", token); + await mngr.SetStateAsync("key2", "value6", token); + Assert.Equal("value5", await mngr.GetStateAsync("key1", token)); + Assert.Equal("value6", await mngr.GetStateAsync("key2", token)); } -} + + [Fact] + public async Task StateWithTTL() + { + var interactor = new Mock(); + var host = ActorHost.CreateForTest(); + host.StateProvider = new DaprStateProvider(interactor.Object, new JsonSerializerOptions()); + var mngr = new ActorStateManager(new TestActor(host)); + var token = new CancellationToken(); + + interactor + .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(new ActorStateResponse("", null))); + + await mngr.AddStateAsync("key1", "value1", TimeSpan.FromSeconds(1), token); + await mngr.AddStateAsync("key2", "value2", TimeSpan.FromSeconds(1), token); + Assert.Equal("value1", await mngr.GetStateAsync("key1", token)); + Assert.Equal("value2", await mngr.GetStateAsync("key2", token)); + + await Task.Delay(TimeSpan.FromSeconds(1.5)); + + await Assert.ThrowsAsync(() => mngr.GetStateAsync("key1", token)); + await Assert.ThrowsAsync(() => mngr.GetStateAsync("key2", token)); + + // Should be able to add state again after expiry and should not expire. + await mngr.AddStateAsync("key1", "value1", token); + await mngr.AddStateAsync("key2", "value2", token); + Assert.Equal("value1", await mngr.GetStateAsync("key1", token)); + Assert.Equal("value2", await mngr.GetStateAsync("key2", token)); + await Task.Delay(TimeSpan.FromSeconds(1.5)); + Assert.Equal("value1", await mngr.GetStateAsync("key1", token)); + Assert.Equal("value2", await mngr.GetStateAsync("key2", token)); + } + + [Fact] + public async Task StateRemoveAddTTL() + { + var interactor = new Mock(); + var host = ActorHost.CreateForTest(); + host.StateProvider = new DaprStateProvider(interactor.Object, new JsonSerializerOptions()); + var mngr = new ActorStateManager(new TestActor(host)); + var token = new CancellationToken(); + + interactor + .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(new ActorStateResponse("", null))); + + await mngr.AddStateAsync("key1", "value1", TimeSpan.FromSeconds(1), token); + await mngr.AddStateAsync("key2", "value2", TimeSpan.FromSeconds(1), token); + Assert.Equal("value1", await mngr.GetStateAsync("key1", token)); + Assert.Equal("value2", await mngr.GetStateAsync("key2", token)); + + await mngr.SetStateAsync("key1", "value1", token); + await mngr.SetStateAsync("key2", "value2", token); + Assert.Equal("value1", await mngr.GetStateAsync("key1", token)); + Assert.Equal("value2", await mngr.GetStateAsync("key2", token)); + + // TTL is removed so state should not expire. + await Task.Delay(TimeSpan.FromSeconds(1.5)); + Assert.Equal("value1", await mngr.GetStateAsync("key1", token)); + Assert.Equal("value2", await mngr.GetStateAsync("key2", token)); + + // Adding TTL back should expire state. + await mngr.SetStateAsync("key1", "value1", TimeSpan.FromSeconds(1), token); + await mngr.SetStateAsync("key2", "value2", TimeSpan.FromSeconds(1), token); + Assert.Equal("value1", await mngr.GetStateAsync("key1", token)); + Assert.Equal("value2", await mngr.GetStateAsync("key2", token)); + await Task.Delay(TimeSpan.FromSeconds(1.5)); + await Assert.ThrowsAsync(() => mngr.GetStateAsync("key1", token)); + await Assert.ThrowsAsync(() => mngr.GetStateAsync("key2", token)); + } + + [Fact] + public async Task StateDaprdExpireTime() + { + var interactor = new Mock(); + var host = ActorHost.CreateForTest(); + host.StateProvider = new DaprStateProvider(interactor.Object, new JsonSerializerOptions()); + var mngr = new ActorStateManager(new TestActor(host)); + var token = new CancellationToken(); + + // Existing key which has an expiry time. + interactor + .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(new ActorStateResponse("\"value1\"", DateTime.UtcNow.AddSeconds(1)))); + + await Assert.ThrowsAsync(() => mngr.AddStateAsync("key1", "value3", token)); + Assert.Equal("value1", await mngr.GetStateAsync("key1", token)); + + // No longer return the value from the state provider. + interactor + .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(new ActorStateResponse("", null))); + + // Key should be expired after 1 seconds. + await Task.Delay(TimeSpan.FromSeconds(1.5)); + await Assert.ThrowsAsync(() => mngr.GetStateAsync("key1", token)); + await Assert.ThrowsAsync(() => mngr.RemoveStateAsync("key1", token)); + await mngr.AddStateAsync("key1", "value2", TimeSpan.FromSeconds(1), token); + Assert.Equal("value2", await mngr.GetStateAsync("key1", token)); + } + + [Fact] + public async Task RemoveState() + { + var interactor = new Mock(); + var host = ActorHost.CreateForTest(); + host.StateProvider = new DaprStateProvider(interactor.Object, new JsonSerializerOptions()); + var mngr = new ActorStateManager(new TestActor(host)); + var token = new CancellationToken(); + + interactor + .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(new ActorStateResponse("", null))); + + await Assert.ThrowsAsync(() => mngr.RemoveStateAsync("key1", token)); + + await mngr.AddStateAsync("key1", "value1", token); + await mngr.AddStateAsync("key2", "value2", token); + Assert.Equal("value1", await mngr.GetStateAsync("key1", token)); + Assert.Equal("value2", await mngr.GetStateAsync("key2", token)); + + await mngr.RemoveStateAsync("key1", token); + await mngr.RemoveStateAsync("key2", token); + + await Assert.ThrowsAsync(() => mngr.GetStateAsync("key1", token)); + await Assert.ThrowsAsync(() => mngr.GetStateAsync("key2", token)); + + // Should be able to add state again after removal. + await mngr.AddStateAsync("key1", "value1", TimeSpan.FromSeconds(1), token); + await mngr.AddStateAsync("key2", "value2", TimeSpan.FromSeconds(1), token); + Assert.Equal("value1", await mngr.GetStateAsync("key1", token)); + Assert.Equal("value2", await mngr.GetStateAsync("key2", token)); + } +} \ No newline at end of file diff --git a/test/Dapr.Actors.Test/ActorUnitTestTests.cs b/test/Dapr.Actors.Test/ActorUnitTestTests.cs index 79476199..27f19a77 100644 --- a/test/Dapr.Actors.Test/ActorUnitTestTests.cs +++ b/test/Dapr.Actors.Test/ActorUnitTestTests.cs @@ -19,176 +19,175 @@ using Dapr.Actors.Runtime; using Moq; using Xunit; -namespace Dapr.Actors +namespace Dapr.Actors; + +// Tests that test that we can test... hmmmm... +public class ActorUnitTestTests { - // Tests that test that we can test... hmmmm... - public class ActorUnitTestTests + [Fact] + public async Task CanTestStartingAndStoppingTimer() { - [Fact] - public async Task CanTestStartingAndStoppingTimer() + var timers = new List(); + + var timerManager = new Mock(MockBehavior.Strict); + timerManager + .Setup(tm => tm.RegisterTimerAsync(It.IsAny())) + .Callback(timer => timers.Add(timer)) + .Returns(Task.CompletedTask); + timerManager + .Setup(tm => tm.UnregisterTimerAsync(It.IsAny())) + .Callback(timer => timers.RemoveAll(t => t.Name == timer.Name)) + .Returns(Task.CompletedTask); + + var host = ActorHost.CreateForTest(new ActorTestOptions(){ TimerManager = timerManager.Object, }); + var actor = new CoolTestActor(host); + + // Start the timer + var message = new Message() + { + Text = "Remind me to tape the hockey game tonite.", + }; + await actor.StartTimerAsync(message); + + var timer = Assert.Single(timers); + Assert.Equal("record", timer.Name); + Assert.Equal(TimeSpan.FromSeconds(5), timer.Period); + Assert.Equal(TimeSpan.Zero, timer.DueTime); + + var state = JsonSerializer.Deserialize(timer.Data); + Assert.Equal(message.Text, state.Text); + + // Simulate invoking the callback + for (var i = 0; i < 10; i++) { - var timers = new List(); - - var timerManager = new Mock(MockBehavior.Strict); - timerManager - .Setup(tm => tm.RegisterTimerAsync(It.IsAny())) - .Callback(timer => timers.Add(timer)) - .Returns(Task.CompletedTask); - timerManager - .Setup(tm => tm.UnregisterTimerAsync(It.IsAny())) - .Callback(timer => timers.RemoveAll(t => t.Name == timer.Name)) - .Returns(Task.CompletedTask); - - var host = ActorHost.CreateForTest(new ActorTestOptions(){ TimerManager = timerManager.Object, }); - var actor = new CoolTestActor(host); - - // Start the timer - var message = new Message() - { - Text = "Remind me to tape the hockey game tonite.", - }; - await actor.StartTimerAsync(message); - - var timer = Assert.Single(timers); - Assert.Equal("record", timer.Name); - Assert.Equal(TimeSpan.FromSeconds(5), timer.Period); - Assert.Equal(TimeSpan.Zero, timer.DueTime); - - var state = JsonSerializer.Deserialize(timer.Data); - Assert.Equal(message.Text, state.Text); - - // Simulate invoking the callback - for (var i = 0; i < 10; i++) - { - await actor.Tick(timer.Data); - } - - // Stop the timer - await actor.StopTimerAsync(); - Assert.Empty(timers); + await actor.Tick(timer.Data); } - [Fact] - public async Task CanTestStartingAndStoppingReminder() + // Stop the timer + await actor.StopTimerAsync(); + Assert.Empty(timers); + } + + [Fact] + public async Task CanTestStartingAndStoppingReminder() + { + var reminders = new List(); + IActorReminder getReminder = null; + + var timerManager = new Mock(MockBehavior.Strict); + timerManager + .Setup(tm => tm.RegisterReminderAsync(It.IsAny())) + .Callback(reminder => reminders.Add(reminder)) + .Returns(Task.CompletedTask); + timerManager + .Setup(tm => tm.UnregisterReminderAsync(It.IsAny())) + .Callback(reminder => reminders.RemoveAll(t => t.Name == reminder.Name)) + .Returns(Task.CompletedTask); + timerManager + .Setup(tm => tm.GetReminderAsync(It.IsAny())) + .Returns(() => Task.FromResult(getReminder)); + + var host = ActorHost.CreateForTest(new ActorTestOptions(){ TimerManager = timerManager.Object, }); + var actor = new CoolTestActor(host); + + // Start the reminder + var message = new Message() + { + Text = "Remind me to tape the hockey game tonite.", + }; + await actor.StartReminderAsync(message); + + var reminder = Assert.Single(reminders); + Assert.Equal("record", reminder.Name); + Assert.Equal(TimeSpan.FromSeconds(5), reminder.Period); + Assert.Equal(TimeSpan.Zero, reminder.DueTime); + + var state = JsonSerializer.Deserialize(reminder.State); + Assert.Equal(message.Text, state.Text); + + // Simulate invoking the reminder interface + for (var i = 0; i < 10; i++) { - var reminders = new List(); - IActorReminder getReminder = null; - - var timerManager = new Mock(MockBehavior.Strict); - timerManager - .Setup(tm => tm.RegisterReminderAsync(It.IsAny())) - .Callback(reminder => reminders.Add(reminder)) - .Returns(Task.CompletedTask); - timerManager - .Setup(tm => tm.UnregisterReminderAsync(It.IsAny())) - .Callback(reminder => reminders.RemoveAll(t => t.Name == reminder.Name)) - .Returns(Task.CompletedTask); - timerManager - .Setup(tm => tm.GetReminderAsync(It.IsAny())) - .Returns(() => Task.FromResult(getReminder)); - - var host = ActorHost.CreateForTest(new ActorTestOptions(){ TimerManager = timerManager.Object, }); - var actor = new CoolTestActor(host); - - // Start the reminder - var message = new Message() - { - Text = "Remind me to tape the hockey game tonite.", - }; - await actor.StartReminderAsync(message); - - var reminder = Assert.Single(reminders); - Assert.Equal("record", reminder.Name); - Assert.Equal(TimeSpan.FromSeconds(5), reminder.Period); - Assert.Equal(TimeSpan.Zero, reminder.DueTime); - - var state = JsonSerializer.Deserialize(reminder.State); - Assert.Equal(message.Text, state.Text); - - // Simulate invoking the reminder interface - for (var i = 0; i < 10; i++) - { - await actor.ReceiveReminderAsync(reminder.Name, reminder.State, reminder.DueTime, reminder.Period); - } - - getReminder = reminder; - var reminderFromGet = await actor.GetReminderAsync(); - Assert.Equal(reminder, reminderFromGet); - - // Stop the reminder - await actor.StopReminderAsync(); - Assert.Empty(reminders); + await actor.ReceiveReminderAsync(reminder.Name, reminder.State, reminder.DueTime, reminder.Period); } - [Fact] - public async Task ReminderReturnsNullIfNotAvailable() - { - var timerManager = new Mock(MockBehavior.Strict); - timerManager - .Setup(tm => tm.GetReminderAsync(It.IsAny())) - .Returns(() => Task.FromResult(null)); - - var host = ActorHost.CreateForTest(new ActorTestOptions() { TimerManager = timerManager.Object, }); - var actor = new CoolTestActor(host); - - //There is no starting reminder, so this should always return null - var retrievedReminder = await actor.GetReminderAsync(); - Assert.Null(retrievedReminder); - } + getReminder = reminder; + var reminderFromGet = await actor.GetReminderAsync(); + Assert.Equal(reminder, reminderFromGet); - public interface ICoolTestActor : IActor + // Stop the reminder + await actor.StopReminderAsync(); + Assert.Empty(reminders); + } + + [Fact] + public async Task ReminderReturnsNullIfNotAvailable() + { + var timerManager = new Mock(MockBehavior.Strict); + timerManager + .Setup(tm => tm.GetReminderAsync(It.IsAny())) + .Returns(() => Task.FromResult(null)); + + var host = ActorHost.CreateForTest(new ActorTestOptions() { TimerManager = timerManager.Object, }); + var actor = new CoolTestActor(host); + + //There is no starting reminder, so this should always return null + var retrievedReminder = await actor.GetReminderAsync(); + Assert.Null(retrievedReminder); + } + + public interface ICoolTestActor : IActor + { + } + + public class Message + { + public string Text { get; set; } + public bool IsImportant { get; set; } + } + + public class CoolTestActor : Actor, ICoolTestActor, IRemindable + { + public CoolTestActor(ActorHost host) + : base(host) { } - public class Message + public async Task StartTimerAsync(Message message) { - public string Text { get; set; } - public bool IsImportant { get; set; } + var bytes = JsonSerializer.SerializeToUtf8Bytes(message); + await this.RegisterTimerAsync("record", nameof(Tick), bytes, dueTime: TimeSpan.Zero, period: TimeSpan.FromSeconds(5)); } - public class CoolTestActor : Actor, ICoolTestActor, IRemindable + public async Task StopTimerAsync() { - public CoolTestActor(ActorHost host) - : base(host) - { - } + await this.UnregisterTimerAsync("record"); + } - public async Task StartTimerAsync(Message message) - { - var bytes = JsonSerializer.SerializeToUtf8Bytes(message); - await this.RegisterTimerAsync("record", nameof(Tick), bytes, dueTime: TimeSpan.Zero, period: TimeSpan.FromSeconds(5)); - } + public async Task StartReminderAsync(Message message) + { + var bytes = JsonSerializer.SerializeToUtf8Bytes(message); + await this.RegisterReminderAsync("record", bytes, dueTime: TimeSpan.Zero, period: TimeSpan.FromSeconds(5)); + } - public async Task StopTimerAsync() - { - await this.UnregisterTimerAsync("record"); - } + public async Task GetReminderAsync() + { + return await this.GetReminderAsync("record"); + } - public async Task StartReminderAsync(Message message) - { - var bytes = JsonSerializer.SerializeToUtf8Bytes(message); - await this.RegisterReminderAsync("record", bytes, dueTime: TimeSpan.Zero, period: TimeSpan.FromSeconds(5)); - } + public async Task StopReminderAsync() + { + await this.UnregisterReminderAsync("record"); + } - public async Task GetReminderAsync() - { - return await this.GetReminderAsync("record"); - } + public Task Tick(byte[] data) + { + return Task.CompletedTask; + } - public async Task StopReminderAsync() - { - await this.UnregisterReminderAsync("record"); - } - - public Task Tick(byte[] data) - { - return Task.CompletedTask; - } - - public Task ReceiveReminderAsync(string reminderName, byte[] state, TimeSpan dueTime, TimeSpan period) - { - return Task.CompletedTask; - } + public Task ReceiveReminderAsync(string reminderName, byte[] state, TimeSpan dueTime, TimeSpan period) + { + return Task.CompletedTask; } } -} +} \ No newline at end of file diff --git a/test/Dapr.Actors.Test/ApiTokenTests.cs b/test/Dapr.Actors.Test/ApiTokenTests.cs index 6e5860be..5eae4c8b 100644 --- a/test/Dapr.Actors.Test/ApiTokenTests.cs +++ b/test/Dapr.Actors.Test/ApiTokenTests.cs @@ -18,100 +18,99 @@ using Dapr.Actors.Client; using Shouldly; using Xunit; -namespace Dapr.Actors.Test +namespace Dapr.Actors.Test; + +public class ApiTokenTests { - public class ApiTokenTests + [Fact(Skip = "https://github.com/dapr/dotnet-sdk/issues/596")] + public async Task CreateProxyWithRemoting_WithApiToken() { - [Fact(Skip = "https://github.com/dapr/dotnet-sdk/issues/596")] - public async Task CreateProxyWithRemoting_WithApiToken() + await using var client = TestClient.CreateForMessageHandler(); + + var actorId = new ActorId("abc"); + var options = new ActorProxyOptions { - await using var client = TestClient.CreateForMessageHandler(); + DaprApiToken = "test_token", + }; - var actorId = new ActorId("abc"); - var options = new ActorProxyOptions - { - DaprApiToken = "test_token", - }; - - var request = await client.CaptureHttpRequestAsync(async handler => - { - var factory = new ActorProxyFactory(options, handler); - var proxy = factory.CreateActorProxy(actorId, "TestActor"); - await proxy.SetCountAsync(1, new CancellationToken()); - }); - - request.Dismiss(); - - var headerValues = request.Request.Headers.GetValues("dapr-api-token"); - headerValues.ShouldContain("test_token"); - } - - [Fact(Skip = "https://github.com/dapr/dotnet-sdk/issues/596")] - public async Task CreateProxyWithRemoting_WithNoApiToken() + var request = await client.CaptureHttpRequestAsync(async handler => { - await using var client = TestClient.CreateForMessageHandler(); + var factory = new ActorProxyFactory(options, handler); + var proxy = factory.CreateActorProxy(actorId, "TestActor"); + await proxy.SetCountAsync(1, new CancellationToken()); + }); - var actorId = new ActorId("abc"); + request.Dismiss(); - var request = await client.CaptureHttpRequestAsync(async handler => - { - var factory = new ActorProxyFactory(null, handler); - var proxy = factory.CreateActorProxy(actorId, "TestActor"); - await proxy.SetCountAsync(1, new CancellationToken()); - }); - - request.Dismiss(); - - Assert.Throws(() => - { - request.Request.Headers.GetValues("dapr-api-token"); - }); - } - - [Fact] - public async Task CreateProxyWithNoRemoting_WithApiToken() - { - await using var client = TestClient.CreateForMessageHandler(); - - var actorId = new ActorId("abc"); - var options = new ActorProxyOptions - { - DaprApiToken = "test_token", - }; - - var request = await client.CaptureHttpRequestAsync(async handler => - { - var factory = new ActorProxyFactory(options, handler); - var proxy = factory.Create(actorId, "TestActor"); - await proxy.InvokeMethodAsync("SetCountAsync", 1, new CancellationToken()); - }); - - request.Dismiss(); - - var headerValues = request.Request.Headers.GetValues("dapr-api-token"); - headerValues.ShouldContain("test_token"); - } - - [Fact] - public async Task CreateProxyWithNoRemoting_WithNoApiToken() - { - await using var client = TestClient.CreateForMessageHandler(); - - var actorId = new ActorId("abc"); - - var request = await client.CaptureHttpRequestAsync(async handler => - { - var factory = new ActorProxyFactory(null, handler); - var proxy = factory.Create(actorId, "TestActor"); - await proxy.InvokeMethodAsync("SetCountAsync", 1, new CancellationToken()); - }); - - request.Dismiss(); - - Assert.Throws(() => - { - request.Request.Headers.GetValues("dapr-api-token"); - }); - } + var headerValues = request.Request.Headers.GetValues("dapr-api-token"); + headerValues.ShouldContain("test_token"); } -} + + [Fact(Skip = "https://github.com/dapr/dotnet-sdk/issues/596")] + public async Task CreateProxyWithRemoting_WithNoApiToken() + { + await using var client = TestClient.CreateForMessageHandler(); + + var actorId = new ActorId("abc"); + + var request = await client.CaptureHttpRequestAsync(async handler => + { + var factory = new ActorProxyFactory(null, handler); + var proxy = factory.CreateActorProxy(actorId, "TestActor"); + await proxy.SetCountAsync(1, new CancellationToken()); + }); + + request.Dismiss(); + + Assert.Throws(() => + { + request.Request.Headers.GetValues("dapr-api-token"); + }); + } + + [Fact] + public async Task CreateProxyWithNoRemoting_WithApiToken() + { + await using var client = TestClient.CreateForMessageHandler(); + + var actorId = new ActorId("abc"); + var options = new ActorProxyOptions + { + DaprApiToken = "test_token", + }; + + var request = await client.CaptureHttpRequestAsync(async handler => + { + var factory = new ActorProxyFactory(options, handler); + var proxy = factory.Create(actorId, "TestActor"); + await proxy.InvokeMethodAsync("SetCountAsync", 1, new CancellationToken()); + }); + + request.Dismiss(); + + var headerValues = request.Request.Headers.GetValues("dapr-api-token"); + headerValues.ShouldContain("test_token"); + } + + [Fact] + public async Task CreateProxyWithNoRemoting_WithNoApiToken() + { + await using var client = TestClient.CreateForMessageHandler(); + + var actorId = new ActorId("abc"); + + var request = await client.CaptureHttpRequestAsync(async handler => + { + var factory = new ActorProxyFactory(null, handler); + var proxy = factory.Create(actorId, "TestActor"); + await proxy.InvokeMethodAsync("SetCountAsync", 1, new CancellationToken()); + }); + + request.Dismiss(); + + Assert.Throws(() => + { + request.Request.Headers.GetValues("dapr-api-token"); + }); + } +} \ No newline at end of file diff --git a/test/Dapr.Actors.Test/DaprFormatTimeSpanTests.cs b/test/Dapr.Actors.Test/DaprFormatTimeSpanTests.cs index 9d44e755..ed493e8a 100644 --- a/test/Dapr.Actors.Test/DaprFormatTimeSpanTests.cs +++ b/test/Dapr.Actors.Test/DaprFormatTimeSpanTests.cs @@ -12,145 +12,144 @@ // ------------------------------------------------------------------------ #pragma warning disable 0618 -namespace Dapr.Actors.Test +namespace Dapr.Actors.Test; + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using Dapr.Actors.Runtime; +using Newtonsoft.Json; +using Xunit; + +public class DaprFormatTimeSpanTests { - using System; - using System.Collections.Generic; - using System.IO; - using System.Threading.Tasks; - using Dapr.Actors.Runtime; - using Newtonsoft.Json; - using Xunit; - - public class DaprFormatTimeSpanTests + public static readonly IEnumerable DaprFormatTimeSpanJsonStringsAndExpectedDeserializedValues = new List { - public static readonly IEnumerable DaprFormatTimeSpanJsonStringsAndExpectedDeserializedValues = new List + new object[] { - new object[] - { - "{\"dueTime\":\"4h15m50s60ms\"", - new TimeSpan(0, 4, 15, 50, 60), - }, - new object[] - { - "{\"dueTime\":\"0h35m10s12ms\"", - new TimeSpan(0, 0, 35, 10, 12), - }, - }; + "{\"dueTime\":\"4h15m50s60ms\"", + new TimeSpan(0, 4, 15, 50, 60), + }, + new object[] + { + "{\"dueTime\":\"0h35m10s12ms\"", + new TimeSpan(0, 0, 35, 10, 12), + }, + }; - public static readonly IEnumerable DaprReminderISO8601FormatTimeSpanAndExpectedDeserializedValues = new List + public static readonly IEnumerable DaprReminderISO8601FormatTimeSpanAndExpectedDeserializedValues = new List + { + new object[] { - new object[] - { - "R10/PT10S", - new TimeSpan(0, 0, 0, 10, 0), - 10 - }, - new object[] - { - "PT10H5M10S", - new TimeSpan(0, 10, 5, 10, 0), - null, - }, - new object[] - { - "P1Y1M1W1DT1H1M1S", - new TimeSpan(403, 1, 1, 1, 0), - null, - }, - new object[] - { - "R3/P2W3DT4H59M", - new TimeSpan(17, 4, 59, 0, 0), - 3, - } - }; - - public static readonly IEnumerable DaprReminderTimeSpanToDaprISO8601Format = new List + "R10/PT10S", + new TimeSpan(0, 0, 0, 10, 0), + 10 + }, + new object[] { - new object[] - { - new TimeSpan(10, 10, 10, 10), - 1, - "R1/P10DT10H10M10S" - }, - new object[] - { - new TimeSpan(17, 4, 59, 0, 0), - 3, - "R3/P17DT4H59M" - }, - new object[] - { - new TimeSpan(0, 7, 23, 12, 0), - null, - "7h23m12s0ms" - } - }; - - [Theory] - [MemberData(nameof(DaprFormatTimeSpanJsonStringsAndExpectedDeserializedValues))] - public void DaprFormat_TimeSpan_Parsing(string daprFormatTimeSpanJsonString, TimeSpan expectedDeserializedValue) + "PT10H5M10S", + new TimeSpan(0, 10, 5, 10, 0), + null, + }, + new object[] { - using var textReader = new StringReader(daprFormatTimeSpanJsonString); - using var jsonTextReader = new JsonTextReader(textReader); + "P1Y1M1W1DT1H1M1S", + new TimeSpan(403, 1, 1, 1, 0), + null, + }, + new object[] + { + "R3/P2W3DT4H59M", + new TimeSpan(17, 4, 59, 0, 0), + 3, + } + }; - while (jsonTextReader.TokenType != JsonToken.String) - { - jsonTextReader.Read(); - } + public static readonly IEnumerable DaprReminderTimeSpanToDaprISO8601Format = new List + { + new object[] + { + new TimeSpan(10, 10, 10, 10), + 1, + "R1/P10DT10H10M10S" + }, + new object[] + { + new TimeSpan(17, 4, 59, 0, 0), + 3, + "R3/P17DT4H59M" + }, + new object[] + { + new TimeSpan(0, 7, 23, 12, 0), + null, + "7h23m12s0ms" + } + }; - var timespanString = (string)jsonTextReader.Value; - var deserializedTimeSpan = ConverterUtils.ConvertTimeSpanFromDaprFormat(timespanString); + [Theory] + [MemberData(nameof(DaprFormatTimeSpanJsonStringsAndExpectedDeserializedValues))] + public void DaprFormat_TimeSpan_Parsing(string daprFormatTimeSpanJsonString, TimeSpan expectedDeserializedValue) + { + using var textReader = new StringReader(daprFormatTimeSpanJsonString); + using var jsonTextReader = new JsonTextReader(textReader); - Assert.Equal(expectedDeserializedValue, deserializedTimeSpan); + while (jsonTextReader.TokenType != JsonToken.String) + { + jsonTextReader.Read(); } - [Fact] - public async Task ConverterUtilsTestEndToEndAsync() - { - static Task SerializeAsync(TimeSpan dueTime, TimeSpan period) - { - var timerInfo = new TimerInfo( - callback: null, - state: null, - dueTime: dueTime, - period: period); - return Task.FromResult(System.Text.Json.JsonSerializer.Serialize(timerInfo)); - } + var timespanString = (string)jsonTextReader.Value; + var deserializedTimeSpan = ConverterUtils.ConvertTimeSpanFromDaprFormat(timespanString); - var inTheFuture = TimeSpan.FromMilliseconds(20); - var never = TimeSpan.FromMilliseconds(-1); - Assert.Equal( - "{\"dueTime\":\"0h0m0s20ms\"}", - await SerializeAsync(inTheFuture, never)); + Assert.Equal(expectedDeserializedValue, deserializedTimeSpan); + } + + [Fact] + public async Task ConverterUtilsTestEndToEndAsync() + { + static Task SerializeAsync(TimeSpan dueTime, TimeSpan period) + { + var timerInfo = new TimerInfo( + callback: null, + state: null, + dueTime: dueTime, + period: period); + return Task.FromResult(System.Text.Json.JsonSerializer.Serialize(timerInfo)); } - [Fact] - public void DaprFormatTimespanEmpty() - { - Func convert = ConverterUtils.ConvertTimeSpanFromDaprFormat; - TimeSpan never = TimeSpan.FromMilliseconds(-1); - Assert.Equal(never, convert(null)); - Assert.Equal(never, convert(string.Empty)); - } + var inTheFuture = TimeSpan.FromMilliseconds(20); + var never = TimeSpan.FromMilliseconds(-1); + Assert.Equal( + "{\"dueTime\":\"0h0m0s20ms\"}", + await SerializeAsync(inTheFuture, never)); + } + + [Fact] + public void DaprFormatTimespanEmpty() + { + Func convert = ConverterUtils.ConvertTimeSpanFromDaprFormat; + TimeSpan never = TimeSpan.FromMilliseconds(-1); + Assert.Equal(never, convert(null)); + Assert.Equal(never, convert(string.Empty)); + } - [Theory] - [MemberData(nameof(DaprReminderISO8601FormatTimeSpanAndExpectedDeserializedValues))] - public void DaprReminderFormat_TimeSpan_Parsing(string valueString, TimeSpan expectedDuration, int? expectedRepetition) - { - (TimeSpan duration, int? repetition) = ConverterUtils.ConvertTimeSpanValueFromISO8601Format(valueString); - Assert.Equal(expectedDuration, duration); - Assert.Equal(expectedRepetition, repetition); - } + [Theory] + [MemberData(nameof(DaprReminderISO8601FormatTimeSpanAndExpectedDeserializedValues))] + public void DaprReminderFormat_TimeSpan_Parsing(string valueString, TimeSpan expectedDuration, int? expectedRepetition) + { + (TimeSpan duration, int? repetition) = ConverterUtils.ConvertTimeSpanValueFromISO8601Format(valueString); + Assert.Equal(expectedDuration, duration); + Assert.Equal(expectedRepetition, repetition); + } - [Theory] - [MemberData(nameof(DaprReminderTimeSpanToDaprISO8601Format))] - public void DaprReminderFormat_ConvertFromTimeSpan_ToDaprFormat(TimeSpan period, int? repetitions, string expectedValue) - { - var actualValue = ConverterUtils.ConvertTimeSpanValueInISO8601Format(period, repetitions); - Assert.Equal(expectedValue, actualValue); - } + [Theory] + [MemberData(nameof(DaprReminderTimeSpanToDaprISO8601Format))] + public void DaprReminderFormat_ConvertFromTimeSpan_ToDaprFormat(TimeSpan period, int? repetitions, string expectedValue) + { + var actualValue = ConverterUtils.ConvertTimeSpanValueInISO8601Format(period, repetitions); + Assert.Equal(expectedValue, actualValue); } } #pragma warning restore 0618 diff --git a/test/Dapr.Actors.Test/DaprHttpInteractorTest.cs b/test/Dapr.Actors.Test/DaprHttpInteractorTest.cs index 30ddb91d..35841f0a 100644 --- a/test/Dapr.Actors.Test/DaprHttpInteractorTest.cs +++ b/test/Dapr.Actors.Test/DaprHttpInteractorTest.cs @@ -11,421 +11,420 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Test +namespace Dapr.Actors.Test; + +using System; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Security.Authentication; +using System.Text.Json; +using System.Threading.Tasks; +using Shouldly; +using Xunit; + +/// +/// Contains tests for DaprHttpInteractor. +/// +public class DaprHttpInteractorTest { - using System; - using System.Globalization; - using System.Linq; - using System.Net; - using System.Net.Http; - using System.Security.Authentication; - using System.Text.Json; - using System.Threading.Tasks; - using Shouldly; - using Xunit; - - /// - /// Contains tests for DaprHttpInteractor. - /// - public class DaprHttpInteractorTest + [Fact] + public async Task GetState_ValidateRequest() { - [Fact] - public async Task GetState_ValidateRequest() + await using var client = TestClient.CreateForDaprHttpInterator(); + + const string actorType = "ActorType_Test"; + const string actorId = "ActorId_Test"; + const string keyName = "StateKey_Test"; + + var request = await client.CaptureHttpRequestAsync(async httpInteractor => { - await using var client = TestClient.CreateForDaprHttpInterator(); + await httpInteractor.GetStateAsync(actorType, actorId, keyName); + }); - const string actorType = "ActorType_Test"; - const string actorId = "ActorId_Test"; - const string keyName = "StateKey_Test"; + request.Dismiss(); - var request = await client.CaptureHttpRequestAsync(async httpInteractor => - { - await httpInteractor.GetStateAsync(actorType, actorId, keyName); - }); + var actualPath = request.Request.RequestUri.LocalPath.TrimStart('/'); + var expectedPath = string.Format(CultureInfo.InvariantCulture, Constants.ActorStateKeyRelativeUrlFormat, actorType, actorId, keyName); - request.Dismiss(); - - var actualPath = request.Request.RequestUri.LocalPath.TrimStart('/'); - var expectedPath = string.Format(CultureInfo.InvariantCulture, Constants.ActorStateKeyRelativeUrlFormat, actorType, actorId, keyName); - - actualPath.ShouldBe(expectedPath); - request.Request.Method.ShouldBe(HttpMethod.Get); - } - - [Fact] - public async Task SaveStateTransactionally_ValidateRequest() - { - await using var client = TestClient.CreateForDaprHttpInterator(); - - const string actorType = "ActorType_Test"; - const string actorId = "ActorId_Test"; - const string data = "StateData"; - - var request = await client.CaptureHttpRequestAsync(async httpInteractor => - { - await httpInteractor.SaveStateTransactionallyAsync(actorType, actorId, data); - }); - - request.Dismiss(); - - var actualPath = request.Request.RequestUri.LocalPath.TrimStart('/'); - var expectedPath = string.Format(CultureInfo.InvariantCulture, Constants.ActorStateRelativeUrlFormat, actorType, actorId); - - actualPath.ShouldBe(expectedPath); - request.Request.Method.ShouldBe(HttpMethod.Put); - } - - [Fact] - public async Task InvokeActorMethodWithoutRemoting_ValidateRequest() - { - await using var client = TestClient.CreateForDaprHttpInterator(); - - const string actorType = "ActorType_Test"; - const string actorId = "ActorId_Test"; - const string methodName = "MethodName"; - const string payload = "JsonData"; - - var request = await client.CaptureHttpRequestAsync(async httpInteractor => - { - await httpInteractor.InvokeActorMethodWithoutRemotingAsync(actorType, actorId, methodName, payload); - }); - - request.Dismiss(); - - var actualPath = request.Request.RequestUri.LocalPath.TrimStart('/'); - var expectedPath = string.Format(CultureInfo.InvariantCulture, Constants.ActorMethodRelativeUrlFormat, actorType, actorId, methodName); - - actualPath.ShouldBe(expectedPath); - request.Request.Method.ShouldBe(HttpMethod.Put); - } - - [Fact] - public async Task RegisterReminder_ValidateRequest() - { - await using var client = TestClient.CreateForDaprHttpInterator(); - - const string actorType = "ActorType_Test"; - const string actorId = "ActorId_Test"; - const string reminderName = "ReminderName"; - const string payload = "JsonData"; - - var request = await client.CaptureHttpRequestAsync(async httpInteractor => - { - await httpInteractor.RegisterReminderAsync(actorType, actorId, reminderName, payload); - }); - - request.Dismiss(); - - var actualPath = request.Request.RequestUri.LocalPath.TrimStart('/'); - var expectedPath = string.Format(CultureInfo.InvariantCulture, Constants.ActorReminderRelativeUrlFormat, actorType, actorId, reminderName); - - actualPath.ShouldBe(expectedPath); - request.Request.Method.ShouldBe(HttpMethod.Put); - } - - [Fact] - public async Task UnregisterReminder_ValidateRequest() - { - await using var client = TestClient.CreateForDaprHttpInterator(); - - const string actorType = "ActorType_Test"; - const string actorId = "ActorId_Test"; - const string reminderName = "ReminderName"; - - var request = await client.CaptureHttpRequestAsync(async httpInteractor => - { - await httpInteractor.UnregisterReminderAsync(actorType, actorId, reminderName); - }); - - request.Dismiss(); - - var actualPath = request.Request.RequestUri.LocalPath.TrimStart('/'); - var expectedPath = string.Format(CultureInfo.InvariantCulture, Constants.ActorReminderRelativeUrlFormat, actorType, actorId, reminderName); - - actualPath.ShouldBe(expectedPath); - request.Request.Method.ShouldBe(HttpMethod.Delete); - } - - [Fact] - public async Task GetReminder_ValidateRequest() - { - await using var client = TestClient.CreateForDaprHttpInterator(); - - const string actorType = "ActorType_Test"; - const string actorId = "ActorId_Test"; - const string reminderName = "ReminderName"; - - var request = await client.CaptureHttpRequestAsync(async httpInteractor => - { - await httpInteractor.GetReminderAsync(actorType, actorId, reminderName); - }); - - request.Dismiss(); - - var actualPath = request.Request.RequestUri.LocalPath.TrimStart('/'); - var expectedPath = string.Format(CultureInfo.InvariantCulture, Constants.ActorReminderRelativeUrlFormat, - actorType, actorId, reminderName); - - actualPath.ShouldBe(expectedPath); - request.Request.Method.ShouldBe(HttpMethod.Get); - } - - [Fact] - public async Task RegisterTimer_ValidateRequest() - { - await using var client = TestClient.CreateForDaprHttpInterator(); - - const string actorType = "ActorType_Test"; - const string actorId = "ActorId_Test"; - const string timerName = "TimerName"; - const string payload = "JsonData"; - - var request = await client.CaptureHttpRequestAsync(async httpInteractor => - { - await httpInteractor.RegisterTimerAsync(actorType, actorId, timerName, payload); - }); - - request.Dismiss(); - - var actualPath = request.Request.RequestUri.LocalPath.TrimStart('/'); - var expectedPath = string.Format(CultureInfo.InvariantCulture, Constants.ActorTimerRelativeUrlFormat, actorType, actorId, timerName); - - actualPath.ShouldBe(expectedPath); - request.Request.Method.ShouldBe(HttpMethod.Put); - } - - [Fact] - public async Task UnregisterTimer_ValidateRequest() - { - await using var client = TestClient.CreateForDaprHttpInterator(); - - var actorType = "ActorType_Test"; - var actorId = "ActorId_Test"; - var timerName = "TimerName"; - - var request = await client.CaptureHttpRequestAsync(async httpInteractor => - { - await httpInteractor.UnregisterTimerAsync(actorType, actorId, timerName); - }); - - request.Dismiss(); - - var actualPath = request.Request.RequestUri.LocalPath.TrimStart('/'); - var expectedPath = string.Format(CultureInfo.InvariantCulture, Constants.ActorTimerRelativeUrlFormat, actorType, actorId, timerName); - - actualPath.ShouldBe(expectedPath); - request.Request.Method.ShouldBe(HttpMethod.Delete); - } - - [Fact] - public async Task Call_WithApiTokenSet() - { - await using var client = TestClient.CreateForDaprHttpInterator(apiToken: "test_token"); - - const string actorType = "ActorType_Test"; - const string actorId = "ActorId_Test"; - const string timerName = "TimerName"; - - var request = await client.CaptureHttpRequestAsync(async httpInteractor => - { - await httpInteractor.UnregisterTimerAsync(actorType, actorId, timerName); - }); - - request.Dismiss(); - - request.Request.Headers.TryGetValues("dapr-api-token", out var headerValues); - headerValues.Count().ShouldBe(1); - headerValues.First().ShouldBe("test_token"); - } - - [Fact] - public async Task Call_WithoutApiToken() - { - await using var client = TestClient.CreateForDaprHttpInterator(); - - const string actorType = "ActorType_Test"; - const string actorId = "ActorId_Test"; - const string timerName = "TimerName"; - - var request = await client.CaptureHttpRequestAsync(async httpInteractor => - { - await httpInteractor.UnregisterTimerAsync(actorType, actorId, timerName); - }); - - request.Dismiss(); - - request.Request.Headers.TryGetValues("dapr-api-token", out var headerValues); - headerValues.ShouldBeNull(); - } - - [Fact] - public async Task Call_ValidateUnsuccessfulResponse() - { - await using var client = TestClient.CreateForDaprHttpInterator(); - - const string actorType = "ActorType_Test"; - const string actorId = "ActorId_Test"; - const string timerName = "TimerName"; - - var request = await client.CaptureHttpRequestAsync(async httpInteractor => - { - await httpInteractor.UnregisterTimerAsync(actorType, actorId, timerName); - }); - - request.Dismiss(); - - var error = new DaprError() - { - ErrorCode = "ERR_STATE_STORE", - Message = "State Store Error" - }; - - var message = new HttpResponseMessage(HttpStatusCode.InternalServerError) - { - Content = new StringContent(JsonSerializer.Serialize(error)) - }; - - await Assert.ThrowsAsync(async () => - { - await request.CompleteAsync(message); - }); - } - - [Fact] - public async Task Call_ValidateUnsuccessful404Response() - { - await using var client = TestClient.CreateForDaprHttpInterator(); - - const string actorType = "ActorType_Test"; - const string actorId = "ActorId_Test"; - const string timerName = "TimerName"; - - var request = await client.CaptureHttpRequestAsync(async httpInteractor => - { - await httpInteractor.UnregisterTimerAsync(actorType, actorId, timerName); - }); - - var message = new HttpResponseMessage(HttpStatusCode.NotFound); - - await Assert.ThrowsAsync(async () => - { - await request.CompleteAsync(message); - }); - } - - [Fact] - public async Task Call_ValidateUnauthorizedResponse() - { - await using var client = TestClient.CreateForDaprHttpInterator(); - - const string actorType = "ActorType_Test"; - const string actorId = "ActorId_Test"; - const string timerName = "TimerName"; - - var request = await client.CaptureHttpRequestAsync(async httpInteractor => - { - await httpInteractor.UnregisterTimerAsync(actorType, actorId, timerName); - }); - - var message = new HttpResponseMessage(HttpStatusCode.Unauthorized); - - await Assert.ThrowsAsync(async () => - { - await request.CompleteAsync(message); - }); - } - - [Fact] - public async Task InvokeActorMethodAddsReentrancyIdIfSet_ValidateHeaders() - { - await using var client = TestClient.CreateForDaprHttpInterator(); - - const string actorType = "ActorType_Test"; - const string actorId = "ActorId_Test"; - const string methodName = "MethodName"; - const string payload = "JsonData"; - - ActorReentrancyContextAccessor.ReentrancyContext = "1"; - var request = await client.CaptureHttpRequestAsync(async httpInteractor => - { - await httpInteractor.InvokeActorMethodWithoutRemotingAsync(actorType, actorId, methodName, payload); - }); - - request.Dismiss(); - Assert.True(request.Request.Headers.Contains(Constants.ReentrancyRequestHeaderName)); - Assert.Contains("1", request.Request.Headers.GetValues(Constants.ReentrancyRequestHeaderName)); - } - - [Fact] - public async Task InvokeActorMethodOmitsReentrancyIdIfNotSet_ValidateHeaders() - { - await using var client = TestClient.CreateForDaprHttpInterator(); - - const string actorType = "ActorType_Test"; - const string actorId = "ActorId_Test"; - const string methodName = "MethodName"; - const string payload = "JsonData"; - - var request = await client.CaptureHttpRequestAsync(async httpInteractor => - { - await httpInteractor.InvokeActorMethodWithoutRemotingAsync(actorType, actorId, methodName, payload); - }); - - request.Dismiss(); - Assert.False(request.Request.Headers.Contains(Constants.ReentrancyRequestHeaderName)); - } - - [Fact] - public async Task GetState_TTLExpireTimeExists() - { - await using var client = TestClient.CreateForDaprHttpInterator(); - - const string actorType = "ActorType_Test"; - const string actorId = "ActorId_Test"; - const string keyName = "StateKey_Test"; - - var request = await client.CaptureHttpRequestAsync(async httpInteractor => - { - return await httpInteractor.GetStateAsync(actorType, actorId, keyName); - }); - - var message = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent("test"), - Headers = - { - { "Metadata.ttlExpireTime", "2023-04-05T23:22:21Z" }, - }, - }; - - var actual = await request.CompleteAsync(message); - Assert.Equal("test", actual.Value); - var expTTL = new DateTimeOffset(2023, 04, 05, 23, 22, 21, 0, new GregorianCalendar(), new TimeSpan(0, 0, 0)); - Assert.Equal(expTTL, actual.TTLExpireTime); - } - - [Fact] - public async Task GetState_TTLExpireTimeNotExists() - { - await using var client = TestClient.CreateForDaprHttpInterator(); - - const string actorType = "ActorType_Test"; - const string actorId = "ActorId_Test"; - const string keyName = "StateKey_Test"; - - var request = await client.CaptureHttpRequestAsync(async httpInteractor => - { - return await httpInteractor.GetStateAsync(actorType, actorId, keyName); - }); - - var message = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent("test"), - }; - - var actual = await request.CompleteAsync(message); - Assert.Equal("test", actual.Value); - Assert.False(actual.TTLExpireTime.HasValue); - } + actualPath.ShouldBe(expectedPath); + request.Request.Method.ShouldBe(HttpMethod.Get); } -} + + [Fact] + public async Task SaveStateTransactionally_ValidateRequest() + { + await using var client = TestClient.CreateForDaprHttpInterator(); + + const string actorType = "ActorType_Test"; + const string actorId = "ActorId_Test"; + const string data = "StateData"; + + var request = await client.CaptureHttpRequestAsync(async httpInteractor => + { + await httpInteractor.SaveStateTransactionallyAsync(actorType, actorId, data); + }); + + request.Dismiss(); + + var actualPath = request.Request.RequestUri.LocalPath.TrimStart('/'); + var expectedPath = string.Format(CultureInfo.InvariantCulture, Constants.ActorStateRelativeUrlFormat, actorType, actorId); + + actualPath.ShouldBe(expectedPath); + request.Request.Method.ShouldBe(HttpMethod.Put); + } + + [Fact] + public async Task InvokeActorMethodWithoutRemoting_ValidateRequest() + { + await using var client = TestClient.CreateForDaprHttpInterator(); + + const string actorType = "ActorType_Test"; + const string actorId = "ActorId_Test"; + const string methodName = "MethodName"; + const string payload = "JsonData"; + + var request = await client.CaptureHttpRequestAsync(async httpInteractor => + { + await httpInteractor.InvokeActorMethodWithoutRemotingAsync(actorType, actorId, methodName, payload); + }); + + request.Dismiss(); + + var actualPath = request.Request.RequestUri.LocalPath.TrimStart('/'); + var expectedPath = string.Format(CultureInfo.InvariantCulture, Constants.ActorMethodRelativeUrlFormat, actorType, actorId, methodName); + + actualPath.ShouldBe(expectedPath); + request.Request.Method.ShouldBe(HttpMethod.Put); + } + + [Fact] + public async Task RegisterReminder_ValidateRequest() + { + await using var client = TestClient.CreateForDaprHttpInterator(); + + const string actorType = "ActorType_Test"; + const string actorId = "ActorId_Test"; + const string reminderName = "ReminderName"; + const string payload = "JsonData"; + + var request = await client.CaptureHttpRequestAsync(async httpInteractor => + { + await httpInteractor.RegisterReminderAsync(actorType, actorId, reminderName, payload); + }); + + request.Dismiss(); + + var actualPath = request.Request.RequestUri.LocalPath.TrimStart('/'); + var expectedPath = string.Format(CultureInfo.InvariantCulture, Constants.ActorReminderRelativeUrlFormat, actorType, actorId, reminderName); + + actualPath.ShouldBe(expectedPath); + request.Request.Method.ShouldBe(HttpMethod.Put); + } + + [Fact] + public async Task UnregisterReminder_ValidateRequest() + { + await using var client = TestClient.CreateForDaprHttpInterator(); + + const string actorType = "ActorType_Test"; + const string actorId = "ActorId_Test"; + const string reminderName = "ReminderName"; + + var request = await client.CaptureHttpRequestAsync(async httpInteractor => + { + await httpInteractor.UnregisterReminderAsync(actorType, actorId, reminderName); + }); + + request.Dismiss(); + + var actualPath = request.Request.RequestUri.LocalPath.TrimStart('/'); + var expectedPath = string.Format(CultureInfo.InvariantCulture, Constants.ActorReminderRelativeUrlFormat, actorType, actorId, reminderName); + + actualPath.ShouldBe(expectedPath); + request.Request.Method.ShouldBe(HttpMethod.Delete); + } + + [Fact] + public async Task GetReminder_ValidateRequest() + { + await using var client = TestClient.CreateForDaprHttpInterator(); + + const string actorType = "ActorType_Test"; + const string actorId = "ActorId_Test"; + const string reminderName = "ReminderName"; + + var request = await client.CaptureHttpRequestAsync(async httpInteractor => + { + await httpInteractor.GetReminderAsync(actorType, actorId, reminderName); + }); + + request.Dismiss(); + + var actualPath = request.Request.RequestUri.LocalPath.TrimStart('/'); + var expectedPath = string.Format(CultureInfo.InvariantCulture, Constants.ActorReminderRelativeUrlFormat, + actorType, actorId, reminderName); + + actualPath.ShouldBe(expectedPath); + request.Request.Method.ShouldBe(HttpMethod.Get); + } + + [Fact] + public async Task RegisterTimer_ValidateRequest() + { + await using var client = TestClient.CreateForDaprHttpInterator(); + + const string actorType = "ActorType_Test"; + const string actorId = "ActorId_Test"; + const string timerName = "TimerName"; + const string payload = "JsonData"; + + var request = await client.CaptureHttpRequestAsync(async httpInteractor => + { + await httpInteractor.RegisterTimerAsync(actorType, actorId, timerName, payload); + }); + + request.Dismiss(); + + var actualPath = request.Request.RequestUri.LocalPath.TrimStart('/'); + var expectedPath = string.Format(CultureInfo.InvariantCulture, Constants.ActorTimerRelativeUrlFormat, actorType, actorId, timerName); + + actualPath.ShouldBe(expectedPath); + request.Request.Method.ShouldBe(HttpMethod.Put); + } + + [Fact] + public async Task UnregisterTimer_ValidateRequest() + { + await using var client = TestClient.CreateForDaprHttpInterator(); + + var actorType = "ActorType_Test"; + var actorId = "ActorId_Test"; + var timerName = "TimerName"; + + var request = await client.CaptureHttpRequestAsync(async httpInteractor => + { + await httpInteractor.UnregisterTimerAsync(actorType, actorId, timerName); + }); + + request.Dismiss(); + + var actualPath = request.Request.RequestUri.LocalPath.TrimStart('/'); + var expectedPath = string.Format(CultureInfo.InvariantCulture, Constants.ActorTimerRelativeUrlFormat, actorType, actorId, timerName); + + actualPath.ShouldBe(expectedPath); + request.Request.Method.ShouldBe(HttpMethod.Delete); + } + + [Fact] + public async Task Call_WithApiTokenSet() + { + await using var client = TestClient.CreateForDaprHttpInterator(apiToken: "test_token"); + + const string actorType = "ActorType_Test"; + const string actorId = "ActorId_Test"; + const string timerName = "TimerName"; + + var request = await client.CaptureHttpRequestAsync(async httpInteractor => + { + await httpInteractor.UnregisterTimerAsync(actorType, actorId, timerName); + }); + + request.Dismiss(); + + request.Request.Headers.TryGetValues("dapr-api-token", out var headerValues); + headerValues.Count().ShouldBe(1); + headerValues.First().ShouldBe("test_token"); + } + + [Fact] + public async Task Call_WithoutApiToken() + { + await using var client = TestClient.CreateForDaprHttpInterator(); + + const string actorType = "ActorType_Test"; + const string actorId = "ActorId_Test"; + const string timerName = "TimerName"; + + var request = await client.CaptureHttpRequestAsync(async httpInteractor => + { + await httpInteractor.UnregisterTimerAsync(actorType, actorId, timerName); + }); + + request.Dismiss(); + + request.Request.Headers.TryGetValues("dapr-api-token", out var headerValues); + headerValues.ShouldBeNull(); + } + + [Fact] + public async Task Call_ValidateUnsuccessfulResponse() + { + await using var client = TestClient.CreateForDaprHttpInterator(); + + const string actorType = "ActorType_Test"; + const string actorId = "ActorId_Test"; + const string timerName = "TimerName"; + + var request = await client.CaptureHttpRequestAsync(async httpInteractor => + { + await httpInteractor.UnregisterTimerAsync(actorType, actorId, timerName); + }); + + request.Dismiss(); + + var error = new DaprError() + { + ErrorCode = "ERR_STATE_STORE", + Message = "State Store Error" + }; + + var message = new HttpResponseMessage(HttpStatusCode.InternalServerError) + { + Content = new StringContent(JsonSerializer.Serialize(error)) + }; + + await Assert.ThrowsAsync(async () => + { + await request.CompleteAsync(message); + }); + } + + [Fact] + public async Task Call_ValidateUnsuccessful404Response() + { + await using var client = TestClient.CreateForDaprHttpInterator(); + + const string actorType = "ActorType_Test"; + const string actorId = "ActorId_Test"; + const string timerName = "TimerName"; + + var request = await client.CaptureHttpRequestAsync(async httpInteractor => + { + await httpInteractor.UnregisterTimerAsync(actorType, actorId, timerName); + }); + + var message = new HttpResponseMessage(HttpStatusCode.NotFound); + + await Assert.ThrowsAsync(async () => + { + await request.CompleteAsync(message); + }); + } + + [Fact] + public async Task Call_ValidateUnauthorizedResponse() + { + await using var client = TestClient.CreateForDaprHttpInterator(); + + const string actorType = "ActorType_Test"; + const string actorId = "ActorId_Test"; + const string timerName = "TimerName"; + + var request = await client.CaptureHttpRequestAsync(async httpInteractor => + { + await httpInteractor.UnregisterTimerAsync(actorType, actorId, timerName); + }); + + var message = new HttpResponseMessage(HttpStatusCode.Unauthorized); + + await Assert.ThrowsAsync(async () => + { + await request.CompleteAsync(message); + }); + } + + [Fact] + public async Task InvokeActorMethodAddsReentrancyIdIfSet_ValidateHeaders() + { + await using var client = TestClient.CreateForDaprHttpInterator(); + + const string actorType = "ActorType_Test"; + const string actorId = "ActorId_Test"; + const string methodName = "MethodName"; + const string payload = "JsonData"; + + ActorReentrancyContextAccessor.ReentrancyContext = "1"; + var request = await client.CaptureHttpRequestAsync(async httpInteractor => + { + await httpInteractor.InvokeActorMethodWithoutRemotingAsync(actorType, actorId, methodName, payload); + }); + + request.Dismiss(); + Assert.True(request.Request.Headers.Contains(Constants.ReentrancyRequestHeaderName)); + Assert.Contains("1", request.Request.Headers.GetValues(Constants.ReentrancyRequestHeaderName)); + } + + [Fact] + public async Task InvokeActorMethodOmitsReentrancyIdIfNotSet_ValidateHeaders() + { + await using var client = TestClient.CreateForDaprHttpInterator(); + + const string actorType = "ActorType_Test"; + const string actorId = "ActorId_Test"; + const string methodName = "MethodName"; + const string payload = "JsonData"; + + var request = await client.CaptureHttpRequestAsync(async httpInteractor => + { + await httpInteractor.InvokeActorMethodWithoutRemotingAsync(actorType, actorId, methodName, payload); + }); + + request.Dismiss(); + Assert.False(request.Request.Headers.Contains(Constants.ReentrancyRequestHeaderName)); + } + + [Fact] + public async Task GetState_TTLExpireTimeExists() + { + await using var client = TestClient.CreateForDaprHttpInterator(); + + const string actorType = "ActorType_Test"; + const string actorId = "ActorId_Test"; + const string keyName = "StateKey_Test"; + + var request = await client.CaptureHttpRequestAsync(async httpInteractor => + { + return await httpInteractor.GetStateAsync(actorType, actorId, keyName); + }); + + var message = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("test"), + Headers = + { + { "Metadata.ttlExpireTime", "2023-04-05T23:22:21Z" }, + }, + }; + + var actual = await request.CompleteAsync(message); + Assert.Equal("test", actual.Value); + var expTTL = new DateTimeOffset(2023, 04, 05, 23, 22, 21, 0, new GregorianCalendar(), new TimeSpan(0, 0, 0)); + Assert.Equal(expTTL, actual.TTLExpireTime); + } + + [Fact] + public async Task GetState_TTLExpireTimeNotExists() + { + await using var client = TestClient.CreateForDaprHttpInterator(); + + const string actorType = "ActorType_Test"; + const string actorId = "ActorId_Test"; + const string keyName = "StateKey_Test"; + + var request = await client.CaptureHttpRequestAsync(async httpInteractor => + { + return await httpInteractor.GetStateAsync(actorType, actorId, keyName); + }); + + var message = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("test"), + }; + + var actual = await request.CompleteAsync(message); + Assert.Equal("test", actual.Value); + Assert.False(actual.TTLExpireTime.HasValue); + } +} \ No newline at end of file diff --git a/test/Dapr.Actors.Test/DaprStateProviderTest.cs b/test/Dapr.Actors.Test/DaprStateProviderTest.cs index 948b14b4..5c6839f7 100644 --- a/test/Dapr.Actors.Test/DaprStateProviderTest.cs +++ b/test/Dapr.Actors.Test/DaprStateProviderTest.cs @@ -11,120 +11,119 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Test +namespace Dapr.Actors.Test; + +using System; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; +using Xunit; +using Dapr.Actors.Communication; +using Dapr.Actors.Runtime; +using Moq; + +/// +/// Contains tests for DaprStateProvider. +/// +public class DaprStateProviderTest { - using System; - using System.Text.Json; - using System.Threading; - using System.Threading.Tasks; - using System.Collections.Generic; - using Xunit; - using Dapr.Actors.Communication; - using Dapr.Actors.Runtime; - using Moq; - - /// - /// Contains tests for DaprStateProvider. - /// - public class DaprStateProviderTest + [Fact] + public async Task SaveStateAsync() { - [Fact] - public async Task SaveStateAsync() - { - var interactor = new Mock(); - var provider = new DaprStateProvider(interactor.Object, new JsonSerializerOptions()); - var token = new CancellationToken(); + var interactor = new Mock(); + var provider = new DaprStateProvider(interactor.Object, new JsonSerializerOptions()); + var token = new CancellationToken(); - var stateChangeList = new List(); - stateChangeList.Add( - new ActorStateChange("key1", typeof(string), "value1", StateChangeKind.Add, DateTimeOffset.UtcNow.Add(TimeSpan.FromSeconds(2)))); - stateChangeList.Add( - new ActorStateChange("key2", typeof(string), "value2", StateChangeKind.Add, null)); + var stateChangeList = new List(); + stateChangeList.Add( + new ActorStateChange("key1", typeof(string), "value1", StateChangeKind.Add, DateTimeOffset.UtcNow.Add(TimeSpan.FromSeconds(2)))); + stateChangeList.Add( + new ActorStateChange("key2", typeof(string), "value2", StateChangeKind.Add, null)); - string content = null; - interactor - .Setup(d => d.SaveStateTransactionallyAsync( + string content = null; + interactor + .Setup(d => d.SaveStateTransactionallyAsync( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Callback((actorType, actorId, data, token) => content = data) - .Returns(Task.FromResult(true)); + .Callback((actorType, actorId, data, token) => content = data) + .Returns(Task.FromResult(true)); - await provider.SaveStateAsync("actorType", "actorId", stateChangeList, token); - Assert.Equal( - "[{\"operation\":\"upsert\",\"request\":{\"key\":\"key1\",\"value\":\"value1\",\"metadata\":{\"ttlInSeconds\":\"2\"}}},{\"operation\":\"upsert\",\"request\":{\"key\":\"key2\",\"value\":\"value2\"}}]", - content - ); - } - - [Fact] - public async Task ContainsStateAsync() - { - var interactor = new Mock(); - var provider = new DaprStateProvider(interactor.Object, new JsonSerializerOptions()); - var token = new CancellationToken(); - - interactor - .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(new ActorStateResponse("", null))); - Assert.False(await provider.ContainsStateAsync("actorType", "actorId", "key", token)); - - interactor - .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(new ActorStateResponse("\"value\"", null))); - Assert.True(await provider.ContainsStateAsync("actorType", "actorId", "key", token)); - - var ttl = DateTime.UtcNow.AddSeconds(1); - interactor - .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(new ActorStateResponse("\"value\"", ttl))); - Assert.True(await provider.ContainsStateAsync("actorType", "actorId", "key", token)); - - ttl = DateTime.UtcNow.AddSeconds(-1); - interactor - .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(new ActorStateResponse("\"value\"", ttl))); - Assert.False(await provider.ContainsStateAsync("actorType", "actorId", "key", token)); - } - - [Fact] - public async Task TryLoadStateAsync() - { - var interactor = new Mock(); - var provider = new DaprStateProvider(interactor.Object, new JsonSerializerOptions()); - var token = new CancellationToken(); - - interactor - .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(new ActorStateResponse("", null))); - var resp = await provider.TryLoadStateAsync("actorType", "actorId", "key", token); - Assert.False(resp.HasValue); - - interactor - .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(new ActorStateResponse("\"value\"", null))); - resp = await provider.TryLoadStateAsync("actorType", "actorId", "key", token); - Assert.True(resp.HasValue); - Assert.Equal("value", resp.Value.Value); - Assert.False(resp.Value.TTLExpireTime.HasValue); - - var ttl = DateTime.UtcNow.AddSeconds(1); - interactor - .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(new ActorStateResponse("\"value\"", ttl))); - resp = await provider.TryLoadStateAsync("actorType", "actorId", "key", token); - Assert.True(resp.HasValue); - Assert.Equal("value", resp.Value.Value); - Assert.True(resp.Value.TTLExpireTime.HasValue); - Assert.Equal(ttl, resp.Value.TTLExpireTime.Value); - - ttl = DateTime.UtcNow.AddSeconds(-1); - interactor - .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(new ActorStateResponse("\"value\"", ttl))); - resp = await provider.TryLoadStateAsync("actorType", "actorId", "key", token); - Assert.False(resp.HasValue); - } + await provider.SaveStateAsync("actorType", "actorId", stateChangeList, token); + Assert.Equal( + "[{\"operation\":\"upsert\",\"request\":{\"key\":\"key1\",\"value\":\"value1\",\"metadata\":{\"ttlInSeconds\":\"2\"}}},{\"operation\":\"upsert\",\"request\":{\"key\":\"key2\",\"value\":\"value2\"}}]", + content + ); } -} + + [Fact] + public async Task ContainsStateAsync() + { + var interactor = new Mock(); + var provider = new DaprStateProvider(interactor.Object, new JsonSerializerOptions()); + var token = new CancellationToken(); + + interactor + .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(new ActorStateResponse("", null))); + Assert.False(await provider.ContainsStateAsync("actorType", "actorId", "key", token)); + + interactor + .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(new ActorStateResponse("\"value\"", null))); + Assert.True(await provider.ContainsStateAsync("actorType", "actorId", "key", token)); + + var ttl = DateTime.UtcNow.AddSeconds(1); + interactor + .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(new ActorStateResponse("\"value\"", ttl))); + Assert.True(await provider.ContainsStateAsync("actorType", "actorId", "key", token)); + + ttl = DateTime.UtcNow.AddSeconds(-1); + interactor + .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(new ActorStateResponse("\"value\"", ttl))); + Assert.False(await provider.ContainsStateAsync("actorType", "actorId", "key", token)); + } + + [Fact] + public async Task TryLoadStateAsync() + { + var interactor = new Mock(); + var provider = new DaprStateProvider(interactor.Object, new JsonSerializerOptions()); + var token = new CancellationToken(); + + interactor + .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(new ActorStateResponse("", null))); + var resp = await provider.TryLoadStateAsync("actorType", "actorId", "key", token); + Assert.False(resp.HasValue); + + interactor + .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(new ActorStateResponse("\"value\"", null))); + resp = await provider.TryLoadStateAsync("actorType", "actorId", "key", token); + Assert.True(resp.HasValue); + Assert.Equal("value", resp.Value.Value); + Assert.False(resp.Value.TTLExpireTime.HasValue); + + var ttl = DateTime.UtcNow.AddSeconds(1); + interactor + .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(new ActorStateResponse("\"value\"", ttl))); + resp = await provider.TryLoadStateAsync("actorType", "actorId", "key", token); + Assert.True(resp.HasValue); + Assert.Equal("value", resp.Value.Value); + Assert.True(resp.Value.TTLExpireTime.HasValue); + Assert.Equal(ttl, resp.Value.TTLExpireTime.Value); + + ttl = DateTime.UtcNow.AddSeconds(-1); + interactor + .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(new ActorStateResponse("\"value\"", ttl))); + resp = await provider.TryLoadStateAsync("actorType", "actorId", "key", token); + Assert.False(resp.HasValue); + } +} \ No newline at end of file diff --git a/test/Dapr.Actors.Test/Description/ActorInterfaceDescriptionTests.cs b/test/Dapr.Actors.Test/Description/ActorInterfaceDescriptionTests.cs index 3193a795..082000e5 100644 --- a/test/Dapr.Actors.Test/Description/ActorInterfaceDescriptionTests.cs +++ b/test/Dapr.Actors.Test/Description/ActorInterfaceDescriptionTests.cs @@ -16,145 +16,144 @@ using System.Threading.Tasks; using Shouldly; using Xunit; -namespace Dapr.Actors.Description +namespace Dapr.Actors.Description; + +public sealed class ActorInterfaceDescriptionTests { - public sealed class ActorInterfaceDescriptionTests + [Fact] + public void ActorInterfaceDescription_CreateActorInterfaceDescription() { - [Fact] - public void ActorInterfaceDescription_CreateActorInterfaceDescription() - { - // Arrange - Type type = typeof(ITestActor); + // Arrange + Type type = typeof(ITestActor); - // Act - var description = ActorInterfaceDescription.Create(type); + // Act + var description = ActorInterfaceDescription.Create(type); - // Assert - description.ShouldNotBeNull(); + // Assert + description.ShouldNotBeNull(); - description.InterfaceType.ShouldBe(type); - description.Id.ShouldNotBe(0); - description.V1Id.ShouldBe(0); - description.Methods.Length.ShouldBe(2); - } - - [Fact] - public void ActorInterfaceDescription_CreateThrowsArgumentException_WhenTypeIsNotAnInterface() - { - // Arrange - Type type = typeof(object); - - // Act - Action action = () => ActorInterfaceDescription.Create(type); - - // Assert - var exception = Should.Throw(action); - exception.Message.ShouldMatch(@"The type 'System.Object' is not an Actor interface as it is not an interface.*"); - exception.ParamName.ShouldBe("actorInterfaceType"); - } - - [Fact] - public void ActorInterfaceDescription_CreateThrowsArgumentException_WhenTypeIsNotAnActorInterface() - { - // Arrange - Type type = typeof(ICloneable); - - // Act - Action action = () => ActorInterfaceDescription.Create(type); - - // Assert - var exception = Should.Throw(action); - exception.Message.ShouldMatch(@"The type 'System.ICloneable' is not an actor interface as it does not derive from the interface 'Dapr.Actors.IActor'.*"); - exception.ParamName.ShouldBe("actorInterfaceType"); - } - - [Fact] - public void ActorInterfaceDescription_CreateThrowsArgumentException_WhenActorInterfaceInheritsNonActorInterfaces() - { - // Arrange - Type type = typeof(IClonableActor); - - // Act - Action action = () => ActorInterfaceDescription.Create(type); - - // Assert - var exception = Should.Throw(action); - exception.Message.ShouldMatch(@"The type '.*\+IClonableActor' is not an actor interface as it derive from a non actor interface 'System.ICloneable'. All actor interfaces must derive from 'Dapr.Actors.IActor'.*"); - exception.ParamName.ShouldBe("actorInterfaceType"); - } - - [Fact] - public void ActorInterfaceDescription_CreateUsingCRCIdActorInterfaceDescription() - { - // Arrange - Type type = typeof(ITestActor); - - // Act - var description = ActorInterfaceDescription.CreateUsingCRCId(type); - - // Assert - description.ShouldNotBeNull(); - - description.InterfaceType.ShouldBe(type); - description.Id.ShouldBe(-934188464); - description.V1Id.ShouldNotBe(0); - description.Methods.Length.ShouldBe(2); - } - - [Fact] - public void ActorInterfaceDescription_CreateUsingCRCIdThrowsArgumentException_WhenTypeIsNotAnInterface() - { - // Arrange - Type type = typeof(object); - - // Act - Action action = () => ActorInterfaceDescription.CreateUsingCRCId(type); - - // Assert - var exception = Should.Throw(action); - exception.Message.ShouldMatch(@"The type 'System.Object' is not an Actor interface as it is not an interface.*"); - exception.ParamName.ShouldBe("actorInterfaceType"); - } - - [Fact] - public void ActorInterfaceDescription_CreateUsingCRCIdThrowsArgumentException_WhenTypeIsNotAnActorInterface() - { - // Arrange - Type type = typeof(ICloneable); - - // Act - Action action = () => ActorInterfaceDescription.CreateUsingCRCId(type); - - // Assert - var exception = Should.Throw(action); - exception.Message.ShouldMatch(@"The type 'System.ICloneable' is not an actor interface as it does not derive from the interface 'Dapr.Actors.IActor'.*"); - exception.ParamName.ShouldBe("actorInterfaceType"); - } - - [Fact] - public void ActorInterfaceDescription_CreateUsingCRCIdThrowsArgumentException_WhenActorInterfaceInheritsNonActorInterfaces() - { - // Arrange - Type type = typeof(IClonableActor); - - // Act - Action action = () => ActorInterfaceDescription.CreateUsingCRCId(type); - - // Assert - var exception = Should.Throw(action); - exception.Message.ShouldMatch(@"The type '.*\+IClonableActor' is not an actor interface as it derive from a non actor interface 'System.ICloneable'. All actor interfaces must derive from 'Dapr.Actors.IActor'.*"); - exception.ParamName.ShouldBe("actorInterfaceType"); - } - - internal interface IClonableActor : ICloneable, IActor - { - } - - internal interface ITestActor : IActor - { - Task GetString(); - - Task MethodWithArguments(int number, bool choice, string information); - } + description.InterfaceType.ShouldBe(type); + description.Id.ShouldNotBe(0); + description.V1Id.ShouldBe(0); + description.Methods.Length.ShouldBe(2); } -} + + [Fact] + public void ActorInterfaceDescription_CreateThrowsArgumentException_WhenTypeIsNotAnInterface() + { + // Arrange + Type type = typeof(object); + + // Act + Action action = () => ActorInterfaceDescription.Create(type); + + // Assert + var exception = Should.Throw(action); + exception.Message.ShouldMatch(@"The type 'System.Object' is not an Actor interface as it is not an interface.*"); + exception.ParamName.ShouldBe("actorInterfaceType"); + } + + [Fact] + public void ActorInterfaceDescription_CreateThrowsArgumentException_WhenTypeIsNotAnActorInterface() + { + // Arrange + Type type = typeof(ICloneable); + + // Act + Action action = () => ActorInterfaceDescription.Create(type); + + // Assert + var exception = Should.Throw(action); + exception.Message.ShouldMatch(@"The type 'System.ICloneable' is not an actor interface as it does not derive from the interface 'Dapr.Actors.IActor'.*"); + exception.ParamName.ShouldBe("actorInterfaceType"); + } + + [Fact] + public void ActorInterfaceDescription_CreateThrowsArgumentException_WhenActorInterfaceInheritsNonActorInterfaces() + { + // Arrange + Type type = typeof(IClonableActor); + + // Act + Action action = () => ActorInterfaceDescription.Create(type); + + // Assert + var exception = Should.Throw(action); + exception.Message.ShouldMatch(@"The type '.*\+IClonableActor' is not an actor interface as it derive from a non actor interface 'System.ICloneable'. All actor interfaces must derive from 'Dapr.Actors.IActor'.*"); + exception.ParamName.ShouldBe("actorInterfaceType"); + } + + [Fact] + public void ActorInterfaceDescription_CreateUsingCRCIdActorInterfaceDescription() + { + // Arrange + Type type = typeof(ITestActor); + + // Act + var description = ActorInterfaceDescription.CreateUsingCRCId(type); + + // Assert + description.ShouldNotBeNull(); + + description.InterfaceType.ShouldBe(type); + description.Id.ShouldBe(-934188464); + description.V1Id.ShouldNotBe(0); + description.Methods.Length.ShouldBe(2); + } + + [Fact] + public void ActorInterfaceDescription_CreateUsingCRCIdThrowsArgumentException_WhenTypeIsNotAnInterface() + { + // Arrange + Type type = typeof(object); + + // Act + Action action = () => ActorInterfaceDescription.CreateUsingCRCId(type); + + // Assert + var exception = Should.Throw(action); + exception.Message.ShouldMatch(@"The type 'System.Object' is not an Actor interface as it is not an interface.*"); + exception.ParamName.ShouldBe("actorInterfaceType"); + } + + [Fact] + public void ActorInterfaceDescription_CreateUsingCRCIdThrowsArgumentException_WhenTypeIsNotAnActorInterface() + { + // Arrange + Type type = typeof(ICloneable); + + // Act + Action action = () => ActorInterfaceDescription.CreateUsingCRCId(type); + + // Assert + var exception = Should.Throw(action); + exception.Message.ShouldMatch(@"The type 'System.ICloneable' is not an actor interface as it does not derive from the interface 'Dapr.Actors.IActor'.*"); + exception.ParamName.ShouldBe("actorInterfaceType"); + } + + [Fact] + public void ActorInterfaceDescription_CreateUsingCRCIdThrowsArgumentException_WhenActorInterfaceInheritsNonActorInterfaces() + { + // Arrange + Type type = typeof(IClonableActor); + + // Act + Action action = () => ActorInterfaceDescription.CreateUsingCRCId(type); + + // Assert + var exception = Should.Throw(action); + exception.Message.ShouldMatch(@"The type '.*\+IClonableActor' is not an actor interface as it derive from a non actor interface 'System.ICloneable'. All actor interfaces must derive from 'Dapr.Actors.IActor'.*"); + exception.ParamName.ShouldBe("actorInterfaceType"); + } + + internal interface IClonableActor : ICloneable, IActor + { + } + + internal interface ITestActor : IActor + { + Task GetString(); + + Task MethodWithArguments(int number, bool choice, string information); + } +} \ No newline at end of file diff --git a/test/Dapr.Actors.Test/Description/InterfaceDescriptionTests.cs b/test/Dapr.Actors.Test/Description/InterfaceDescriptionTests.cs index 83f4de01..e0fd5f99 100644 --- a/test/Dapr.Actors.Test/Description/InterfaceDescriptionTests.cs +++ b/test/Dapr.Actors.Test/Description/InterfaceDescriptionTests.cs @@ -17,239 +17,238 @@ using System.Threading.Tasks; using Shouldly; using Xunit; -namespace Dapr.Actors.Description +namespace Dapr.Actors.Description; + +public sealed class InterfaceDescriptionTests { - public sealed class InterfaceDescriptionTests + [Fact] + public void InterfaceDescription_CreateInterfaceDescription() { - [Fact] - public void InterfaceDescription_CreateInterfaceDescription() + // Arrange + Type type = typeof(ITestActor); + + // Act + TestDescription description = new(type); + + // Assert + description.ShouldNotBeNull(); + + description.InterfaceType.ShouldBe(type); + description.Id.ShouldNotBe(0); + description.V1Id.ShouldBe(0); + description.Methods.ShouldBeEmpty(); + } + + [Fact] + public void InterfaceDescription_CreateCrcIdAndV1Id_WhenUseCrcIdGenerationIsSet() + { + // Arrange + Type type = typeof(ITestActor); + + // Act + TestDescription description = new(type, useCRCIdGeneration: true); + + // Assert + description.Id.ShouldBe(-934188464); + description.V1Id.ShouldNotBe(0); + } + + [Fact] + public void InterfaceDescription_CreateThrowsArgumentException_WhenTypeIsGenericDefinition() + { + // Arrange + Type type = typeof(IGenericActor<>); + + // Act + Action action = () => { TestDescription _ = new(type); }; + + // Assert + var exception = Should.Throw(action); + exception.Message.ShouldMatch(@"The actor interface '.*\+IGenericActor`1' is using generics. Generic interfaces cannot be remoted.*"); + exception.ParamName.ShouldBe("actorInterfaceType"); + } + + [Fact] + public void InterfaceDescription_CreateThrowsArgumentException_WhenTypeIsGeneric() + { + // Arrange + Type type = typeof(IGenericActor); + + // Act + Action action = () => { TestDescription _ = new(type); }; + + // Assert + var exception = Should.Throw(action); + exception.Message.ShouldMatch(@"The actor interface '.*\+IGenericActor`1\[.*IActor.*\]' is using generics. Generic interfaces cannot be remoted.*"); + exception.ParamName.ShouldBe("actorInterfaceType"); + } + + [Fact] + public void InterfaceDescription_CreateGeneratesMethodDescriptions_WhenTypeHasTaskMethods_ButDoesNotSeeInheritedMethods() + { + // Arrange + Type type = typeof(IChildActor); + + // Act + TestDescription description = new(type); + + // Assert + description.Methods.ShouldNotBeNull(); + description.Methods.ShouldBeOfType(); + description.Methods.Select(m => new {m.Name}).ShouldBe(new[] {new {Name = "GetInt"}}); + } + + [Fact] + public void InterfaceDescription_CreateGeneratesMethodDescriptions_WhenTypeHasVoidMethods() + { + // Arrange + Type type = typeof(IVoidActor); + + // Act + TestDescription description = new(type, methodReturnCheck: MethodReturnCheck.EnsureReturnsVoid); + + // Assert + description.Methods.ShouldNotBeNull(); + description.Methods.ShouldBeOfType(); + description.Methods.Select(m => new {m.Name}).ShouldBe(new[] {new {Name = "GetString"}, new {Name="MethodWithArguments"}}); + } + + [Fact] + public void InterfaceDescription_CreateThrowsArgumentException_WhenMethodsAreNotReturningTask() + { + // Arrange + Type type = typeof(IVoidActor); + + // Act + Action action = () => { - // Arrange - Type type = typeof(ITestActor); + TestDescription _ = new(type); + }; - // Act - TestDescription description = new(type); + // Assert + var exception = Should.Throw(action); + exception.Message.ShouldMatch(@"Method 'GetString' of actor interface '.*\+IVoidActor' does not return Task or Task<>. The actor interface methods must be async and must return either Task or Task<>.*"); + exception.ParamName.ShouldBe("actorInterfaceType"); + } - // Assert - description.ShouldNotBeNull(); + [Fact] + public void InterfaceDescription_CreateThrowsArgumentException_WhenMethodsAreNotReturningVoid() + { + // Arrange + Type type = typeof(IMethodActor); - description.InterfaceType.ShouldBe(type); - description.Id.ShouldNotBe(0); - description.V1Id.ShouldBe(0); - description.Methods.ShouldBeEmpty(); - } + // Act + Action action = () => { TestDescription _ = new(type, methodReturnCheck: MethodReturnCheck.EnsureReturnsVoid); }; - [Fact] - public void InterfaceDescription_CreateCrcIdAndV1Id_WhenUseCrcIdGenerationIsSet() + // Assert + var exception = Should.Throw(action); + exception.Message.ShouldMatch(@"Method 'GetString' of actor interface '.*\+IMethodActor' returns '.*\.Task`1\[.*System.String.*\]'.*"); + exception.ParamName.ShouldBe("actorInterfaceType"); + } + + [Fact] + public void InterfaceDescription_CreateThrowsArgumentException_WhenMethodsAreOverloaded() + { + // Arrange + Type type = typeof(IOverloadedMethodActor); + + // Act + Action action = () => { TestDescription _ = new(type); }; + + // Assert + var exception = Should.Throw(action); + exception.Message.ShouldMatch(@"Method 'GetString' of actor interface '.*\+IOverloadedMethodActor' is overloaded. The actor interface methods cannot be overloaded.*"); + exception.ParamName.ShouldBe("actorInterfaceType"); + } + + [Fact] + public void InterfaceDescription_CreateThrowsArgumentException_WhenMethodIsGeneric() + { + // Arrange + Type type = typeof(IGenericMethodActor); + + // Act + Action action = () => { TestDescription _ = new(type); }; + + // Assert + var exception = Should.Throw(action); + exception.Message.ShouldMatch(@"Method 'Get' of actor interface '.*\+IGenericMethodActor' is using generics. The actor interface methods cannot use generics.*"); + exception.ParamName.ShouldBe("actorInterfaceType"); + } + + [Fact] + public void InterfaceDescription_CreateThrowsArgumentException_WhenMethodHasVariableArguments() + { + // Arrange + Type type = typeof(IVariableActor); + + // Act + Action action = () => { TestDescription _ = new(type); }; + + // Assert + var exception = Should.Throw(action); + exception.Message.ShouldMatch(@"Method 'MethodWithVarArgs' of actor interface '.*\+IVariableActor' is using a variable argument list. The actor interface methods cannot have a variable argument list.*"); + exception.ParamName.ShouldBe("actorInterfaceType"); + } + + internal interface ITestActor : IActor + { + } + + internal interface IGenericActor : IActor + { + } + + internal interface IMethodActor : IActor + { + Task GetString(); + + Task MethodWithArguments(int number, bool choice, string information); + } + + internal interface IChildActor : IMethodActor + { + Task GetInt(); + } + + internal interface IVariableActor : IActor + { + Task MethodWithVarArgs(__arglist); + } + + internal interface IVoidActor : IActor + { + void GetString(); + + void MethodWithArguments(int number, bool choice, string information); + } + + internal interface IOverloadedActor : IMethodActor + { + Task GetString(string parameter); + } + + internal interface IOverloadedMethodActor : IActor + { + Task GetString(); + + Task GetString(string parameter); + } + + internal interface IGenericMethodActor : IActor + { + Task Get(); + } + + internal class TestDescription : InterfaceDescription + { + public TestDescription( + Type remotedInterfaceType, + string remotedInterfaceKindName = "actor", + bool useCRCIdGeneration = false, + MethodReturnCheck methodReturnCheck = MethodReturnCheck.EnsureReturnsTask) + : base(remotedInterfaceKindName, remotedInterfaceType, useCRCIdGeneration, methodReturnCheck) { - // Arrange - Type type = typeof(ITestActor); - - // Act - TestDescription description = new(type, useCRCIdGeneration: true); - - // Assert - description.Id.ShouldBe(-934188464); - description.V1Id.ShouldNotBe(0); - } - - [Fact] - public void InterfaceDescription_CreateThrowsArgumentException_WhenTypeIsGenericDefinition() - { - // Arrange - Type type = typeof(IGenericActor<>); - - // Act - Action action = () => { TestDescription _ = new(type); }; - - // Assert - var exception = Should.Throw(action); - exception.Message.ShouldMatch(@"The actor interface '.*\+IGenericActor`1' is using generics. Generic interfaces cannot be remoted.*"); - exception.ParamName.ShouldBe("actorInterfaceType"); - } - - [Fact] - public void InterfaceDescription_CreateThrowsArgumentException_WhenTypeIsGeneric() - { - // Arrange - Type type = typeof(IGenericActor); - - // Act - Action action = () => { TestDescription _ = new(type); }; - - // Assert - var exception = Should.Throw(action); - exception.Message.ShouldMatch(@"The actor interface '.*\+IGenericActor`1\[.*IActor.*\]' is using generics. Generic interfaces cannot be remoted.*"); - exception.ParamName.ShouldBe("actorInterfaceType"); - } - - [Fact] - public void InterfaceDescription_CreateGeneratesMethodDescriptions_WhenTypeHasTaskMethods_ButDoesNotSeeInheritedMethods() - { - // Arrange - Type type = typeof(IChildActor); - - // Act - TestDescription description = new(type); - - // Assert - description.Methods.ShouldNotBeNull(); - description.Methods.ShouldBeOfType(); - description.Methods.Select(m => new {m.Name}).ShouldBe(new[] {new {Name = "GetInt"}}); - } - - [Fact] - public void InterfaceDescription_CreateGeneratesMethodDescriptions_WhenTypeHasVoidMethods() - { - // Arrange - Type type = typeof(IVoidActor); - - // Act - TestDescription description = new(type, methodReturnCheck: MethodReturnCheck.EnsureReturnsVoid); - - // Assert - description.Methods.ShouldNotBeNull(); - description.Methods.ShouldBeOfType(); - description.Methods.Select(m => new {m.Name}).ShouldBe(new[] {new {Name = "GetString"}, new {Name="MethodWithArguments"}}); - } - - [Fact] - public void InterfaceDescription_CreateThrowsArgumentException_WhenMethodsAreNotReturningTask() - { - // Arrange - Type type = typeof(IVoidActor); - - // Act - Action action = () => - { - TestDescription _ = new(type); - }; - - // Assert - var exception = Should.Throw(action); - exception.Message.ShouldMatch(@"Method 'GetString' of actor interface '.*\+IVoidActor' does not return Task or Task<>. The actor interface methods must be async and must return either Task or Task<>.*"); - exception.ParamName.ShouldBe("actorInterfaceType"); - } - - [Fact] - public void InterfaceDescription_CreateThrowsArgumentException_WhenMethodsAreNotReturningVoid() - { - // Arrange - Type type = typeof(IMethodActor); - - // Act - Action action = () => { TestDescription _ = new(type, methodReturnCheck: MethodReturnCheck.EnsureReturnsVoid); }; - - // Assert - var exception = Should.Throw(action); - exception.Message.ShouldMatch(@"Method 'GetString' of actor interface '.*\+IMethodActor' returns '.*\.Task`1\[.*System.String.*\]'.*"); - exception.ParamName.ShouldBe("actorInterfaceType"); - } - - [Fact] - public void InterfaceDescription_CreateThrowsArgumentException_WhenMethodsAreOverloaded() - { - // Arrange - Type type = typeof(IOverloadedMethodActor); - - // Act - Action action = () => { TestDescription _ = new(type); }; - - // Assert - var exception = Should.Throw(action); - exception.Message.ShouldMatch(@"Method 'GetString' of actor interface '.*\+IOverloadedMethodActor' is overloaded. The actor interface methods cannot be overloaded.*"); - exception.ParamName.ShouldBe("actorInterfaceType"); - } - - [Fact] - public void InterfaceDescription_CreateThrowsArgumentException_WhenMethodIsGeneric() - { - // Arrange - Type type = typeof(IGenericMethodActor); - - // Act - Action action = () => { TestDescription _ = new(type); }; - - // Assert - var exception = Should.Throw(action); - exception.Message.ShouldMatch(@"Method 'Get' of actor interface '.*\+IGenericMethodActor' is using generics. The actor interface methods cannot use generics.*"); - exception.ParamName.ShouldBe("actorInterfaceType"); - } - - [Fact] - public void InterfaceDescription_CreateThrowsArgumentException_WhenMethodHasVariableArguments() - { - // Arrange - Type type = typeof(IVariableActor); - - // Act - Action action = () => { TestDescription _ = new(type); }; - - // Assert - var exception = Should.Throw(action); - exception.Message.ShouldMatch(@"Method 'MethodWithVarArgs' of actor interface '.*\+IVariableActor' is using a variable argument list. The actor interface methods cannot have a variable argument list.*"); - exception.ParamName.ShouldBe("actorInterfaceType"); - } - - internal interface ITestActor : IActor - { - } - - internal interface IGenericActor : IActor - { - } - - internal interface IMethodActor : IActor - { - Task GetString(); - - Task MethodWithArguments(int number, bool choice, string information); - } - - internal interface IChildActor : IMethodActor - { - Task GetInt(); - } - - internal interface IVariableActor : IActor - { - Task MethodWithVarArgs(__arglist); - } - - internal interface IVoidActor : IActor - { - void GetString(); - - void MethodWithArguments(int number, bool choice, string information); - } - - internal interface IOverloadedActor : IMethodActor - { - Task GetString(string parameter); - } - - internal interface IOverloadedMethodActor : IActor - { - Task GetString(); - - Task GetString(string parameter); - } - - internal interface IGenericMethodActor : IActor - { - Task Get(); - } - - internal class TestDescription : InterfaceDescription - { - public TestDescription( - Type remotedInterfaceType, - string remotedInterfaceKindName = "actor", - bool useCRCIdGeneration = false, - MethodReturnCheck methodReturnCheck = MethodReturnCheck.EnsureReturnsTask) - : base(remotedInterfaceKindName, remotedInterfaceType, useCRCIdGeneration, methodReturnCheck) - { - } } } -} +} \ No newline at end of file diff --git a/test/Dapr.Actors.Test/Description/MethodArgumentDescriptionTests.cs b/test/Dapr.Actors.Test/Description/MethodArgumentDescriptionTests.cs index 0841298a..ec390a94 100644 --- a/test/Dapr.Actors.Test/Description/MethodArgumentDescriptionTests.cs +++ b/test/Dapr.Actors.Test/Description/MethodArgumentDescriptionTests.cs @@ -17,106 +17,105 @@ using System.Threading.Tasks; using Shouldly; using Xunit; -namespace Dapr.Actors.Description +namespace Dapr.Actors.Description; + +public sealed class MethodArgumentDescriptionTests { - public sealed class MethodArgumentDescriptionTests + [Fact] + public void MethodArgumentDescription_CreatMethodArgumentDescription() { - [Fact] - public void MethodArgumentDescription_CreatMethodArgumentDescription() - { - // Arrange - MethodInfo methodInfo = typeof(ITestActor).GetMethod("MethodWithArguments"); - ParameterInfo parameterInfo = methodInfo.GetParameters()[0]; + // Arrange + MethodInfo methodInfo = typeof(ITestActor).GetMethod("MethodWithArguments"); + ParameterInfo parameterInfo = methodInfo.GetParameters()[0]; - // Act - var description = MethodArgumentDescription.Create("actor", methodInfo, parameterInfo); + // Act + var description = MethodArgumentDescription.Create("actor", methodInfo, parameterInfo); - // Assert - description.ShouldNotBeNull(); + // Assert + description.ShouldNotBeNull(); - description.Name.ShouldBe("number"); - description.ArgumentType.ShouldBe(typeof(int)); - } - - [Fact] - public void MethodDescription_CreateThrowsArgumentException_WhenParameterHasVariableLength() - { - // Arrange - Type type = typeof(ITestActor); - MethodInfo methodInfo = type.GetMethod("MethodWithParams"); - ParameterInfo parameterInfo = methodInfo.GetParameters()[0]; - - // Act - Action action = () => MethodArgumentDescription.Create("actor", methodInfo, parameterInfo); - - // Assert - var exception = Should.Throw(action); - exception.Message.ShouldMatch(@"Method 'MethodWithParams' of actor interface '.*\+ITestActor' has variable length parameter 'values'. The actor interface methods must not have variable length parameters.*"); - exception.ParamName.ShouldBe("actorInterfaceType"); - } - - [Fact] - public void MethodDescription_CreateThrowsArgumentException_WhenParameterIsInput() - { - // Arrange - Type type = typeof(ITestActor); - MethodInfo methodInfo = type.GetMethod("MethodWithIn"); - ParameterInfo parameterInfo = methodInfo.GetParameters()[0]; - - // Act - Action action = () => MethodArgumentDescription.Create("actor", methodInfo, parameterInfo); - - // Assert - var exception = Should.Throw(action); - exception.Message.ShouldMatch(@"Method 'MethodWithIn' of actor interface '.*\+ITestActor' has out/ref/optional parameter 'value'. The actor interface methods must not have out, ref or optional parameters.*"); - exception.ParamName.ShouldBe("actorInterfaceType"); - } - - [Fact] - public void MethodDescription_CreateThrowsArgumentException_WhenParameterIsOutput() - { - // Arrange - Type type = typeof(ITestActor); - MethodInfo methodInfo = type.GetMethod("MethodWithOut"); - ParameterInfo parameterInfo = methodInfo.GetParameters()[0]; - - // Act - Action action = () => MethodArgumentDescription.Create("actor", methodInfo, parameterInfo); - - // Assert - var exception = Should.Throw(action); - exception.Message.ShouldMatch(@"Method 'MethodWithOut' of actor interface '.*\+ITestActor' has out/ref/optional parameter 'value'. The actor interface methods must not have out, ref or optional parameters.*"); - exception.ParamName.ShouldBe("actorInterfaceType"); - } - - [Fact] - public void MethodDescription_CreateThrowsArgumentException_WhenParameterIsOptional() - { - // Arrange - Type type = typeof(ITestActor); - MethodInfo methodInfo = type.GetMethod("MethodWithOptional"); - ParameterInfo parameterInfo = methodInfo.GetParameters()[0]; - - // Act - Action action = () => MethodArgumentDescription.Create("actor", methodInfo, parameterInfo); - - // Assert - var exception = Should.Throw(action); - exception.Message.ShouldMatch(@"Method 'MethodWithOptional' of actor interface '.*\+ITestActor' has out/ref/optional parameter 'value'. The actor interface methods must not have out, ref or optional parameters.*"); - exception.ParamName.ShouldBe("actorInterfaceType"); - } - - internal interface ITestActor : IActor - { - Task MethodWithArguments(int number, bool choice, string information); - - Task MethodWithParams(params string[] values); - - Task MethodWithOut(out int value); - - Task MethodWithIn(in int value); - - Task MethodWithOptional(int value = 1); - } + description.Name.ShouldBe("number"); + description.ArgumentType.ShouldBe(typeof(int)); } -} + + [Fact] + public void MethodDescription_CreateThrowsArgumentException_WhenParameterHasVariableLength() + { + // Arrange + Type type = typeof(ITestActor); + MethodInfo methodInfo = type.GetMethod("MethodWithParams"); + ParameterInfo parameterInfo = methodInfo.GetParameters()[0]; + + // Act + Action action = () => MethodArgumentDescription.Create("actor", methodInfo, parameterInfo); + + // Assert + var exception = Should.Throw(action); + exception.Message.ShouldMatch(@"Method 'MethodWithParams' of actor interface '.*\+ITestActor' has variable length parameter 'values'. The actor interface methods must not have variable length parameters.*"); + exception.ParamName.ShouldBe("actorInterfaceType"); + } + + [Fact] + public void MethodDescription_CreateThrowsArgumentException_WhenParameterIsInput() + { + // Arrange + Type type = typeof(ITestActor); + MethodInfo methodInfo = type.GetMethod("MethodWithIn"); + ParameterInfo parameterInfo = methodInfo.GetParameters()[0]; + + // Act + Action action = () => MethodArgumentDescription.Create("actor", methodInfo, parameterInfo); + + // Assert + var exception = Should.Throw(action); + exception.Message.ShouldMatch(@"Method 'MethodWithIn' of actor interface '.*\+ITestActor' has out/ref/optional parameter 'value'. The actor interface methods must not have out, ref or optional parameters.*"); + exception.ParamName.ShouldBe("actorInterfaceType"); + } + + [Fact] + public void MethodDescription_CreateThrowsArgumentException_WhenParameterIsOutput() + { + // Arrange + Type type = typeof(ITestActor); + MethodInfo methodInfo = type.GetMethod("MethodWithOut"); + ParameterInfo parameterInfo = methodInfo.GetParameters()[0]; + + // Act + Action action = () => MethodArgumentDescription.Create("actor", methodInfo, parameterInfo); + + // Assert + var exception = Should.Throw(action); + exception.Message.ShouldMatch(@"Method 'MethodWithOut' of actor interface '.*\+ITestActor' has out/ref/optional parameter 'value'. The actor interface methods must not have out, ref or optional parameters.*"); + exception.ParamName.ShouldBe("actorInterfaceType"); + } + + [Fact] + public void MethodDescription_CreateThrowsArgumentException_WhenParameterIsOptional() + { + // Arrange + Type type = typeof(ITestActor); + MethodInfo methodInfo = type.GetMethod("MethodWithOptional"); + ParameterInfo parameterInfo = methodInfo.GetParameters()[0]; + + // Act + Action action = () => MethodArgumentDescription.Create("actor", methodInfo, parameterInfo); + + // Assert + var exception = Should.Throw(action); + exception.Message.ShouldMatch(@"Method 'MethodWithOptional' of actor interface '.*\+ITestActor' has out/ref/optional parameter 'value'. The actor interface methods must not have out, ref or optional parameters.*"); + exception.ParamName.ShouldBe("actorInterfaceType"); + } + + internal interface ITestActor : IActor + { + Task MethodWithArguments(int number, bool choice, string information); + + Task MethodWithParams(params string[] values); + + Task MethodWithOut(out int value); + + Task MethodWithIn(in int value); + + Task MethodWithOptional(int value = 1); + } +} \ No newline at end of file diff --git a/test/Dapr.Actors.Test/Description/MethodDescriptionTests.cs b/test/Dapr.Actors.Test/Description/MethodDescriptionTests.cs index c8162f5f..a90a01bb 100644 --- a/test/Dapr.Actors.Test/Description/MethodDescriptionTests.cs +++ b/test/Dapr.Actors.Test/Description/MethodDescriptionTests.cs @@ -19,114 +19,113 @@ using System.Threading.Tasks; using Shouldly; using Xunit; -namespace Dapr.Actors.Description +namespace Dapr.Actors.Description; + +public sealed class MethodDescriptionTests { - public sealed class MethodDescriptionTests + [Fact] + public void MethodDescription_CreatMethodDescription() { - [Fact] - public void MethodDescription_CreatMethodDescription() - { - // Arrange - MethodInfo methodInfo = typeof(ITestActor).GetMethod("GetString"); + // Arrange + MethodInfo methodInfo = typeof(ITestActor).GetMethod("GetString"); - // Act - var description = MethodDescription.Create("actor", methodInfo, false); + // Act + var description = MethodDescription.Create("actor", methodInfo, false); - // Assert - description.ShouldNotBeNull(); + // Assert + description.ShouldNotBeNull(); - description.MethodInfo.ShouldBeSameAs(methodInfo); - description.Name.ShouldBe("GetString"); - description.ReturnType.ShouldBe(typeof(Task)); - description.Id.ShouldNotBe(0); - description.Arguments.ShouldBeEmpty(); - description.HasCancellationToken.ShouldBeFalse(); - } - - [Fact] - public void MethodDescription_CreateCrcId_WhenUseCrcIdGenerationIsSet() - { - // Arrange - MethodInfo methodInfo = typeof(ITestActor).GetMethod("GetString"); - - // Act - var description = MethodDescription.Create("actor", methodInfo, true); - - // Assert - description.Id.ShouldBe(70257263); - } - - [Fact] - public void MethodDescription_CreateGeneratesArgumentDescriptions_WhenMethodHasArguments() - { - // Arrange - MethodInfo methodInfo = typeof(ITestActor).GetMethod("MethodWithArguments"); - - // Act - var description = MethodDescription.Create("actor", methodInfo, false); - - // Assert - description.Arguments.ShouldNotBeNull(); - description.Arguments.ShouldBeOfType(); - description.Arguments.Select(m => new {m.Name}).ShouldBe(new[] {new {Name = "number"}, new {Name = "choice"}, new {Name = "information"}}); - } - - [Fact] - public void MethodDescription_CreateSetsHasCancellationTokenToTrue_WhenMethodHasTokenAsArgument() - { - // Arrange - MethodInfo methodInfo = typeof(ITestActor).GetMethod("MethodWithToken"); - - // Act - var description = MethodDescription.Create("actor", methodInfo, false); - - // Assert - description.HasCancellationToken.ShouldBeTrue(); - } - - [Fact] - public void MethodDescription_CreateThrowsArgumentException_WhenMethodHasTokenAsNonLastArgument() - { - // Arrange - Type type = typeof(ITestActor); - MethodInfo methodInfo = type.GetMethod("MethodWithTokenNotLast"); - - // Act - Action action = () => MethodDescription.Create("actor", methodInfo, false); - - // Assert - var exception = Should.Throw(action); - exception.Message.ShouldMatch(@"Method 'MethodWithTokenNotLast' of actor interface '.*\+ITestActor' has a '.*\.CancellationToken' parameter that is not the last parameter. If an actor method accepts a '.*\.CancellationToken' parameter, it must be the last parameter\..*"); - exception.ParamName.ShouldBe("actorInterfaceType"); - } - - [Fact] - public void MethodDescription_CreateThrowsArgumentException_WhenMethodHasMultipleCancellationTokens() - { - // Arrange - Type type = typeof(ITestActor); - MethodInfo methodInfo = type.GetMethod("MethodWithMultipleTokens"); - - // Act - Action action = () => MethodDescription.Create("actor", methodInfo, false); - - // Assert - var exception = Should.Throw(action); - exception.Message.ShouldMatch(@"Method 'MethodWithMultipleTokens' of actor interface '.*\+ITestActor' has a '.*\.CancellationToken' parameter that is not the last parameter. If an actor method accepts a '.*\.CancellationToken' parameter, it must be the last parameter.*"); - exception.ParamName.ShouldBe("actorInterfaceType"); - } - - internal interface ITestActor : IActor - { - Task GetString(); - - Task MethodWithArguments(int number, bool choice, string information); - - Task MethodWithToken(CancellationToken cancellationToken); - - Task MethodWithMultipleTokens(CancellationToken cancellationToken, CancellationToken cancellationTokenToo); - - Task MethodWithTokenNotLast(CancellationToken cancellationToken, bool additionalArgument); - } + description.MethodInfo.ShouldBeSameAs(methodInfo); + description.Name.ShouldBe("GetString"); + description.ReturnType.ShouldBe(typeof(Task)); + description.Id.ShouldNotBe(0); + description.Arguments.ShouldBeEmpty(); + description.HasCancellationToken.ShouldBeFalse(); } -} + + [Fact] + public void MethodDescription_CreateCrcId_WhenUseCrcIdGenerationIsSet() + { + // Arrange + MethodInfo methodInfo = typeof(ITestActor).GetMethod("GetString"); + + // Act + var description = MethodDescription.Create("actor", methodInfo, true); + + // Assert + description.Id.ShouldBe(70257263); + } + + [Fact] + public void MethodDescription_CreateGeneratesArgumentDescriptions_WhenMethodHasArguments() + { + // Arrange + MethodInfo methodInfo = typeof(ITestActor).GetMethod("MethodWithArguments"); + + // Act + var description = MethodDescription.Create("actor", methodInfo, false); + + // Assert + description.Arguments.ShouldNotBeNull(); + description.Arguments.ShouldBeOfType(); + description.Arguments.Select(m => new {m.Name}).ShouldBe(new[] {new {Name = "number"}, new {Name = "choice"}, new {Name = "information"}}); + } + + [Fact] + public void MethodDescription_CreateSetsHasCancellationTokenToTrue_WhenMethodHasTokenAsArgument() + { + // Arrange + MethodInfo methodInfo = typeof(ITestActor).GetMethod("MethodWithToken"); + + // Act + var description = MethodDescription.Create("actor", methodInfo, false); + + // Assert + description.HasCancellationToken.ShouldBeTrue(); + } + + [Fact] + public void MethodDescription_CreateThrowsArgumentException_WhenMethodHasTokenAsNonLastArgument() + { + // Arrange + Type type = typeof(ITestActor); + MethodInfo methodInfo = type.GetMethod("MethodWithTokenNotLast"); + + // Act + Action action = () => MethodDescription.Create("actor", methodInfo, false); + + // Assert + var exception = Should.Throw(action); + exception.Message.ShouldMatch(@"Method 'MethodWithTokenNotLast' of actor interface '.*\+ITestActor' has a '.*\.CancellationToken' parameter that is not the last parameter. If an actor method accepts a '.*\.CancellationToken' parameter, it must be the last parameter\..*"); + exception.ParamName.ShouldBe("actorInterfaceType"); + } + + [Fact] + public void MethodDescription_CreateThrowsArgumentException_WhenMethodHasMultipleCancellationTokens() + { + // Arrange + Type type = typeof(ITestActor); + MethodInfo methodInfo = type.GetMethod("MethodWithMultipleTokens"); + + // Act + Action action = () => MethodDescription.Create("actor", methodInfo, false); + + // Assert + var exception = Should.Throw(action); + exception.Message.ShouldMatch(@"Method 'MethodWithMultipleTokens' of actor interface '.*\+ITestActor' has a '.*\.CancellationToken' parameter that is not the last parameter. If an actor method accepts a '.*\.CancellationToken' parameter, it must be the last parameter.*"); + exception.ParamName.ShouldBe("actorInterfaceType"); + } + + internal interface ITestActor : IActor + { + Task GetString(); + + Task MethodWithArguments(int number, bool choice, string information); + + Task MethodWithToken(CancellationToken cancellationToken); + + Task MethodWithMultipleTokens(CancellationToken cancellationToken, CancellationToken cancellationTokenToo); + + Task MethodWithTokenNotLast(CancellationToken cancellationToken, bool additionalArgument); + } +} \ No newline at end of file diff --git a/test/Dapr.Actors.Test/Runtime/ActorManagerTests.cs b/test/Dapr.Actors.Test/Runtime/ActorManagerTests.cs index b27e9afe..0054a73b 100644 --- a/test/Dapr.Actors.Test/Runtime/ActorManagerTests.cs +++ b/test/Dapr.Actors.Test/Runtime/ActorManagerTests.cs @@ -18,242 +18,241 @@ using Dapr.Actors.Client; using Microsoft.Extensions.Logging.Abstractions; using Xunit; -namespace Dapr.Actors.Runtime +namespace Dapr.Actors.Runtime; + +public sealed class ActorManagerTests { - public sealed class ActorManagerTests + private ActorManager CreateActorManager(Type type, ActorActivator activator = null) { - private ActorManager CreateActorManager(Type type, ActorActivator activator = null) - { - var registration = new ActorRegistration(ActorTypeInformation.Get(type, actorTypeName: null)); - var interactor = new DaprHttpInteractor(clientHandler: null, "http://localhost:3500", apiToken: null, requestTimeout: null); - return new ActorManager(registration, activator ?? new DefaultActorActivator(), JsonSerializerDefaults.Web, false, NullLoggerFactory.Instance, ActorProxy.DefaultProxyFactory, interactor); - } + var registration = new ActorRegistration(ActorTypeInformation.Get(type, actorTypeName: null)); + var interactor = new DaprHttpInteractor(clientHandler: null, "http://localhost:3500", apiToken: null, requestTimeout: null); + return new ActorManager(registration, activator ?? new DefaultActorActivator(), JsonSerializerDefaults.Web, false, NullLoggerFactory.Instance, ActorProxy.DefaultProxyFactory, interactor); + } - [Fact] - public async Task ActivateActorAsync_CreatesActorAndCallsActivateLifecycleMethod() - { - var manager = CreateActorManager(typeof(TestActor)); + [Fact] + public async Task ActivateActorAsync_CreatesActorAndCallsActivateLifecycleMethod() + { + var manager = CreateActorManager(typeof(TestActor)); - var id = ActorId.CreateRandom(); + var id = ActorId.CreateRandom(); + await manager.ActivateActorAsync(id); + + Assert.True(manager.TryGetActorAsync(id, out var actor)); + Assert.True(Assert.IsType(actor).IsActivated); + } + + [Fact] + public async Task ActivateActorAsync_CanActivateMultipleActors() + { + var manager = CreateActorManager(typeof(TestActor)); + + await manager.ActivateActorAsync(new ActorId("1")); + Assert.True(manager.TryGetActorAsync(new ActorId("1"), out var actor1)); + + await manager.ActivateActorAsync(new ActorId("2")); + Assert.True(manager.TryGetActorAsync(new ActorId("2"), out var actor2)); + + Assert.NotSame(actor1, actor2); + } + + [Fact] + public async Task ActivateActorAsync_UsesActivator() + { + var activator = new TestActivator(); + + var manager = CreateActorManager(typeof(TestActor), activator); + + var id = ActorId.CreateRandom(); + await manager.ActivateActorAsync(id); + + Assert.Equal(1, activator.CreateCallCount); + } + + [Fact] + public async Task ActivateActorAsync_DoubleActivation_DeactivatesNewActor() + { + // We have to use the activator to observe the behavior here. We don't + // have a way to interact with the "new" actor that gets destroyed immediately. + var activator = new TestActivator(); + + var manager = CreateActorManager(typeof(TestActor), activator); + + var id = ActorId.CreateRandom(); + await manager.ActivateActorAsync(id); + + Assert.True(manager.TryGetActorAsync(id, out var original)); + + // It's a double-activation! We don't expect the runtime to do this, but the code + // handles it. + await manager.ActivateActorAsync(id); + + // Still holding the original actor + Assert.True(manager.TryGetActorAsync(id, out var another)); + Assert.Same(original, another); + Assert.False(Assert.IsType(another).IsDeactivated); + Assert.False(Assert.IsType(another).IsDisposed); + + // We should have seen 2 create operations and 1 delete + Assert.Equal(2, activator.CreateCallCount); + Assert.Equal(1, activator.DeleteCallCount); + } + + [Fact] + public async Task ActivateActorAsync_ExceptionDuringActivation_ActorNotStoredAndDeleted() + { + var activator = new TestActivator(); + + var manager = CreateActorManager(typeof(ThrowsDuringOnActivateAsync), activator); + + var id = ActorId.CreateRandom(); + + await Assert.ThrowsAsync(async () => + { await manager.ActivateActorAsync(id); + }); - Assert.True(manager.TryGetActorAsync(id, out var actor)); - Assert.True(Assert.IsType(actor).IsActivated); - } + Assert.False(manager.TryGetActorAsync(id, out _)); + Assert.Equal(1, activator.DeleteCallCount); + } - [Fact] - public async Task ActivateActorAsync_CanActivateMultipleActors() - { - var manager = CreateActorManager(typeof(TestActor)); + [Fact] + public async Task DectivateActorAsync_DeletesActorAndCallsDeactivateLifecycleMethod() + { + var manager = CreateActorManager(typeof(TestActor)); - await manager.ActivateActorAsync(new ActorId("1")); - Assert.True(manager.TryGetActorAsync(new ActorId("1"), out var actor1)); - - await manager.ActivateActorAsync(new ActorId("2")); - Assert.True(manager.TryGetActorAsync(new ActorId("2"), out var actor2)); - - Assert.NotSame(actor1, actor2); - } - - [Fact] - public async Task ActivateActorAsync_UsesActivator() - { - var activator = new TestActivator(); - - var manager = CreateActorManager(typeof(TestActor), activator); - - var id = ActorId.CreateRandom(); - await manager.ActivateActorAsync(id); - - Assert.Equal(1, activator.CreateCallCount); - } - - [Fact] - public async Task ActivateActorAsync_DoubleActivation_DeactivatesNewActor() - { - // We have to use the activator to observe the behavior here. We don't - // have a way to interact with the "new" actor that gets destroyed immediately. - var activator = new TestActivator(); - - var manager = CreateActorManager(typeof(TestActor), activator); - - var id = ActorId.CreateRandom(); - await manager.ActivateActorAsync(id); - - Assert.True(manager.TryGetActorAsync(id, out var original)); - - // It's a double-activation! We don't expect the runtime to do this, but the code - // handles it. - await manager.ActivateActorAsync(id); - - // Still holding the original actor - Assert.True(manager.TryGetActorAsync(id, out var another)); - Assert.Same(original, another); - Assert.False(Assert.IsType(another).IsDeactivated); - Assert.False(Assert.IsType(another).IsDisposed); - - // We should have seen 2 create operations and 1 delete - Assert.Equal(2, activator.CreateCallCount); - Assert.Equal(1, activator.DeleteCallCount); - } - - [Fact] - public async Task ActivateActorAsync_ExceptionDuringActivation_ActorNotStoredAndDeleted() - { - var activator = new TestActivator(); - - var manager = CreateActorManager(typeof(ThrowsDuringOnActivateAsync), activator); - - var id = ActorId.CreateRandom(); - - await Assert.ThrowsAsync(async () => - { - await manager.ActivateActorAsync(id); - }); - - Assert.False(manager.TryGetActorAsync(id, out _)); - Assert.Equal(1, activator.DeleteCallCount); - } - - [Fact] - public async Task DectivateActorAsync_DeletesActorAndCallsDeactivateLifecycleMethod() - { - var manager = CreateActorManager(typeof(TestActor)); - - var id = ActorId.CreateRandom(); - await manager.ActivateActorAsync(id); + var id = ActorId.CreateRandom(); + await manager.ActivateActorAsync(id); - Assert.True(manager.TryGetActorAsync(id, out var actor)); + Assert.True(manager.TryGetActorAsync(id, out var actor)); + await manager.DeactivateActorAsync(id); + + Assert.True(Assert.IsType(actor).IsDeactivated); + Assert.True(Assert.IsType(actor).IsDisposed); + } + + [Fact] + public async Task DeactivateActorAsync_ItsOkToDeactivateNonExistentActor() + { + var manager = CreateActorManager(typeof(TestActor)); + + var id = ActorId.CreateRandom(); + Assert.False(manager.TryGetActorAsync(id, out _)); + await manager.DeactivateActorAsync(id); + } + + [Fact] + public async Task DeactivateActorAsync_UsesActivator() + { + var activator = new TestActivator(); + + var manager = CreateActorManager(typeof(TestActor), activator); + + var id = ActorId.CreateRandom(); + await manager.ActivateActorAsync(id); + await manager.DeactivateActorAsync(id); + + Assert.Equal(1, activator.CreateCallCount); + Assert.Equal(1, activator.DeleteCallCount); + } + + [Fact] + public async Task DeactivateActorAsync_ExceptionDuringDeactivation_ActorIsRemovedAndDeleted() + { + var activator = new TestActivator(); + + var manager = CreateActorManager(typeof(ThrowsDuringOnDeactivateAsync), activator); + + var id = ActorId.CreateRandom(); + await manager.ActivateActorAsync(id); + Assert.True(manager.TryGetActorAsync(id, out _)); + + await Assert.ThrowsAsync(async () => + { await manager.DeactivateActorAsync(id); + }); - Assert.True(Assert.IsType(actor).IsDeactivated); - Assert.True(Assert.IsType(actor).IsDisposed); + Assert.False(manager.TryGetActorAsync(id, out _)); + Assert.Equal(1, activator.DeleteCallCount); + } + + private interface ITestActor : IActor { } + + private class TestActor : Actor, ITestActor, IDisposable + { + private static int counter; + + public TestActor(ActorHost host) : base(host) + { + Sequence = Interlocked.Increment(ref counter); } - [Fact] - public async Task DeactivateActorAsync_ItsOkToDeactivateNonExistentActor() - { - var manager = CreateActorManager(typeof(TestActor)); + // Makes instances easier to tell apart for debugging. + public int Sequence { get; } - var id = ActorId.CreateRandom(); - Assert.False(manager.TryGetActorAsync(id, out _)); - await manager.DeactivateActorAsync(id); + public bool IsActivated { get; set; } + + public bool IsDeactivated { get; set; } + + public bool IsDisposed { get; set; } + + public void Dispose() + { + IsDisposed = true; } - [Fact] - public async Task DeactivateActorAsync_UsesActivator() + protected override Task OnActivateAsync() { - var activator = new TestActivator(); - - var manager = CreateActorManager(typeof(TestActor), activator); - - var id = ActorId.CreateRandom(); - await manager.ActivateActorAsync(id); - await manager.DeactivateActorAsync(id); - - Assert.Equal(1, activator.CreateCallCount); - Assert.Equal(1, activator.DeleteCallCount); + IsActivated = true; + return Task.CompletedTask; } - [Fact] - public async Task DeactivateActorAsync_ExceptionDuringDeactivation_ActorIsRemovedAndDeleted() + protected override Task OnDeactivateAsync() { - var activator = new TestActivator(); - - var manager = CreateActorManager(typeof(ThrowsDuringOnDeactivateAsync), activator); - - var id = ActorId.CreateRandom(); - await manager.ActivateActorAsync(id); - Assert.True(manager.TryGetActorAsync(id, out _)); - - await Assert.ThrowsAsync(async () => - { - await manager.DeactivateActorAsync(id); - }); - - Assert.False(manager.TryGetActorAsync(id, out _)); - Assert.Equal(1, activator.DeleteCallCount); - } - - private interface ITestActor : IActor { } - - private class TestActor : Actor, ITestActor, IDisposable - { - private static int counter; - - public TestActor(ActorHost host) : base(host) - { - Sequence = Interlocked.Increment(ref counter); - } - - // Makes instances easier to tell apart for debugging. - public int Sequence { get; } - - public bool IsActivated { get; set; } - - public bool IsDeactivated { get; set; } - - public bool IsDisposed { get; set; } - - public void Dispose() - { - IsDisposed = true; - } - - protected override Task OnActivateAsync() - { - IsActivated = true; - return Task.CompletedTask; - } - - protected override Task OnDeactivateAsync() - { - IsDeactivated = true; - return Task.CompletedTask; - } - } - - private class ThrowsDuringOnActivateAsync : Actor, ITestActor - { - public ThrowsDuringOnActivateAsync(ActorHost host) : base(host) - { - } - - protected override Task OnActivateAsync() - { - throw new InvalidTimeZoneException(); - } - } - - private class ThrowsDuringOnDeactivateAsync : Actor, ITestActor - { - public ThrowsDuringOnDeactivateAsync(ActorHost host) : base(host) - { - } - - protected override Task OnDeactivateAsync() - { - throw new InvalidTimeZoneException(); - } - } - - private class TestActivator : DefaultActorActivator - { - public int CreateCallCount { get; set; } - - public int DeleteCallCount { get; set; } - - public override Task CreateAsync(ActorHost host) - { - CreateCallCount++;; - return base.CreateAsync(host); - } - - public override Task DeleteAsync(ActorActivatorState state) - { - DeleteCallCount++; - return base.DeleteAsync(state); - } + IsDeactivated = true; + return Task.CompletedTask; } } -} + + private class ThrowsDuringOnActivateAsync : Actor, ITestActor + { + public ThrowsDuringOnActivateAsync(ActorHost host) : base(host) + { + } + + protected override Task OnActivateAsync() + { + throw new InvalidTimeZoneException(); + } + } + + private class ThrowsDuringOnDeactivateAsync : Actor, ITestActor + { + public ThrowsDuringOnDeactivateAsync(ActorHost host) : base(host) + { + } + + protected override Task OnDeactivateAsync() + { + throw new InvalidTimeZoneException(); + } + } + + private class TestActivator : DefaultActorActivator + { + public int CreateCallCount { get; set; } + + public int DeleteCallCount { get; set; } + + public override Task CreateAsync(ActorHost host) + { + CreateCallCount++;; + return base.CreateAsync(host); + } + + public override Task DeleteAsync(ActorActivatorState state) + { + DeleteCallCount++; + return base.DeleteAsync(state); + } + } +} \ No newline at end of file diff --git a/test/Dapr.Actors.Test/Runtime/ActorReminderInfoTests.cs b/test/Dapr.Actors.Test/Runtime/ActorReminderInfoTests.cs index 9ff0ad2b..971a7c8a 100644 --- a/test/Dapr.Actors.Test/Runtime/ActorReminderInfoTests.cs +++ b/test/Dapr.Actors.Test/Runtime/ActorReminderInfoTests.cs @@ -4,31 +4,30 @@ using System.Text; using System.Threading.Tasks; using Xunit; -namespace Dapr.Actors.Runtime +namespace Dapr.Actors.Runtime; + +public class ActorReminderInfoTests { - public class ActorReminderInfoTests + [Fact] + public async Task TestActorReminderInfo_SerializeExcludesNullTtl() { - [Fact] - public async Task TestActorReminderInfo_SerializeExcludesNullTtl() - { - var info = new ReminderInfo(new byte[] { }, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(10), 1); - var serialized = await info.SerializeAsync(); + var info = new ReminderInfo(new byte[] { }, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(10), 1); + var serialized = await info.SerializeAsync(); - Assert.DoesNotContain("ttl", serialized); - var info2 = await ReminderInfo.DeserializeAsync(new MemoryStream(Encoding.UTF8.GetBytes(serialized))); - Assert.Null(info2.Ttl); - } - - [Fact] - public async Task TestActorReminderInfo_SerializeIncludesTtlWhenSet() - { - var info = new ReminderInfo(new byte[] { }, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(10), 1, TimeSpan.FromSeconds(1)); - var serialized = await info.SerializeAsync(); - - Assert.Contains("ttl", serialized); - var info2 = await ReminderInfo.DeserializeAsync(new MemoryStream(Encoding.UTF8.GetBytes(serialized))); - Assert.NotNull(info2.Ttl); - Assert.Equal(TimeSpan.FromSeconds(1), info2.Ttl); - } + Assert.DoesNotContain("ttl", serialized); + var info2 = await ReminderInfo.DeserializeAsync(new MemoryStream(Encoding.UTF8.GetBytes(serialized))); + Assert.Null(info2.Ttl); } -} + + [Fact] + public async Task TestActorReminderInfo_SerializeIncludesTtlWhenSet() + { + var info = new ReminderInfo(new byte[] { }, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(10), 1, TimeSpan.FromSeconds(1)); + var serialized = await info.SerializeAsync(); + + Assert.Contains("ttl", serialized); + var info2 = await ReminderInfo.DeserializeAsync(new MemoryStream(Encoding.UTF8.GetBytes(serialized))); + Assert.NotNull(info2.Ttl); + Assert.Equal(TimeSpan.FromSeconds(1), info2.Ttl); + } +} \ No newline at end of file diff --git a/test/Dapr.Actors.Test/Runtime/ActorRuntimeOptionsTests.cs b/test/Dapr.Actors.Test/Runtime/ActorRuntimeOptionsTests.cs index 6701535f..9d7c0c69 100644 --- a/test/Dapr.Actors.Test/Runtime/ActorRuntimeOptionsTests.cs +++ b/test/Dapr.Actors.Test/Runtime/ActorRuntimeOptionsTests.cs @@ -11,141 +11,140 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Test.Runtime +namespace Dapr.Actors.Test.Runtime; + +using Dapr.Actors.Runtime; +using Moq; +using Xunit; +using System; +using Shouldly; + +public sealed class ActorRuntimeOptionsTests { - using Dapr.Actors.Runtime; - using Moq; - using Xunit; - using System; - using Shouldly; - - public sealed class ActorRuntimeOptionsTests + [Fact] + public void TestRegisterActor_SavesActivator() { - [Fact] - public void TestRegisterActor_SavesActivator() + var actorType = typeof(TestActor); + var actorTypeInformation = ActorTypeInformation.Get(actorType, actorTypeName: null); + + var activator = Mock.Of(); + + var actorRuntimeOptions = new ActorRuntimeOptions(); + actorRuntimeOptions.Actors.RegisterActor(registration => { - var actorType = typeof(TestActor); - var actorTypeInformation = ActorTypeInformation.Get(actorType, actorTypeName: null); + registration.Activator = activator; + }); - var activator = Mock.Of(); - - var actorRuntimeOptions = new ActorRuntimeOptions(); - actorRuntimeOptions.Actors.RegisterActor(registration => + Assert.Collection( + actorRuntimeOptions.Actors, + registration => { - registration.Activator = activator; + Assert.Same(actorTypeInformation.ImplementationType, registration.Type.ImplementationType); + Assert.Same(activator, registration.Activator); }); - - Assert.Collection( - actorRuntimeOptions.Actors, - registration => - { - Assert.Same(actorTypeInformation.ImplementationType, registration.Type.ImplementationType); - Assert.Same(activator, registration.Activator); - }); - } - - [Fact] - public void SettingActorIdleTimeout_Succeeds() - { - var options = new ActorRuntimeOptions(); - options.ActorIdleTimeout = TimeSpan.FromSeconds(1); - - Assert.Equal(TimeSpan.FromSeconds(1), options.ActorIdleTimeout); - } - - [Fact] - public void SettingActorIdleTimeoutToLessThanZero_Fails() - { - var options = new ActorRuntimeOptions(); - Action action = () => options.ActorIdleTimeout = TimeSpan.FromSeconds(-1); - - action.ShouldThrow(); - } - - - [Fact] - public void SettingActorScanInterval_Succeeds() - { - var options = new ActorRuntimeOptions(); - options.ActorScanInterval = TimeSpan.FromSeconds(1); - - Assert.Equal(TimeSpan.FromSeconds(1), options.ActorScanInterval); - } - - [Fact] - public void SettingActorScanIntervalToLessThanZero_Fails() - { - var options = new ActorRuntimeOptions(); - Action action = () => options.ActorScanInterval = TimeSpan.FromSeconds(-1); - - action.ShouldThrow(); - } - - [Fact] - public void SettingDrainOngoingCallTimeout_Succeeds() - { - var options = new ActorRuntimeOptions(); - options.DrainOngoingCallTimeout = TimeSpan.FromSeconds(1); - - Assert.Equal(TimeSpan.FromSeconds(1), options.DrainOngoingCallTimeout); - } - - [Fact] - public void SettingDrainOngoingCallTimeoutToLessThanZero_Fails() - { - var options = new ActorRuntimeOptions(); - Action action = () => options.DrainOngoingCallTimeout = TimeSpan.FromSeconds(-1); - - action.ShouldThrow(); - } - - [Fact] - public void SettingJsonSerializerOptions_Succeeds() - { - var serializerOptions = new System.Text.Json.JsonSerializerOptions(); - var options = new ActorRuntimeOptions(); - options.JsonSerializerOptions = serializerOptions; - - Assert.Same(serializerOptions, options.JsonSerializerOptions); - } - - [Fact] - public void SettingJsonSerializerOptionsToNull_Fails() - { - var options = new ActorRuntimeOptions(); - Action action = () => options.JsonSerializerOptions = null; - - action.ShouldThrow(); - } - - [Fact] - public void SettingRemindersStoragePartitionsToLessThanZero_Fails() - { - var options = new ActorRuntimeOptions(); - Action action = () => options.RemindersStoragePartitions = -1; - - action.ShouldThrow(); - } - - [Fact] - public void SettingReentrancyConfigWithoutMaxStackDepth_Succeeds() - { - var options = new ActorRuntimeOptions(); - options.ReentrancyConfig.Enabled = true; - - Assert.True(options.ReentrancyConfig.Enabled); - Assert.Null(options.ReentrancyConfig.MaxStackDepth); - } - - [Fact] - public void SettingReentrancyConfigWithMaxStackDepth_Succeeds() - { - var options = new ActorRuntimeOptions(); - options.ReentrancyConfig.Enabled = true; - options.ReentrancyConfig.MaxStackDepth = 64; - - Assert.True(options.ReentrancyConfig.Enabled); - Assert.Equal(64, options.ReentrancyConfig.MaxStackDepth); - } } -} + + [Fact] + public void SettingActorIdleTimeout_Succeeds() + { + var options = new ActorRuntimeOptions(); + options.ActorIdleTimeout = TimeSpan.FromSeconds(1); + + Assert.Equal(TimeSpan.FromSeconds(1), options.ActorIdleTimeout); + } + + [Fact] + public void SettingActorIdleTimeoutToLessThanZero_Fails() + { + var options = new ActorRuntimeOptions(); + Action action = () => options.ActorIdleTimeout = TimeSpan.FromSeconds(-1); + + action.ShouldThrow(); + } + + + [Fact] + public void SettingActorScanInterval_Succeeds() + { + var options = new ActorRuntimeOptions(); + options.ActorScanInterval = TimeSpan.FromSeconds(1); + + Assert.Equal(TimeSpan.FromSeconds(1), options.ActorScanInterval); + } + + [Fact] + public void SettingActorScanIntervalToLessThanZero_Fails() + { + var options = new ActorRuntimeOptions(); + Action action = () => options.ActorScanInterval = TimeSpan.FromSeconds(-1); + + action.ShouldThrow(); + } + + [Fact] + public void SettingDrainOngoingCallTimeout_Succeeds() + { + var options = new ActorRuntimeOptions(); + options.DrainOngoingCallTimeout = TimeSpan.FromSeconds(1); + + Assert.Equal(TimeSpan.FromSeconds(1), options.DrainOngoingCallTimeout); + } + + [Fact] + public void SettingDrainOngoingCallTimeoutToLessThanZero_Fails() + { + var options = new ActorRuntimeOptions(); + Action action = () => options.DrainOngoingCallTimeout = TimeSpan.FromSeconds(-1); + + action.ShouldThrow(); + } + + [Fact] + public void SettingJsonSerializerOptions_Succeeds() + { + var serializerOptions = new System.Text.Json.JsonSerializerOptions(); + var options = new ActorRuntimeOptions(); + options.JsonSerializerOptions = serializerOptions; + + Assert.Same(serializerOptions, options.JsonSerializerOptions); + } + + [Fact] + public void SettingJsonSerializerOptionsToNull_Fails() + { + var options = new ActorRuntimeOptions(); + Action action = () => options.JsonSerializerOptions = null; + + action.ShouldThrow(); + } + + [Fact] + public void SettingRemindersStoragePartitionsToLessThanZero_Fails() + { + var options = new ActorRuntimeOptions(); + Action action = () => options.RemindersStoragePartitions = -1; + + action.ShouldThrow(); + } + + [Fact] + public void SettingReentrancyConfigWithoutMaxStackDepth_Succeeds() + { + var options = new ActorRuntimeOptions(); + options.ReentrancyConfig.Enabled = true; + + Assert.True(options.ReentrancyConfig.Enabled); + Assert.Null(options.ReentrancyConfig.MaxStackDepth); + } + + [Fact] + public void SettingReentrancyConfigWithMaxStackDepth_Succeeds() + { + var options = new ActorRuntimeOptions(); + options.ReentrancyConfig.Enabled = true; + options.ReentrancyConfig.MaxStackDepth = 64; + + Assert.True(options.ReentrancyConfig.Enabled); + Assert.Equal(64, options.ReentrancyConfig.MaxStackDepth); + } +} \ No newline at end of file diff --git a/test/Dapr.Actors.Test/Runtime/ActorRuntimeTests.cs b/test/Dapr.Actors.Test/Runtime/ActorRuntimeTests.cs index 6ebfe1bb..c39e940e 100644 --- a/test/Dapr.Actors.Test/Runtime/ActorRuntimeTests.cs +++ b/test/Dapr.Actors.Test/Runtime/ActorRuntimeTests.cs @@ -11,468 +11,467 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Test +namespace Dapr.Actors.Test; + +using System; +using System.Buffers; +using System.Linq; +using System.IO; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using Dapr.Actors; +using Dapr.Actors.Runtime; +using Microsoft.Extensions.Logging; +using Xunit; +using Dapr.Actors.Client; +using System.Reflection; +using System.Threading; + +public sealed class ActorRuntimeTests { - using System; - using System.Buffers; - using System.Linq; - using System.IO; - using System.Text; - using System.Text.Json; - using System.Threading.Tasks; - using Dapr.Actors; - using Dapr.Actors.Runtime; - using Microsoft.Extensions.Logging; - using Xunit; - using Dapr.Actors.Client; - using System.Reflection; - using System.Threading; + private const string RenamedActorTypeName = "MyRenamedActor"; + private const string ParamActorTypeName = "AnotherRenamedActor"; + private readonly ILoggerFactory loggerFactory = new LoggerFactory(); + private readonly ActorActivatorFactory activatorFactory = new DefaultActorActivatorFactory(); - public sealed class ActorRuntimeTests + private readonly IActorProxyFactory proxyFactory = ActorProxy.DefaultProxyFactory; + + private interface ITestActor : IActor { - private const string RenamedActorTypeName = "MyRenamedActor"; - private const string ParamActorTypeName = "AnotherRenamedActor"; - private readonly ILoggerFactory loggerFactory = new LoggerFactory(); - private readonly ActorActivatorFactory activatorFactory = new DefaultActorActivatorFactory(); + } - private readonly IActorProxyFactory proxyFactory = ActorProxy.DefaultProxyFactory; - - private interface ITestActor : IActor - { - } - - [Fact] - public void TestInferredActorType() - { - var actorType = typeof(TestActor); + [Fact] + public void TestInferredActorType() + { + var actorType = typeof(TestActor); - var options = new ActorRuntimeOptions(); - options.Actors.RegisterActor(); - var runtime = new ActorRuntime(options, loggerFactory, activatorFactory, proxyFactory); + var options = new ActorRuntimeOptions(); + options.Actors.RegisterActor(); + var runtime = new ActorRuntime(options, loggerFactory, activatorFactory, proxyFactory); - Assert.Contains(actorType.Name, runtime.RegisteredActors.Select(a => a.Type.ActorTypeName), StringComparer.InvariantCulture); + Assert.Contains(actorType.Name, runtime.RegisteredActors.Select(a => a.Type.ActorTypeName), StringComparer.InvariantCulture); + } + + [Fact] + public void TestExplicitActorType() + { + var actorType = typeof(RenamedActor); + var options = new ActorRuntimeOptions(); + options.Actors.RegisterActor(); + var runtime = new ActorRuntime(options, loggerFactory, activatorFactory, proxyFactory); + + Assert.NotEqual(RenamedActorTypeName, actorType.Name); + Assert.Contains(RenamedActorTypeName, runtime.RegisteredActors.Select(a => a.Type.ActorTypeName), StringComparer.InvariantCulture); + } + + [Fact] + public void TestExplicitActorTypeAsParamShouldOverrideInferred() + { + var actorType = typeof(TestActor); + var options = new ActorRuntimeOptions(); + options.Actors.RegisterActor(ParamActorTypeName); + var runtime = new ActorRuntime(options, loggerFactory, activatorFactory, proxyFactory); + + Assert.NotEqual(ParamActorTypeName, actorType.Name); + Assert.Contains(ParamActorTypeName, runtime.RegisteredActors.Select(a => a.Type.ActorTypeName), StringComparer.InvariantCulture); + } + + [Fact] + public void TestExplicitActorTypeAsParamShouldOverrideActorAttribute() + { + var actorType = typeof(RenamedActor); + var options = new ActorRuntimeOptions(); + options.Actors.RegisterActor(ParamActorTypeName); + var runtime = new ActorRuntime(options, loggerFactory, activatorFactory, proxyFactory); + + Assert.NotEqual(ParamActorTypeName, actorType.Name); + Assert.NotEqual(ParamActorTypeName, actorType.GetCustomAttribute().TypeName); + Assert.Contains(ParamActorTypeName, runtime.RegisteredActors.Select(a => a.Type.ActorTypeName), StringComparer.InvariantCulture); + } + + // This tests the change that removed the Activate message from Dapr runtime -> app. + [Fact] + public async Task NoActivateMessageFromRuntime() + { + var actorType = typeof(MyActor); + + var options = new ActorRuntimeOptions(); + options.Actors.RegisterActor(); + var runtime = new ActorRuntime(options, loggerFactory, activatorFactory, proxyFactory); + Assert.Contains(actorType.Name, runtime.RegisteredActors.Select(a => a.Type.ActorTypeName), StringComparer.InvariantCulture); + + var output = new MemoryStream(); + await runtime.DispatchWithoutRemotingAsync(actorType.Name, "abc", nameof(MyActor.MyMethod), new MemoryStream(), output); + var text = Encoding.UTF8.GetString(output.ToArray()); + + Assert.Equal("\"hi\"", text); + Assert.Contains(actorType.Name, runtime.RegisteredActors.Select(a => a.Type.ActorTypeName), StringComparer.InvariantCulture); + } + + public interface INotRemotedActor : IActor + { + Task NoArgumentsAsync(); + + Task NoArgumentsWithCancellationAsync(CancellationToken cancellationToken = default); + + Task SingleArgumentAsync(bool arg); + + Task SingleArgumentWithCancellationAsync(bool arg, CancellationToken cancellationToken = default); + } + + public sealed class NotRemotedActor : Actor, INotRemotedActor + { + public NotRemotedActor(ActorHost host) + : base(host) + { } - [Fact] - public void TestExplicitActorType() + public Task NoArgumentsAsync() { - var actorType = typeof(RenamedActor); - var options = new ActorRuntimeOptions(); - options.Actors.RegisterActor(); - var runtime = new ActorRuntime(options, loggerFactory, activatorFactory, proxyFactory); - - Assert.NotEqual(RenamedActorTypeName, actorType.Name); - Assert.Contains(RenamedActorTypeName, runtime.RegisteredActors.Select(a => a.Type.ActorTypeName), StringComparer.InvariantCulture); + return Task.FromResult(nameof(NoArgumentsAsync)); } - [Fact] - public void TestExplicitActorTypeAsParamShouldOverrideInferred() + public Task NoArgumentsWithCancellationAsync(CancellationToken cancellationToken = default) { - var actorType = typeof(TestActor); - var options = new ActorRuntimeOptions(); - options.Actors.RegisterActor(ParamActorTypeName); - var runtime = new ActorRuntime(options, loggerFactory, activatorFactory, proxyFactory); - - Assert.NotEqual(ParamActorTypeName, actorType.Name); - Assert.Contains(ParamActorTypeName, runtime.RegisteredActors.Select(a => a.Type.ActorTypeName), StringComparer.InvariantCulture); + return Task.FromResult(nameof(NoArgumentsWithCancellationAsync)); } - [Fact] - public void TestExplicitActorTypeAsParamShouldOverrideActorAttribute() + public Task SingleArgumentAsync(bool arg) { - var actorType = typeof(RenamedActor); - var options = new ActorRuntimeOptions(); - options.Actors.RegisterActor(ParamActorTypeName); - var runtime = new ActorRuntime(options, loggerFactory, activatorFactory, proxyFactory); - - Assert.NotEqual(ParamActorTypeName, actorType.Name); - Assert.NotEqual(ParamActorTypeName, actorType.GetCustomAttribute().TypeName); - Assert.Contains(ParamActorTypeName, runtime.RegisteredActors.Select(a => a.Type.ActorTypeName), StringComparer.InvariantCulture); + return Task.FromResult(nameof(SingleArgumentAsync)); } - // This tests the change that removed the Activate message from Dapr runtime -> app. - [Fact] - public async Task NoActivateMessageFromRuntime() + public Task SingleArgumentWithCancellationAsync(bool arg, CancellationToken cancellationToken = default) { - var actorType = typeof(MyActor); - - var options = new ActorRuntimeOptions(); - options.Actors.RegisterActor(); - var runtime = new ActorRuntime(options, loggerFactory, activatorFactory, proxyFactory); - Assert.Contains(actorType.Name, runtime.RegisteredActors.Select(a => a.Type.ActorTypeName), StringComparer.InvariantCulture); - - var output = new MemoryStream(); - await runtime.DispatchWithoutRemotingAsync(actorType.Name, "abc", nameof(MyActor.MyMethod), new MemoryStream(), output); - var text = Encoding.UTF8.GetString(output.ToArray()); - - Assert.Equal("\"hi\"", text); - Assert.Contains(actorType.Name, runtime.RegisteredActors.Select(a => a.Type.ActorTypeName), StringComparer.InvariantCulture); - } - - public interface INotRemotedActor : IActor - { - Task NoArgumentsAsync(); - - Task NoArgumentsWithCancellationAsync(CancellationToken cancellationToken = default); - - Task SingleArgumentAsync(bool arg); - - Task SingleArgumentWithCancellationAsync(bool arg, CancellationToken cancellationToken = default); - } - - public sealed class NotRemotedActor : Actor, INotRemotedActor - { - public NotRemotedActor(ActorHost host) - : base(host) - { - } - - public Task NoArgumentsAsync() - { - return Task.FromResult(nameof(NoArgumentsAsync)); - } - - public Task NoArgumentsWithCancellationAsync(CancellationToken cancellationToken = default) - { - return Task.FromResult(nameof(NoArgumentsWithCancellationAsync)); - } - - public Task SingleArgumentAsync(bool arg) - { - return Task.FromResult(nameof(SingleArgumentAsync)); - } - - public Task SingleArgumentWithCancellationAsync(bool arg, CancellationToken cancellationToken = default) - { - return Task.FromResult(nameof(SingleArgumentWithCancellationAsync)); - } - } - - public async Task InvokeMethod(string methodName, object arg = null) where T : Actor - { - var options = new ActorRuntimeOptions(); - - options.Actors.RegisterActor(); - - var runtime = new ActorRuntime(options, loggerFactory, activatorFactory, proxyFactory); - - using var input = new MemoryStream(); - - if (arg is not null) - { - JsonSerializer.Serialize(input, arg); - - input.Seek(0, SeekOrigin.Begin); - } - - using var output = new MemoryStream(); - - await runtime.DispatchWithoutRemotingAsync(typeof(T).Name, ActorId.CreateRandom().ToString(), methodName, input, output); - - output.Seek(0, SeekOrigin.Begin); - - return JsonSerializer.Deserialize(output); - } - - [Fact] - public async Task NoRemotingMethodWithNoArguments() - { - string methodName = nameof(INotRemotedActor.NoArgumentsAsync); - - string result = await InvokeMethod(methodName); - - Assert.Equal(methodName, result); - } - - [Fact] - public async Task NoRemotingMethodWithNoArgumentsWithCancellation() - { - string methodName = nameof(INotRemotedActor.NoArgumentsWithCancellationAsync); - - string result = await InvokeMethod(methodName); - - Assert.Equal(methodName, result); - } - - [Fact] - public async Task NoRemotingMethodWithSingleArgument() - { - string methodName = nameof(INotRemotedActor.SingleArgumentAsync); - - string result = await InvokeMethod(methodName, true); - - Assert.Equal(methodName, result); - } - - [Fact] - public async Task NoRemotingMethodWithSingleArgumentWithCancellation() - { - string methodName = nameof(INotRemotedActor.SingleArgumentWithCancellationAsync); - - string result = await InvokeMethod(methodName, true); - - Assert.Equal(methodName, result); - } - - [Fact] - public async Task Actor_UsesCustomActivator() - { - var activator = new TestActivator(); - var actorType = typeof(MyActor); - - var options = new ActorRuntimeOptions(); - options.Actors.RegisterActor(options => - { - options.Activator = activator; - }); - var runtime = new ActorRuntime(options, loggerFactory, activatorFactory, proxyFactory); - Assert.Contains(actorType.Name, runtime.RegisteredActors.Select(a => a.Type.ActorTypeName), StringComparer.InvariantCulture); - - var output = new MemoryStream(); - await runtime.DispatchWithoutRemotingAsync(actorType.Name, "abc", nameof(MyActor.MyMethod), new MemoryStream(), output); - - var text = Encoding.UTF8.GetString(output.ToArray()); - Assert.Equal("\"hi\"", text); - - await runtime.DeactivateAsync(actorType.Name, "abc"); - Assert.Equal(1, activator.CreateCallCount); - Assert.Equal(1, activator.DeleteCallCount); - } - - [Fact] - public async Task TestActorSettings() - { - var actorType = typeof(TestActor); - - var options = new ActorRuntimeOptions(); - options.Actors.RegisterActor(); - options.ActorIdleTimeout = TimeSpan.FromSeconds(33); - options.ActorScanInterval = TimeSpan.FromSeconds(44); - options.DrainOngoingCallTimeout = TimeSpan.FromSeconds(55); - options.DrainRebalancedActors = true; - - var runtime = new ActorRuntime(options, loggerFactory, activatorFactory, proxyFactory); - - Assert.Contains(actorType.Name, runtime.RegisteredActors.Select(a => a.Type.ActorTypeName), StringComparer.InvariantCulture); - - ArrayBufferWriter writer = new ArrayBufferWriter(); - await runtime.SerializeSettingsAndRegisteredTypes(writer); - - // read back the serialized json - var array = writer.WrittenSpan.ToArray(); - string s = Encoding.UTF8.GetString(array, 0, array.Length); - - JsonDocument document = JsonDocument.Parse(s); - JsonElement root = document.RootElement; - - // parse out the entities array - JsonElement element = root.GetProperty("entities"); - Assert.Equal(1, element.GetArrayLength()); - - JsonElement arrayElement = element[0]; - string actor = arrayElement.GetString(); - Assert.Equal("TestActor", actor); - - // validate the other properties have expected values - element = root.GetProperty("actorIdleTimeout"); - Assert.Equal(TimeSpan.FromSeconds(33), ConverterUtils.ConvertTimeSpanFromDaprFormat(element.GetString())); - - element = root.GetProperty("actorScanInterval"); - Assert.Equal(TimeSpan.FromSeconds(44), ConverterUtils.ConvertTimeSpanFromDaprFormat(element.GetString())); - - element = root.GetProperty("drainOngoingCallTimeout"); - Assert.Equal(TimeSpan.FromSeconds(55), ConverterUtils.ConvertTimeSpanFromDaprFormat(element.GetString())); - - element = root.GetProperty("drainRebalancedActors"); - Assert.True(element.GetBoolean()); - - bool found = root.TryGetProperty("remindersStoragePartitions", out element); - Assert.False(found, "remindersStoragePartitions should not be serialized"); - - JsonElement jsonValue; - Assert.False(root.GetProperty("reentrancy").TryGetProperty("maxStackDepth", out jsonValue)); - } - - [Fact] - public async Task TestActorSettingsWithRemindersStoragePartitions() - { - var actorType = typeof(TestActor); - - var options = new ActorRuntimeOptions(); - options.Actors.RegisterActor(); - options.RemindersStoragePartitions = 12; - - var runtime = new ActorRuntime(options, loggerFactory, activatorFactory, proxyFactory); - - Assert.Contains(actorType.Name, runtime.RegisteredActors.Select(a => a.Type.ActorTypeName), StringComparer.InvariantCulture); - - ArrayBufferWriter writer = new ArrayBufferWriter(); - await runtime.SerializeSettingsAndRegisteredTypes(writer); - - // read back the serialized json - var array = writer.WrittenSpan.ToArray(); - string s = Encoding.UTF8.GetString(array, 0, array.Length); - - JsonDocument document = JsonDocument.Parse(s); - JsonElement root = document.RootElement; - - // parse out the entities array - JsonElement element = root.GetProperty("entities"); - Assert.Equal(1, element.GetArrayLength()); - - JsonElement arrayElement = element[0]; - string actor = arrayElement.GetString(); - Assert.Equal("TestActor", actor); - - element = root.GetProperty("remindersStoragePartitions"); - Assert.Equal(12, element.GetInt64()); - } - - [Fact] - public async Task TestActorSettingsWithReentrancy() - { - var actorType = typeof(TestActor); - - var options = new ActorRuntimeOptions(); - options.Actors.RegisterActor(); - options.ActorIdleTimeout = TimeSpan.FromSeconds(33); - options.ActorScanInterval = TimeSpan.FromSeconds(44); - options.DrainOngoingCallTimeout = TimeSpan.FromSeconds(55); - options.DrainRebalancedActors = true; - options.ReentrancyConfig.Enabled = true; - options.ReentrancyConfig.MaxStackDepth = 64; - - var runtime = new ActorRuntime(options, loggerFactory, activatorFactory, proxyFactory); - - Assert.Contains(actorType.Name, runtime.RegisteredActors.Select(a => a.Type.ActorTypeName), StringComparer.InvariantCulture); - - ArrayBufferWriter writer = new ArrayBufferWriter(); - await runtime.SerializeSettingsAndRegisteredTypes(writer); - - // read back the serialized json - var array = writer.WrittenSpan.ToArray(); - string s = Encoding.UTF8.GetString(array, 0, array.Length); - - JsonDocument document = JsonDocument.Parse(s); - JsonElement root = document.RootElement; - - // parse out the entities array - JsonElement element = root.GetProperty("entities"); - Assert.Equal(1, element.GetArrayLength()); - - element = root.GetProperty("reentrancy").GetProperty("enabled"); - Assert.True(element.GetBoolean()); - - element = root.GetProperty("reentrancy").GetProperty("maxStackDepth"); - Assert.Equal(64, element.GetInt32()); - } - - [Fact] - public async Task TestActorSettingsWithPerActorConfigurations() - { - var actorType = typeof(TestActor); - var options = new ActorRuntimeOptions(); - options.ActorIdleTimeout = TimeSpan.FromSeconds(33); - options.ActorScanInterval = TimeSpan.FromSeconds(44); - options.DrainOngoingCallTimeout = TimeSpan.FromSeconds(55); - options.DrainRebalancedActors = true; - options.ReentrancyConfig.Enabled = true; - options.ReentrancyConfig.MaxStackDepth = 32; - options.Actors.RegisterActor(options); - - var runtime = new ActorRuntime(options, loggerFactory, activatorFactory, proxyFactory); - - Assert.Contains(actorType.Name, runtime.RegisteredActors.Select(a => a.Type.ActorTypeName), StringComparer.InvariantCulture); - - ArrayBufferWriter writer = new ArrayBufferWriter(); - await runtime.SerializeSettingsAndRegisteredTypes(writer); - - // read back the serialized json - var array = writer.WrittenSpan.ToArray(); - string s = Encoding.UTF8.GetString(array, 0, array.Length); - - JsonDocument document = JsonDocument.Parse(s); - JsonElement root = document.RootElement; - - JsonElement element = root.GetProperty("entities"); - Assert.Equal(1, element.GetArrayLength()); - - element = root.GetProperty("entitiesConfig"); - Assert.Equal(1, element.GetArrayLength()); - - var perEntityConfig = element[0]; - - element = perEntityConfig.GetProperty("actorIdleTimeout"); - Assert.Equal(TimeSpan.FromSeconds(33), ConverterUtils.ConvertTimeSpanFromDaprFormat(element.GetString())); - - element = perEntityConfig.GetProperty("actorScanInterval"); - Assert.Equal(TimeSpan.FromSeconds(44), ConverterUtils.ConvertTimeSpanFromDaprFormat(element.GetString())); - - element = perEntityConfig.GetProperty("drainOngoingCallTimeout"); - Assert.Equal(TimeSpan.FromSeconds(55), ConverterUtils.ConvertTimeSpanFromDaprFormat(element.GetString())); - - element = perEntityConfig.GetProperty("drainRebalancedActors"); - Assert.True(element.GetBoolean()); - - element = root.GetProperty("reentrancy").GetProperty("enabled"); - Assert.True(element.GetBoolean()); - - element = root.GetProperty("reentrancy").GetProperty("maxStackDepth"); - Assert.Equal(32, element.GetInt32()); - } - - private sealed class TestActor : Actor, ITestActor - { - public TestActor(ActorHost host) - : base(host) - { - } - } - - [Actor(TypeName = RenamedActorTypeName)] - private sealed class RenamedActor : Actor, ITestActor - { - public RenamedActor(ActorHost host) - : base(host) - { - } - } - - private interface IAnotherActor : IActor - { - public Task MyMethod(); - } - - private sealed class MyActor : Actor, IAnotherActor - { - public MyActor(ActorHost host) - : base(host) - { - } - - public Task MyMethod() - { - return Task.FromResult("hi"); - } - } - - private class TestActivator : DefaultActorActivator - { - public int CreateCallCount { get; set; } - - public int DeleteCallCount { get; set; } - - public override Task CreateAsync(ActorHost host) - { - CreateCallCount++;; - return base.CreateAsync(host); - } - - public override Task DeleteAsync(ActorActivatorState state) - { - DeleteCallCount++; - return base.DeleteAsync(state); - } + return Task.FromResult(nameof(SingleArgumentWithCancellationAsync)); } } -} + + public async Task InvokeMethod(string methodName, object arg = null) where T : Actor + { + var options = new ActorRuntimeOptions(); + + options.Actors.RegisterActor(); + + var runtime = new ActorRuntime(options, loggerFactory, activatorFactory, proxyFactory); + + using var input = new MemoryStream(); + + if (arg is not null) + { + JsonSerializer.Serialize(input, arg); + + input.Seek(0, SeekOrigin.Begin); + } + + using var output = new MemoryStream(); + + await runtime.DispatchWithoutRemotingAsync(typeof(T).Name, ActorId.CreateRandom().ToString(), methodName, input, output); + + output.Seek(0, SeekOrigin.Begin); + + return JsonSerializer.Deserialize(output); + } + + [Fact] + public async Task NoRemotingMethodWithNoArguments() + { + string methodName = nameof(INotRemotedActor.NoArgumentsAsync); + + string result = await InvokeMethod(methodName); + + Assert.Equal(methodName, result); + } + + [Fact] + public async Task NoRemotingMethodWithNoArgumentsWithCancellation() + { + string methodName = nameof(INotRemotedActor.NoArgumentsWithCancellationAsync); + + string result = await InvokeMethod(methodName); + + Assert.Equal(methodName, result); + } + + [Fact] + public async Task NoRemotingMethodWithSingleArgument() + { + string methodName = nameof(INotRemotedActor.SingleArgumentAsync); + + string result = await InvokeMethod(methodName, true); + + Assert.Equal(methodName, result); + } + + [Fact] + public async Task NoRemotingMethodWithSingleArgumentWithCancellation() + { + string methodName = nameof(INotRemotedActor.SingleArgumentWithCancellationAsync); + + string result = await InvokeMethod(methodName, true); + + Assert.Equal(methodName, result); + } + + [Fact] + public async Task Actor_UsesCustomActivator() + { + var activator = new TestActivator(); + var actorType = typeof(MyActor); + + var options = new ActorRuntimeOptions(); + options.Actors.RegisterActor(options => + { + options.Activator = activator; + }); + var runtime = new ActorRuntime(options, loggerFactory, activatorFactory, proxyFactory); + Assert.Contains(actorType.Name, runtime.RegisteredActors.Select(a => a.Type.ActorTypeName), StringComparer.InvariantCulture); + + var output = new MemoryStream(); + await runtime.DispatchWithoutRemotingAsync(actorType.Name, "abc", nameof(MyActor.MyMethod), new MemoryStream(), output); + + var text = Encoding.UTF8.GetString(output.ToArray()); + Assert.Equal("\"hi\"", text); + + await runtime.DeactivateAsync(actorType.Name, "abc"); + Assert.Equal(1, activator.CreateCallCount); + Assert.Equal(1, activator.DeleteCallCount); + } + + [Fact] + public async Task TestActorSettings() + { + var actorType = typeof(TestActor); + + var options = new ActorRuntimeOptions(); + options.Actors.RegisterActor(); + options.ActorIdleTimeout = TimeSpan.FromSeconds(33); + options.ActorScanInterval = TimeSpan.FromSeconds(44); + options.DrainOngoingCallTimeout = TimeSpan.FromSeconds(55); + options.DrainRebalancedActors = true; + + var runtime = new ActorRuntime(options, loggerFactory, activatorFactory, proxyFactory); + + Assert.Contains(actorType.Name, runtime.RegisteredActors.Select(a => a.Type.ActorTypeName), StringComparer.InvariantCulture); + + ArrayBufferWriter writer = new ArrayBufferWriter(); + await runtime.SerializeSettingsAndRegisteredTypes(writer); + + // read back the serialized json + var array = writer.WrittenSpan.ToArray(); + string s = Encoding.UTF8.GetString(array, 0, array.Length); + + JsonDocument document = JsonDocument.Parse(s); + JsonElement root = document.RootElement; + + // parse out the entities array + JsonElement element = root.GetProperty("entities"); + Assert.Equal(1, element.GetArrayLength()); + + JsonElement arrayElement = element[0]; + string actor = arrayElement.GetString(); + Assert.Equal("TestActor", actor); + + // validate the other properties have expected values + element = root.GetProperty("actorIdleTimeout"); + Assert.Equal(TimeSpan.FromSeconds(33), ConverterUtils.ConvertTimeSpanFromDaprFormat(element.GetString())); + + element = root.GetProperty("actorScanInterval"); + Assert.Equal(TimeSpan.FromSeconds(44), ConverterUtils.ConvertTimeSpanFromDaprFormat(element.GetString())); + + element = root.GetProperty("drainOngoingCallTimeout"); + Assert.Equal(TimeSpan.FromSeconds(55), ConverterUtils.ConvertTimeSpanFromDaprFormat(element.GetString())); + + element = root.GetProperty("drainRebalancedActors"); + Assert.True(element.GetBoolean()); + + bool found = root.TryGetProperty("remindersStoragePartitions", out element); + Assert.False(found, "remindersStoragePartitions should not be serialized"); + + JsonElement jsonValue; + Assert.False(root.GetProperty("reentrancy").TryGetProperty("maxStackDepth", out jsonValue)); + } + + [Fact] + public async Task TestActorSettingsWithRemindersStoragePartitions() + { + var actorType = typeof(TestActor); + + var options = new ActorRuntimeOptions(); + options.Actors.RegisterActor(); + options.RemindersStoragePartitions = 12; + + var runtime = new ActorRuntime(options, loggerFactory, activatorFactory, proxyFactory); + + Assert.Contains(actorType.Name, runtime.RegisteredActors.Select(a => a.Type.ActorTypeName), StringComparer.InvariantCulture); + + ArrayBufferWriter writer = new ArrayBufferWriter(); + await runtime.SerializeSettingsAndRegisteredTypes(writer); + + // read back the serialized json + var array = writer.WrittenSpan.ToArray(); + string s = Encoding.UTF8.GetString(array, 0, array.Length); + + JsonDocument document = JsonDocument.Parse(s); + JsonElement root = document.RootElement; + + // parse out the entities array + JsonElement element = root.GetProperty("entities"); + Assert.Equal(1, element.GetArrayLength()); + + JsonElement arrayElement = element[0]; + string actor = arrayElement.GetString(); + Assert.Equal("TestActor", actor); + + element = root.GetProperty("remindersStoragePartitions"); + Assert.Equal(12, element.GetInt64()); + } + + [Fact] + public async Task TestActorSettingsWithReentrancy() + { + var actorType = typeof(TestActor); + + var options = new ActorRuntimeOptions(); + options.Actors.RegisterActor(); + options.ActorIdleTimeout = TimeSpan.FromSeconds(33); + options.ActorScanInterval = TimeSpan.FromSeconds(44); + options.DrainOngoingCallTimeout = TimeSpan.FromSeconds(55); + options.DrainRebalancedActors = true; + options.ReentrancyConfig.Enabled = true; + options.ReentrancyConfig.MaxStackDepth = 64; + + var runtime = new ActorRuntime(options, loggerFactory, activatorFactory, proxyFactory); + + Assert.Contains(actorType.Name, runtime.RegisteredActors.Select(a => a.Type.ActorTypeName), StringComparer.InvariantCulture); + + ArrayBufferWriter writer = new ArrayBufferWriter(); + await runtime.SerializeSettingsAndRegisteredTypes(writer); + + // read back the serialized json + var array = writer.WrittenSpan.ToArray(); + string s = Encoding.UTF8.GetString(array, 0, array.Length); + + JsonDocument document = JsonDocument.Parse(s); + JsonElement root = document.RootElement; + + // parse out the entities array + JsonElement element = root.GetProperty("entities"); + Assert.Equal(1, element.GetArrayLength()); + + element = root.GetProperty("reentrancy").GetProperty("enabled"); + Assert.True(element.GetBoolean()); + + element = root.GetProperty("reentrancy").GetProperty("maxStackDepth"); + Assert.Equal(64, element.GetInt32()); + } + + [Fact] + public async Task TestActorSettingsWithPerActorConfigurations() + { + var actorType = typeof(TestActor); + var options = new ActorRuntimeOptions(); + options.ActorIdleTimeout = TimeSpan.FromSeconds(33); + options.ActorScanInterval = TimeSpan.FromSeconds(44); + options.DrainOngoingCallTimeout = TimeSpan.FromSeconds(55); + options.DrainRebalancedActors = true; + options.ReentrancyConfig.Enabled = true; + options.ReentrancyConfig.MaxStackDepth = 32; + options.Actors.RegisterActor(options); + + var runtime = new ActorRuntime(options, loggerFactory, activatorFactory, proxyFactory); + + Assert.Contains(actorType.Name, runtime.RegisteredActors.Select(a => a.Type.ActorTypeName), StringComparer.InvariantCulture); + + ArrayBufferWriter writer = new ArrayBufferWriter(); + await runtime.SerializeSettingsAndRegisteredTypes(writer); + + // read back the serialized json + var array = writer.WrittenSpan.ToArray(); + string s = Encoding.UTF8.GetString(array, 0, array.Length); + + JsonDocument document = JsonDocument.Parse(s); + JsonElement root = document.RootElement; + + JsonElement element = root.GetProperty("entities"); + Assert.Equal(1, element.GetArrayLength()); + + element = root.GetProperty("entitiesConfig"); + Assert.Equal(1, element.GetArrayLength()); + + var perEntityConfig = element[0]; + + element = perEntityConfig.GetProperty("actorIdleTimeout"); + Assert.Equal(TimeSpan.FromSeconds(33), ConverterUtils.ConvertTimeSpanFromDaprFormat(element.GetString())); + + element = perEntityConfig.GetProperty("actorScanInterval"); + Assert.Equal(TimeSpan.FromSeconds(44), ConverterUtils.ConvertTimeSpanFromDaprFormat(element.GetString())); + + element = perEntityConfig.GetProperty("drainOngoingCallTimeout"); + Assert.Equal(TimeSpan.FromSeconds(55), ConverterUtils.ConvertTimeSpanFromDaprFormat(element.GetString())); + + element = perEntityConfig.GetProperty("drainRebalancedActors"); + Assert.True(element.GetBoolean()); + + element = root.GetProperty("reentrancy").GetProperty("enabled"); + Assert.True(element.GetBoolean()); + + element = root.GetProperty("reentrancy").GetProperty("maxStackDepth"); + Assert.Equal(32, element.GetInt32()); + } + + private sealed class TestActor : Actor, ITestActor + { + public TestActor(ActorHost host) + : base(host) + { + } + } + + [Actor(TypeName = RenamedActorTypeName)] + private sealed class RenamedActor : Actor, ITestActor + { + public RenamedActor(ActorHost host) + : base(host) + { + } + } + + private interface IAnotherActor : IActor + { + public Task MyMethod(); + } + + private sealed class MyActor : Actor, IAnotherActor + { + public MyActor(ActorHost host) + : base(host) + { + } + + public Task MyMethod() + { + return Task.FromResult("hi"); + } + } + + private class TestActivator : DefaultActorActivator + { + public int CreateCallCount { get; set; } + + public int DeleteCallCount { get; set; } + + public override Task CreateAsync(ActorHost host) + { + CreateCallCount++;; + return base.CreateAsync(host); + } + + public override Task DeleteAsync(ActorActivatorState state) + { + DeleteCallCount++; + return base.DeleteAsync(state); + } + } +} \ No newline at end of file diff --git a/test/Dapr.Actors.Test/Runtime/ActorTypeInformationTests.cs b/test/Dapr.Actors.Test/Runtime/ActorTypeInformationTests.cs index 74b407c7..25f02fbf 100644 --- a/test/Dapr.Actors.Test/Runtime/ActorTypeInformationTests.cs +++ b/test/Dapr.Actors.Test/Runtime/ActorTypeInformationTests.cs @@ -11,72 +11,71 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Actors.Test +namespace Dapr.Actors.Test; + +using System; +using System.Reflection; +using Dapr.Actors; +using Dapr.Actors.Runtime; +using Xunit; + +public sealed class ActorTypeInformationTests { - using System; - using System.Reflection; - using Dapr.Actors; - using Dapr.Actors.Runtime; - using Xunit; + private const string RenamedActorTypeName = "MyRenamedActor"; + private const string ParamActorTypeName = "AnotherRenamedActor"; - public sealed class ActorTypeInformationTests + private interface ITestActor : IActor { - private const string RenamedActorTypeName = "MyRenamedActor"; - private const string ParamActorTypeName = "AnotherRenamedActor"; + } - private interface ITestActor : IActor + [Fact] + public void TestInferredActorType() + { + var actorType = typeof(TestActor); + var actorTypeInformation = ActorTypeInformation.Get(actorType, actorTypeName: null); + + Assert.Equal(actorType.Name, actorTypeInformation.ActorTypeName); + } + + [Fact] + public void TestExplicitActorType() + { + var actorType = typeof(RenamedActor); + + Assert.NotEqual(RenamedActorTypeName, actorType.Name); + + var actorTypeInformation = ActorTypeInformation.Get(actorType, actorTypeName: null); + + Assert.Equal(RenamedActorTypeName, actorTypeInformation.ActorTypeName); + } + + [Theory] + [InlineData(typeof(TestActor))] + [InlineData(typeof(RenamedActor))] + public void TestExplicitActorTypeAsParam(Type actorType) + { + Assert.NotEqual(ParamActorTypeName, actorType.Name); + Assert.NotEqual(ParamActorTypeName, actorType.GetCustomAttribute()?.TypeName); + + var actorTypeInformation = ActorTypeInformation.Get(actorType, ParamActorTypeName); + + Assert.Equal(ParamActorTypeName, actorTypeInformation.ActorTypeName); + } + + private sealed class TestActor : Actor, ITestActor + { + public TestActor(ActorHost host) + : base(host) { } - - [Fact] - public void TestInferredActorType() - { - var actorType = typeof(TestActor); - var actorTypeInformation = ActorTypeInformation.Get(actorType, actorTypeName: null); - - Assert.Equal(actorType.Name, actorTypeInformation.ActorTypeName); - } - - [Fact] - public void TestExplicitActorType() - { - var actorType = typeof(RenamedActor); - - Assert.NotEqual(RenamedActorTypeName, actorType.Name); - - var actorTypeInformation = ActorTypeInformation.Get(actorType, actorTypeName: null); - - Assert.Equal(RenamedActorTypeName, actorTypeInformation.ActorTypeName); - } - - [Theory] - [InlineData(typeof(TestActor))] - [InlineData(typeof(RenamedActor))] - public void TestExplicitActorTypeAsParam(Type actorType) - { - Assert.NotEqual(ParamActorTypeName, actorType.Name); - Assert.NotEqual(ParamActorTypeName, actorType.GetCustomAttribute()?.TypeName); - - var actorTypeInformation = ActorTypeInformation.Get(actorType, ParamActorTypeName); - - Assert.Equal(ParamActorTypeName, actorTypeInformation.ActorTypeName); - } - - private sealed class TestActor : Actor, ITestActor - { - public TestActor(ActorHost host) - : base(host) - { - } - } - - [Actor(TypeName = RenamedActorTypeName)] - private sealed class RenamedActor : Actor, ITestActor - { - public RenamedActor(ActorHost host) - : base(host) - { - } - } } -} + + [Actor(TypeName = RenamedActorTypeName)] + private sealed class RenamedActor : Actor, ITestActor + { + public RenamedActor(ActorHost host) + : base(host) + { + } + } +} \ No newline at end of file diff --git a/test/Dapr.Actors.Test/Runtime/DefaultActorActivatorTests.cs b/test/Dapr.Actors.Test/Runtime/DefaultActorActivatorTests.cs index 87045b29..9861a126 100644 --- a/test/Dapr.Actors.Test/Runtime/DefaultActorActivatorTests.cs +++ b/test/Dapr.Actors.Test/Runtime/DefaultActorActivatorTests.cs @@ -15,101 +15,100 @@ using System; using System.Threading.Tasks; using Xunit; -namespace Dapr.Actors.Runtime +namespace Dapr.Actors.Runtime; + +public class DefaultActorActivatorTests { - public class DefaultActorActivatorTests + [Fact] + public async Task CreateAsync_CallsConstructor() { - [Fact] - public async Task CreateAsync_CallsConstructor() + var activator = new DefaultActorActivator(); + + var host = ActorHost.CreateForTest(); + var state = await activator.CreateAsync(host); + Assert.IsType(state.Actor); + } + + [Fact] + public async Task DeleteAsync_NotDisposable() + { + var activator = new DefaultActorActivator(); + + var host = ActorHost.CreateForTest(); + var actor = new TestActor(host); + var state = new ActorActivatorState(actor); + + await activator.DeleteAsync(state); // does not throw + } + + [Fact] + public async Task DeleteAsync_Disposable() + { + var activator = new DefaultActorActivator(); + + var host = ActorHost.CreateForTest(); + var actor = new DisposableActor(host); + var state = new ActorActivatorState(actor); + + await activator.DeleteAsync(state); // does not throw + + Assert.True(actor.IsDisposed); + } + + [Fact] + public async Task DeleteAsync_AsyncDisposable() + { + var activator = new DefaultActorActivator(); + + var host = ActorHost.CreateForTest(); + var actor = new AsyncDisposableActor(host); + var state = new ActorActivatorState(actor); + + await activator.DeleteAsync(state); + + Assert.True(actor.IsDisposed); + } + + private interface ITestActor : IActor + { + } + + private class TestActor : Actor, ITestActor + { + public TestActor(ActorHost host) + : base(host) { - var activator = new DefaultActorActivator(); - - var host = ActorHost.CreateForTest(); - var state = await activator.CreateAsync(host); - Assert.IsType(state.Actor); - } - - [Fact] - public async Task DeleteAsync_NotDisposable() - { - var activator = new DefaultActorActivator(); - - var host = ActorHost.CreateForTest(); - var actor = new TestActor(host); - var state = new ActorActivatorState(actor); - - await activator.DeleteAsync(state); // does not throw - } - - [Fact] - public async Task DeleteAsync_Disposable() - { - var activator = new DefaultActorActivator(); - - var host = ActorHost.CreateForTest(); - var actor = new DisposableActor(host); - var state = new ActorActivatorState(actor); - - await activator.DeleteAsync(state); // does not throw - - Assert.True(actor.IsDisposed); - } - - [Fact] - public async Task DeleteAsync_AsyncDisposable() - { - var activator = new DefaultActorActivator(); - - var host = ActorHost.CreateForTest(); - var actor = new AsyncDisposableActor(host); - var state = new ActorActivatorState(actor); - - await activator.DeleteAsync(state); - - Assert.True(actor.IsDisposed); - } - - private interface ITestActor : IActor - { - } - - private class TestActor : Actor, ITestActor - { - public TestActor(ActorHost host) - : base(host) - { - } - } - - private class DisposableActor : Actor, ITestActor, IDisposable - { - public DisposableActor(ActorHost host) - : base(host) - { - } - - public bool IsDisposed { get; set; } - - public void Dispose() - { - IsDisposed = true; - } - } - - private class AsyncDisposableActor : Actor, ITestActor, IAsyncDisposable - { - public AsyncDisposableActor(ActorHost host) - : base(host) - { - } - - public bool IsDisposed { get; set; } - - public ValueTask DisposeAsync() - { - IsDisposed = true; - return new ValueTask(); - } } } -} + + private class DisposableActor : Actor, ITestActor, IDisposable + { + public DisposableActor(ActorHost host) + : base(host) + { + } + + public bool IsDisposed { get; set; } + + public void Dispose() + { + IsDisposed = true; + } + } + + private class AsyncDisposableActor : Actor, ITestActor, IAsyncDisposable + { + public AsyncDisposableActor(ActorHost host) + : base(host) + { + } + + public bool IsDisposed { get; set; } + + public ValueTask DisposeAsync() + { + IsDisposed = true; + return new ValueTask(); + } + } +} \ No newline at end of file diff --git a/test/Dapr.AspNetCore.IntegrationTest.App/CustomTopicAttribute.cs b/test/Dapr.AspNetCore.IntegrationTest.App/CustomTopicAttribute.cs index 6f65f060..1888ca36 100644 --- a/test/Dapr.AspNetCore.IntegrationTest.App/CustomTopicAttribute.cs +++ b/test/Dapr.AspNetCore.IntegrationTest.App/CustomTopicAttribute.cs @@ -11,24 +11,23 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.AspNetCore.IntegrationTest.App +namespace Dapr.AspNetCore.IntegrationTest.App; + +using System; + +public class CustomTopicAttribute : Attribute, ITopicMetadata { - using System; - - public class CustomTopicAttribute : Attribute, ITopicMetadata + public CustomTopicAttribute(string pubsubName, string name) { - public CustomTopicAttribute(string pubsubName, string name) - { - this.Name = "custom-" + name; - this.PubsubName = "custom-" + pubsubName; - } - - public string Name { get; } - - public string PubsubName { get; } - - public new string Match { get; } - - public int Priority { get; } + this.Name = "custom-" + name; + this.PubsubName = "custom-" + pubsubName; } -} + + public string Name { get; } + + public string PubsubName { get; } + + public new string Match { get; } + + public int Priority { get; } +} \ No newline at end of file diff --git a/test/Dapr.AspNetCore.IntegrationTest.App/DaprController.cs b/test/Dapr.AspNetCore.IntegrationTest.App/DaprController.cs index 13f72ff7..31f31b01 100644 --- a/test/Dapr.AspNetCore.IntegrationTest.App/DaprController.cs +++ b/test/Dapr.AspNetCore.IntegrationTest.App/DaprController.cs @@ -11,164 +11,163 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.AspNetCore.IntegrationTest.App +namespace Dapr.AspNetCore.IntegrationTest.App; + +using System.Text; +using System.Threading.Tasks; +using Dapr; +using Dapr.Client; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.WebUtilities; + +[ApiController] +public class DaprController : ControllerBase { - using System.Text; - using System.Threading.Tasks; - using Dapr; - using Dapr.Client; - using Microsoft.AspNetCore.Authorization; - using Microsoft.AspNetCore.Mvc; - using Microsoft.AspNetCore.WebUtilities; - - [ApiController] - public class DaprController : ControllerBase + [Topic("pubsub", "B")] + [HttpPost("/B")] + public void TopicB() { - [Topic("pubsub", "B")] - [HttpPost("/B")] - public void TopicB() - { - } - - [CustomTopic("pubsub", "C")] - [HttpPost("/C")] - public void TopicC() - { - } - - [Topic("pubsub", "D", true)] - [HttpPost("/D")] - public void TopicD() - { - } - - [Topic("pubsub", "E", false)] - [HttpPost("/E")] - public void TopicE() - { - } - - [Topic("pubsub", "E", false, "event.type == \"critical\"", 1)] - [HttpPost("/E-Critical")] - public void TopicECritical() - { - } - - [Topic("pubsub", "E", false, "event.type == \"important\"", 2)] - [HttpPost("/E-Important")] - public void TopicEImportant() - { - } - - [BulkSubscribe("F")] - [Topic("pubsub", "F")] - [Topic("pubsub", "F.1", true)] - [HttpPost("/multiTopicAttr")] - public void MultipleTopics() - { - } - - [BulkSubscribe("G", 300)] - [Topic("pubsub", "G", "deadLetterTopicName", false)] - [HttpPost("/G")] - public void TopicG() - { - } - - [BulkSubscribe("metadata.1", 500, 2000)] - [Topic("pubsub", "metadata", new string[1] { "id1" })] - [Topic("pubsub", "metadata.1", true)] - [HttpPost("/multiMetadataTopicAttr")] - [TopicMetadata("n1", "v1")] - [TopicMetadata("id1", "n2", "v2")] - [TopicMetadata("id1", "n2", "v3")] - public void MultipleMetadataTopics() - { - } - - [Topic("pubsub", "metadataseparator", metadataSeparator: "|")] - [HttpPost("/topicmetadataseparatorattr")] - [TopicMetadata("n1", "v1")] - [TopicMetadata("n1", "v2")] - public void TopicMetadataSeparator() - { - } - - [Topic("pubsub", "metadataseparatorbyemptytring")] - [HttpPost("/topicmetadataseparatorattrbyemptytring")] - [TopicMetadata("n1", "v1")] - [TopicMetadata("n1", "")] - public void TopicMetadataSeparatorByemptytring () - { - } - - [Topic("pubsub", "splitTopicAttr", true)] - [HttpPost("/splitTopics")] - public void SplitTopic() - { - } - - [Topic("pubsub", "register-user")] - [HttpPost("/register-user")] - public ActionResult RegisterUser(UserInfo user) - { - return user; // echo back the user for testing - } - - [Topic("pubsub", "register-user-plaintext")] - [HttpPost("/register-user-plaintext")] - public async Task RegisterUserPlaintext() - { - using var reader = new HttpRequestStreamReader(Request.Body, Encoding.UTF8); - var user = await reader.ReadToEndAsync(); - return Content(user, "text/plain"); // echo back the user for testing - } - - [HttpPost("/controllerwithoutstateentry/{widget}")] - public async Task AddOneWithoutStateEntry([FromServices] DaprClient state, [FromState("testStore")] Widget widget) - { - widget.Count++; - await state.SaveStateAsync("testStore", (string)this.HttpContext.Request.RouteValues["widget"], widget); - } - - [HttpPost("/controllerwithstateentry/{widget}")] - public async Task AddOneWithStateEntry([FromState("testStore")] StateEntry widget) - { - widget.Value.Count++; - await widget.SaveAsync(); - } - - [HttpPost("/controllerwithstateentryandcustomkey/{widget}")] - public async Task AddOneWithStateEntryAndCustomKey([FromState("testStore", "widget")] StateEntry state) - { - state.Value.Count++; - await state.SaveAsync(); - } - - [HttpPost("/echo-user")] - public ActionResult EchoUser([FromQuery] UserInfo user) - { - // To simulate an action where there's no Dapr attribute, yet MVC still checks the list of available model binder providers. - return user; - } - - [HttpGet("controllerwithoutstateentry/{widget}")] - public ActionResult Get([FromState("testStore")] Widget widget) - { - return widget; - } - - [HttpGet("controllerwithstateentry/{widgetStateEntry}")] - public ActionResult Get([FromState("testStore")] StateEntry widgetStateEntry) - { - return widgetStateEntry.Value; - } - - [Authorize("Dapr")] - [HttpPost("/requires-api-token")] - public ActionResult RequiresApiToken(UserInfo user) - { - return user; - } } -} + + [CustomTopic("pubsub", "C")] + [HttpPost("/C")] + public void TopicC() + { + } + + [Topic("pubsub", "D", true)] + [HttpPost("/D")] + public void TopicD() + { + } + + [Topic("pubsub", "E", false)] + [HttpPost("/E")] + public void TopicE() + { + } + + [Topic("pubsub", "E", false, "event.type == \"critical\"", 1)] + [HttpPost("/E-Critical")] + public void TopicECritical() + { + } + + [Topic("pubsub", "E", false, "event.type == \"important\"", 2)] + [HttpPost("/E-Important")] + public void TopicEImportant() + { + } + + [BulkSubscribe("F")] + [Topic("pubsub", "F")] + [Topic("pubsub", "F.1", true)] + [HttpPost("/multiTopicAttr")] + public void MultipleTopics() + { + } + + [BulkSubscribe("G", 300)] + [Topic("pubsub", "G", "deadLetterTopicName", false)] + [HttpPost("/G")] + public void TopicG() + { + } + + [BulkSubscribe("metadata.1", 500, 2000)] + [Topic("pubsub", "metadata", new string[1] { "id1" })] + [Topic("pubsub", "metadata.1", true)] + [HttpPost("/multiMetadataTopicAttr")] + [TopicMetadata("n1", "v1")] + [TopicMetadata("id1", "n2", "v2")] + [TopicMetadata("id1", "n2", "v3")] + public void MultipleMetadataTopics() + { + } + + [Topic("pubsub", "metadataseparator", metadataSeparator: "|")] + [HttpPost("/topicmetadataseparatorattr")] + [TopicMetadata("n1", "v1")] + [TopicMetadata("n1", "v2")] + public void TopicMetadataSeparator() + { + } + + [Topic("pubsub", "metadataseparatorbyemptytring")] + [HttpPost("/topicmetadataseparatorattrbyemptytring")] + [TopicMetadata("n1", "v1")] + [TopicMetadata("n1", "")] + public void TopicMetadataSeparatorByemptytring () + { + } + + [Topic("pubsub", "splitTopicAttr", true)] + [HttpPost("/splitTopics")] + public void SplitTopic() + { + } + + [Topic("pubsub", "register-user")] + [HttpPost("/register-user")] + public ActionResult RegisterUser(UserInfo user) + { + return user; // echo back the user for testing + } + + [Topic("pubsub", "register-user-plaintext")] + [HttpPost("/register-user-plaintext")] + public async Task RegisterUserPlaintext() + { + using var reader = new HttpRequestStreamReader(Request.Body, Encoding.UTF8); + var user = await reader.ReadToEndAsync(); + return Content(user, "text/plain"); // echo back the user for testing + } + + [HttpPost("/controllerwithoutstateentry/{widget}")] + public async Task AddOneWithoutStateEntry([FromServices] DaprClient state, [FromState("testStore")] Widget widget) + { + widget.Count++; + await state.SaveStateAsync("testStore", (string)this.HttpContext.Request.RouteValues["widget"], widget); + } + + [HttpPost("/controllerwithstateentry/{widget}")] + public async Task AddOneWithStateEntry([FromState("testStore")] StateEntry widget) + { + widget.Value.Count++; + await widget.SaveAsync(); + } + + [HttpPost("/controllerwithstateentryandcustomkey/{widget}")] + public async Task AddOneWithStateEntryAndCustomKey([FromState("testStore", "widget")] StateEntry state) + { + state.Value.Count++; + await state.SaveAsync(); + } + + [HttpPost("/echo-user")] + public ActionResult EchoUser([FromQuery] UserInfo user) + { + // To simulate an action where there's no Dapr attribute, yet MVC still checks the list of available model binder providers. + return user; + } + + [HttpGet("controllerwithoutstateentry/{widget}")] + public ActionResult Get([FromState("testStore")] Widget widget) + { + return widget; + } + + [HttpGet("controllerwithstateentry/{widgetStateEntry}")] + public ActionResult Get([FromState("testStore")] StateEntry widgetStateEntry) + { + return widgetStateEntry.Value; + } + + [Authorize("Dapr")] + [HttpPost("/requires-api-token")] + public ActionResult RequiresApiToken(UserInfo user) + { + return user; + } +} \ No newline at end of file diff --git a/test/Dapr.AspNetCore.IntegrationTest.App/Program.cs b/test/Dapr.AspNetCore.IntegrationTest.App/Program.cs index 39ef84bc..f6f13dd6 100644 --- a/test/Dapr.AspNetCore.IntegrationTest.App/Program.cs +++ b/test/Dapr.AspNetCore.IntegrationTest.App/Program.cs @@ -11,23 +11,22 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.AspNetCore.IntegrationTest.App +namespace Dapr.AspNetCore.IntegrationTest.App; + +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +public class Program { - using Microsoft.AspNetCore.Hosting; - using Microsoft.Extensions.Hosting; - - public class Program + public static void Main(string[] args) { - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } - - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }); + CreateHostBuilder(args).Build().Run(); } -} + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); +} \ No newline at end of file diff --git a/test/Dapr.AspNetCore.IntegrationTest.App/Startup.cs b/test/Dapr.AspNetCore.IntegrationTest.App/Startup.cs index e776fa46..75e384d2 100644 --- a/test/Dapr.AspNetCore.IntegrationTest.App/Startup.cs +++ b/test/Dapr.AspNetCore.IntegrationTest.App/Startup.cs @@ -11,71 +11,70 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.AspNetCore.IntegrationTest.App +namespace Dapr.AspNetCore.IntegrationTest.App; + +using System.Collections.Generic; +using System.Threading.Tasks; +using Dapr.Client; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +public class Startup { - using System.Collections.Generic; - using System.Threading.Tasks; - using Dapr.Client; - using Microsoft.AspNetCore.Authentication; - using Microsoft.AspNetCore.Authorization; - using Microsoft.AspNetCore.Builder; - using Microsoft.AspNetCore.Hosting; - using Microsoft.Extensions.Configuration; - using Microsoft.Extensions.DependencyInjection; - using Microsoft.Extensions.Hosting; - - public class Startup + public Startup(IConfiguration configuration) { - public Startup(IConfiguration configuration) - { - this.Configuration = configuration; - } - - public IConfiguration Configuration { get; } - - public void ConfigureServices(IServiceCollection services) - { - services.AddAuthentication().AddDapr(options => options.Token = "abcdefg"); - - services.AddAuthorization(o => o.AddDapr()); - - services.AddControllers().AddDapr(); - } - - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.UseRouting(); - - app.UseAuthentication(); - - app.UseAuthorization(); - - app.UseCloudEvents(); - - app.UseEndpoints(endpoints => - { - endpoints.MapSubscribeHandler(); - endpoints.MapControllers(); - - endpoints.MapPost("/topic-a", context => Task.CompletedTask).WithTopic("testpubsub", "A").WithTopic("testpubsub", "A.1"); - - endpoints.MapPost("/splitTopics", context => Task.CompletedTask).WithTopic("pubsub", "splitTopicBuilder"); - - endpoints.MapPost("/splitMetadataTopics", context => Task.CompletedTask).WithTopic("pubsub", "splitMetadataTopicBuilder", new Dictionary { { "n1", "v1" }, { "n2", "v1" } }); - - endpoints.MapPost("/routingwithstateentry/{widget}", async context => - { - var daprClient = context.RequestServices.GetRequiredService(); - var state = await daprClient.GetStateEntryAsync("testStore", (string)context.Request.RouteValues["widget"]); - state.Value.Count++; - await state.SaveAsync(); - }); - }); - } + this.Configuration = configuration; } -} + + public IConfiguration Configuration { get; } + + public void ConfigureServices(IServiceCollection services) + { + services.AddAuthentication().AddDapr(options => options.Token = "abcdefg"); + + services.AddAuthorization(o => o.AddDapr()); + + services.AddControllers().AddDapr(); + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseRouting(); + + app.UseAuthentication(); + + app.UseAuthorization(); + + app.UseCloudEvents(); + + app.UseEndpoints(endpoints => + { + endpoints.MapSubscribeHandler(); + endpoints.MapControllers(); + + endpoints.MapPost("/topic-a", context => Task.CompletedTask).WithTopic("testpubsub", "A").WithTopic("testpubsub", "A.1"); + + endpoints.MapPost("/splitTopics", context => Task.CompletedTask).WithTopic("pubsub", "splitTopicBuilder"); + + endpoints.MapPost("/splitMetadataTopics", context => Task.CompletedTask).WithTopic("pubsub", "splitMetadataTopicBuilder", new Dictionary { { "n1", "v1" }, { "n2", "v1" } }); + + endpoints.MapPost("/routingwithstateentry/{widget}", async context => + { + var daprClient = context.RequestServices.GetRequiredService(); + var state = await daprClient.GetStateEntryAsync("testStore", (string)context.Request.RouteValues["widget"]); + state.Value.Count++; + await state.SaveAsync(); + }); + }); + } +} \ No newline at end of file diff --git a/test/Dapr.AspNetCore.IntegrationTest.App/UserInfo.cs b/test/Dapr.AspNetCore.IntegrationTest.App/UserInfo.cs index 51fa1eb0..54eba52e 100644 --- a/test/Dapr.AspNetCore.IntegrationTest.App/UserInfo.cs +++ b/test/Dapr.AspNetCore.IntegrationTest.App/UserInfo.cs @@ -11,13 +11,12 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.AspNetCore.IntegrationTest.App -{ - using System.ComponentModel.DataAnnotations; +namespace Dapr.AspNetCore.IntegrationTest.App; - public class UserInfo - { - [Required] - public string Name { get; set; } - } +using System.ComponentModel.DataAnnotations; + +public class UserInfo +{ + [Required] + public string Name { get; set; } } \ No newline at end of file diff --git a/test/Dapr.AspNetCore.IntegrationTest.App/Widget.cs b/test/Dapr.AspNetCore.IntegrationTest.App/Widget.cs index aecec6ee..3177ef08 100644 --- a/test/Dapr.AspNetCore.IntegrationTest.App/Widget.cs +++ b/test/Dapr.AspNetCore.IntegrationTest.App/Widget.cs @@ -11,12 +11,11 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.AspNetCore.IntegrationTest.App -{ - public class Widget - { - public string Size { get; set; } +namespace Dapr.AspNetCore.IntegrationTest.App; - public int Count { get; set; } - } +public class Widget +{ + public string Size { get; set; } + + public int Count { get; set; } } \ No newline at end of file diff --git a/test/Dapr.AspNetCore.IntegrationTest/AppWebApplicationFactory.cs b/test/Dapr.AspNetCore.IntegrationTest/AppWebApplicationFactory.cs index 1eb494ca..24df9b66 100644 --- a/test/Dapr.AspNetCore.IntegrationTest/AppWebApplicationFactory.cs +++ b/test/Dapr.AspNetCore.IntegrationTest/AppWebApplicationFactory.cs @@ -11,30 +11,29 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.AspNetCore.IntegrationTest +namespace Dapr.AspNetCore.IntegrationTest; + +using Dapr.AspNetCore.IntegrationTest.App; +using Dapr.Client; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +public class AppWebApplicationFactory : WebApplicationFactory { - using Dapr.AspNetCore.IntegrationTest.App; - using Dapr.Client; - using Microsoft.AspNetCore.Mvc.Testing; - using Microsoft.Extensions.DependencyInjection; - using Microsoft.Extensions.Hosting; - using Microsoft.Extensions.Logging; + internal StateTestClient DaprClient { get; } = new StateTestClient(); - public class AppWebApplicationFactory : WebApplicationFactory + protected override IHostBuilder CreateHostBuilder() { - internal StateTestClient DaprClient { get; } = new StateTestClient(); - - protected override IHostBuilder CreateHostBuilder() + var builder = base.CreateHostBuilder(); + builder.ConfigureLogging(b => { - var builder = base.CreateHostBuilder(); - builder.ConfigureLogging(b => - { - b.SetMinimumLevel(LogLevel.None); - }); - return builder.ConfigureServices((context, services) => - { - services.AddSingleton(this.DaprClient); - }); - } + b.SetMinimumLevel(LogLevel.None); + }); + return builder.ConfigureServices((context, services) => + { + services.AddSingleton(this.DaprClient); + }); } -} +} \ No newline at end of file diff --git a/test/Dapr.AspNetCore.IntegrationTest/AuthenticationTest.cs b/test/Dapr.AspNetCore.IntegrationTest/AuthenticationTest.cs index 3684e709..ba91828f 100644 --- a/test/Dapr.AspNetCore.IntegrationTest/AuthenticationTest.cs +++ b/test/Dapr.AspNetCore.IntegrationTest/AuthenticationTest.cs @@ -20,52 +20,51 @@ using Shouldly; using Newtonsoft.Json; using Xunit; -namespace Dapr.AspNetCore.IntegrationTest +namespace Dapr.AspNetCore.IntegrationTest; + +public class AuthenticationTest { - public class AuthenticationTest + [Fact] + public async Task ValidToken_ShouldBeAuthenticatedAndAuthorized() { - [Fact] - public async Task ValidToken_ShouldBeAuthenticatedAndAuthorized() + using (var factory = new AppWebApplicationFactory()) { - using (var factory = new AppWebApplicationFactory()) + var userInfo = new UserInfo { - var userInfo = new UserInfo - { - Name = "jimmy" - }; - var httpClient = factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions { HandleCookies = false }); - var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/requires-api-token") - { - Content = new StringContent(JsonConvert.SerializeObject(userInfo), Encoding.UTF8, "application/json") - }; - request.Headers.Add("Dapr-Api-Token", "abcdefg"); - var response = await httpClient.SendAsync(request); - response.EnsureSuccessStatusCode(); - var responseContent = await response.Content.ReadAsStringAsync(); - var responseUserInfo = JsonConvert.DeserializeObject(responseContent); - responseUserInfo.Name.ShouldBe(userInfo.Name); - } - } - - [Fact] - public async Task InvalidToken_ShouldBeUnauthorized() - { - using (var factory = new AppWebApplicationFactory()) + Name = "jimmy" + }; + var httpClient = factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions { HandleCookies = false }); + var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/requires-api-token") { - var userInfo = new UserInfo - { - Name = "jimmy" - }; - var httpClient = factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions { HandleCookies = false }); - var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/requires-api-token") - { - Content = new StringContent(JsonConvert.SerializeObject(userInfo), Encoding.UTF8, "application/json") - }; - request.Headers.Add("Dapr-Api-Token", "asdfgh"); - var response = await httpClient.SendAsync(request); - - response.StatusCode.ShouldBe(HttpStatusCode.Unauthorized); - } + Content = new StringContent(JsonConvert.SerializeObject(userInfo), Encoding.UTF8, "application/json") + }; + request.Headers.Add("Dapr-Api-Token", "abcdefg"); + var response = await httpClient.SendAsync(request); + response.EnsureSuccessStatusCode(); + var responseContent = await response.Content.ReadAsStringAsync(); + var responseUserInfo = JsonConvert.DeserializeObject(responseContent); + responseUserInfo.Name.ShouldBe(userInfo.Name); } } -} + + [Fact] + public async Task InvalidToken_ShouldBeUnauthorized() + { + using (var factory = new AppWebApplicationFactory()) + { + var userInfo = new UserInfo + { + Name = "jimmy" + }; + var httpClient = factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions { HandleCookies = false }); + var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/requires-api-token") + { + Content = new StringContent(JsonConvert.SerializeObject(userInfo), Encoding.UTF8, "application/json") + }; + request.Headers.Add("Dapr-Api-Token", "asdfgh"); + var response = await httpClient.SendAsync(request); + + response.StatusCode.ShouldBe(HttpStatusCode.Unauthorized); + } + } +} \ No newline at end of file diff --git a/test/Dapr.AspNetCore.IntegrationTest/CloudEventsIntegrationTest.cs b/test/Dapr.AspNetCore.IntegrationTest/CloudEventsIntegrationTest.cs index 928286fb..dde6751c 100644 --- a/test/Dapr.AspNetCore.IntegrationTest/CloudEventsIntegrationTest.cs +++ b/test/Dapr.AspNetCore.IntegrationTest/CloudEventsIntegrationTest.cs @@ -11,54 +11,54 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.AspNetCore.IntegrationTest +namespace Dapr.AspNetCore.IntegrationTest; + +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using Dapr.AspNetCore.IntegrationTest.App; +using Shouldly; +using Xunit; + +public class CloudEventsIntegrationTest { - using System.Net.Http; - using System.Net.Http.Headers; - using System.Text; - using System.Text.Json; - using System.Threading.Tasks; - using Dapr.AspNetCore.IntegrationTest.App; - using Shouldly; - using Xunit; - - public class CloudEventsIntegrationTest + private readonly JsonSerializerOptions options = new JsonSerializerOptions() { - private readonly JsonSerializerOptions options = new JsonSerializerOptions() - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - PropertyNameCaseInsensitive = true, - }; + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = true, + }; - [Fact] - public async Task CanSendEmptyStructuredCloudEvent() + [Fact] + public async Task CanSendEmptyStructuredCloudEvent() + { + using (var factory = new AppWebApplicationFactory()) { - using (var factory = new AppWebApplicationFactory()) + var httpClient = factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions { HandleCookies = false }); + + var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/B") { - var httpClient = factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions { HandleCookies = false }); + Content = new StringContent("{}", Encoding.UTF8) + }; + request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/cloudevents+json"); - var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/B") - { - Content = new StringContent("{}", Encoding.UTF8) - }; - request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/cloudevents+json"); - - var response = await httpClient.SendAsync(request); - response.EnsureSuccessStatusCode(); - } + var response = await httpClient.SendAsync(request); + response.EnsureSuccessStatusCode(); } + } - [Fact] - public async Task CanSendStructuredCloudEvent() + [Fact] + public async Task CanSendStructuredCloudEvent() + { + using (var factory = new AppWebApplicationFactory()) { - using (var factory = new AppWebApplicationFactory()) - { - var httpClient = factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions { HandleCookies = false }); + var httpClient = factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions { HandleCookies = false }); - var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/register-user") - { - Content = new StringContent( + var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/register-user") + { + Content = new StringContent( JsonSerializer.Serialize( new { @@ -68,27 +68,27 @@ namespace Dapr.AspNetCore.IntegrationTest }, }), Encoding.UTF8) - }; - request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/cloudevents+json"); + }; + request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/cloudevents+json"); - var response = await httpClient.SendAsync(request); - response.EnsureSuccessStatusCode(); + var response = await httpClient.SendAsync(request); + response.EnsureSuccessStatusCode(); - var userInfo = await JsonSerializer.DeserializeAsync(await response.Content.ReadAsStreamAsync(), this.options); - userInfo.Name.ShouldBe("jimmy"); - } + var userInfo = await JsonSerializer.DeserializeAsync(await response.Content.ReadAsStreamAsync(), this.options); + userInfo.Name.ShouldBe("jimmy"); } + } - [Fact] - public async Task CanSendStructuredCloudEvent_WithContentType() + [Fact] + public async Task CanSendStructuredCloudEvent_WithContentType() + { + using (var factory = new AppWebApplicationFactory()) { - using (var factory = new AppWebApplicationFactory()) - { - var httpClient = factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions { HandleCookies = false }); + var httpClient = factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions { HandleCookies = false }); - var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/register-user") - { - Content = new StringContent( + var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/register-user") + { + Content = new StringContent( JsonSerializer.Serialize( new { @@ -99,27 +99,27 @@ namespace Dapr.AspNetCore.IntegrationTest datacontenttype = "text/json", }), Encoding.UTF8) - }; - request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/cloudevents+json"); + }; + request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/cloudevents+json"); - var response = await httpClient.SendAsync(request); - response.EnsureSuccessStatusCode(); + var response = await httpClient.SendAsync(request); + response.EnsureSuccessStatusCode(); - var userInfo = await JsonSerializer.DeserializeAsync(await response.Content.ReadAsStreamAsync(), this.options); - userInfo.Name.ShouldBe("jimmy"); - } + var userInfo = await JsonSerializer.DeserializeAsync(await response.Content.ReadAsStreamAsync(), this.options); + userInfo.Name.ShouldBe("jimmy"); } + } - [Fact] - public async Task CanSendStructuredCloudEvent_WithNonJsonContentType() + [Fact] + public async Task CanSendStructuredCloudEvent_WithNonJsonContentType() + { + using (var factory = new AppWebApplicationFactory()) { - using (var factory = new AppWebApplicationFactory()) - { - var httpClient = factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions { HandleCookies = false }); + var httpClient = factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions { HandleCookies = false }); - var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/register-user-plaintext") - { - Content = new StringContent( + var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/register-user-plaintext") + { + Content = new StringContent( JsonSerializer.Serialize( new { @@ -127,45 +127,44 @@ namespace Dapr.AspNetCore.IntegrationTest datacontenttype = "text/plain", }), Encoding.UTF8) - }; - request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/cloudevents+json"); + }; + request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/cloudevents+json"); - var response = await httpClient.SendAsync(request); - response.EnsureSuccessStatusCode(); + var response = await httpClient.SendAsync(request); + response.EnsureSuccessStatusCode(); - var user = await response.Content.ReadAsStringAsync(); - user.ShouldBe("jimmy \"the cool guy\" smith"); - } + var user = await response.Content.ReadAsStringAsync(); + user.ShouldBe("jimmy \"the cool guy\" smith"); } + } - // Yeah, I know, binary isn't a great term for this, it's what the cloudevents spec uses. - // Basically this is here to test that an endpoint can handle requests with and without - // an envelope. - [Fact] - public async Task CanSendBinaryCloudEvent_WithContentType() + // Yeah, I know, binary isn't a great term for this, it's what the cloudevents spec uses. + // Basically this is here to test that an endpoint can handle requests with and without + // an envelope. + [Fact] + public async Task CanSendBinaryCloudEvent_WithContentType() + { + using (var factory = new AppWebApplicationFactory()) { - using (var factory = new AppWebApplicationFactory()) - { - var httpClient = factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions { HandleCookies = false }); + var httpClient = factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions { HandleCookies = false }); - var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/register-user") - { - Content = new StringContent( + var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/register-user") + { + Content = new StringContent( JsonSerializer.Serialize( new { name = "jimmy", }), Encoding.UTF8) - }; - request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + }; + request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); - var response = await httpClient.SendAsync(request); - response.EnsureSuccessStatusCode(); + var response = await httpClient.SendAsync(request); + response.EnsureSuccessStatusCode(); - var userInfo = await JsonSerializer.DeserializeAsync(await response.Content.ReadAsStreamAsync(), this.options); - userInfo.Name.ShouldBe("jimmy"); - } + var userInfo = await JsonSerializer.DeserializeAsync(await response.Content.ReadAsStreamAsync(), this.options); + userInfo.Name.ShouldBe("jimmy"); } } -} +} \ No newline at end of file diff --git a/test/Dapr.AspNetCore.IntegrationTest/ControllerIntegrationTest.cs b/test/Dapr.AspNetCore.IntegrationTest/ControllerIntegrationTest.cs index d3eb87cd..8bf7316e 100644 --- a/test/Dapr.AspNetCore.IntegrationTest/ControllerIntegrationTest.cs +++ b/test/Dapr.AspNetCore.IntegrationTest/ControllerIntegrationTest.cs @@ -11,157 +11,156 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.AspNetCore.IntegrationTest +namespace Dapr.AspNetCore.IntegrationTest; + +using System.Net.Http; +using System.Threading.Tasks; +using Dapr.AspNetCore.IntegrationTest.App; +using Shouldly; +using Newtonsoft.Json; +using Xunit; + +public class ControllerIntegrationTest { - using System.Net.Http; - using System.Threading.Tasks; - using Dapr.AspNetCore.IntegrationTest.App; - using Shouldly; - using Newtonsoft.Json; - using Xunit; - - public class ControllerIntegrationTest + [Fact] + public async Task ModelBinder_CanBindFromState() { - [Fact] - public async Task ModelBinder_CanBindFromState() + using (var factory = new AppWebApplicationFactory()) { - using (var factory = new AppWebApplicationFactory()) - { - var httpClient = factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions { HandleCookies = false }); - var daprClient = factory.DaprClient; + var httpClient = factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions { HandleCookies = false }); + var daprClient = factory.DaprClient; - await daprClient.SaveStateAsync("testStore", "test", new Widget() { Size = "small", Count = 17, }); + await daprClient.SaveStateAsync("testStore", "test", new Widget() { Size = "small", Count = 17, }); - var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/controllerwithoutstateentry/test"); - var response = await httpClient.SendAsync(request); - response.EnsureSuccessStatusCode(); + var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/controllerwithoutstateentry/test"); + var response = await httpClient.SendAsync(request); + response.EnsureSuccessStatusCode(); - var widget = await daprClient.GetStateAsync("testStore", "test"); - widget.Count.ShouldBe(18); - } - } - - [Fact] - public async Task ModelBinder_GetFromStateEntryWithKeyPresentInStateStore_ReturnsStateValue() - { - using (var factory = new AppWebApplicationFactory()) - { - var httpClient = factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions { HandleCookies = false }); - var daprClient = factory.DaprClient; - - var widget = new Widget() { Size = "small", Count = 17, }; - await daprClient.SaveStateAsync("testStore", "test", widget); - var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/controllerwithoutstateentry/test"); - var response = await httpClient.SendAsync(request); - response.EnsureSuccessStatusCode(); - var responseContent = await response.Content.ReadAsStringAsync(); - var responseWidget = JsonConvert.DeserializeObject(responseContent); - responseWidget.Size.ShouldBe(widget.Size); - responseWidget.Count.ShouldBe(widget.Count); - } - } - - [Fact] - public async Task ModelBinder_GetFromStateEntryWithKeyNotInStateStore_ReturnsNull() - { - using (var factory = new AppWebApplicationFactory()) - { - var httpClient = factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions { HandleCookies = false }); - - var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/controllerwithoutstateentry/test"); - var response = await httpClient.SendAsync(request); - response.EnsureSuccessStatusCode(); - var responseContent = await response.Content.ReadAsStringAsync(); - var responseWidget = JsonConvert.DeserializeObject(responseContent); - Assert.Null(responseWidget); - } - } - - [Fact] - public async Task ModelBinder_CanBindFromState_WithStateEntry() - { - using (var factory = new AppWebApplicationFactory()) - { - var httpClient = factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions { HandleCookies = false }); - var daprClient = factory.DaprClient; - - await daprClient.SaveStateAsync("testStore", "test", new Widget() { Size = "small", Count = 17, }); - - var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/controllerwithstateentry/test"); - var response = await httpClient.SendAsync(request); - response.EnsureSuccessStatusCode(); - - var widget = await daprClient.GetStateAsync("testStore", "test"); - widget.Count.ShouldBe(18); - } - } - - [Fact] - public async Task ModelBinder_CanBindFromState_WithStateEntryAndCustomKey() - { - using (var factory = new AppWebApplicationFactory()) - { - var httpClient = factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions { HandleCookies = false }); - var daprClient = factory.DaprClient; - - await daprClient.SaveStateAsync("testStore", "test", new Widget() { Size = "small", Count = 17, }); - - var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/controllerwithstateentryandcustomkey/test"); - var response = await httpClient.SendAsync(request); - response.EnsureSuccessStatusCode(); - - var widget = await daprClient.GetStateAsync("testStore", "test"); - widget.Count.ShouldBe(18); - } - } - - [Fact] - public async Task ModelBinder_GetFromStateEntryWithStateEntry_WithKeyPresentInStateStore() - { - using (var factory = new AppWebApplicationFactory()) - { - var httpClient = factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions { HandleCookies = false }); - var daprClient = factory.DaprClient; - - var widget = new Widget() { Size = "small", Count = 17, }; - await daprClient.SaveStateAsync("testStore", "test", widget); - var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/controllerwithstateentry/test"); - var response = await httpClient.SendAsync(request); - response.EnsureSuccessStatusCode(); - var responseContent = await response.Content.ReadAsStringAsync(); - var responseWidget = JsonConvert.DeserializeObject(responseContent); - responseWidget.Size.ShouldBe(widget.Size); - responseWidget.Count.ShouldBe(widget.Count); - } - } - - [Fact] - public async Task ModelBinder_GetFromStateEntryWithStateEntry_WithKeyNotInStateStore() - { - using (var factory = new AppWebApplicationFactory()) - { - var httpClient = factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions { HandleCookies = false }); - - var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/controllerwithstateentry/test"); - var response = await httpClient.SendAsync(request); - response.EnsureSuccessStatusCode(); - var responseContent = await response.Content.ReadAsStringAsync(); - var responseWidget = JsonConvert.DeserializeObject(responseContent); - Assert.Null(responseWidget); - } - } - - [Fact] - public async Task ModelBinder_CanGetOutOfTheWayWhenTheresNoBinding() - { - using (var factory = new AppWebApplicationFactory()) - { - var httpClient = factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions { HandleCookies = false }); - - var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/echo-user?name=jimmy"); - var response = await httpClient.SendAsync(request); - response.EnsureSuccessStatusCode(); - } + var widget = await daprClient.GetStateAsync("testStore", "test"); + widget.Count.ShouldBe(18); } } -} + + [Fact] + public async Task ModelBinder_GetFromStateEntryWithKeyPresentInStateStore_ReturnsStateValue() + { + using (var factory = new AppWebApplicationFactory()) + { + var httpClient = factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions { HandleCookies = false }); + var daprClient = factory.DaprClient; + + var widget = new Widget() { Size = "small", Count = 17, }; + await daprClient.SaveStateAsync("testStore", "test", widget); + var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/controllerwithoutstateentry/test"); + var response = await httpClient.SendAsync(request); + response.EnsureSuccessStatusCode(); + var responseContent = await response.Content.ReadAsStringAsync(); + var responseWidget = JsonConvert.DeserializeObject(responseContent); + responseWidget.Size.ShouldBe(widget.Size); + responseWidget.Count.ShouldBe(widget.Count); + } + } + + [Fact] + public async Task ModelBinder_GetFromStateEntryWithKeyNotInStateStore_ReturnsNull() + { + using (var factory = new AppWebApplicationFactory()) + { + var httpClient = factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions { HandleCookies = false }); + + var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/controllerwithoutstateentry/test"); + var response = await httpClient.SendAsync(request); + response.EnsureSuccessStatusCode(); + var responseContent = await response.Content.ReadAsStringAsync(); + var responseWidget = JsonConvert.DeserializeObject(responseContent); + Assert.Null(responseWidget); + } + } + + [Fact] + public async Task ModelBinder_CanBindFromState_WithStateEntry() + { + using (var factory = new AppWebApplicationFactory()) + { + var httpClient = factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions { HandleCookies = false }); + var daprClient = factory.DaprClient; + + await daprClient.SaveStateAsync("testStore", "test", new Widget() { Size = "small", Count = 17, }); + + var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/controllerwithstateentry/test"); + var response = await httpClient.SendAsync(request); + response.EnsureSuccessStatusCode(); + + var widget = await daprClient.GetStateAsync("testStore", "test"); + widget.Count.ShouldBe(18); + } + } + + [Fact] + public async Task ModelBinder_CanBindFromState_WithStateEntryAndCustomKey() + { + using (var factory = new AppWebApplicationFactory()) + { + var httpClient = factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions { HandleCookies = false }); + var daprClient = factory.DaprClient; + + await daprClient.SaveStateAsync("testStore", "test", new Widget() { Size = "small", Count = 17, }); + + var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/controllerwithstateentryandcustomkey/test"); + var response = await httpClient.SendAsync(request); + response.EnsureSuccessStatusCode(); + + var widget = await daprClient.GetStateAsync("testStore", "test"); + widget.Count.ShouldBe(18); + } + } + + [Fact] + public async Task ModelBinder_GetFromStateEntryWithStateEntry_WithKeyPresentInStateStore() + { + using (var factory = new AppWebApplicationFactory()) + { + var httpClient = factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions { HandleCookies = false }); + var daprClient = factory.DaprClient; + + var widget = new Widget() { Size = "small", Count = 17, }; + await daprClient.SaveStateAsync("testStore", "test", widget); + var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/controllerwithstateentry/test"); + var response = await httpClient.SendAsync(request); + response.EnsureSuccessStatusCode(); + var responseContent = await response.Content.ReadAsStringAsync(); + var responseWidget = JsonConvert.DeserializeObject(responseContent); + responseWidget.Size.ShouldBe(widget.Size); + responseWidget.Count.ShouldBe(widget.Count); + } + } + + [Fact] + public async Task ModelBinder_GetFromStateEntryWithStateEntry_WithKeyNotInStateStore() + { + using (var factory = new AppWebApplicationFactory()) + { + var httpClient = factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions { HandleCookies = false }); + + var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/controllerwithstateentry/test"); + var response = await httpClient.SendAsync(request); + response.EnsureSuccessStatusCode(); + var responseContent = await response.Content.ReadAsStringAsync(); + var responseWidget = JsonConvert.DeserializeObject(responseContent); + Assert.Null(responseWidget); + } + } + + [Fact] + public async Task ModelBinder_CanGetOutOfTheWayWhenTheresNoBinding() + { + using (var factory = new AppWebApplicationFactory()) + { + var httpClient = factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions { HandleCookies = false }); + + var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/echo-user?name=jimmy"); + var response = await httpClient.SendAsync(request); + response.EnsureSuccessStatusCode(); + } + } +} \ No newline at end of file diff --git a/test/Dapr.AspNetCore.IntegrationTest/RoutingIntegrationTest.cs b/test/Dapr.AspNetCore.IntegrationTest/RoutingIntegrationTest.cs index 89130c3b..321302e4 100644 --- a/test/Dapr.AspNetCore.IntegrationTest/RoutingIntegrationTest.cs +++ b/test/Dapr.AspNetCore.IntegrationTest/RoutingIntegrationTest.cs @@ -11,33 +11,32 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.AspNetCore.IntegrationTest +namespace Dapr.AspNetCore.IntegrationTest; + +using System.Net.Http; +using System.Threading.Tasks; +using Dapr.AspNetCore.IntegrationTest.App; +using Shouldly; +using Xunit; + +public class RoutingIntegrationTest { - using System.Net.Http; - using System.Threading.Tasks; - using Dapr.AspNetCore.IntegrationTest.App; - using Shouldly; - using Xunit; - - public class RoutingIntegrationTest + [Fact] + public async Task StateClient_CanBindFromState() { - [Fact] - public async Task StateClient_CanBindFromState() + using (var factory = new AppWebApplicationFactory()) { - using (var factory = new AppWebApplicationFactory()) - { - var httpClient = factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions { HandleCookies = false }); - var daprClient = factory.DaprClient; + var httpClient = factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions { HandleCookies = false }); + var daprClient = factory.DaprClient; - await daprClient.SaveStateAsync("testStore", "test", new Widget() { Size = "small", Count = 17, }); + await daprClient.SaveStateAsync("testStore", "test", new Widget() { Size = "small", Count = 17, }); - var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/routingwithstateentry/test"); - var response = await httpClient.SendAsync(request); - response.EnsureSuccessStatusCode(); + var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/routingwithstateentry/test"); + var response = await httpClient.SendAsync(request); + response.EnsureSuccessStatusCode(); - var widget = await daprClient.GetStateAsync("testStore", "test"); - widget.Count.ShouldBe(18); - } + var widget = await daprClient.GetStateAsync("testStore", "test"); + widget.Count.ShouldBe(18); } } -} +} \ No newline at end of file diff --git a/test/Dapr.AspNetCore.IntegrationTest/StateTestClient.cs b/test/Dapr.AspNetCore.IntegrationTest/StateTestClient.cs index b4940835..cd18226a 100644 --- a/test/Dapr.AspNetCore.IntegrationTest/StateTestClient.cs +++ b/test/Dapr.AspNetCore.IntegrationTest/StateTestClient.cs @@ -11,112 +11,111 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Client +namespace Dapr.Client; + +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Grpc.Net.Client; +using Autogenerated = Dapr.Client.Autogen.Grpc.v1; + +internal class StateTestClient : DaprClientGrpc { - using System; - using System.Collections.Generic; - using System.Net.Http; - using System.Threading; - using System.Threading.Tasks; - using Grpc.Net.Client; - using Autogenerated = Dapr.Client.Autogen.Grpc.v1; + public Dictionary State { get; } = new Dictionary(); + private static readonly GrpcChannel channel = GrpcChannel.ForAddress("http://localhost"); - internal class StateTestClient : DaprClientGrpc + /// + /// Initializes a new instance of the class. + /// + internal StateTestClient() + : base(channel, new Autogenerated.Dapr.DaprClient(channel), new HttpClient(), new Uri("http://localhost"), null, default) { - public Dictionary State { get; } = new Dictionary(); - private static readonly GrpcChannel channel = GrpcChannel.ForAddress("http://localhost"); + } - /// - /// Initializes a new instance of the class. - /// - internal StateTestClient() - : base(channel, new Autogenerated.Dapr.DaprClient(channel), new HttpClient(), new Uri("http://localhost"), null, default) + public override 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)); + + if (this.State.TryGetValue(key, out var obj)) { + return Task.FromResult((TValue)obj); } - - public override Task GetStateAsync(string storeName, string key, ConsistencyMode? consistencyMode = default, IReadOnlyDictionary metadata = default, CancellationToken cancellationToken = default) + else { - ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); - ArgumentVerifier.ThrowIfNullOrEmpty(key, nameof(key)); - - if (this.State.TryGetValue(key, out var obj)) - { - return Task.FromResult((TValue)obj); - } - else - { - return Task.FromResult(default(TValue)); - } - } - - public override Task> GetBulkStateAsync(string storeName, IReadOnlyList keys, int? parallelism, IReadOnlyDictionary metadata = default, CancellationToken cancellationToken = default) - { - ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); - - var response = new List(); - - foreach (var key in keys) - { - if (this.State.TryGetValue(key, out var obj)) - { - response.Add(new BulkStateItem(key, obj.ToString(), "")); - } - else - { - response.Add(new BulkStateItem(key, "", "")); - } - } - - return Task.FromResult>(response); - } - - public override 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)); - - if (this.State.TryGetValue(key, out var obj)) - { - return Task.FromResult(((TValue)obj, "test_etag")); - } - else - { - return Task.FromResult((default(TValue), "test_etag")); - } - } - - public override 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)); - - this.State[key] = value; - return Task.CompletedTask; - } - - public override 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)); - - this.State.Remove(key); - return Task.CompletedTask; + return Task.FromResult(default(TValue)); } } -} + + public override Task> GetBulkStateAsync(string storeName, IReadOnlyList keys, int? parallelism, IReadOnlyDictionary metadata = default, CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); + + var response = new List(); + + foreach (var key in keys) + { + if (this.State.TryGetValue(key, out var obj)) + { + response.Add(new BulkStateItem(key, obj.ToString(), "")); + } + else + { + response.Add(new BulkStateItem(key, "", "")); + } + } + + return Task.FromResult>(response); + } + + public override 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)); + + if (this.State.TryGetValue(key, out var obj)) + { + return Task.FromResult(((TValue)obj, "test_etag")); + } + else + { + return Task.FromResult((default(TValue), "test_etag")); + } + } + + public override 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)); + + this.State[key] = value; + return Task.CompletedTask; + } + + public override 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)); + + this.State.Remove(key); + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/test/Dapr.AspNetCore.IntegrationTest/SubscribeEndpointTest.cs b/test/Dapr.AspNetCore.IntegrationTest/SubscribeEndpointTest.cs index 7354f6aa..e0fdaba6 100644 --- a/test/Dapr.AspNetCore.IntegrationTest/SubscribeEndpointTest.cs +++ b/test/Dapr.AspNetCore.IntegrationTest/SubscribeEndpointTest.cs @@ -11,134 +11,133 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.AspNetCore.IntegrationTest +namespace Dapr.AspNetCore.IntegrationTest; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text.Json; +using System.Threading.Tasks; +using Shouldly; +using Xunit; + +public class SubscribeEndpointTest { - using System; - using System.Collections.Generic; - using System.Linq; - using System.Net.Http; - using System.Text.Json; - using System.Threading.Tasks; - using Shouldly; - using Xunit; - - public class SubscribeEndpointTest + [Fact] + public async Task SubscribeEndpoint_ReportsTopics() { - [Fact] - public async Task SubscribeEndpoint_ReportsTopics() + using (var factory = new AppWebApplicationFactory()) { - using (var factory = new AppWebApplicationFactory()) + var httpClient = factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions { HandleCookies = false }); + + var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/dapr/subscribe"); + var response = await httpClient.SendAsync(request); + response.EnsureSuccessStatusCode(); + + using (var stream = await response.Content.ReadAsStreamAsync()) { - var httpClient = factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions { HandleCookies = false }); + var json = await JsonSerializer.DeserializeAsync(stream); - var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/dapr/subscribe"); - var response = await httpClient.SendAsync(request); - response.EnsureSuccessStatusCode(); + json.ValueKind.ShouldBe(JsonValueKind.Array); + json.GetArrayLength().ShouldBe(18); - using (var stream = await response.Content.ReadAsStreamAsync()) + var subscriptions = new List<(string PubsubName, string Topic, string Route, string rawPayload, + string match, string metadata, string DeadLetterTopic, string bulkSubscribeMetadata)>(); + + foreach (var element in json.EnumerateArray()) { - var json = await JsonSerializer.DeserializeAsync(stream); + var pubsubName = element.GetProperty("pubsubName").GetString(); + var topic = element.GetProperty("topic").GetString(); + var rawPayload = string.Empty; + var deadLetterTopic = string.Empty; + var bulkSubscribeMetadata = string.Empty; + //JsonElement bulkSubscribeMetadata; + Dictionary originalMetadata = new Dictionary(); - json.ValueKind.ShouldBe(JsonValueKind.Array); - json.GetArrayLength().ShouldBe(18); - - var subscriptions = new List<(string PubsubName, string Topic, string Route, string rawPayload, - string match, string metadata, string DeadLetterTopic, string bulkSubscribeMetadata)>(); - - foreach (var element in json.EnumerateArray()) + if (element.TryGetProperty("bulkSubscribe", out var BulkSubscribeMetadata)) { - var pubsubName = element.GetProperty("pubsubName").GetString(); - var topic = element.GetProperty("topic").GetString(); - var rawPayload = string.Empty; - var deadLetterTopic = string.Empty; - var bulkSubscribeMetadata = string.Empty; - //JsonElement bulkSubscribeMetadata; - Dictionary originalMetadata = new Dictionary(); - - if (element.TryGetProperty("bulkSubscribe", out var BulkSubscribeMetadata)) + bulkSubscribeMetadata = BulkSubscribeMetadata.ToString(); + } + if (element.TryGetProperty("deadLetterTopic", out JsonElement DeadLetterTopic)) + { + deadLetterTopic = DeadLetterTopic.GetString(); + } + if (element.TryGetProperty("metadata", out JsonElement metadata)) + { + if (metadata.TryGetProperty("rawPayload", out JsonElement rawPayloadJson)) { - bulkSubscribeMetadata = BulkSubscribeMetadata.ToString(); - } - if (element.TryGetProperty("deadLetterTopic", out JsonElement DeadLetterTopic)) - { - deadLetterTopic = DeadLetterTopic.GetString(); - } - if (element.TryGetProperty("metadata", out JsonElement metadata)) - { - if (metadata.TryGetProperty("rawPayload", out JsonElement rawPayloadJson)) - { - rawPayload = rawPayloadJson.GetString(); - } - - foreach (var originalMetadataProperty in metadata.EnumerateObject().OrderBy(c => c.Name)) - { - if (!originalMetadataProperty.Name.Equals("rawPayload")) - { - originalMetadata.Add(originalMetadataProperty.Name, originalMetadataProperty.Value.GetString()); - } - } - } - var originalMetadataString = string.Empty; - if (originalMetadata.Count > 0) - { - originalMetadataString = string.Join(";", originalMetadata.Select(c => $"{c.Key}={c.Value}")); + rawPayload = rawPayloadJson.GetString(); } - if (element.TryGetProperty("route", out JsonElement route)) + foreach (var originalMetadataProperty in metadata.EnumerateObject().OrderBy(c => c.Name)) { - subscriptions.Add((pubsubName, topic, route.GetString(), rawPayload, string.Empty, - originalMetadataString, deadLetterTopic, bulkSubscribeMetadata)); - } - else if (element.TryGetProperty("routes", out JsonElement routes)) - { - if (routes.TryGetProperty("rules", out JsonElement rules)) + if (!originalMetadataProperty.Name.Equals("rawPayload")) { - foreach (var rule in rules.EnumerateArray()) - { - var match = rule.GetProperty("match").GetString(); - var path = rule.GetProperty("path").GetString(); - subscriptions.Add((pubsubName, topic, path, rawPayload, match, - originalMetadataString, deadLetterTopic, bulkSubscribeMetadata)); - } - } - if (routes.TryGetProperty("default", out JsonElement defaultProperty)) - { - subscriptions.Add((pubsubName, topic, defaultProperty.GetString(), rawPayload, - string.Empty, originalMetadataString, deadLetterTopic, bulkSubscribeMetadata)); + originalMetadata.Add(originalMetadataProperty.Name, originalMetadataProperty.Value.GetString()); } } } + var originalMetadataString = string.Empty; + if (originalMetadata.Count > 0) + { + originalMetadataString = string.Join(";", originalMetadata.Select(c => $"{c.Key}={c.Value}")); + } - subscriptions.ShouldContain(("testpubsub", "A", "topic-a", string.Empty, string.Empty, string.Empty, string.Empty, String.Empty)); - subscriptions.ShouldContain(("testpubsub", "A.1", "topic-a", string.Empty, string.Empty, string.Empty, string.Empty, String.Empty)); - subscriptions.ShouldContain(("pubsub", "B", "B", string.Empty, string.Empty, string.Empty, string.Empty, String.Empty)); - subscriptions.ShouldContain(("custom-pubsub", "custom-C", "C", string.Empty, string.Empty, string.Empty, string.Empty, String.Empty)); - subscriptions.ShouldContain(("pubsub", "register-user", "register-user", string.Empty, string.Empty, string.Empty, string.Empty, String.Empty)); - subscriptions.ShouldContain(("pubsub", "register-user-plaintext", "register-user-plaintext", string.Empty, string.Empty, string.Empty, string.Empty, String.Empty)); - subscriptions.ShouldContain(("pubsub", "D", "D", "true", string.Empty, string.Empty, string.Empty, String.Empty)); - subscriptions.ShouldContain(("pubsub", "E", "E", string.Empty, string.Empty, string.Empty, string.Empty, String.Empty)); - subscriptions.ShouldContain(("pubsub", "E", "E-Critical", string.Empty, "event.type == \"critical\"", string.Empty, string.Empty, String.Empty)); - subscriptions.ShouldContain(("pubsub", "E", "E-Important", string.Empty, "event.type == \"important\"", string.Empty, string.Empty, String.Empty)); - subscriptions.ShouldContain(("pubsub", "F", "multiTopicAttr", string.Empty, string.Empty, string.Empty, string.Empty, - "{\"enabled\":true,\"maxMessagesCount\":100,\"maxAwaitDurationMs\":1000}")); - subscriptions.ShouldContain(("pubsub", "F.1", "multiTopicAttr", "true", string.Empty, string.Empty, string.Empty, String.Empty)); - subscriptions.ShouldContain(("pubsub", "G", "G", string.Empty, string.Empty, string.Empty, "deadLetterTopicName", - "{\"enabled\":true,\"maxMessagesCount\":300,\"maxAwaitDurationMs\":1000}")); - subscriptions.ShouldContain(("pubsub", "splitTopicBuilder", "splitTopics", string.Empty, string.Empty, string.Empty, string.Empty, String.Empty)); - subscriptions.ShouldContain(("pubsub", "splitTopicAttr", "splitTopics", "true", string.Empty, string.Empty, string.Empty, String.Empty)); - subscriptions.ShouldContain(("pubsub", "metadata", "multiMetadataTopicAttr", string.Empty, string.Empty, "n1=v1;n2=v2,v3", string.Empty, String.Empty)); - subscriptions.ShouldContain(("pubsub", "metadata.1", "multiMetadataTopicAttr", "true", string.Empty, "n1=v1", string.Empty, - "{\"enabled\":true,\"maxMessagesCount\":500,\"maxAwaitDurationMs\":2000}")); - subscriptions.ShouldContain(("pubsub", "splitMetadataTopicBuilder", "splitMetadataTopics", string.Empty, string.Empty, "n1=v1;n2=v1", string.Empty, String.Empty)); - subscriptions.ShouldContain(("pubsub", "metadataseparatorbyemptytring", "topicmetadataseparatorattrbyemptytring", string.Empty, string.Empty, "n1=v1,", string.Empty, String.Empty)); - // Test priority route sorting - var eTopic = subscriptions.FindAll(e => e.Topic == "E"); - eTopic.Count.ShouldBe(3); - eTopic[0].Route.ShouldBe("E-Critical"); - eTopic[1].Route.ShouldBe("E-Important"); - eTopic[2].Route.ShouldBe("E"); + if (element.TryGetProperty("route", out JsonElement route)) + { + subscriptions.Add((pubsubName, topic, route.GetString(), rawPayload, string.Empty, + originalMetadataString, deadLetterTopic, bulkSubscribeMetadata)); + } + else if (element.TryGetProperty("routes", out JsonElement routes)) + { + if (routes.TryGetProperty("rules", out JsonElement rules)) + { + foreach (var rule in rules.EnumerateArray()) + { + var match = rule.GetProperty("match").GetString(); + var path = rule.GetProperty("path").GetString(); + subscriptions.Add((pubsubName, topic, path, rawPayload, match, + originalMetadataString, deadLetterTopic, bulkSubscribeMetadata)); + } + } + if (routes.TryGetProperty("default", out JsonElement defaultProperty)) + { + subscriptions.Add((pubsubName, topic, defaultProperty.GetString(), rawPayload, + string.Empty, originalMetadataString, deadLetterTopic, bulkSubscribeMetadata)); + } + } } + + subscriptions.ShouldContain(("testpubsub", "A", "topic-a", string.Empty, string.Empty, string.Empty, string.Empty, String.Empty)); + subscriptions.ShouldContain(("testpubsub", "A.1", "topic-a", string.Empty, string.Empty, string.Empty, string.Empty, String.Empty)); + subscriptions.ShouldContain(("pubsub", "B", "B", string.Empty, string.Empty, string.Empty, string.Empty, String.Empty)); + subscriptions.ShouldContain(("custom-pubsub", "custom-C", "C", string.Empty, string.Empty, string.Empty, string.Empty, String.Empty)); + subscriptions.ShouldContain(("pubsub", "register-user", "register-user", string.Empty, string.Empty, string.Empty, string.Empty, String.Empty)); + subscriptions.ShouldContain(("pubsub", "register-user-plaintext", "register-user-plaintext", string.Empty, string.Empty, string.Empty, string.Empty, String.Empty)); + subscriptions.ShouldContain(("pubsub", "D", "D", "true", string.Empty, string.Empty, string.Empty, String.Empty)); + subscriptions.ShouldContain(("pubsub", "E", "E", string.Empty, string.Empty, string.Empty, string.Empty, String.Empty)); + subscriptions.ShouldContain(("pubsub", "E", "E-Critical", string.Empty, "event.type == \"critical\"", string.Empty, string.Empty, String.Empty)); + subscriptions.ShouldContain(("pubsub", "E", "E-Important", string.Empty, "event.type == \"important\"", string.Empty, string.Empty, String.Empty)); + subscriptions.ShouldContain(("pubsub", "F", "multiTopicAttr", string.Empty, string.Empty, string.Empty, string.Empty, + "{\"enabled\":true,\"maxMessagesCount\":100,\"maxAwaitDurationMs\":1000}")); + subscriptions.ShouldContain(("pubsub", "F.1", "multiTopicAttr", "true", string.Empty, string.Empty, string.Empty, String.Empty)); + subscriptions.ShouldContain(("pubsub", "G", "G", string.Empty, string.Empty, string.Empty, "deadLetterTopicName", + "{\"enabled\":true,\"maxMessagesCount\":300,\"maxAwaitDurationMs\":1000}")); + subscriptions.ShouldContain(("pubsub", "splitTopicBuilder", "splitTopics", string.Empty, string.Empty, string.Empty, string.Empty, String.Empty)); + subscriptions.ShouldContain(("pubsub", "splitTopicAttr", "splitTopics", "true", string.Empty, string.Empty, string.Empty, String.Empty)); + subscriptions.ShouldContain(("pubsub", "metadata", "multiMetadataTopicAttr", string.Empty, string.Empty, "n1=v1;n2=v2,v3", string.Empty, String.Empty)); + subscriptions.ShouldContain(("pubsub", "metadata.1", "multiMetadataTopicAttr", "true", string.Empty, "n1=v1", string.Empty, + "{\"enabled\":true,\"maxMessagesCount\":500,\"maxAwaitDurationMs\":2000}")); + subscriptions.ShouldContain(("pubsub", "splitMetadataTopicBuilder", "splitMetadataTopics", string.Empty, string.Empty, "n1=v1;n2=v1", string.Empty, String.Empty)); + subscriptions.ShouldContain(("pubsub", "metadataseparatorbyemptytring", "topicmetadataseparatorattrbyemptytring", string.Empty, string.Empty, "n1=v1,", string.Empty, String.Empty)); + // Test priority route sorting + var eTopic = subscriptions.FindAll(e => e.Topic == "E"); + eTopic.Count.ShouldBe(3); + eTopic[0].Route.ShouldBe("E-Critical"); + eTopic[1].Route.ShouldBe("E-Important"); + eTopic[2].Route.ShouldBe("E"); } } } -} +} \ No newline at end of file diff --git a/test/Dapr.AspNetCore.Test/CloudEventsMiddlewareTest.cs b/test/Dapr.AspNetCore.Test/CloudEventsMiddlewareTest.cs index ae8db689..54e099be 100644 --- a/test/Dapr.AspNetCore.Test/CloudEventsMiddlewareTest.cs +++ b/test/Dapr.AspNetCore.Test/CloudEventsMiddlewareTest.cs @@ -13,470 +13,469 @@ using Microsoft.Extensions.DependencyInjection; -namespace Dapr.AspNetCore.Test +namespace Dapr.AspNetCore.Test; + +using System.IO; +using System.Net; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using Shouldly; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Xunit; + +public class CloudEventsMiddlewareTest { - using System.IO; - using System.Net; - using System.Text; - using System.Text.Json; - using System.Threading.Tasks; - using Shouldly; - using Microsoft.AspNetCore.Builder; - using Microsoft.AspNetCore.Http; - using Xunit; - - public class CloudEventsMiddlewareTest + [Theory] + [InlineData("text/plain")] + [InlineData("application/json")] // "binary" format + [InlineData("application/cloudevents")] // no format + [InlineData("application/cloudevents+xml")] // wrong format + [InlineData("application/cloudevents-batch+json")] // we don't support batch + public async Task InvokeAsync_IgnoresOtherContentTypes(string contentType) { - [Theory] - [InlineData("text/plain")] - [InlineData("application/json")] // "binary" format - [InlineData("application/cloudevents")] // no format - [InlineData("application/cloudevents+xml")] // wrong format - [InlineData("application/cloudevents-batch+json")] // we don't support batch - public async Task InvokeAsync_IgnoresOtherContentTypes(string contentType) - { - var serviceCollection = new ServiceCollection(); - var provider = serviceCollection.BuildServiceProvider(); + var serviceCollection = new ServiceCollection(); + var provider = serviceCollection.BuildServiceProvider(); - var app = new ApplicationBuilder(provider); - app.UseCloudEvents(); + var app = new ApplicationBuilder(provider); + app.UseCloudEvents(); - // Do verification in the scope of the middleware - app.Run(httpContext => - { - httpContext.Request.ContentType.ShouldBe(contentType); - ReadBody(httpContext.Request.Body).ShouldBe("Hello, world!"); - return Task.CompletedTask; - }); - - var pipeline = app.Build(); - - var context = new DefaultHttpContext - { - Request = { ContentType = contentType, Body = MakeBody("Hello, world!") } - }; - - await pipeline.Invoke(context); - } - - [Theory] - [InlineData(null, null)] // assumes application/json + utf8 - [InlineData("application/json", null)] // assumes utf8 - [InlineData("application/json", "utf-8")] - [InlineData("application/json", "UTF-8")] - [InlineData("application/person+json", "UTF-16")] // arbitrary content type and charset - public async Task InvokeAsync_ReplacesBodyJson(string dataContentType, string charSet) + // Do verification in the scope of the middleware + app.Run(httpContext => { - var encoding = charSet == null ? null : Encoding.GetEncoding(charSet); - var serviceCollection = new ServiceCollection(); - var provider = serviceCollection.BuildServiceProvider(); + httpContext.Request.ContentType.ShouldBe(contentType); + ReadBody(httpContext.Request.Body).ShouldBe("Hello, world!"); + return Task.CompletedTask; + }); + + var pipeline = app.Build(); + + var context = new DefaultHttpContext + { + Request = { ContentType = contentType, Body = MakeBody("Hello, world!") } + }; + + await pipeline.Invoke(context); + } + + [Theory] + [InlineData(null, null)] // assumes application/json + utf8 + [InlineData("application/json", null)] // assumes utf8 + [InlineData("application/json", "utf-8")] + [InlineData("application/json", "UTF-8")] + [InlineData("application/person+json", "UTF-16")] // arbitrary content type and charset + public async Task InvokeAsync_ReplacesBodyJson(string dataContentType, string charSet) + { + var encoding = charSet == null ? null : Encoding.GetEncoding(charSet); + var serviceCollection = new ServiceCollection(); + var provider = serviceCollection.BuildServiceProvider(); - var app = new ApplicationBuilder(provider); - app.UseCloudEvents(); + var app = new ApplicationBuilder(provider); + app.UseCloudEvents(); - // Do verification in the scope of the middleware - app.Run(httpContext => + // Do verification in the scope of the middleware + app.Run(httpContext => + { + httpContext.Request.ContentType.ShouldBe(dataContentType ?? "application/json"); + ReadBody(httpContext.Request.Body).ShouldBe("{\"name\":\"jimmy\"}"); + return Task.CompletedTask; + }); + + var pipeline = app.Build(); + + var context = new DefaultHttpContext { Request = { - httpContext.Request.ContentType.ShouldBe(dataContentType ?? "application/json"); - ReadBody(httpContext.Request.Body).ShouldBe("{\"name\":\"jimmy\"}"); - return Task.CompletedTask; - }); + ContentType = + charSet == null + ? "application/cloudevents+json" + : $"application/cloudevents+json;charset={charSet}", + Body = dataContentType == null ? + MakeBody("{ \"data\": { \"name\":\"jimmy\" } }", encoding) : + MakeBody($"{{ \"datacontenttype\": \"{dataContentType}\", \"data\": {{ \"name\":\"jimmy\" }} }}", encoding) + } + }; - var pipeline = app.Build(); - - var context = new DefaultHttpContext { Request = - { - ContentType = - charSet == null - ? "application/cloudevents+json" - : $"application/cloudevents+json;charset={charSet}", - Body = dataContentType == null ? - MakeBody("{ \"data\": { \"name\":\"jimmy\" } }", encoding) : - MakeBody($"{{ \"datacontenttype\": \"{dataContentType}\", \"data\": {{ \"name\":\"jimmy\" }} }}", encoding) - } - }; - - await pipeline.Invoke(context); - } + await pipeline.Invoke(context); + } - [Theory] - [InlineData(null, null)] // assumes application/json + utf8 - [InlineData("application/json", null)] // assumes utf8 - [InlineData("application/json", "utf-8")] - [InlineData("application/json", "UTF-8")] - [InlineData("application/person+json", "UTF-16")] // arbitrary content type and charset - public async Task InvokeAsync_ReplacesPascalCasedBodyJson(string dataContentType, string charSet) - { - var encoding = charSet == null ? null : Encoding.GetEncoding(charSet); - var serviceCollection = new ServiceCollection(); - var provider = serviceCollection.BuildServiceProvider(); + [Theory] + [InlineData(null, null)] // assumes application/json + utf8 + [InlineData("application/json", null)] // assumes utf8 + [InlineData("application/json", "utf-8")] + [InlineData("application/json", "UTF-8")] + [InlineData("application/person+json", "UTF-16")] // arbitrary content type and charset + public async Task InvokeAsync_ReplacesPascalCasedBodyJson(string dataContentType, string charSet) + { + var encoding = charSet == null ? null : Encoding.GetEncoding(charSet); + var serviceCollection = new ServiceCollection(); + var provider = serviceCollection.BuildServiceProvider(); - var app = new ApplicationBuilder(provider); - app.UseCloudEvents(); + var app = new ApplicationBuilder(provider); + app.UseCloudEvents(); - // Do verification in the scope of the middleware - app.Run(httpContext => + // Do verification in the scope of the middleware + app.Run(httpContext => + { + httpContext.Request.ContentType.ShouldBe(dataContentType ?? "application/json"); + ReadBody(httpContext.Request.Body).ShouldBe("{\"name\":\"jimmy\"}"); + return Task.CompletedTask; + }); + + var pipeline = app.Build(); + + var context = new DefaultHttpContext { Request = { - httpContext.Request.ContentType.ShouldBe(dataContentType ?? "application/json"); - ReadBody(httpContext.Request.Body).ShouldBe("{\"name\":\"jimmy\"}"); - return Task.CompletedTask; - }); + ContentType = + charSet == null + ? "application/cloudevents+json" + : $"application/cloudevents+json;charset={charSet}", + Body = dataContentType == null ? + MakeBody("{ \"Data\": { \"name\":\"jimmy\" } }", encoding) : + MakeBody($"{{ \"DataContentType\": \"{dataContentType}\", \"Data\": {{ \"name\":\"jimmy\" }} }}", encoding) + } + }; - var pipeline = app.Build(); - - var context = new DefaultHttpContext { Request = - { - ContentType = - charSet == null - ? "application/cloudevents+json" - : $"application/cloudevents+json;charset={charSet}", - Body = dataContentType == null ? - MakeBody("{ \"Data\": { \"name\":\"jimmy\" } }", encoding) : - MakeBody($"{{ \"DataContentType\": \"{dataContentType}\", \"Data\": {{ \"name\":\"jimmy\" }} }}", encoding) - } - }; - - await pipeline.Invoke(context); - } + await pipeline.Invoke(context); + } - [Theory] - [InlineData(null, null)] // assumes application/json + utf8 - [InlineData("application/json", null)] // assumes utf8 - [InlineData("application/json", "utf-8")] - [InlineData("application/json", "UTF-8")] - [InlineData("application/person+json", "UTF-16")] // arbitrary content type and charset - public async Task InvokeAsync_ForwardsJsonPropertiesAsHeaders(string dataContentType, string charSet) - { - var encoding = charSet == null ? null : Encoding.GetEncoding(charSet); - var serviceCollection = new ServiceCollection(); - var provider = serviceCollection.BuildServiceProvider(); + [Theory] + [InlineData(null, null)] // assumes application/json + utf8 + [InlineData("application/json", null)] // assumes utf8 + [InlineData("application/json", "utf-8")] + [InlineData("application/json", "UTF-8")] + [InlineData("application/person+json", "UTF-16")] // arbitrary content type and charset + public async Task InvokeAsync_ForwardsJsonPropertiesAsHeaders(string dataContentType, string charSet) + { + var encoding = charSet == null ? null : Encoding.GetEncoding(charSet); + var serviceCollection = new ServiceCollection(); + var provider = serviceCollection.BuildServiceProvider(); - var app = new ApplicationBuilder(provider); - app.UseCloudEvents(new CloudEventsMiddlewareOptions + var app = new ApplicationBuilder(provider); + app.UseCloudEvents(new CloudEventsMiddlewareOptions + { + ForwardCloudEventPropertiesAsHeaders = true + }); + + // Do verification in the scope of the middleware + app.Run(httpContext => + { + httpContext.Request.ContentType.ShouldBe(dataContentType ?? "application/json"); + ReadBody(httpContext.Request.Body).ShouldBe("{\"name\":\"jimmy\"}"); + + httpContext.Request.Headers.ShouldContainKey("Cloudevent.type"); + httpContext.Request.Headers["Cloudevent.type"].ToString().ShouldBe("Test.Type"); + httpContext.Request.Headers.ShouldContainKey("Cloudevent.subject"); + httpContext.Request.Headers["Cloudevent.subject"].ToString().ShouldBe("Test.Subject"); + return Task.CompletedTask; + }); + + var pipeline = app.Build(); + + var context = new DefaultHttpContext { Request = { - ForwardCloudEventPropertiesAsHeaders = true - }); + ContentType = + charSet == null + ? "application/cloudevents+json" + : $"application/cloudevents+json;charset={charSet}", + Body = dataContentType == null ? + MakeBody("{ \"type\": \"Test.Type\", \"subject\": \"Test.Subject\", \"data\": { \"name\":\"jimmy\" } }", encoding) : + MakeBody($"{{ \"datacontenttype\": \"{dataContentType}\", \"type\":\"Test.Type\", \"subject\": \"Test.Subject\", \"data\": {{ \"name\":\"jimmy\" }} }}", encoding) + } + }; - // Do verification in the scope of the middleware - app.Run(httpContext => - { - httpContext.Request.ContentType.ShouldBe(dataContentType ?? "application/json"); - ReadBody(httpContext.Request.Body).ShouldBe("{\"name\":\"jimmy\"}"); - - httpContext.Request.Headers.ShouldContainKey("Cloudevent.type"); - httpContext.Request.Headers["Cloudevent.type"].ToString().ShouldBe("Test.Type"); - httpContext.Request.Headers.ShouldContainKey("Cloudevent.subject"); - httpContext.Request.Headers["Cloudevent.subject"].ToString().ShouldBe("Test.Subject"); - return Task.CompletedTask; - }); - - var pipeline = app.Build(); - - var context = new DefaultHttpContext { Request = - { - ContentType = - charSet == null - ? "application/cloudevents+json" - : $"application/cloudevents+json;charset={charSet}", - Body = dataContentType == null ? - MakeBody("{ \"type\": \"Test.Type\", \"subject\": \"Test.Subject\", \"data\": { \"name\":\"jimmy\" } }", encoding) : - MakeBody($"{{ \"datacontenttype\": \"{dataContentType}\", \"type\":\"Test.Type\", \"subject\": \"Test.Subject\", \"data\": {{ \"name\":\"jimmy\" }} }}", encoding) - } - }; - - await pipeline.Invoke(context); - } + await pipeline.Invoke(context); + } - [Theory] - [InlineData(null, null)] // assumes application/json + utf8 - [InlineData("application/json", null)] // assumes utf8 - [InlineData("application/json", "utf-8")] - [InlineData("application/json", "UTF-8")] - [InlineData("application/person+json", "UTF-16")] // arbitrary content type and charset - public async Task InvokeAsync_ForwardsIncludedJsonPropertiesAsHeaders(string dataContentType, string charSet) - { - var encoding = charSet == null ? null : Encoding.GetEncoding(charSet); - var serviceCollection = new ServiceCollection(); - var provider = serviceCollection.BuildServiceProvider(); + [Theory] + [InlineData(null, null)] // assumes application/json + utf8 + [InlineData("application/json", null)] // assumes utf8 + [InlineData("application/json", "utf-8")] + [InlineData("application/json", "UTF-8")] + [InlineData("application/person+json", "UTF-16")] // arbitrary content type and charset + public async Task InvokeAsync_ForwardsIncludedJsonPropertiesAsHeaders(string dataContentType, string charSet) + { + var encoding = charSet == null ? null : Encoding.GetEncoding(charSet); + var serviceCollection = new ServiceCollection(); + var provider = serviceCollection.BuildServiceProvider(); - var app = new ApplicationBuilder(provider); - app.UseCloudEvents(new CloudEventsMiddlewareOptions + var app = new ApplicationBuilder(provider); + app.UseCloudEvents(new CloudEventsMiddlewareOptions + { + ForwardCloudEventPropertiesAsHeaders = true, + IncludedCloudEventPropertiesAsHeaders = new []{"type"} + }); + + // Do verification in the scope of the middleware + app.Run(httpContext => + { + httpContext.Request.ContentType.ShouldBe(dataContentType ?? "application/json"); + ReadBody(httpContext.Request.Body).ShouldBe("{\"name\":\"jimmy\"}"); + + httpContext.Request.Headers.ShouldContainKey("Cloudevent.type"); + httpContext.Request.Headers["Cloudevent.type"].ToString().ShouldBe("Test.Type"); + httpContext.Request.Headers.ShouldNotContainKey("Cloudevent.subject"); + return Task.CompletedTask; + }); + + var pipeline = app.Build(); + + var context = new DefaultHttpContext { Request = { - ForwardCloudEventPropertiesAsHeaders = true, - IncludedCloudEventPropertiesAsHeaders = new []{"type"} - }); + ContentType = + charSet == null + ? "application/cloudevents+json" + : $"application/cloudevents+json;charset={charSet}", + Body = dataContentType == null ? + MakeBody("{ \"type\": \"Test.Type\", \"subject\": \"Test.Subject\", \"data\": { \"name\":\"jimmy\" } }", encoding) : + MakeBody($"{{ \"datacontenttype\": \"{dataContentType}\", \"type\":\"Test.Type\", \"subject\": \"Test.Subject\", \"data\": {{ \"name\":\"jimmy\" }} }}", encoding) + } + }; - // Do verification in the scope of the middleware - app.Run(httpContext => - { - httpContext.Request.ContentType.ShouldBe(dataContentType ?? "application/json"); - ReadBody(httpContext.Request.Body).ShouldBe("{\"name\":\"jimmy\"}"); - - httpContext.Request.Headers.ShouldContainKey("Cloudevent.type"); - httpContext.Request.Headers["Cloudevent.type"].ToString().ShouldBe("Test.Type"); - httpContext.Request.Headers.ShouldNotContainKey("Cloudevent.subject"); - return Task.CompletedTask; - }); - - var pipeline = app.Build(); - - var context = new DefaultHttpContext { Request = - { - ContentType = - charSet == null - ? "application/cloudevents+json" - : $"application/cloudevents+json;charset={charSet}", - Body = dataContentType == null ? - MakeBody("{ \"type\": \"Test.Type\", \"subject\": \"Test.Subject\", \"data\": { \"name\":\"jimmy\" } }", encoding) : - MakeBody($"{{ \"datacontenttype\": \"{dataContentType}\", \"type\":\"Test.Type\", \"subject\": \"Test.Subject\", \"data\": {{ \"name\":\"jimmy\" }} }}", encoding) - } - }; - - await pipeline.Invoke(context); - } + await pipeline.Invoke(context); + } - [Theory] - [InlineData(null, null)] // assumes application/json + utf8 - [InlineData("application/json", null)] // assumes utf8 - [InlineData("application/json", "utf-8")] - [InlineData("application/json", "UTF-8")] - [InlineData("application/person+json", "UTF-16")] // arbitrary content type and charset - public async Task InvokeAsync_DoesNotForwardExcludedJsonPropertiesAsHeaders(string dataContentType, string charSet) - { - var encoding = charSet == null ? null : Encoding.GetEncoding(charSet); - var serviceCollection = new ServiceCollection(); - var provider = serviceCollection.BuildServiceProvider(); + [Theory] + [InlineData(null, null)] // assumes application/json + utf8 + [InlineData("application/json", null)] // assumes utf8 + [InlineData("application/json", "utf-8")] + [InlineData("application/json", "UTF-8")] + [InlineData("application/person+json", "UTF-16")] // arbitrary content type and charset + public async Task InvokeAsync_DoesNotForwardExcludedJsonPropertiesAsHeaders(string dataContentType, string charSet) + { + var encoding = charSet == null ? null : Encoding.GetEncoding(charSet); + var serviceCollection = new ServiceCollection(); + var provider = serviceCollection.BuildServiceProvider(); - var app = new ApplicationBuilder(provider); - app.UseCloudEvents(new CloudEventsMiddlewareOptions + var app = new ApplicationBuilder(provider); + app.UseCloudEvents(new CloudEventsMiddlewareOptions + { + ForwardCloudEventPropertiesAsHeaders = true, + ExcludedCloudEventPropertiesFromHeaders = new []{"type"} + }); + + // Do verification in the scope of the middleware + app.Run(httpContext => + { + httpContext.Request.ContentType.ShouldBe(dataContentType ?? "application/json"); + ReadBody(httpContext.Request.Body).ShouldBe("{\"name\":\"jimmy\"}"); + + httpContext.Request.Headers.ShouldNotContainKey("Cloudevent.type"); + httpContext.Request.Headers.ShouldContainKey("Cloudevent.subject"); + httpContext.Request.Headers["Cloudevent.subject"].ToString().ShouldBe("Test.Subject"); + return Task.CompletedTask; + }); + + var pipeline = app.Build(); + + var context = new DefaultHttpContext { Request = { - ForwardCloudEventPropertiesAsHeaders = true, - ExcludedCloudEventPropertiesFromHeaders = new []{"type"} - }); + ContentType = + charSet == null + ? "application/cloudevents+json" + : $"application/cloudevents+json;charset={charSet}", + Body = dataContentType == null ? + MakeBody("{ \"type\": \"Test.Type\", \"subject\": \"Test.Subject\", \"data\": { \"name\":\"jimmy\" } }", encoding) : + MakeBody($"{{ \"datacontenttype\": \"{dataContentType}\", \"type\":\"Test.Type\", \"subject\": \"Test.Subject\", \"data\": {{ \"name\":\"jimmy\" }} }}", encoding) + } + }; - // Do verification in the scope of the middleware - app.Run(httpContext => - { - httpContext.Request.ContentType.ShouldBe(dataContentType ?? "application/json"); - ReadBody(httpContext.Request.Body).ShouldBe("{\"name\":\"jimmy\"}"); - - httpContext.Request.Headers.ShouldNotContainKey("Cloudevent.type"); - httpContext.Request.Headers.ShouldContainKey("Cloudevent.subject"); - httpContext.Request.Headers["Cloudevent.subject"].ToString().ShouldBe("Test.Subject"); - return Task.CompletedTask; - }); - - var pipeline = app.Build(); - - var context = new DefaultHttpContext { Request = - { - ContentType = - charSet == null - ? "application/cloudevents+json" - : $"application/cloudevents+json;charset={charSet}", - Body = dataContentType == null ? - MakeBody("{ \"type\": \"Test.Type\", \"subject\": \"Test.Subject\", \"data\": { \"name\":\"jimmy\" } }", encoding) : - MakeBody($"{{ \"datacontenttype\": \"{dataContentType}\", \"type\":\"Test.Type\", \"subject\": \"Test.Subject\", \"data\": {{ \"name\":\"jimmy\" }} }}", encoding) - } - }; - - await pipeline.Invoke(context); - } + await pipeline.Invoke(context); + } - [Fact] - public async Task InvokeAsync_ReplacesBodyNonJsonData() + [Fact] + public async Task InvokeAsync_ReplacesBodyNonJsonData() + { + // Our logic is based on the content-type, not the content. + // Since this is for text-plain content, we're going to encode it as a JSON string + // and store it in the data attribute - the middleware should JSON-decode it. + const string input = "{ \"message\": \"hello, world\"}"; + var expected = input; + + var serviceCollection = new ServiceCollection(); + var provider = serviceCollection.BuildServiceProvider(); + + var app = new ApplicationBuilder(provider); + app.UseCloudEvents(); + + // Do verification in the scope of the middleware + app.Run(httpContext => { - // Our logic is based on the content-type, not the content. - // Since this is for text-plain content, we're going to encode it as a JSON string - // and store it in the data attribute - the middleware should JSON-decode it. - const string input = "{ \"message\": \"hello, world\"}"; - var expected = input; + httpContext.Request.ContentType.ShouldBe("text/plain"); + ReadBody(httpContext.Request.Body).ShouldBe(expected); + return Task.CompletedTask; + }); - var serviceCollection = new ServiceCollection(); - var provider = serviceCollection.BuildServiceProvider(); - var app = new ApplicationBuilder(provider); - app.UseCloudEvents(); + var pipeline = app.Build(); - // Do verification in the scope of the middleware - app.Run(httpContext => + var context = new DefaultHttpContext { Request = { - httpContext.Request.ContentType.ShouldBe("text/plain"); - ReadBody(httpContext.Request.Body).ShouldBe(expected); - return Task.CompletedTask; - }); + ContentType = "application/cloudevents+json", + Body = MakeBody($"{{ \"datacontenttype\": \"text/plain\", \"data\": {JsonSerializer.Serialize(input)} }}") + } + }; + await pipeline.Invoke(context); + } + + [Fact] + public async Task InvokeAsync_ReplacesBodyNonJsonData_ExceptWhenSuppressed() + { + // Our logic is based on the content-type, not the content. This test tests the old bad behavior. + const string input = "{ \"message\": \"hello, world\"}"; + var expected = JsonSerializer.Serialize(input); + + var serviceCollection = new ServiceCollection(); + var provider = serviceCollection.BuildServiceProvider(); - var pipeline = app.Build(); + var app = new ApplicationBuilder(provider); + app.UseCloudEvents(new CloudEventsMiddlewareOptions() { SuppressJsonDecodingOfTextPayloads = true, }); - var context = new DefaultHttpContext { Request = - { - ContentType = "application/cloudevents+json", - Body = MakeBody($"{{ \"datacontenttype\": \"text/plain\", \"data\": {JsonSerializer.Serialize(input)} }}") - } - }; - - await pipeline.Invoke(context); - } - - [Fact] - public async Task InvokeAsync_ReplacesBodyNonJsonData_ExceptWhenSuppressed() + // Do verification in the scope of the middleware + app.Run(httpContext => { - // Our logic is based on the content-type, not the content. This test tests the old bad behavior. - const string input = "{ \"message\": \"hello, world\"}"; - var expected = JsonSerializer.Serialize(input); + httpContext.Request.ContentType.ShouldBe("text/plain"); + ReadBody(httpContext.Request.Body).ShouldBe(expected); + return Task.CompletedTask; + }); - var serviceCollection = new ServiceCollection(); - var provider = serviceCollection.BuildServiceProvider(); - var app = new ApplicationBuilder(provider); - app.UseCloudEvents(new CloudEventsMiddlewareOptions() { SuppressJsonDecodingOfTextPayloads = true, }); + var pipeline = app.Build(); - // Do verification in the scope of the middleware - app.Run(httpContext => + var context = new DefaultHttpContext { Request = { - httpContext.Request.ContentType.ShouldBe("text/plain"); - ReadBody(httpContext.Request.Body).ShouldBe(expected); - return Task.CompletedTask; - }); + ContentType = "application/cloudevents+json", + Body = MakeBody($"{{ \"datacontenttype\": \"text/plain\", \"data\": {JsonSerializer.Serialize(input)} }}") + } + }; + await pipeline.Invoke(context); + } + + // This is a special case. S.T.Json will always output utf8, so we have to reinterpret the charset + // of the datacontenttype. + [Fact] + public async Task InvokeAsync_ReplacesBodyJson_NormalizesPayloadCharset() + { + const string dataContentType = "application/person+json;charset=UTF-16"; + const string charSet = "UTF-16"; + var encoding = Encoding.GetEncoding(charSet); + var serviceCollection = new ServiceCollection(); + var provider = serviceCollection.BuildServiceProvider(); - var pipeline = app.Build(); + var app = new ApplicationBuilder(provider); + app.UseCloudEvents(); - var context = new DefaultHttpContext { Request = - { - ContentType = "application/cloudevents+json", - Body = MakeBody($"{{ \"datacontenttype\": \"text/plain\", \"data\": {JsonSerializer.Serialize(input)} }}") - } - }; - - await pipeline.Invoke(context); - } - - // This is a special case. S.T.Json will always output utf8, so we have to reinterpret the charset - // of the datacontenttype. - [Fact] - public async Task InvokeAsync_ReplacesBodyJson_NormalizesPayloadCharset() + // Do verification in the scope of the middleware + app.Run(httpContext => { - const string dataContentType = "application/person+json;charset=UTF-16"; - const string charSet = "UTF-16"; - var encoding = Encoding.GetEncoding(charSet); - var serviceCollection = new ServiceCollection(); - var provider = serviceCollection.BuildServiceProvider(); - - var app = new ApplicationBuilder(provider); - app.UseCloudEvents(); + httpContext.Request.ContentType.ShouldBe("application/person+json"); + ReadBody(httpContext.Request.Body).ShouldBe("{\"name\":\"jimmy\"}"); + return Task.CompletedTask; + }); - // Do verification in the scope of the middleware - app.Run(httpContext => + var pipeline = app.Build(); + + var context = new DefaultHttpContext { Request = { - httpContext.Request.ContentType.ShouldBe("application/person+json"); - ReadBody(httpContext.Request.Body).ShouldBe("{\"name\":\"jimmy\"}"); - return Task.CompletedTask; - }); + ContentType = $"application/cloudevents+json;charset={charSet}", Body = MakeBody($"{{ \"datacontenttype\": \"{dataContentType}\", \"data\": {{ \"name\":\"jimmy\" }} }}", encoding) + } + }; - var pipeline = app.Build(); + await pipeline.Invoke(context); + } - var context = new DefaultHttpContext { Request = - { - ContentType = $"application/cloudevents+json;charset={charSet}", Body = MakeBody($"{{ \"datacontenttype\": \"{dataContentType}\", \"data\": {{ \"name\":\"jimmy\" }} }}", encoding) - } - }; + [Fact] + public async Task InvokeAsync_ReadsBinaryData() + { + const string dataContentType = "application/octet-stream"; + var serviceCollection = new ServiceCollection(); + var provider = serviceCollection.BuildServiceProvider(); + + var app = new ApplicationBuilder(provider); + app.UseCloudEvents(); + var data = new byte[] { 1, 2, 3 }; - await pipeline.Invoke(context); - } - - [Fact] - public async Task InvokeAsync_ReadsBinaryData() + // Do verification in the scope of the middleware + app.Run(httpContext => { - const string dataContentType = "application/octet-stream"; - var serviceCollection = new ServiceCollection(); - var provider = serviceCollection.BuildServiceProvider(); - - var app = new ApplicationBuilder(provider); - app.UseCloudEvents(); - var data = new byte[] { 1, 2, 3 }; - - // Do verification in the scope of the middleware - app.Run(httpContext => - { - httpContext.Request.ContentType.ShouldBe(dataContentType); - var bytes = new byte[httpContext.Request.Body.Length]; + httpContext.Request.ContentType.ShouldBe(dataContentType); + var bytes = new byte[httpContext.Request.Body.Length]; #if NET9_0 - httpContext.Request.Body.ReadExactly(bytes, 0, bytes.Length); + httpContext.Request.Body.ReadExactly(bytes, 0, bytes.Length); #else httpContext.Request.Body.Read(bytes, 0, bytes.Length); #endif - bytes.ShouldBe(data); - return Task.CompletedTask; - }); + bytes.ShouldBe(data); + return Task.CompletedTask; + }); - var pipeline = app.Build(); + var pipeline = app.Build(); - var context = new DefaultHttpContext { Request = { ContentType = "application/cloudevents+json" } }; - var base64Str = System.Convert.ToBase64String(data); + var context = new DefaultHttpContext { Request = { ContentType = "application/cloudevents+json" } }; + var base64Str = System.Convert.ToBase64String(data); - context.Request.Body = - MakeBody($"{{ \"datacontenttype\": \"{dataContentType}\", \"data_base64\": \"{base64Str}\"}}"); + context.Request.Body = + MakeBody($"{{ \"datacontenttype\": \"{dataContentType}\", \"data_base64\": \"{base64Str}\"}}"); - await pipeline.Invoke(context); - } + await pipeline.Invoke(context); + } - [Fact] - public async Task InvokeAsync_DataAndData64Set_ReturnsBadRequest() - { - const string dataContentType = "application/octet-stream"; - var serviceCollection = new ServiceCollection(); - var provider = serviceCollection.BuildServiceProvider(); + [Fact] + public async Task InvokeAsync_DataAndData64Set_ReturnsBadRequest() + { + const string dataContentType = "application/octet-stream"; + var serviceCollection = new ServiceCollection(); + var provider = serviceCollection.BuildServiceProvider(); - var app = new ApplicationBuilder(provider); - app.UseCloudEvents(); - const string data = "{\"id\": \"1\"}"; + var app = new ApplicationBuilder(provider); + app.UseCloudEvents(); + const string data = "{\"id\": \"1\"}"; - // Do verification in the scope of the middleware - app.Run(httpContext => - { - httpContext.Request.ContentType.ShouldBe("application/json"); - var body = ReadBody(httpContext.Request.Body); - body.ShouldBe(data); - return Task.CompletedTask; - }); - - var pipeline = app.Build(); - - var context = new DefaultHttpContext { Request = { ContentType = "application/cloudevents+json" } }; - var bytes = Encoding.UTF8.GetBytes(data); - var base64Str = System.Convert.ToBase64String(bytes); - context.Request.Body = - MakeBody($"{{ \"datacontenttype\": \"{dataContentType}\", \"data_base64\": \"{base64Str}\", \"data\": {data} }}"); - - await pipeline.Invoke(context); - context.Response.StatusCode.ShouldBe((int)HttpStatusCode.BadRequest); - } - - private static Stream MakeBody(string text, Encoding encoding = null) + // Do verification in the scope of the middleware + app.Run(httpContext => { - encoding ??= Encoding.UTF8; + httpContext.Request.ContentType.ShouldBe("application/json"); + var body = ReadBody(httpContext.Request.Body); + body.ShouldBe(data); + return Task.CompletedTask; + }); - var stream = new MemoryStream(); - var bytes = encoding.GetBytes(text); - stream.Write(bytes); - stream.Seek(0L, SeekOrigin.Begin); - return stream; - } + var pipeline = app.Build(); - private static string ReadBody(Stream stream, Encoding encoding = null) - { - encoding ??= Encoding.UTF8; + var context = new DefaultHttpContext { Request = { ContentType = "application/cloudevents+json" } }; + var bytes = Encoding.UTF8.GetBytes(data); + var base64Str = System.Convert.ToBase64String(bytes); + context.Request.Body = + MakeBody($"{{ \"datacontenttype\": \"{dataContentType}\", \"data_base64\": \"{base64Str}\", \"data\": {data} }}"); - var bytes = new byte[stream.Length]; + await pipeline.Invoke(context); + context.Response.StatusCode.ShouldBe((int)HttpStatusCode.BadRequest); + } + + private static Stream MakeBody(string text, Encoding encoding = null) + { + encoding ??= Encoding.UTF8; + + var stream = new MemoryStream(); + var bytes = encoding.GetBytes(text); + stream.Write(bytes); + stream.Seek(0L, SeekOrigin.Begin); + return stream; + } + + private static string ReadBody(Stream stream, Encoding encoding = null) + { + encoding ??= Encoding.UTF8; + + var bytes = new byte[stream.Length]; #if NET9_0 - stream.ReadExactly(bytes, 0, bytes.Length); + stream.ReadExactly(bytes, 0, bytes.Length); #else stream.Read(bytes, 0, bytes.Length); #endif - var str = encoding.GetString(bytes); - return str; - } + var str = encoding.GetString(bytes); + return str; } -} +} \ No newline at end of file diff --git a/test/Dapr.AspNetCore.Test/DaprClientBuilderTest.cs b/test/Dapr.AspNetCore.Test/DaprClientBuilderTest.cs index 45cbcc15..22f62fcf 100644 --- a/test/Dapr.AspNetCore.Test/DaprClientBuilderTest.cs +++ b/test/Dapr.AspNetCore.Test/DaprClientBuilderTest.cs @@ -17,108 +17,106 @@ using Dapr.Client; using Grpc.Net.Client; using Xunit; -namespace Dapr.AspNetCore.Test +namespace Dapr.AspNetCore.Test; + +public class DaprClientBuilderTest { - public class DaprClientBuilderTest + [Fact] + public void DaprClientBuilder_UsesPropertyNameCaseHandlingInsensitiveByDefault() { - [Fact] - public void DaprClientBuilder_UsesPropertyNameCaseHandlingInsensitiveByDefault() - { - DaprClientBuilder builder = new DaprClientBuilder(); - Assert.True(builder.JsonSerializerOptions.PropertyNameCaseInsensitive); - } - - [Fact] - public void DaprClientBuilder_UsesPropertyNameCaseHandlingAsSpecified() - { - var builder = new DaprClientBuilder(); - builder.UseJsonSerializationOptions(new JsonSerializerOptions - { - PropertyNameCaseInsensitive = false - }); - Assert.False(builder.JsonSerializerOptions.PropertyNameCaseInsensitive); - } - - [Fact] - public void DaprClientBuilder_UsesThrowOperationCanceledOnCancellation_ByDefault() - { - var builder = new DaprClientBuilder(); - - Assert.True(builder.GrpcChannelOptions.ThrowOperationCanceledOnCancellation); - } - - [Fact] - public void DaprClientBuilder_DoesNotOverrideUserGrpcChannelOptions() - { - var builder = new DaprClientBuilder(); - builder.UseGrpcChannelOptions(new GrpcChannelOptions()).Build(); - Assert.False(builder.GrpcChannelOptions.ThrowOperationCanceledOnCancellation); - } - - [Fact] - public void DaprClientBuilder_ValidatesGrpcEndpointScheme() - { - var builder = new DaprClientBuilder(); - builder.UseGrpcEndpoint("ftp://example.com"); - - var ex = Assert.Throws(() => builder.Build()); - Assert.Equal("The gRPC endpoint must use http or https.", ex.Message); - } - - [Fact] - public void DaprClientBuilder_ValidatesHttpEndpointScheme() - { - var builder = new DaprClientBuilder(); - builder.UseHttpEndpoint("ftp://example.com"); - - var ex = Assert.Throws(() => builder.Build()); - Assert.Equal("The HTTP endpoint must use http or https.", ex.Message); - } - - [Fact] - public void DaprClientBuilder_SetsApiToken() - { - var builder = new DaprClientBuilder(); - builder.UseDaprApiToken("test_token"); - builder.Build(); - Assert.Equal("test_token", builder.DaprApiToken); - } - - [Fact] - public void DaprClientBuilder_SetsNullApiToken() - { - var builder = new DaprClientBuilder(); - builder.UseDaprApiToken(null); - builder.Build(); - Assert.Null(builder.DaprApiToken); - } - - [Fact] - public void DaprClientBuilder_ApiTokenSet_SetsApiTokenHeader() - { - var builder = new DaprClientBuilder(); - builder.UseDaprApiToken("test_token"); - var entry = DaprClient.GetDaprApiTokenHeader(builder.DaprApiToken); - Assert.NotNull(entry); - Assert.Equal("test_token", entry.Value.Value); - } - - [Fact] - public void DaprClientBuilder_ApiTokenNotSet_EmptyApiTokenHeader() - { - var builder = new DaprClientBuilder(); - var entry = DaprClient.GetDaprApiTokenHeader(builder.DaprApiToken); - Assert.Equal(default, entry); - } - - [Fact] - public void DaprClientBuilder_SetsTimeout() - { - var builder = new DaprClientBuilder(); - builder.UseTimeout(TimeSpan.FromSeconds(2)); - builder.Build(); - Assert.Equal(2, builder.Timeout.Seconds); - } + DaprClientBuilder builder = new DaprClientBuilder(); + Assert.True(builder.JsonSerializerOptions.PropertyNameCaseInsensitive); } -} + [Fact] + public void DaprClientBuilder_UsesPropertyNameCaseHandlingAsSpecified() + { + var builder = new DaprClientBuilder(); + builder.UseJsonSerializationOptions(new JsonSerializerOptions + { + PropertyNameCaseInsensitive = false + }); + Assert.False(builder.JsonSerializerOptions.PropertyNameCaseInsensitive); + } + + [Fact] + public void DaprClientBuilder_UsesThrowOperationCanceledOnCancellation_ByDefault() + { + var builder = new DaprClientBuilder(); + + Assert.True(builder.GrpcChannelOptions.ThrowOperationCanceledOnCancellation); + } + + [Fact] + public void DaprClientBuilder_DoesNotOverrideUserGrpcChannelOptions() + { + var builder = new DaprClientBuilder(); + builder.UseGrpcChannelOptions(new GrpcChannelOptions()).Build(); + Assert.False(builder.GrpcChannelOptions.ThrowOperationCanceledOnCancellation); + } + + [Fact] + public void DaprClientBuilder_ValidatesGrpcEndpointScheme() + { + var builder = new DaprClientBuilder(); + builder.UseGrpcEndpoint("ftp://example.com"); + + var ex = Assert.Throws(() => builder.Build()); + Assert.Equal("The gRPC endpoint must use http or https.", ex.Message); + } + + [Fact] + public void DaprClientBuilder_ValidatesHttpEndpointScheme() + { + var builder = new DaprClientBuilder(); + builder.UseHttpEndpoint("ftp://example.com"); + + var ex = Assert.Throws(() => builder.Build()); + Assert.Equal("The HTTP endpoint must use http or https.", ex.Message); + } + + [Fact] + public void DaprClientBuilder_SetsApiToken() + { + var builder = new DaprClientBuilder(); + builder.UseDaprApiToken("test_token"); + builder.Build(); + Assert.Equal("test_token", builder.DaprApiToken); + } + + [Fact] + public void DaprClientBuilder_SetsNullApiToken() + { + var builder = new DaprClientBuilder(); + builder.UseDaprApiToken(null); + builder.Build(); + Assert.Null(builder.DaprApiToken); + } + + [Fact] + public void DaprClientBuilder_ApiTokenSet_SetsApiTokenHeader() + { + var builder = new DaprClientBuilder(); + builder.UseDaprApiToken("test_token"); + var entry = DaprClient.GetDaprApiTokenHeader(builder.DaprApiToken); + Assert.NotNull(entry); + Assert.Equal("test_token", entry.Value.Value); + } + + [Fact] + public void DaprClientBuilder_ApiTokenNotSet_EmptyApiTokenHeader() + { + var builder = new DaprClientBuilder(); + var entry = DaprClient.GetDaprApiTokenHeader(builder.DaprApiToken); + Assert.Equal(default, entry); + } + + [Fact] + public void DaprClientBuilder_SetsTimeout() + { + var builder = new DaprClientBuilder(); + builder.UseTimeout(TimeSpan.FromSeconds(2)); + builder.Build(); + Assert.Equal(2, builder.Timeout.Seconds); + } +} \ No newline at end of file diff --git a/test/Dapr.AspNetCore.Test/DaprMvcBuilderExtensionsTest.cs b/test/Dapr.AspNetCore.Test/DaprMvcBuilderExtensionsTest.cs index a1df1fe1..e72d9e4e 100644 --- a/test/Dapr.AspNetCore.Test/DaprMvcBuilderExtensionsTest.cs +++ b/test/Dapr.AspNetCore.Test/DaprMvcBuilderExtensionsTest.cs @@ -17,90 +17,89 @@ using Dapr.Client; using Microsoft.Extensions.DependencyInjection; using Xunit; -namespace Dapr.AspNetCore.Test +namespace Dapr.AspNetCore.Test; + +public class DaprMvcBuilderExtensionsTest { - public class DaprMvcBuilderExtensionsTest + [Fact] + public void AddDapr_UsesSpecifiedDaprClientBuilderConfig() { - [Fact] - public void AddDapr_UsesSpecifiedDaprClientBuilderConfig() - { - var services = new ServiceCollection(); + var services = new ServiceCollection(); - var clientBuilder = new Action( - builder => builder.UseJsonSerializationOptions( - new JsonSerializerOptions() - { - PropertyNameCaseInsensitive = false - } - ) - ); + var clientBuilder = new Action( + builder => builder.UseJsonSerializationOptions( + new JsonSerializerOptions() + { + PropertyNameCaseInsensitive = false + } + ) + ); - services.AddControllers().AddDapr(clientBuilder); + services.AddControllers().AddDapr(clientBuilder); - var serviceProvider = services.BuildServiceProvider(); + var serviceProvider = services.BuildServiceProvider(); - DaprClientGrpc daprClient = serviceProvider.GetService() as DaprClientGrpc; + DaprClientGrpc daprClient = serviceProvider.GetService() as DaprClientGrpc; - Assert.False(daprClient.JsonSerializerOptions.PropertyNameCaseInsensitive); - } + Assert.False(daprClient.JsonSerializerOptions.PropertyNameCaseInsensitive); + } - [Fact] - public void AddDapr_UsesDefaultDaprClientBuilderConfig() - { - var services = new ServiceCollection(); + [Fact] + public void AddDapr_UsesDefaultDaprClientBuilderConfig() + { + var services = new ServiceCollection(); - services.AddControllers().AddDapr(); + services.AddControllers().AddDapr(); - var serviceProvider = services.BuildServiceProvider(); + var serviceProvider = services.BuildServiceProvider(); - DaprClientGrpc daprClient = serviceProvider.GetService() as DaprClientGrpc; + DaprClientGrpc daprClient = serviceProvider.GetService() as DaprClientGrpc; - Assert.True(daprClient.JsonSerializerOptions.PropertyNameCaseInsensitive); - } + Assert.True(daprClient.JsonSerializerOptions.PropertyNameCaseInsensitive); + } - [Fact] - public void AddDapr_RegistersDaprOnlyOnce() - { - var services = new ServiceCollection(); + [Fact] + public void AddDapr_RegistersDaprOnlyOnce() + { + var services = new ServiceCollection(); - var clientBuilder = new Action( - builder => builder.UseJsonSerializationOptions( - new JsonSerializerOptions() - { - PropertyNameCaseInsensitive = false - } - ) - ); + var clientBuilder = new Action( + builder => builder.UseJsonSerializationOptions( + new JsonSerializerOptions() + { + PropertyNameCaseInsensitive = false + } + ) + ); - // register with JsonSerializerOptions.PropertyNameCaseInsensitive = false - services.AddControllers().AddDapr(clientBuilder); + // register with JsonSerializerOptions.PropertyNameCaseInsensitive = false + services.AddControllers().AddDapr(clientBuilder); - // register with JsonSerializerOptions.PropertyNameCaseInsensitive = true (default) - services.AddControllers().AddDapr(); + // register with JsonSerializerOptions.PropertyNameCaseInsensitive = true (default) + services.AddControllers().AddDapr(); - var serviceProvider = services.BuildServiceProvider(); + var serviceProvider = services.BuildServiceProvider(); - DaprClientGrpc daprClient = serviceProvider.GetService() as DaprClientGrpc; + DaprClientGrpc daprClient = serviceProvider.GetService() as DaprClientGrpc; - Assert.False(daprClient.JsonSerializerOptions.PropertyNameCaseInsensitive); - } + Assert.False(daprClient.JsonSerializerOptions.PropertyNameCaseInsensitive); + } #if NET8_0_OR_GREATER - [Fact] - public void AddDapr_WithKeyedServices() - { - var services = new ServiceCollection(); + [Fact] + public void AddDapr_WithKeyedServices() + { + var services = new ServiceCollection(); - services.AddKeyedSingleton("key1", new Object()); + services.AddKeyedSingleton("key1", new Object()); - services.AddControllers().AddDapr(); + services.AddControllers().AddDapr(); - var serviceProvider = services.BuildServiceProvider(); + var serviceProvider = services.BuildServiceProvider(); - var daprClient = serviceProvider.GetService(); + var daprClient = serviceProvider.GetService(); - Assert.NotNull(daprClient); - } -#endif + Assert.NotNull(daprClient); } -} +#endif +} \ No newline at end of file diff --git a/test/Dapr.AspNetCore.Test/StateEntryApplicationModelProviderTest.cs b/test/Dapr.AspNetCore.Test/StateEntryApplicationModelProviderTest.cs index 243b60bb..06055cb0 100644 --- a/test/Dapr.AspNetCore.Test/StateEntryApplicationModelProviderTest.cs +++ b/test/Dapr.AspNetCore.Test/StateEntryApplicationModelProviderTest.cs @@ -11,81 +11,80 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.AspNetCore.Test +namespace Dapr.AspNetCore.Test; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Dapr.AspNetCore.Resources; +using Shouldly; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Xunit; + +public class StateEntryApplicationModelProviderTest { - using System; - using System.Collections.Generic; - using System.Linq; - using System.Reflection; - using Dapr.AspNetCore.Resources; - using Shouldly; - using Microsoft.AspNetCore.Mvc; - using Microsoft.AspNetCore.Mvc.ApplicationModels; - using Microsoft.AspNetCore.Mvc.ModelBinding; - using Xunit; - - public class StateEntryApplicationModelProviderTest + [Fact] + public void OnProvidersExecuted_NullActionsBindingSource() { - [Fact] - public void OnProvidersExecuted_NullActionsBindingSource() - { - var provider = new StateEntryApplicationModelProvider(); - var context = CreateContext(nameof(ApplicationModelProviderTestController.Get)); + var provider = new StateEntryApplicationModelProvider(); + var context = CreateContext(nameof(ApplicationModelProviderTestController.Get)); - Action action = () => provider.OnProvidersExecuted(context); + Action action = () => provider.OnProvidersExecuted(context); - action.ShouldNotThrow(); - } - - [Fact] - public void OnProvidersExecuted_StateEntryParameterThrows() - { - var provider = new StateEntryApplicationModelProvider(); - var context = CreateContext(nameof(ApplicationModelProviderTestController.Post)); - - Action action = () => provider.OnProvidersExecuted(context); - - action - .ShouldThrow(SR.ErrorStateStoreNameNotProvidedForStateEntry); - } - - private ApplicationModelProviderContext CreateContext(string methodName) - { - var controllerType = typeof(ApplicationModelProviderTestController).GetTypeInfo(); - var typeInfoList = new List { controllerType }; - - var context = new ApplicationModelProviderContext(typeInfoList); - var controllerModel = new ControllerModel(controllerType, new List(0)); - - context.Result.Controllers.Add(controllerModel); - - var methodInfo = controllerType.AsType().GetMethods().First(m => m.Name.Equals(methodName)); - var actionModel = new ActionModel(methodInfo, controllerModel.Attributes) - { - Controller = controllerModel - }; - - controllerModel.Actions.Add(actionModel); - var parameterInfo = actionModel.ActionMethod.GetParameters().First(); - var parameterModel = new ParameterModel(parameterInfo, controllerModel.Attributes) - { - BindingInfo = new BindingInfo(), - Action = actionModel, - }; - - actionModel.Parameters.Add(parameterModel); - - return context; - } - - [Controller] - private class ApplicationModelProviderTestController : Controller - { - [HttpGet] - public void Get([Bind(Prefix = "s")]int someId) { } - - [HttpPost] - public void Post(StateEntry bogusEntry) { } - } + action.ShouldNotThrow(); } -} + + [Fact] + public void OnProvidersExecuted_StateEntryParameterThrows() + { + var provider = new StateEntryApplicationModelProvider(); + var context = CreateContext(nameof(ApplicationModelProviderTestController.Post)); + + Action action = () => provider.OnProvidersExecuted(context); + + action + .ShouldThrow(SR.ErrorStateStoreNameNotProvidedForStateEntry); + } + + private ApplicationModelProviderContext CreateContext(string methodName) + { + var controllerType = typeof(ApplicationModelProviderTestController).GetTypeInfo(); + var typeInfoList = new List { controllerType }; + + var context = new ApplicationModelProviderContext(typeInfoList); + var controllerModel = new ControllerModel(controllerType, new List(0)); + + context.Result.Controllers.Add(controllerModel); + + var methodInfo = controllerType.AsType().GetMethods().First(m => m.Name.Equals(methodName)); + var actionModel = new ActionModel(methodInfo, controllerModel.Attributes) + { + Controller = controllerModel + }; + + controllerModel.Actions.Add(actionModel); + var parameterInfo = actionModel.ActionMethod.GetParameters().First(); + var parameterModel = new ParameterModel(parameterInfo, controllerModel.Attributes) + { + BindingInfo = new BindingInfo(), + Action = actionModel, + }; + + actionModel.Parameters.Add(parameterModel); + + return context; + } + + [Controller] + private class ApplicationModelProviderTestController : Controller + { + [HttpGet] + public void Get([Bind(Prefix = "s")]int someId) { } + + [HttpPost] + public void Post(StateEntry bogusEntry) { } + } +} \ No newline at end of file diff --git a/test/Dapr.AspNetCore.Test/StateEntryModelBinderTest.cs b/test/Dapr.AspNetCore.Test/StateEntryModelBinderTest.cs index 8cdcd32d..85170d82 100644 --- a/test/Dapr.AspNetCore.Test/StateEntryModelBinderTest.cs +++ b/test/Dapr.AspNetCore.Test/StateEntryModelBinderTest.cs @@ -11,186 +11,185 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.AspNetCore.Test +namespace Dapr.AspNetCore.Test; + +using System; +using System.Text.Json; +using System.Threading.Tasks; +using Dapr.Client; +using Dapr.Client.Autogen.Grpc.v1; +using Shouldly; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +public class StateEntryModelBinderTest { - using System; - using System.Text.Json; - using System.Threading.Tasks; - using Dapr.Client; - using Dapr.Client.Autogen.Grpc.v1; - using Shouldly; - using Microsoft.AspNetCore.Http; - using Microsoft.AspNetCore.Mvc; - using Microsoft.AspNetCore.Mvc.ModelBinding; - using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; - using Microsoft.Extensions.DependencyInjection; - using Xunit; - - public class StateEntryModelBinderTest + [Fact] + public async Task BindAsync_WithoutMatchingRouteValue_ReportsError() { - [Fact] - public async Task BindAsync_WithoutMatchingRouteValue_ReportsError() - { - await using var client = TestClient.CreateForDaprClient(); + await using var client = TestClient.CreateForDaprClient(); - var binder = new StateEntryModelBinder("testStore", "test", isStateEntry: false, typeof(Widget)); - var context = CreateContext(CreateServices(client.InnerClient)); + var binder = new StateEntryModelBinder("testStore", "test", isStateEntry: false, typeof(Widget)); + var context = CreateContext(CreateServices(client.InnerClient)); - await binder.BindModelAsync(context); + await binder.BindModelAsync(context); - context.Result.IsModelSet.ShouldBeFalse(); - context.ModelState.ErrorCount.ShouldBe(1); - context.ModelState["testParameter"].Errors.Count.ShouldBe(1); + context.Result.IsModelSet.ShouldBeFalse(); + context.ModelState.ErrorCount.ShouldBe(1); + context.ModelState["testParameter"].Errors.Count.ShouldBe(1); - // No request to state store, validated by disposing client - } - - [Fact] - public async Task BindAsync_CanBindValue() - { - await using var client = TestClient.CreateForDaprClient(); - - var binder = new StateEntryModelBinder("testStore", "id", isStateEntry: false, typeof(Widget)); - - // Configure Client - var context = CreateContext(CreateServices(client.InnerClient)); - context.HttpContext.Request.RouteValues["id"] = "test"; - - var request = await client.CaptureGrpcRequestAsync(async _ => - { - await binder.BindModelAsync(context); - }); - - // Create Response & Respond - var state = new Widget() { Size = "small", Color = "yellow", }; - await SendResponseWithState(state, request); - - // Get response and validate - context.Result.IsModelSet.ShouldBeTrue(); - ((Widget)context.Result.Model).Size.ShouldBe("small"); - ((Widget)context.Result.Model).Color.ShouldBe("yellow"); - - context.ValidationState.Count.ShouldBe(1); - context.ValidationState[context.Result.Model].SuppressValidation.ShouldBeTrue(); - } - - [Fact] - public async Task BindAsync_CanBindStateEntry() - { - await using var client = TestClient.CreateForDaprClient(); - - var binder = new StateEntryModelBinder("testStore", "id", isStateEntry: true, typeof(Widget)); - - // Configure Client - var context = CreateContext(CreateServices(client.InnerClient)); - context.HttpContext.Request.RouteValues["id"] = "test"; - - var request = await client.CaptureGrpcRequestAsync(async _ => - { - await binder.BindModelAsync(context); - }); - - // Create Response & Respond - var state = new Widget() { Size = "small", Color = "yellow", }; - await SendResponseWithState(state, request); - - // Get response and validate - context.Result.IsModelSet.ShouldBeTrue(); - ((StateEntry)context.Result.Model).Key.ShouldBe("test"); - ((StateEntry)context.Result.Model).Value.Size.ShouldBe("small"); - ((StateEntry)context.Result.Model).Value.Color.ShouldBe("yellow"); - - context.ValidationState.Count.ShouldBe(1); - context.ValidationState[context.Result.Model].SuppressValidation.ShouldBeTrue(); - } - - [Fact] - public async Task BindAsync_ReturnsNullForNonExistentStateEntry() - { - await using var client = TestClient.CreateForDaprClient(); - - var binder = new StateEntryModelBinder("testStore", "id", isStateEntry: false, typeof(Widget)); - - // Configure Client - var context = CreateContext(CreateServices(client.InnerClient)); - context.HttpContext.Request.RouteValues["id"] = "test"; - - var request = await client.CaptureGrpcRequestAsync(async _ => - { - await binder.BindModelAsync(context); - }); - - await SendResponseWithState(null, request); - - context.ModelState.IsValid.ShouldBeTrue(); - context.Result.IsModelSet.ShouldBeFalse(); - context.Result.ShouldBe(ModelBindingResult.Failed()); - } - - [Fact] - public async Task BindAsync_WithStateEntry_ForNonExistentStateEntry() - { - await using var client = TestClient.CreateForDaprClient(); - - var binder = new StateEntryModelBinder("testStore", "id", isStateEntry: true, typeof(Widget)); - - // Configure Client - var context = CreateContext(CreateServices(client.InnerClient)); - context.HttpContext.Request.RouteValues["id"] = "test"; - - var request = await client.CaptureGrpcRequestAsync(async _ => - { - await binder.BindModelAsync(context); - }); - - await SendResponseWithState(null, request); - - context.ModelState.IsValid.ShouldBeTrue(); - context.Result.IsModelSet.ShouldBeTrue(); - ((StateEntry)context.Result.Model).Value.ShouldBeNull(); - } - - private static ModelBindingContext CreateContext(IServiceProvider services) - { - return new DefaultModelBindingContext() - { - ActionContext = new ActionContext() - { - HttpContext = new DefaultHttpContext() - { - RequestServices = services, - }, - }, - ModelState = new ModelStateDictionary(), - ModelName = "testParameter", - ValidationState = new ValidationStateDictionary(), - }; - } - - private async Task SendResponseWithState(T state, TestClient.TestGrpcRequest request) - { - var stateData = TypeConverters.ToJsonByteString(state, new JsonSerializerOptions(JsonSerializerDefaults.Web)); - var stateResponse = new GetStateResponse() - { - Data = stateData, - Etag = "test", - }; - - await request.CompleteWithMessageAsync(stateResponse); - } - - private static IServiceProvider CreateServices(DaprClient daprClient) - { - var services = new ServiceCollection(); - services.AddSingleton(daprClient); - return services.BuildServiceProvider(); - } - - private class Widget - { - public string Size { get; set; } - - public string Color { get; set; } - } + // No request to state store, validated by disposing client } -} + + [Fact] + public async Task BindAsync_CanBindValue() + { + await using var client = TestClient.CreateForDaprClient(); + + var binder = new StateEntryModelBinder("testStore", "id", isStateEntry: false, typeof(Widget)); + + // Configure Client + var context = CreateContext(CreateServices(client.InnerClient)); + context.HttpContext.Request.RouteValues["id"] = "test"; + + var request = await client.CaptureGrpcRequestAsync(async _ => + { + await binder.BindModelAsync(context); + }); + + // Create Response & Respond + var state = new Widget() { Size = "small", Color = "yellow", }; + await SendResponseWithState(state, request); + + // Get response and validate + context.Result.IsModelSet.ShouldBeTrue(); + ((Widget)context.Result.Model).Size.ShouldBe("small"); + ((Widget)context.Result.Model).Color.ShouldBe("yellow"); + + context.ValidationState.Count.ShouldBe(1); + context.ValidationState[context.Result.Model].SuppressValidation.ShouldBeTrue(); + } + + [Fact] + public async Task BindAsync_CanBindStateEntry() + { + await using var client = TestClient.CreateForDaprClient(); + + var binder = new StateEntryModelBinder("testStore", "id", isStateEntry: true, typeof(Widget)); + + // Configure Client + var context = CreateContext(CreateServices(client.InnerClient)); + context.HttpContext.Request.RouteValues["id"] = "test"; + + var request = await client.CaptureGrpcRequestAsync(async _ => + { + await binder.BindModelAsync(context); + }); + + // Create Response & Respond + var state = new Widget() { Size = "small", Color = "yellow", }; + await SendResponseWithState(state, request); + + // Get response and validate + context.Result.IsModelSet.ShouldBeTrue(); + ((StateEntry)context.Result.Model).Key.ShouldBe("test"); + ((StateEntry)context.Result.Model).Value.Size.ShouldBe("small"); + ((StateEntry)context.Result.Model).Value.Color.ShouldBe("yellow"); + + context.ValidationState.Count.ShouldBe(1); + context.ValidationState[context.Result.Model].SuppressValidation.ShouldBeTrue(); + } + + [Fact] + public async Task BindAsync_ReturnsNullForNonExistentStateEntry() + { + await using var client = TestClient.CreateForDaprClient(); + + var binder = new StateEntryModelBinder("testStore", "id", isStateEntry: false, typeof(Widget)); + + // Configure Client + var context = CreateContext(CreateServices(client.InnerClient)); + context.HttpContext.Request.RouteValues["id"] = "test"; + + var request = await client.CaptureGrpcRequestAsync(async _ => + { + await binder.BindModelAsync(context); + }); + + await SendResponseWithState(null, request); + + context.ModelState.IsValid.ShouldBeTrue(); + context.Result.IsModelSet.ShouldBeFalse(); + context.Result.ShouldBe(ModelBindingResult.Failed()); + } + + [Fact] + public async Task BindAsync_WithStateEntry_ForNonExistentStateEntry() + { + await using var client = TestClient.CreateForDaprClient(); + + var binder = new StateEntryModelBinder("testStore", "id", isStateEntry: true, typeof(Widget)); + + // Configure Client + var context = CreateContext(CreateServices(client.InnerClient)); + context.HttpContext.Request.RouteValues["id"] = "test"; + + var request = await client.CaptureGrpcRequestAsync(async _ => + { + await binder.BindModelAsync(context); + }); + + await SendResponseWithState(null, request); + + context.ModelState.IsValid.ShouldBeTrue(); + context.Result.IsModelSet.ShouldBeTrue(); + ((StateEntry)context.Result.Model).Value.ShouldBeNull(); + } + + private static ModelBindingContext CreateContext(IServiceProvider services) + { + return new DefaultModelBindingContext() + { + ActionContext = new ActionContext() + { + HttpContext = new DefaultHttpContext() + { + RequestServices = services, + }, + }, + ModelState = new ModelStateDictionary(), + ModelName = "testParameter", + ValidationState = new ValidationStateDictionary(), + }; + } + + private async Task SendResponseWithState(T state, TestClient.TestGrpcRequest request) + { + var stateData = TypeConverters.ToJsonByteString(state, new JsonSerializerOptions(JsonSerializerDefaults.Web)); + var stateResponse = new GetStateResponse() + { + Data = stateData, + Etag = "test", + }; + + await request.CompleteWithMessageAsync(stateResponse); + } + + private static IServiceProvider CreateServices(DaprClient daprClient) + { + var services = new ServiceCollection(); + services.AddSingleton(daprClient); + return services.BuildServiceProvider(); + } + + private class Widget + { + public string Size { get; set; } + + public string Color { get; set; } + } +} \ No newline at end of file diff --git a/test/Dapr.Client.Test/ArgumentVerifierTest.cs b/test/Dapr.Client.Test/ArgumentVerifierTest.cs index c839ac3e..9b386b0f 100644 --- a/test/Dapr.Client.Test/ArgumentVerifierTest.cs +++ b/test/Dapr.Client.Test/ArgumentVerifierTest.cs @@ -11,44 +11,43 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Client.Test +namespace Dapr.Client.Test; + +using System; +using Xunit; + +public class ArgumentVerifierTest { - using System; - using Xunit; - - public class ArgumentVerifierTest + [Fact] + public void ThrowIfNull_RespectsArgumentName() { - [Fact] - public void ThrowIfNull_RespectsArgumentName() + var ex = Assert.Throws(() => { - var ex = Assert.Throws(() => - { - ArgumentVerifier.ThrowIfNull(null, "args"); - }); + ArgumentVerifier.ThrowIfNull(null, "args"); + }); - Assert.Contains("args", ex.Message); - } - - [Fact] - public void ThrowIfNullOrEmpty_RespectsArgumentName_WhenValueIsNull() - { - var ex = Assert.Throws(() => - { - ArgumentVerifier.ThrowIfNullOrEmpty(null, "args"); - }); - - Assert.Contains("args", ex.Message); - } - - [Fact] - public void ThrowIfNullOrEmpty_RespectsArgumentName_WhenValueIsEmpty() - { - var ex = Assert.Throws(() => - { - ArgumentVerifier.ThrowIfNullOrEmpty(string.Empty, "args"); - }); - - Assert.Contains("args", ex.Message); - } + Assert.Contains("args", ex.Message); } -} + + [Fact] + public void ThrowIfNullOrEmpty_RespectsArgumentName_WhenValueIsNull() + { + var ex = Assert.Throws(() => + { + ArgumentVerifier.ThrowIfNullOrEmpty(null, "args"); + }); + + Assert.Contains("args", ex.Message); + } + + [Fact] + public void ThrowIfNullOrEmpty_RespectsArgumentName_WhenValueIsEmpty() + { + var ex = Assert.Throws(() => + { + ArgumentVerifier.ThrowIfNullOrEmpty(string.Empty, "args"); + }); + + Assert.Contains("args", ex.Message); + } +} \ No newline at end of file diff --git a/test/Dapr.Client.Test/BulkPublishEventApiTest.cs b/test/Dapr.Client.Test/BulkPublishEventApiTest.cs index 14b946d6..95d10bcd 100644 --- a/test/Dapr.Client.Test/BulkPublishEventApiTest.cs +++ b/test/Dapr.Client.Test/BulkPublishEventApiTest.cs @@ -11,330 +11,329 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Client.Test +namespace Dapr.Client.Test; + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Autogenerated = Dapr.Client.Autogen.Grpc.v1; +using Shouldly; +using Grpc.Core; +using Moq; +using Xunit; + +public class BulkPublishEventApiTest { - using System; - using System.Collections.Generic; - using System.Text.Json; - using System.Threading; - using System.Threading.Tasks; - using Autogenerated = Dapr.Client.Autogen.Grpc.v1; - using Shouldly; - using Grpc.Core; - using Moq; - using Xunit; + const string TestPubsubName = "testpubsubname"; + const string TestTopicName = "test"; + const string TestContentType = "application/json"; + static readonly List bulkPublishData = new List() { "hello", "world" }; - public class BulkPublishEventApiTest + [Fact] + public async Task BulkPublishEventAsync_CanPublishTopicWithEvents() { - const string TestPubsubName = "testpubsubname"; - const string TestTopicName = "test"; - const string TestContentType = "application/json"; - static readonly List bulkPublishData = new List() { "hello", "world" }; + await using var client = TestClient.CreateForDaprClient(); - [Fact] - public async Task BulkPublishEventAsync_CanPublishTopicWithEvents() + var request = await client.CaptureGrpcRequestAsync(async daprClient => + await daprClient.BulkPublishEventAsync(TestPubsubName, TestTopicName, bulkPublishData)); + + request.Dismiss(); + + var envelope = await request.GetRequestEnvelopeAsync(); + + envelope.Entries.Count.ShouldBe(2); + envelope.PubsubName.ShouldBe(TestPubsubName); + envelope.Topic.ShouldBe(TestTopicName); + envelope.Metadata.Count.ShouldBe(0); + + var firstEntry = envelope.Entries[0]; + + firstEntry.EntryId.ShouldBe("0"); + firstEntry.ContentType.ShouldBe(TestContentType); + firstEntry.Event.ToStringUtf8().ShouldBe(JsonSerializer.Serialize(bulkPublishData[0], client.InnerClient.JsonSerializerOptions)); + firstEntry.Metadata.ShouldBeEmpty(); + + var secondEntry = envelope.Entries[1]; + + secondEntry.EntryId.ShouldBe("1"); + secondEntry.ContentType.ShouldBe(TestContentType); + secondEntry.Event.ToStringUtf8().ShouldBe(JsonSerializer.Serialize(bulkPublishData[1], client.InnerClient.JsonSerializerOptions)); + secondEntry.Metadata.ShouldBeEmpty(); + + // Create Response & Respond + var response = new Autogenerated.BulkPublishResponse { - await using var client = TestClient.CreateForDaprClient(); - - var request = await client.CaptureGrpcRequestAsync(async daprClient => - await daprClient.BulkPublishEventAsync(TestPubsubName, TestTopicName, bulkPublishData)); - - request.Dismiss(); - - var envelope = await request.GetRequestEnvelopeAsync(); - - envelope.Entries.Count.ShouldBe(2); - envelope.PubsubName.ShouldBe(TestPubsubName); - envelope.Topic.ShouldBe(TestTopicName); - envelope.Metadata.Count.ShouldBe(0); - - var firstEntry = envelope.Entries[0]; - - firstEntry.EntryId.ShouldBe("0"); - firstEntry.ContentType.ShouldBe(TestContentType); - firstEntry.Event.ToStringUtf8().ShouldBe(JsonSerializer.Serialize(bulkPublishData[0], client.InnerClient.JsonSerializerOptions)); - firstEntry.Metadata.ShouldBeEmpty(); - - var secondEntry = envelope.Entries[1]; - - secondEntry.EntryId.ShouldBe("1"); - secondEntry.ContentType.ShouldBe(TestContentType); - secondEntry.Event.ToStringUtf8().ShouldBe(JsonSerializer.Serialize(bulkPublishData[1], client.InnerClient.JsonSerializerOptions)); - secondEntry.Metadata.ShouldBeEmpty(); + FailedEntries = { } + }; + var bulkPublishResponse = await request.CompleteWithMessageAsync(response); - // Create Response & Respond - var response = new Autogenerated.BulkPublishResponse - { - FailedEntries = { } - }; - var bulkPublishResponse = await request.CompleteWithMessageAsync(response); - - // Get response and validate - bulkPublishResponse.FailedEntries.Count.ShouldBe(0); - } + // Get response and validate + bulkPublishResponse.FailedEntries.Count.ShouldBe(0); + } - [Fact] - public async Task BulkPublishEventAsync_CanPublishTopicWithEvents_WithMetadata() + [Fact] + public async Task BulkPublishEventAsync_CanPublishTopicWithEvents_WithMetadata() + { + await using var client = TestClient.CreateForDaprClient(); + + var metadata = new Dictionary { { "key1", "value1" }, { "key2", "value2" } }; + + var request = await client.CaptureGrpcRequestAsync(async daprClient => + await daprClient.BulkPublishEventAsync(TestPubsubName, TestTopicName, bulkPublishData, + metadata)); + + request.Dismiss(); + + var envelope = await request.GetRequestEnvelopeAsync(); + + envelope.Entries.Count.ShouldBe(2); + envelope.PubsubName.ShouldBe(TestPubsubName); + envelope.Topic.ShouldBe(TestTopicName); + + envelope.Metadata.Count.ShouldBe(2); + envelope.Metadata.Keys.Contains("key1").ShouldBeTrue(); + envelope.Metadata.Keys.Contains("key2").ShouldBeTrue(); + envelope.Metadata["key1"].ShouldBe("value1"); + envelope.Metadata["key2"].ShouldBe("value2"); + + var firstEntry = envelope.Entries[0]; + + firstEntry.EntryId.ShouldBe("0"); + firstEntry.ContentType.ShouldBe(TestContentType); + firstEntry.Event.ToStringUtf8().ShouldBe(JsonSerializer.Serialize(bulkPublishData[0], client.InnerClient.JsonSerializerOptions)); + firstEntry.Metadata.ShouldBeEmpty(); + + var secondEntry = envelope.Entries[1]; + + secondEntry.EntryId.ShouldBe("1"); + secondEntry.ContentType.ShouldBe(TestContentType); + secondEntry.Event.ToStringUtf8().ShouldBe(JsonSerializer.Serialize(bulkPublishData[1], client.InnerClient.JsonSerializerOptions)); + secondEntry.Metadata.ShouldBeEmpty(); + + // Create Response & Respond + var response = new Autogenerated.BulkPublishResponse { - await using var client = TestClient.CreateForDaprClient(); - - var metadata = new Dictionary { { "key1", "value1" }, { "key2", "value2" } }; - - var request = await client.CaptureGrpcRequestAsync(async daprClient => - await daprClient.BulkPublishEventAsync(TestPubsubName, TestTopicName, bulkPublishData, - metadata)); - - request.Dismiss(); - - var envelope = await request.GetRequestEnvelopeAsync(); + FailedEntries = { } + }; + var bulkPublishResponse = await request.CompleteWithMessageAsync(response); - envelope.Entries.Count.ShouldBe(2); - envelope.PubsubName.ShouldBe(TestPubsubName); - envelope.Topic.ShouldBe(TestTopicName); + // Get response and validate + bulkPublishResponse.FailedEntries.Count.ShouldBe(0); + } - envelope.Metadata.Count.ShouldBe(2); - envelope.Metadata.Keys.Contains("key1").ShouldBeTrue(); - envelope.Metadata.Keys.Contains("key2").ShouldBeTrue(); - envelope.Metadata["key1"].ShouldBe("value1"); - envelope.Metadata["key2"].ShouldBe("value2"); + [Fact] + public async Task BulkPublishEventAsync_CanPublishTopicWithNoContent() + { + await using var client = TestClient.CreateForDaprClient(); - var firstEntry = envelope.Entries[0]; + var request = await client.CaptureGrpcRequestAsync(async daprClient => + await daprClient.BulkPublishEventAsync(TestPubsubName, TestTopicName, bulkPublishData, + null)); - firstEntry.EntryId.ShouldBe("0"); - firstEntry.ContentType.ShouldBe(TestContentType); - firstEntry.Event.ToStringUtf8().ShouldBe(JsonSerializer.Serialize(bulkPublishData[0], client.InnerClient.JsonSerializerOptions)); - firstEntry.Metadata.ShouldBeEmpty(); + request.Dismiss(); - var secondEntry = envelope.Entries[1]; + var envelope = await request.GetRequestEnvelopeAsync(); + envelope.Entries.Count.ShouldBe(2); + envelope.PubsubName.ShouldBe(TestPubsubName); + envelope.Topic.ShouldBe(TestTopicName); + envelope.Metadata.Count.ShouldBe(0); - secondEntry.EntryId.ShouldBe("1"); - secondEntry.ContentType.ShouldBe(TestContentType); - secondEntry.Event.ToStringUtf8().ShouldBe(JsonSerializer.Serialize(bulkPublishData[1], client.InnerClient.JsonSerializerOptions)); - secondEntry.Metadata.ShouldBeEmpty(); + var firstEntry = envelope.Entries[0]; + + firstEntry.EntryId.ShouldBe("0"); + firstEntry.ContentType.ShouldBe(TestContentType); + firstEntry.Event.ToStringUtf8().ShouldBe(JsonSerializer.Serialize(bulkPublishData[0], client.InnerClient.JsonSerializerOptions)); + firstEntry.Metadata.ShouldBeEmpty(); + + var secondEntry = envelope.Entries[1]; + + secondEntry.EntryId.ShouldBe("1"); + secondEntry.ContentType.ShouldBe(TestContentType); + secondEntry.Event.ToStringUtf8().ShouldBe(JsonSerializer.Serialize(bulkPublishData[1], client.InnerClient.JsonSerializerOptions)); + secondEntry.Metadata.ShouldBeEmpty(); - // Create Response & Respond - var response = new Autogenerated.BulkPublishResponse - { - FailedEntries = { } - }; - var bulkPublishResponse = await request.CompleteWithMessageAsync(response); - - // Get response and validate - bulkPublishResponse.FailedEntries.Count.ShouldBe(0); - } - - [Fact] - public async Task BulkPublishEventAsync_CanPublishTopicWithNoContent() + // Create Response & Respond + var response = new Autogenerated.BulkPublishResponse { - await using var client = TestClient.CreateForDaprClient(); - - var request = await client.CaptureGrpcRequestAsync(async daprClient => - await daprClient.BulkPublishEventAsync(TestPubsubName, TestTopicName, bulkPublishData, - null)); - - request.Dismiss(); - - var envelope = await request.GetRequestEnvelopeAsync(); - envelope.Entries.Count.ShouldBe(2); - envelope.PubsubName.ShouldBe(TestPubsubName); - envelope.Topic.ShouldBe(TestTopicName); - envelope.Metadata.Count.ShouldBe(0); - - var firstEntry = envelope.Entries[0]; - - firstEntry.EntryId.ShouldBe("0"); - firstEntry.ContentType.ShouldBe(TestContentType); - firstEntry.Event.ToStringUtf8().ShouldBe(JsonSerializer.Serialize(bulkPublishData[0], client.InnerClient.JsonSerializerOptions)); - firstEntry.Metadata.ShouldBeEmpty(); - - var secondEntry = envelope.Entries[1]; - - secondEntry.EntryId.ShouldBe("1"); - secondEntry.ContentType.ShouldBe(TestContentType); - secondEntry.Event.ToStringUtf8().ShouldBe(JsonSerializer.Serialize(bulkPublishData[1], client.InnerClient.JsonSerializerOptions)); - secondEntry.Metadata.ShouldBeEmpty(); + FailedEntries = { } + }; + var bulkPublishResponse = await request.CompleteWithMessageAsync(response); - // Create Response & Respond - var response = new Autogenerated.BulkPublishResponse - { - FailedEntries = { } - }; - var bulkPublishResponse = await request.CompleteWithMessageAsync(response); - - // Get response and validate - bulkPublishResponse.FailedEntries.Count.ShouldBe(0); - } + // Get response and validate + bulkPublishResponse.FailedEntries.Count.ShouldBe(0); + } - [Fact] - public async Task BulkPublishEventAsync_CanPublishTopicWithNoContent_WithMetadata() + [Fact] + public async Task BulkPublishEventAsync_CanPublishTopicWithNoContent_WithMetadata() + { + await using var client = TestClient.CreateForDaprClient(); + + var metadata = new Dictionary { { "key1", "value1" }, { "key2", "value2" } }; + + var request = await client.CaptureGrpcRequestAsync(async daprClient => + await daprClient.BulkPublishEventAsync(TestPubsubName, TestTopicName, bulkPublishData, + metadata)); + + request.Dismiss(); + + var envelope = await request.GetRequestEnvelopeAsync(); + envelope.Entries.Count.ShouldBe(2); + envelope.PubsubName.ShouldBe(TestPubsubName); + envelope.Topic.ShouldBe(TestTopicName); + + envelope.Metadata.Count.ShouldBe(2); + envelope.Metadata.Keys.Contains("key1").ShouldBeTrue(); + envelope.Metadata.Keys.Contains("key2").ShouldBeTrue(); + envelope.Metadata["key1"].ShouldBe("value1"); + envelope.Metadata["key2"].ShouldBe("value2"); + + var firstEntry = envelope.Entries[0]; + + firstEntry.EntryId.ShouldBe("0"); + firstEntry.ContentType.ShouldBe(TestContentType); + firstEntry.Event.ToStringUtf8().ShouldBe(JsonSerializer.Serialize(bulkPublishData[0], client.InnerClient.JsonSerializerOptions)); + firstEntry.Metadata.ShouldBeEmpty(); + + var secondEntry = envelope.Entries[1]; + + secondEntry.EntryId.ShouldBe("1"); + secondEntry.ContentType.ShouldBe(TestContentType); + secondEntry.Event.ToStringUtf8().ShouldBe(JsonSerializer.Serialize(bulkPublishData[1], client.InnerClient.JsonSerializerOptions)); + secondEntry.Metadata.ShouldBeEmpty(); + + // Create Response & Respond + var response = new Autogenerated.BulkPublishResponse { - await using var client = TestClient.CreateForDaprClient(); - - var metadata = new Dictionary { { "key1", "value1" }, { "key2", "value2" } }; - - var request = await client.CaptureGrpcRequestAsync(async daprClient => - await daprClient.BulkPublishEventAsync(TestPubsubName, TestTopicName, bulkPublishData, - metadata)); - - request.Dismiss(); - - var envelope = await request.GetRequestEnvelopeAsync(); - envelope.Entries.Count.ShouldBe(2); - envelope.PubsubName.ShouldBe(TestPubsubName); - envelope.Topic.ShouldBe(TestTopicName); - - envelope.Metadata.Count.ShouldBe(2); - envelope.Metadata.Keys.Contains("key1").ShouldBeTrue(); - envelope.Metadata.Keys.Contains("key2").ShouldBeTrue(); - envelope.Metadata["key1"].ShouldBe("value1"); - envelope.Metadata["key2"].ShouldBe("value2"); - - var firstEntry = envelope.Entries[0]; - - firstEntry.EntryId.ShouldBe("0"); - firstEntry.ContentType.ShouldBe(TestContentType); - firstEntry.Event.ToStringUtf8().ShouldBe(JsonSerializer.Serialize(bulkPublishData[0], client.InnerClient.JsonSerializerOptions)); - firstEntry.Metadata.ShouldBeEmpty(); - - var secondEntry = envelope.Entries[1]; - - secondEntry.EntryId.ShouldBe("1"); - secondEntry.ContentType.ShouldBe(TestContentType); - secondEntry.Event.ToStringUtf8().ShouldBe(JsonSerializer.Serialize(bulkPublishData[1], client.InnerClient.JsonSerializerOptions)); - secondEntry.Metadata.ShouldBeEmpty(); + FailedEntries = { } + }; + var bulkPublishResponse = await request.CompleteWithMessageAsync(response); - // Create Response & Respond - var response = new Autogenerated.BulkPublishResponse - { - FailedEntries = { } - }; - var bulkPublishResponse = await request.CompleteWithMessageAsync(response); - - // Get response and validate - bulkPublishResponse.FailedEntries.Count.ShouldBe(0); - } + // Get response and validate + bulkPublishResponse.FailedEntries.Count.ShouldBe(0); + } - [Fact] - public async Task BulkPublishEventAsync_CanPublishTopicWithEventsObject() - { - await using var client = TestClient.CreateForDaprClient(); + [Fact] + public async Task BulkPublishEventAsync_CanPublishTopicWithEventsObject() + { + await using var client = TestClient.CreateForDaprClient(); - var bulkPublishDataObject = new Widget() { Size = "Big", Color = "Green" }; + var bulkPublishDataObject = new Widget() { Size = "Big", Color = "Green" }; - var request = await client.CaptureGrpcRequestAsync(async daprClient => - await daprClient.BulkPublishEventAsync(TestPubsubName, TestTopicName, + var request = await client.CaptureGrpcRequestAsync(async daprClient => + await daprClient.BulkPublishEventAsync(TestPubsubName, TestTopicName, new List { bulkPublishDataObject }, null)); - request.Dismiss(); + request.Dismiss(); - var envelope = await request.GetRequestEnvelopeAsync(); - envelope.Entries.Count.ShouldBe(1); - envelope.PubsubName.ShouldBe(TestPubsubName); - envelope.Topic.ShouldBe(TestTopicName); - envelope.Metadata.Count.ShouldBe(0); + var envelope = await request.GetRequestEnvelopeAsync(); + envelope.Entries.Count.ShouldBe(1); + envelope.PubsubName.ShouldBe(TestPubsubName); + envelope.Topic.ShouldBe(TestTopicName); + envelope.Metadata.Count.ShouldBe(0); - var firstEntry = envelope.Entries[0]; + var firstEntry = envelope.Entries[0]; - firstEntry.EntryId.ShouldBe("0"); - firstEntry.ContentType.ShouldBe(TestContentType); - firstEntry.Event.ToStringUtf8().ShouldBe(JsonSerializer.Serialize(bulkPublishDataObject, client.InnerClient.JsonSerializerOptions)); - firstEntry.Metadata.ShouldBeEmpty(); + firstEntry.EntryId.ShouldBe("0"); + firstEntry.ContentType.ShouldBe(TestContentType); + firstEntry.Event.ToStringUtf8().ShouldBe(JsonSerializer.Serialize(bulkPublishDataObject, client.InnerClient.JsonSerializerOptions)); + firstEntry.Metadata.ShouldBeEmpty(); - // Create Response & Respond - var response = new Autogenerated.BulkPublishResponse - { - FailedEntries = { } - }; - var bulkPublishResponse = await request.CompleteWithMessageAsync(response); + // Create Response & Respond + var response = new Autogenerated.BulkPublishResponse + { + FailedEntries = { } + }; + var bulkPublishResponse = await request.CompleteWithMessageAsync(response); - // Get response and validate - bulkPublishResponse.FailedEntries.Count.ShouldBe(0); - } - - [Fact] - public async Task BulkPublishEventAsync_WithCancelledToken() - { - await using var client = TestClient.CreateForDaprClient(); - - var cts = new CancellationTokenSource(); - cts.Cancel(); - - await Assert.ThrowsAsync(async () => - { - await client.InnerClient.BulkPublishEventAsync(TestPubsubName, TestTopicName, bulkPublishData, - null, cancellationToken: cts.Token); - }); - } - - [Fact] - public async Task BulkPublishEventAsync_WrapsRpcException() - { - var client = new MockClient(); - - var rpcStatus = new Status(StatusCode.Internal, "not gonna work"); - var rpcException = new RpcException(rpcStatus, new Metadata(), "not gonna work"); - - // Setup the mock client to throw an Rpc Exception with the expected details info - client.Mock - .Setup(m => m.BulkPublishEventAlpha1Async( - It.IsAny(), - It.IsAny())) - .Throws(rpcException); - - var ex = await Assert.ThrowsAsync(async () => - { - await client.DaprClient.BulkPublishEventAsync(TestPubsubName, TestTopicName, - bulkPublishData); - }); - Assert.Same(rpcException, ex.InnerException); - } - - [Fact] - public async Task BulkPublishEventAsync_FailureResponse() - { - await using var client = TestClient.CreateForDaprClient(); - - var metadata = new Dictionary { { "key1", "value1" }, { "key2", "value2" } }; - - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.BulkPublishEventAsync(TestPubsubName, TestTopicName, - bulkPublishData, metadata); - }); - - request.Dismiss(); - - // Create Response & Respond - var response = new Autogenerated.BulkPublishResponse - { - FailedEntries = - { - new Autogenerated.BulkPublishResponseFailedEntry - { - EntryId = "0", - Error = "Failed to publish", - }, - new Autogenerated.BulkPublishResponseFailedEntry - { - EntryId = "1", - Error = "Failed to publish", - }, - } - }; - var bulkPublishResponse = await request.CompleteWithMessageAsync(response); - - // Get response and validate - bulkPublishResponse.FailedEntries[0].Entry.EntryId.ShouldBe("0"); - bulkPublishResponse.FailedEntries[0].ErrorMessage.ShouldBe("Failed to publish"); - - bulkPublishResponse.FailedEntries[1].Entry.EntryId.ShouldBe("1"); - bulkPublishResponse.FailedEntries[1].ErrorMessage.ShouldBe("Failed to publish"); - } - - private class Widget - { - public string Size { get; set; } - public string Color { get; set; } - } + // Get response and validate + bulkPublishResponse.FailedEntries.Count.ShouldBe(0); } -} + + [Fact] + public async Task BulkPublishEventAsync_WithCancelledToken() + { + await using var client = TestClient.CreateForDaprClient(); + + var cts = new CancellationTokenSource(); + cts.Cancel(); + + await Assert.ThrowsAsync(async () => + { + await client.InnerClient.BulkPublishEventAsync(TestPubsubName, TestTopicName, bulkPublishData, + null, cancellationToken: cts.Token); + }); + } + + [Fact] + public async Task BulkPublishEventAsync_WrapsRpcException() + { + var client = new MockClient(); + + var rpcStatus = new Status(StatusCode.Internal, "not gonna work"); + var rpcException = new RpcException(rpcStatus, new Metadata(), "not gonna work"); + + // Setup the mock client to throw an Rpc Exception with the expected details info + client.Mock + .Setup(m => m.BulkPublishEventAlpha1Async( + It.IsAny(), + It.IsAny())) + .Throws(rpcException); + + var ex = await Assert.ThrowsAsync(async () => + { + await client.DaprClient.BulkPublishEventAsync(TestPubsubName, TestTopicName, + bulkPublishData); + }); + Assert.Same(rpcException, ex.InnerException); + } + + [Fact] + public async Task BulkPublishEventAsync_FailureResponse() + { + await using var client = TestClient.CreateForDaprClient(); + + var metadata = new Dictionary { { "key1", "value1" }, { "key2", "value2" } }; + + var request = await client.CaptureGrpcRequestAsync(async daprClient => + { + return await daprClient.BulkPublishEventAsync(TestPubsubName, TestTopicName, + bulkPublishData, metadata); + }); + + request.Dismiss(); + + // Create Response & Respond + var response = new Autogenerated.BulkPublishResponse + { + FailedEntries = + { + new Autogenerated.BulkPublishResponseFailedEntry + { + EntryId = "0", + Error = "Failed to publish", + }, + new Autogenerated.BulkPublishResponseFailedEntry + { + EntryId = "1", + Error = "Failed to publish", + }, + } + }; + var bulkPublishResponse = await request.CompleteWithMessageAsync(response); + + // Get response and validate + bulkPublishResponse.FailedEntries[0].Entry.EntryId.ShouldBe("0"); + bulkPublishResponse.FailedEntries[0].ErrorMessage.ShouldBe("Failed to publish"); + + bulkPublishResponse.FailedEntries[1].Entry.EntryId.ShouldBe("1"); + bulkPublishResponse.FailedEntries[1].ErrorMessage.ShouldBe("Failed to publish"); + } + + private class Widget + { + public string Size { get; set; } + public string Color { get; set; } + } +} \ No newline at end of file diff --git a/test/Dapr.Client.Test/ConfigurationApiTest.cs b/test/Dapr.Client.Test/ConfigurationApiTest.cs index 495026fc..9595f042 100644 --- a/test/Dapr.Client.Test/ConfigurationApiTest.cs +++ b/test/Dapr.Client.Test/ConfigurationApiTest.cs @@ -17,128 +17,127 @@ using Autogenerated = Dapr.Client.Autogen.Grpc.v1; using Xunit; using Shouldly; -namespace Dapr.Client.Test +namespace Dapr.Client.Test; + +public class ConfigurationApiTest { - public class ConfigurationApiTest + [Fact] + public async Task GetConfigurationAsync_WithAllValues_ValidateRequest() { - [Fact] - public async Task GetConfigurationAsync_WithAllValues_ValidateRequest() + await using var client = TestClient.CreateForDaprClient(); + var metadata = new Dictionary { - await using var client = TestClient.CreateForDaprClient(); - var metadata = new Dictionary - { - { "key1", "value1" }, - { "key2", "value2" } - }; - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.GetConfiguration("testStore", new List() { "test_key" }, metadata); - }); - - // Get Request and validate - var envelope = await request.GetRequestEnvelopeAsync(); - envelope.StoreName.ShouldBe("testStore"); - envelope.Keys.ShouldContain("test_key"); - envelope.Metadata.Count.ShouldBe(2); - envelope.Metadata.Keys.Contains("key1").ShouldBeTrue(); - envelope.Metadata.Keys.Contains("key2").ShouldBeTrue(); - envelope.Metadata["key1"].ShouldBe("value1"); - envelope.Metadata["key2"].ShouldBe("value2"); - - // Get response and validate - var invokeResponse = new Autogenerated.GetConfigurationResponse(); - invokeResponse.Items["testKey"] = new Autogenerated.ConfigurationItem() - { - Value = "testValue", - Version = "v1" - }; - - var domainResponse = await request.CompleteWithMessageAsync(invokeResponse); - var configItem = domainResponse.Items["testKey"]; - configItem.Value.ShouldBe("testValue"); - configItem.Version.ShouldBe("v1"); - } - - [Fact] - public async Task GetConfigurationAsync_WithNullKeys_ValidateRequest() + { "key1", "value1" }, + { "key2", "value2" } + }; + var request = await client.CaptureGrpcRequestAsync(async daprClient => { - await using var client = TestClient.CreateForDaprClient(); - var metadata = new Dictionary - { - { "key1", "value1" }, - { "key2", "value2" } - }; - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.GetConfiguration("testStore", null, metadata); - }); + return await daprClient.GetConfiguration("testStore", new List() { "test_key" }, metadata); + }); - // Get Request and validate - var envelope = await request.GetRequestEnvelopeAsync(); - envelope.StoreName.ShouldBe("testStore"); - envelope.Keys.ShouldBeEmpty(); - envelope.Metadata.Count.ShouldBe(2); - envelope.Metadata.Keys.Contains("key1").ShouldBeTrue(); - envelope.Metadata.Keys.Contains("key2").ShouldBeTrue(); - envelope.Metadata["key1"].ShouldBe("value1"); - envelope.Metadata["key2"].ShouldBe("value2"); + // Get Request and validate + var envelope = await request.GetRequestEnvelopeAsync(); + envelope.StoreName.ShouldBe("testStore"); + envelope.Keys.ShouldContain("test_key"); + envelope.Metadata.Count.ShouldBe(2); + envelope.Metadata.Keys.Contains("key1").ShouldBeTrue(); + envelope.Metadata.Keys.Contains("key2").ShouldBeTrue(); + envelope.Metadata["key1"].ShouldBe("value1"); + envelope.Metadata["key2"].ShouldBe("value2"); - // Get response and validate - var invokeResponse = new Autogenerated.GetConfigurationResponse(); - invokeResponse.Items["testKey"] = new Autogenerated.ConfigurationItem() - { - Value = "testValue", - Version = "v1" - }; - - var domainResponse = await request.CompleteWithMessageAsync(invokeResponse); - var configItem = domainResponse.Items["testKey"]; - configItem.Value.ShouldBe("testValue"); - configItem.Version.ShouldBe("v1"); - } - - [Fact] - public async Task GetConfigurationAsync_WithNullMetadata_ValidateRequest() + // Get response and validate + var invokeResponse = new Autogenerated.GetConfigurationResponse(); + invokeResponse.Items["testKey"] = new Autogenerated.ConfigurationItem() { - await using var client = TestClient.CreateForDaprClient(); - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.GetConfiguration("testStore", new List() { "test_key" }); - }); + Value = "testValue", + Version = "v1" + }; - // Get Request and validate - var envelope = await request.GetRequestEnvelopeAsync(); - envelope.StoreName.ShouldBe("testStore"); - envelope.Keys.ShouldContain("test_key"); - envelope.Metadata.Count.ShouldBe(0); - - // Get response and validate - var invokeResponse = new Autogenerated.GetConfigurationResponse(); - invokeResponse.Items["testKey"] = new Autogenerated.ConfigurationItem() - { - Value = "testValue", - Version = "v1" - }; - - var domainResponse = await request.CompleteWithMessageAsync(invokeResponse); - var configItem = domainResponse.Items["testKey"]; - configItem.Value.ShouldBe("testValue"); - configItem.Version.ShouldBe("v1"); - } - - [Fact] - public async Task UnsubscribeConfiguration_ValidateRequest() - { - await using var client = TestClient.CreateForDaprClient(); - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.UnsubscribeConfiguration("testStore", "testId"); - }); - - var envelope = await request.GetRequestEnvelopeAsync(); - envelope.StoreName.ShouldBe("testStore"); - envelope.Id.ShouldBe("testId"); - request.Dismiss(); - } + var domainResponse = await request.CompleteWithMessageAsync(invokeResponse); + var configItem = domainResponse.Items["testKey"]; + configItem.Value.ShouldBe("testValue"); + configItem.Version.ShouldBe("v1"); } -} + + [Fact] + public async Task GetConfigurationAsync_WithNullKeys_ValidateRequest() + { + await using var client = TestClient.CreateForDaprClient(); + var metadata = new Dictionary + { + { "key1", "value1" }, + { "key2", "value2" } + }; + var request = await client.CaptureGrpcRequestAsync(async daprClient => + { + return await daprClient.GetConfiguration("testStore", null, metadata); + }); + + // Get Request and validate + var envelope = await request.GetRequestEnvelopeAsync(); + envelope.StoreName.ShouldBe("testStore"); + envelope.Keys.ShouldBeEmpty(); + envelope.Metadata.Count.ShouldBe(2); + envelope.Metadata.Keys.Contains("key1").ShouldBeTrue(); + envelope.Metadata.Keys.Contains("key2").ShouldBeTrue(); + envelope.Metadata["key1"].ShouldBe("value1"); + envelope.Metadata["key2"].ShouldBe("value2"); + + // Get response and validate + var invokeResponse = new Autogenerated.GetConfigurationResponse(); + invokeResponse.Items["testKey"] = new Autogenerated.ConfigurationItem() + { + Value = "testValue", + Version = "v1" + }; + + var domainResponse = await request.CompleteWithMessageAsync(invokeResponse); + var configItem = domainResponse.Items["testKey"]; + configItem.Value.ShouldBe("testValue"); + configItem.Version.ShouldBe("v1"); + } + + [Fact] + public async Task GetConfigurationAsync_WithNullMetadata_ValidateRequest() + { + await using var client = TestClient.CreateForDaprClient(); + var request = await client.CaptureGrpcRequestAsync(async daprClient => + { + return await daprClient.GetConfiguration("testStore", new List() { "test_key" }); + }); + + // Get Request and validate + var envelope = await request.GetRequestEnvelopeAsync(); + envelope.StoreName.ShouldBe("testStore"); + envelope.Keys.ShouldContain("test_key"); + envelope.Metadata.Count.ShouldBe(0); + + // Get response and validate + var invokeResponse = new Autogenerated.GetConfigurationResponse(); + invokeResponse.Items["testKey"] = new Autogenerated.ConfigurationItem() + { + Value = "testValue", + Version = "v1" + }; + + var domainResponse = await request.CompleteWithMessageAsync(invokeResponse); + var configItem = domainResponse.Items["testKey"]; + configItem.Value.ShouldBe("testValue"); + configItem.Version.ShouldBe("v1"); + } + + [Fact] + public async Task UnsubscribeConfiguration_ValidateRequest() + { + await using var client = TestClient.CreateForDaprClient(); + var request = await client.CaptureGrpcRequestAsync(async daprClient => + { + return await daprClient.UnsubscribeConfiguration("testStore", "testId"); + }); + + var envelope = await request.GetRequestEnvelopeAsync(); + envelope.StoreName.ShouldBe("testStore"); + envelope.Id.ShouldBe("testId"); + request.Dismiss(); + } +} \ No newline at end of file diff --git a/test/Dapr.Client.Test/ConfigurationSourceTest.cs b/test/Dapr.Client.Test/ConfigurationSourceTest.cs index c381d41d..d9f59bac 100644 --- a/test/Dapr.Client.Test/ConfigurationSourceTest.cs +++ b/test/Dapr.Client.Test/ConfigurationSourceTest.cs @@ -7,98 +7,97 @@ using Moq; using Xunit; using Autogenerated = Dapr.Client.Autogen.Grpc.v1; -namespace Dapr.Client.Test +namespace Dapr.Client.Test; + +public class ConfigurationSourceTest { - public class ConfigurationSourceTest + private readonly string StoreName = "testStore"; + private readonly string SubscribeId = "testSubscribe"; + + [Fact] + public async Task TestStreamingConfigurationSourceCanBeRead() { - private readonly string StoreName = "testStore"; - private readonly string SubscribeId = "testSubscribe"; - - [Fact] - public async Task TestStreamingConfigurationSourceCanBeRead() + // Standard values that we don't need to Mock. + using var cts = new CancellationTokenSource(); + var streamRequest = new Autogenerated.SubscribeConfigurationRequest() { - // Standard values that we don't need to Mock. - using var cts = new CancellationTokenSource(); - var streamRequest = new Autogenerated.SubscribeConfigurationRequest() - { - StoreName = StoreName - }; - var callOptions = new CallOptions(cancellationToken: cts.Token); - var item1 = new Autogenerated.ConfigurationItem() - { - Value = "testValue1", - Version = "V1", - }; - var item2 = new Autogenerated.ConfigurationItem() - { - Value = "testValue2", - Version = "V1", - }; - var responses = new List() - { - new Autogenerated.SubscribeConfigurationResponse() { Id = SubscribeId }, - new Autogenerated.SubscribeConfigurationResponse() { Id = SubscribeId }, - }; - responses[0].Items["testKey1"] = item1; - responses[1].Items["testKey2"] = item2; + StoreName = StoreName + }; + var callOptions = new CallOptions(cancellationToken: cts.Token); + var item1 = new Autogenerated.ConfigurationItem() + { + Value = "testValue1", + Version = "V1", + }; + var item2 = new Autogenerated.ConfigurationItem() + { + Value = "testValue2", + Version = "V1", + }; + var responses = new List() + { + new Autogenerated.SubscribeConfigurationResponse() { Id = SubscribeId }, + new Autogenerated.SubscribeConfigurationResponse() { Id = SubscribeId }, + }; + responses[0].Items["testKey1"] = item1; + responses[1].Items["testKey2"] = item2; - // Setup the Mock and actions. - var internalClient = Mock.Of(); - var responseStream = new TestAsyncStreamReader(responses, TimeSpan.FromMilliseconds(100)); - var response = new AsyncServerStreamingCall(responseStream, null, null, null, async () => await Task.Delay(TimeSpan.FromMilliseconds(1))); - Mock.Get(internalClient).Setup(client => client.SubscribeConfiguration(streamRequest, callOptions)) - .Returns(response); + // Setup the Mock and actions. + var internalClient = Mock.Of(); + var responseStream = new TestAsyncStreamReader(responses, TimeSpan.FromMilliseconds(100)); + var response = new AsyncServerStreamingCall(responseStream, null, null, null, async () => await Task.Delay(TimeSpan.FromMilliseconds(1))); + Mock.Get(internalClient).Setup(client => client.SubscribeConfiguration(streamRequest, callOptions)) + .Returns(response); - // Try and actually use the source. - var source = new SubscribeConfigurationResponse(new DaprSubscribeConfigurationSource(response)); - Dictionary readItems = new Dictionary(); - await foreach (var items in source.Source) + // Try and actually use the source. + var source = new SubscribeConfigurationResponse(new DaprSubscribeConfigurationSource(response)); + Dictionary readItems = new Dictionary(); + await foreach (var items in source.Source) + { + foreach (var item in items) { - foreach (var item in items) - { - readItems[item.Key] = new ConfigurationItem(item.Value.Value, item.Value.Version, item.Value.Metadata); - } + readItems[item.Key] = new ConfigurationItem(item.Value.Value, item.Value.Version, item.Value.Metadata); } - - var expectedItems = new Dictionary(); - expectedItems["testKey1"] = new ConfigurationItem("testValue1", "V1", null); - expectedItems["testKey2"] = new ConfigurationItem("testValue2", "V1", null); - Assert.Equal(SubscribeId, source.Id); - Assert.Equal(expectedItems.Count, readItems.Count); - // The gRPC metadata stops us from just doing the direct list comparison. - - var expectedConfigItem1 = expectedItems["testKey1"]; - var expectedConfigItem2 = expectedItems["testKey2"]; - var readConfigItem1 = expectedItems["testKey1"]; - var readConfigItem2 = expectedItems["testKey2"]; - - Assert.Equal(expectedConfigItem1.Value, readConfigItem1.Value); - Assert.Equal(expectedConfigItem1.Version, readConfigItem1.Version); - Assert.Equal(expectedConfigItem1.Metadata, readConfigItem1.Metadata); - Assert.Equal(expectedConfigItem2.Value, readConfigItem2.Value); - Assert.Equal(expectedConfigItem2.Version, readConfigItem2.Version); - Assert.Equal(expectedConfigItem2.Metadata, readConfigItem2.Metadata); } - private class TestAsyncStreamReader : IAsyncStreamReader + var expectedItems = new Dictionary(); + expectedItems["testKey1"] = new ConfigurationItem("testValue1", "V1", null); + expectedItems["testKey2"] = new ConfigurationItem("testValue2", "V1", null); + Assert.Equal(SubscribeId, source.Id); + Assert.Equal(expectedItems.Count, readItems.Count); + // The gRPC metadata stops us from just doing the direct list comparison. + + var expectedConfigItem1 = expectedItems["testKey1"]; + var expectedConfigItem2 = expectedItems["testKey2"]; + var readConfigItem1 = expectedItems["testKey1"]; + var readConfigItem2 = expectedItems["testKey2"]; + + Assert.Equal(expectedConfigItem1.Value, readConfigItem1.Value); + Assert.Equal(expectedConfigItem1.Version, readConfigItem1.Version); + Assert.Equal(expectedConfigItem1.Metadata, readConfigItem1.Metadata); + Assert.Equal(expectedConfigItem2.Value, readConfigItem2.Value); + Assert.Equal(expectedConfigItem2.Version, readConfigItem2.Version); + Assert.Equal(expectedConfigItem2.Metadata, readConfigItem2.Metadata); + } + + private class TestAsyncStreamReader : IAsyncStreamReader + { + private IEnumerator enumerator; + private TimeSpan simulatedWaitTime; + + public TestAsyncStreamReader(IList items, TimeSpan simulatedWaitTime) { - private IEnumerator enumerator; - private TimeSpan simulatedWaitTime; + this.enumerator = items.GetEnumerator(); + this.simulatedWaitTime = simulatedWaitTime; + } - public TestAsyncStreamReader(IList items, TimeSpan simulatedWaitTime) - { - this.enumerator = items.GetEnumerator(); - this.simulatedWaitTime = simulatedWaitTime; - } + public T Current => enumerator.Current; - public T Current => enumerator.Current; - - public async Task MoveNext(CancellationToken cancellationToken) - { - // Add a little delay to pretend we're getting responses from a server stream. - await Task.Delay(simulatedWaitTime, cancellationToken); - return enumerator.MoveNext(); - } + public async Task MoveNext(CancellationToken cancellationToken) + { + // Add a little delay to pretend we're getting responses from a server stream. + await Task.Delay(simulatedWaitTime, cancellationToken); + return enumerator.MoveNext(); } } -} +} \ No newline at end of file diff --git a/test/Dapr.Client.Test/CryptographyApiTest.cs b/test/Dapr.Client.Test/CryptographyApiTest.cs index a7d57a09..4bc0915e 100644 --- a/test/Dapr.Client.Test/CryptographyApiTest.cs +++ b/test/Dapr.Client.Test/CryptographyApiTest.cs @@ -5,91 +5,90 @@ using System.Threading.Tasks; using Xunit; #pragma warning disable CS0618 // Type or member is obsolete -namespace Dapr.Client.Test +namespace Dapr.Client.Test; + +public class CryptographyApiTest { - public class CryptographyApiTest + [Fact] + public async Task EncryptAsync_ByteArray_VaultResourceName_ArgumentVerifierException() { - [Fact] - public async Task EncryptAsync_ByteArray_VaultResourceName_ArgumentVerifierException() - { - var client = new DaprClientBuilder().Build(); - const string vaultResourceName = ""; - //Get response and validate - await Assert.ThrowsAsync(async () => await client.EncryptAsync(vaultResourceName, - (ReadOnlyMemory)Array.Empty(), "MyKey", new EncryptionOptions(KeyWrapAlgorithm.Rsa), - CancellationToken.None)); - } - - [Fact] - public async Task EncryptAsync_ByteArray_KeyName_ArgumentVerifierException() - { - var client = new DaprClientBuilder().Build(); - const string keyName = ""; - //Get response and validate - await Assert.ThrowsAsync(async () => await client.EncryptAsync( "myVault", - (ReadOnlyMemory) Array.Empty(), keyName, new EncryptionOptions(KeyWrapAlgorithm.Rsa), CancellationToken.None)); - } - - [Fact] - public async Task EncryptAsync_Stream_VaultResourceName_ArgumentVerifierException() - { - var client = new DaprClientBuilder().Build(); - const string vaultResourceName = ""; - //Get response and validate - await Assert.ThrowsAsync(async () => await client.EncryptAsync(vaultResourceName, - new MemoryStream(), "MyKey", new EncryptionOptions(KeyWrapAlgorithm.Rsa), - CancellationToken.None)); - } - - [Fact] - public async Task EncryptAsync_Stream_KeyName_ArgumentVerifierException() - { - var client = new DaprClientBuilder().Build(); - const string keyName = ""; - //Get response and validate - await Assert.ThrowsAsync(async () => await client.EncryptAsync("myVault", - (Stream) new MemoryStream(), keyName, new EncryptionOptions(KeyWrapAlgorithm.Rsa), - CancellationToken.None)); - } - - [Fact] - public async Task DecryptAsync_ByteArray_VaultResourceName_ArgumentVerifierException() - { - var client = new DaprClientBuilder().Build(); - const string vaultResourceName = ""; - //Get response and validate - await Assert.ThrowsAsync(async () => await client.DecryptAsync(vaultResourceName, - Array.Empty(), "myKey", new DecryptionOptions(), CancellationToken.None)); - } - - [Fact] - public async Task DecryptAsync_ByteArray_KeyName_ArgumentVerifierException() - { - var client = new DaprClientBuilder().Build(); - const string keyName = ""; - //Get response and validate - await Assert.ThrowsAsync(async () => await client.DecryptAsync("myVault", - Array.Empty(), keyName, new DecryptionOptions(), CancellationToken.None)); - } - - [Fact] - public async Task DecryptAsync_Stream_VaultResourceName_ArgumentVerifierException() - { - var client = new DaprClientBuilder().Build(); - const string vaultResourceName = ""; - //Get response and validate - await Assert.ThrowsAsync(async () => await client.DecryptAsync(vaultResourceName, - new MemoryStream(), "MyKey", new DecryptionOptions(), CancellationToken.None)); - } - - [Fact] - public async Task DecryptAsync_Stream_KeyName_ArgumentVerifierException() - { - var client = new DaprClientBuilder().Build(); - const string keyName = ""; - //Get response and validate - await Assert.ThrowsAsync(async () => await client.DecryptAsync("myVault", - new MemoryStream(), keyName, new DecryptionOptions(), CancellationToken.None)); - } + var client = new DaprClientBuilder().Build(); + const string vaultResourceName = ""; + //Get response and validate + await Assert.ThrowsAsync(async () => await client.EncryptAsync(vaultResourceName, + (ReadOnlyMemory)Array.Empty(), "MyKey", new EncryptionOptions(KeyWrapAlgorithm.Rsa), + CancellationToken.None)); } -} + + [Fact] + public async Task EncryptAsync_ByteArray_KeyName_ArgumentVerifierException() + { + var client = new DaprClientBuilder().Build(); + const string keyName = ""; + //Get response and validate + await Assert.ThrowsAsync(async () => await client.EncryptAsync( "myVault", + (ReadOnlyMemory) Array.Empty(), keyName, new EncryptionOptions(KeyWrapAlgorithm.Rsa), CancellationToken.None)); + } + + [Fact] + public async Task EncryptAsync_Stream_VaultResourceName_ArgumentVerifierException() + { + var client = new DaprClientBuilder().Build(); + const string vaultResourceName = ""; + //Get response and validate + await Assert.ThrowsAsync(async () => await client.EncryptAsync(vaultResourceName, + new MemoryStream(), "MyKey", new EncryptionOptions(KeyWrapAlgorithm.Rsa), + CancellationToken.None)); + } + + [Fact] + public async Task EncryptAsync_Stream_KeyName_ArgumentVerifierException() + { + var client = new DaprClientBuilder().Build(); + const string keyName = ""; + //Get response and validate + await Assert.ThrowsAsync(async () => await client.EncryptAsync("myVault", + (Stream) new MemoryStream(), keyName, new EncryptionOptions(KeyWrapAlgorithm.Rsa), + CancellationToken.None)); + } + + [Fact] + public async Task DecryptAsync_ByteArray_VaultResourceName_ArgumentVerifierException() + { + var client = new DaprClientBuilder().Build(); + const string vaultResourceName = ""; + //Get response and validate + await Assert.ThrowsAsync(async () => await client.DecryptAsync(vaultResourceName, + Array.Empty(), "myKey", new DecryptionOptions(), CancellationToken.None)); + } + + [Fact] + public async Task DecryptAsync_ByteArray_KeyName_ArgumentVerifierException() + { + var client = new DaprClientBuilder().Build(); + const string keyName = ""; + //Get response and validate + await Assert.ThrowsAsync(async () => await client.DecryptAsync("myVault", + Array.Empty(), keyName, new DecryptionOptions(), CancellationToken.None)); + } + + [Fact] + public async Task DecryptAsync_Stream_VaultResourceName_ArgumentVerifierException() + { + var client = new DaprClientBuilder().Build(); + const string vaultResourceName = ""; + //Get response and validate + await Assert.ThrowsAsync(async () => await client.DecryptAsync(vaultResourceName, + new MemoryStream(), "MyKey", new DecryptionOptions(), CancellationToken.None)); + } + + [Fact] + public async Task DecryptAsync_Stream_KeyName_ArgumentVerifierException() + { + var client = new DaprClientBuilder().Build(); + const string keyName = ""; + //Get response and validate + await Assert.ThrowsAsync(async () => await client.DecryptAsync("myVault", + new MemoryStream(), keyName, new DecryptionOptions(), CancellationToken.None)); + } +} \ No newline at end of file diff --git a/test/Dapr.Client.Test/DaprApiTokenTest.cs b/test/Dapr.Client.Test/DaprApiTokenTest.cs index e96d29d0..6b6b32b5 100644 --- a/test/Dapr.Client.Test/DaprApiTokenTest.cs +++ b/test/Dapr.Client.Test/DaprApiTokenTest.cs @@ -11,55 +11,54 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Client.Test +namespace Dapr.Client.Test; + +using System.Linq; +using System.Threading.Tasks; +using Shouldly; +using Xunit; +using Autogenerated = Dapr.Client.Autogen.Grpc.v1; + +public class DaprApiTokenTest { - using System.Linq; - using System.Threading.Tasks; - using Shouldly; - using Xunit; - using Autogenerated = Dapr.Client.Autogen.Grpc.v1; - - public class DaprApiTokenTest + [Fact] + public async Task DaprCall_WithApiTokenSet() { - [Fact] - public async Task DaprCall_WithApiTokenSet() + // Configure Client + await using var client = TestClient.CreateForDaprClient(c => c.UseDaprApiToken("test_token")); + + var request = await client.CaptureGrpcRequestAsync(async daprClient => { - // Configure Client - await using var client = TestClient.CreateForDaprClient(c => c.UseDaprApiToken("test_token")); + return await daprClient.GetSecretAsync("testStore", "test_key"); + }); - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.GetSecretAsync("testStore", "test_key"); - }); + request.Dismiss(); - request.Dismiss(); + // Get Request and validate + await request.GetRequestEnvelopeAsync(); - // Get Request and validate - await request.GetRequestEnvelopeAsync(); - - request.Request.Headers.TryGetValues("dapr-api-token", out var headerValues); - headerValues.Count().ShouldBe(1); - headerValues.First().ShouldBe("test_token"); - } - - [Fact] - public async Task DaprCall_WithoutApiToken() - { - // Configure Client - await using var client = TestClient.CreateForDaprClient(); - - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.GetSecretAsync("testStore", "test_key"); - }); - - request.Dismiss(); - - // Get Request and validate - await request.GetRequestEnvelopeAsync(); - - request.Request.Headers.TryGetValues("dapr-api-token", out var headerValues); - headerValues.ShouldBeNull(); - } + request.Request.Headers.TryGetValues("dapr-api-token", out var headerValues); + headerValues.Count().ShouldBe(1); + headerValues.First().ShouldBe("test_token"); } -} + + [Fact] + public async Task DaprCall_WithoutApiToken() + { + // Configure Client + await using var client = TestClient.CreateForDaprClient(); + + var request = await client.CaptureGrpcRequestAsync(async daprClient => + { + return await daprClient.GetSecretAsync("testStore", "test_key"); + }); + + request.Dismiss(); + + // Get Request and validate + await request.GetRequestEnvelopeAsync(); + + request.Request.Headers.TryGetValues("dapr-api-token", out var headerValues); + headerValues.ShouldBeNull(); + } +} \ No newline at end of file diff --git a/test/Dapr.Client.Test/DaprClientTest.CreateInvokableHttpClientTest.cs b/test/Dapr.Client.Test/DaprClientTest.CreateInvokableHttpClientTest.cs index 99fbd497..a37fe892 100644 --- a/test/Dapr.Client.Test/DaprClientTest.CreateInvokableHttpClientTest.cs +++ b/test/Dapr.Client.Test/DaprClientTest.CreateInvokableHttpClientTest.cs @@ -14,39 +14,38 @@ using System; using Xunit; -namespace Dapr.Client +namespace Dapr.Client; + +public partial class DaprClientTest { - public partial class DaprClientTest + [Fact] + public void CreateInvokableHttpClient_WithAppId_FromDaprClient() { - [Fact] - public void CreateInvokableHttpClient_WithAppId_FromDaprClient() - { - var daprClient = new MockClient().DaprClient; - var client = daprClient.CreateInvokableHttpClient(appId: "bank"); - Assert.Equal("http://bank/", client.BaseAddress.AbsoluteUri); - } - - [Fact] - public void CreateInvokableHttpClient_InvalidAppId_FromDaprClient() - { - var daprClient = new MockClient().DaprClient; - var ex = Assert.Throws(() => - { - // The appId needs to be something that can be used as hostname in a URI. - _ = daprClient.CreateInvokableHttpClient(appId: ""); - }); - - Assert.Contains("The appId must be a valid hostname.", ex.Message); - Assert.IsType(ex.InnerException); - } - - [Fact] - public void CreateInvokableHttpClient_WithoutAppId_FromDaprClient() - { - var daprClient = new MockClient().DaprClient; - - var client = daprClient.CreateInvokableHttpClient(); - Assert.Null(client.BaseAddress); - } + var daprClient = new MockClient().DaprClient; + var client = daprClient.CreateInvokableHttpClient(appId: "bank"); + Assert.Equal("http://bank/", client.BaseAddress.AbsoluteUri); } -} + + [Fact] + public void CreateInvokableHttpClient_InvalidAppId_FromDaprClient() + { + var daprClient = new MockClient().DaprClient; + var ex = Assert.Throws(() => + { + // The appId needs to be something that can be used as hostname in a URI. + _ = daprClient.CreateInvokableHttpClient(appId: ""); + }); + + Assert.Contains("The appId must be a valid hostname.", ex.Message); + Assert.IsType(ex.InnerException); + } + + [Fact] + public void CreateInvokableHttpClient_WithoutAppId_FromDaprClient() + { + var daprClient = new MockClient().DaprClient; + + var client = daprClient.CreateInvokableHttpClient(); + Assert.Null(client.BaseAddress); + } +} \ No newline at end of file diff --git a/test/Dapr.Client.Test/DaprClientTest.InvokeMethodAsync.cs b/test/Dapr.Client.Test/DaprClientTest.InvokeMethodAsync.cs index 58663227..89f93aa5 100644 --- a/test/Dapr.Client.Test/DaprClientTest.InvokeMethodAsync.cs +++ b/test/Dapr.Client.Test/DaprClientTest.InvokeMethodAsync.cs @@ -15,736 +15,735 @@ using System.Collections.Generic; using System.Linq; using System.Net.Http.Headers; -namespace Dapr.Client.Test -{ - using System; - using System.Net; - using System.Net.Http; - using System.Net.Http.Json; - using System.Text; - using System.Text.Json; - using System.Threading; - using System.Threading.Tasks; - using Dapr.Client; - using Xunit; +namespace Dapr.Client.Test; - // Most of the InvokeMethodAsync functionality on DaprClient is non-abstract methods that - // forward to a few different request points to create a message, or send a message and process - // its result. - // - // So we write basic tests for all of those that every parameter passing is correct, and then - // test the specialized methods in detail. - public partial class DaprClientTest +using System; +using System.Net; +using System.Net.Http; +using System.Net.Http.Json; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Dapr.Client; +using Xunit; + +// Most of the InvokeMethodAsync functionality on DaprClient is non-abstract methods that +// forward to a few different request points to create a message, or send a message and process +// its result. +// +// So we write basic tests for all of those that every parameter passing is correct, and then +// test the specialized methods in detail. +public partial class DaprClientTest +{ + private readonly JsonSerializerOptions jsonSerializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web) { - private readonly JsonSerializerOptions jsonSerializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web) + // Use case sensitive settings for tests, this way we verify that the same settings are being + // used in all calls. + PropertyNameCaseInsensitive = false, + }; + + [Fact] + public async Task InvokeMethodAsync_VoidVoidNoHttpMethod_Success() + { + await using var client = TestClient.CreateForDaprClient(c => { - // Use case sensitive settings for tests, this way we verify that the same settings are being - // used in all calls. - PropertyNameCaseInsensitive = false, + c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions); + }); + + var request = await client.CaptureHttpRequestAsync(async daprClient => + { + await daprClient.InvokeMethodAsync("app1", "mymethod"); + }); + + // Get Request and validate + Assert.Equal(request.Request.Method, HttpMethod.Post); + Assert.Equal(new Uri("https://test-endpoint:3501/v1.0/invoke/app1/method/mymethod").AbsoluteUri, request.Request.RequestUri.AbsoluteUri); + Assert.Null(request.Request.Content); + + await request.CompleteAsync(new HttpResponseMessage()); + } + + [Fact] + public async Task InvokeMethodAsync_VoidVoidWithHttpMethod_Success() + { + await using var client = TestClient.CreateForDaprClient(c => + { + c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions); + }); + + var request = await client.CaptureHttpRequestAsync(async daprClient => + { + await daprClient.InvokeMethodAsync(HttpMethod.Put, "app1", "mymethod"); + }); + + // Get Request and validate + Assert.Equal(request.Request.Method, HttpMethod.Put); + Assert.Equal(new Uri("https://test-endpoint:3501/v1.0/invoke/app1/method/mymethod").AbsoluteUri, request.Request.RequestUri.AbsoluteUri); + Assert.Null(request.Request.Content); + + await request.CompleteAsync(new HttpResponseMessage()); + } + + [Fact] + public async Task InvokeMethodAsync_VoidResponseNoHttpMethod_Success() + { + await using var client = TestClient.CreateForDaprClient(c => + { + c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions); + }); + + var request = await client.CaptureHttpRequestAsync(async daprClient => + { + return await daprClient.InvokeMethodAsync("app1", "mymethod"); + }); + + // Get Request and validate + Assert.Equal(request.Request.Method, HttpMethod.Post); + Assert.Equal(new Uri("https://test-endpoint:3501/v1.0/invoke/app1/method/mymethod").AbsoluteUri, request.Request.RequestUri.AbsoluteUri); + Assert.Null(request.Request.Content); + + var expected = new Widget() + { + Color = "red", }; - [Fact] - public async Task InvokeMethodAsync_VoidVoidNoHttpMethod_Success() - { - await using var client = TestClient.CreateForDaprClient(c => - { - c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions); - }); - - var request = await client.CaptureHttpRequestAsync(async daprClient => - { - await daprClient.InvokeMethodAsync("app1", "mymethod"); - }); - - // Get Request and validate - Assert.Equal(request.Request.Method, HttpMethod.Post); - Assert.Equal(new Uri("https://test-endpoint:3501/v1.0/invoke/app1/method/mymethod").AbsoluteUri, request.Request.RequestUri.AbsoluteUri); - Assert.Null(request.Request.Content); - - await request.CompleteAsync(new HttpResponseMessage()); - } - - [Fact] - public async Task InvokeMethodAsync_VoidVoidWithHttpMethod_Success() - { - await using var client = TestClient.CreateForDaprClient(c => - { - c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions); - }); - - var request = await client.CaptureHttpRequestAsync(async daprClient => - { - await daprClient.InvokeMethodAsync(HttpMethod.Put, "app1", "mymethod"); - }); - - // Get Request and validate - Assert.Equal(request.Request.Method, HttpMethod.Put); - Assert.Equal(new Uri("https://test-endpoint:3501/v1.0/invoke/app1/method/mymethod").AbsoluteUri, request.Request.RequestUri.AbsoluteUri); - Assert.Null(request.Request.Content); - - await request.CompleteAsync(new HttpResponseMessage()); - } - - [Fact] - public async Task InvokeMethodAsync_VoidResponseNoHttpMethod_Success() - { - await using var client = TestClient.CreateForDaprClient(c => - { - c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions); - }); - - var request = await client.CaptureHttpRequestAsync(async daprClient => - { - return await daprClient.InvokeMethodAsync("app1", "mymethod"); - }); - - // Get Request and validate - Assert.Equal(request.Request.Method, HttpMethod.Post); - Assert.Equal(new Uri("https://test-endpoint:3501/v1.0/invoke/app1/method/mymethod").AbsoluteUri, request.Request.RequestUri.AbsoluteUri); - Assert.Null(request.Request.Content); - - var expected = new Widget() - { - Color = "red", - }; - - var actual = await request.CompleteWithJsonAsync(expected, jsonSerializerOptions); - Assert.Equal(expected.Color, actual.Color); - } - - [Fact] - public async Task InvokeMethodAsync_VoidResponseWithHttpMethod_Success() - { - await using var client = TestClient.CreateForDaprClient(c => - { - c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions); - }); - - var request = await client.CaptureHttpRequestAsync(async daprClient => - { - return await daprClient.InvokeMethodAsync(HttpMethod.Put, "app1", "mymethod"); - }); - - Assert.Equal(request.Request.Method, HttpMethod.Put); - Assert.Equal(new Uri("https://test-endpoint:3501/v1.0/invoke/app1/method/mymethod").AbsoluteUri, request.Request.RequestUri.AbsoluteUri); - Assert.Null(request.Request.Content); - - var expected = new Widget() - { - Color = "red", - }; - - var actual = await request.CompleteWithJsonAsync(expected, jsonSerializerOptions); - Assert.Equal(expected.Color, actual.Color); - } - - [Fact] - public async Task InvokeMethodAsync_RequestVoidNoHttpMethod_Success() - { - await using var client = TestClient.CreateForDaprClient(c => - { - c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions); - }); - - var data = new Widget() - { - Color = "red", - }; - - var request = await client.CaptureHttpRequestAsync(async daprClient => - { - await daprClient.InvokeMethodAsync("app1", "mymethod", data); - }); - - // Get Request and validate - Assert.Equal(request.Request.Method, HttpMethod.Post); - Assert.Equal(new Uri("https://test-endpoint:3501/v1.0/invoke/app1/method/mymethod").AbsoluteUri, request.Request.RequestUri.AbsoluteUri); - - var content = Assert.IsType(request.Request.Content); - Assert.Equal(data.GetType(), content.ObjectType); - Assert.Same(data, content.Value); - - await request.CompleteAsync(new HttpResponseMessage()); - } - - [Fact] - public async Task InvokeMethodAsync_RequestVoidWithHttpMethod_Success() - { - await using var client = TestClient.CreateForDaprClient(c => - { - c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions); - }); - - var data = new Widget() - { - Color = "red", - }; - - var request = await client.CaptureHttpRequestAsync(async daprClient => - { - await daprClient.InvokeMethodAsync(HttpMethod.Put, "app1", "mymethod", data); - }); - - // Get Request and validate - Assert.Equal(request.Request.Method, HttpMethod.Put); - Assert.Equal(new Uri("https://test-endpoint:3501/v1.0/invoke/app1/method/mymethod").AbsoluteUri, request.Request.RequestUri.AbsoluteUri); - - var content = Assert.IsType(request.Request.Content); - Assert.Equal(data.GetType(), content.ObjectType); - Assert.Same(data, content.Value); - - await request.CompleteAsync(new HttpResponseMessage()); - } - - [Fact] - public async Task InvokeMethodAsync_RequestResponseNoHttpMethod_Success() - { - await using var client = TestClient.CreateForDaprClient(c => - { - c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions); - }); - - var data = new Widget() - { - Color = "red", - }; - - var request = await client.CaptureHttpRequestAsync(async daprClient => - { - return await daprClient.InvokeMethodAsync("app1", "mymethod", data); - }); - - // Get Request and validate - Assert.Equal(request.Request.Method, HttpMethod.Post); - Assert.Equal(new Uri("https://test-endpoint:3501/v1.0/invoke/app1/method/mymethod").AbsoluteUri, request.Request.RequestUri.AbsoluteUri); - - var content = Assert.IsType(request.Request.Content); - Assert.Equal(data.GetType(), content.ObjectType); - Assert.Same(data, content.Value); - - var actual = await request.CompleteWithJsonAsync(data, jsonSerializerOptions); - Assert.Equal(data.Color, actual.Color); - } - - [Fact] - public async Task InvokeMethodAsync_RequestResponseWithHttpMethod_Success() - { - await using var client = TestClient.CreateForDaprClient(c => - { - c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions); - }); - - var data = new Widget() - { - Color = "red", - }; - - var request = await client.CaptureHttpRequestAsync(async daprClient => - { - return await daprClient.InvokeMethodAsync(HttpMethod.Put, "app1", "mymethod", data); - }); - - // Get Request and validate - Assert.Equal(request.Request.Method, HttpMethod.Put); - Assert.Equal(new Uri("https://test-endpoint:3501/v1.0/invoke/app1/method/mymethod").AbsoluteUri, request.Request.RequestUri.AbsoluteUri); - - var content = Assert.IsType(request.Request.Content); - Assert.Equal(data.GetType(), content.ObjectType); - Assert.Same(data, content.Value); - - var actual = await request.CompleteWithJsonAsync(data, jsonSerializerOptions); - Assert.Equal(data.Color, actual.Color); - } - - [Fact] - public async Task CheckHealthAsync_Success() - { - await using var client = TestClient.CreateForDaprClient(c => - { - c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions); - }); - - var request = await client.CaptureHttpRequestAsync(async daprClient => - await daprClient.CheckHealthAsync()); - - // Get Request and validate - Assert.Equal(request.Request.Method, HttpMethod.Get); - Assert.Equal(new Uri("https://test-endpoint:3501/v1.0/healthz").AbsoluteUri, request.Request.RequestUri.AbsoluteUri); - - var result = await request.CompleteAsync(new HttpResponseMessage()); - Assert.True(result); - } - - [Fact] - public async Task CheckHealthAsync_NotSuccess() - { - await using var client = TestClient.CreateForDaprClient(c => - { - c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions); - }); - - var request = await client.CaptureHttpRequestAsync(async daprClient => - await daprClient.CheckHealthAsync()); - - // Get Request and validate - Assert.Equal(request.Request.Method, HttpMethod.Get); - Assert.Equal(new Uri("https://test-endpoint:3501/v1.0/healthz").AbsoluteUri, request.Request.RequestUri.AbsoluteUri); - - var result = await request.CompleteAsync(new HttpResponseMessage(HttpStatusCode.InternalServerError)); - Assert.False(result); - } - - [Fact] - public async Task CheckHealthAsync_WrapsHttpRequestException() - { - await using var client = TestClient.CreateForDaprClient(c => - { - c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions); - }); - - var request = await client.CaptureHttpRequestAsync(async daprClient => - await daprClient.CheckHealthAsync()); - - Assert.Equal(request.Request.Method, HttpMethod.Get); - Assert.Equal(new Uri("https://test-endpoint:3501/v1.0/healthz").AbsoluteUri, request.Request.RequestUri.AbsoluteUri); - - var exception = new HttpRequestException(); - var result = await request.CompleteWithExceptionAndResultAsync(exception); - Assert.False(result); - } - - [Fact] - public async Task CheckOutboundHealthAsync_Success() - { - await using var client = TestClient.CreateForDaprClient(c => - { - c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions); - }); - var request = await client.CaptureHttpRequestAsync(async daprClient => await daprClient.CheckOutboundHealthAsync()); - - Assert.Equal(request.Request.Method, HttpMethod.Get); - Assert.Equal(new Uri("https://test-endpoint:3501/v1.0/healthz/outbound").AbsoluteUri, request.Request.RequestUri.AbsoluteUri); - - var result = await request.CompleteAsync(new HttpResponseMessage()); - Assert.True(result); - } - - [Fact] - public async Task CheckOutboundHealthAsync_NotSuccess() - { - await using var client = TestClient.CreateForDaprClient(c => - { - c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions); - }); - var request = await client.CaptureHttpRequestAsync(async daprClient => await daprClient.CheckOutboundHealthAsync()); - - Assert.Equal(request.Request.Method, HttpMethod.Get); - Assert.Equal(new Uri("https://test-endpoint:3501/v1.0/healthz/outbound").AbsoluteUri, request.Request.RequestUri.AbsoluteUri); - - var result = await request.CompleteAsync(new HttpResponseMessage(HttpStatusCode.InternalServerError)); - Assert.False(result); - } - - [Fact] - public async Task CheckOutboundHealthAsync_WrapsRequestException() - { - await using var client = TestClient.CreateForDaprClient(c => - { - c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions); - }); - var request = await client.CaptureHttpRequestAsync(async daprClient => await daprClient.CheckOutboundHealthAsync()); - - Assert.Equal(request.Request.Method, HttpMethod.Get); - Assert.Equal(new Uri("https://test-endpoint:3501/v1.0/healthz/outbound").AbsoluteUri, request.Request.RequestUri.AbsoluteUri); - - var result = await request.CompleteWithExceptionAndResultAsync(new HttpRequestException()); - Assert.False(result); - } - - [Fact] - public async Task WaitForSidecarAsync_SuccessWhenSidecarHealthy() - { - await using var client = TestClient.CreateForDaprClient(); - var request = await client.CaptureHttpRequestAsync(async daprClient => await daprClient.WaitForSidecarAsync()); - - // If we don't throw, we're good. - await request.CompleteAsync(new HttpResponseMessage()); - } - - [Fact] - public async Task WaitForSidecarAsync_NotSuccessWhenSidecarNotHealthy() - { - await using var client = TestClient.CreateForDaprClient(); - using var cts = new CancellationTokenSource(); - var waitRequest = await client.CaptureHttpRequestAsync(async daprClient => await daprClient.WaitForSidecarAsync(cts.Token)); - var healthRequest = await client.CaptureHttpRequestAsync(async daprClient => await daprClient.CheckOutboundHealthAsync()); - - cts.Cancel(); - - await healthRequest.CompleteAsync(new HttpResponseMessage(HttpStatusCode.InternalServerError)); - await Assert.ThrowsAsync(async () => await waitRequest.CompleteWithExceptionAsync(new TaskCanceledException())); - } - - [Fact] - public async Task InvokeMethodAsync_WrapsHttpRequestException() - { - await using var client = TestClient.CreateForDaprClient(c => - { - c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions); - }); - - var request = await client.CaptureHttpRequestAsync(async daprClient => - { - var request = daprClient.CreateInvokeMethodRequest("test-app", "test"); - await daprClient.InvokeMethodAsync(request); - }); - - var exception = new HttpRequestException(); - var thrown = await Assert.ThrowsAsync(async () => await request.CompleteWithExceptionAsync(exception)); - Assert.Equal("test-app", thrown.AppId); - Assert.Equal("test", thrown.MethodName); - Assert.Same(exception, thrown.InnerException); - Assert.Null(thrown.Response); - } - - [Fact] - public async Task InvokeMethodAsync_WrapsHttpRequestException_FromEnsureSuccessStatus() - { - await using var client = TestClient.CreateForDaprClient(c => - { - c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions); - }); - - var request = await client.CaptureHttpRequestAsync(async daprClient => - { - var request = daprClient.CreateInvokeMethodRequest("test-app", "test"); - await daprClient.InvokeMethodAsync(request); - }); - - var response = new HttpResponseMessage(HttpStatusCode.NotFound); - var thrown = await Assert.ThrowsAsync(async () => await request.CompleteAsync(response)); - Assert.Equal("test-app", thrown.AppId); - Assert.Equal("test", thrown.MethodName); - Assert.IsType(thrown.InnerException); - Assert.Same(response, thrown.Response); - } - - [Fact] - public async Task InvokeMethodAsync_WithBody_WrapsHttpRequestException() - { - await using var client = TestClient.CreateForDaprClient(c => - { - c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions); - }); - - var request = await client.CaptureHttpRequestAsync(async daprClient => - { - var request = daprClient.CreateInvokeMethodRequest("test-app", "test"); - return await daprClient.InvokeMethodAsync(request); - }); - - var exception = new HttpRequestException(); - var thrown = await Assert.ThrowsAsync(async () => await request.CompleteWithExceptionAsync(exception)); - Assert.Equal("test-app", thrown.AppId); - Assert.Equal("test", thrown.MethodName); - Assert.Same(exception, thrown.InnerException); - Assert.Null(thrown.Response); - } - - [Fact] - public async Task InvokeMethodAsync_WithBody_WrapsHttpRequestException_FromEnsureSuccessStatus() - { - await using var client = TestClient.CreateForDaprClient(c => - { - c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions); - }); - - var request = await client.CaptureHttpRequestAsync(async daprClient => - { - var request = daprClient.CreateInvokeMethodRequest("test-app", "test"); - return await daprClient.InvokeMethodAsync(request); - }); - - - - var response = new HttpResponseMessage(HttpStatusCode.NotFound); - var thrown = await Assert.ThrowsAsync(async () => await request.CompleteAsync(response)); - Assert.Equal("test-app", thrown.AppId); - Assert.Equal("test", thrown.MethodName); - Assert.IsType(thrown.InnerException); - Assert.Same(response, thrown.Response); - } - - [Fact] - public async Task InvokeMethodAsync_WrapsHttpRequestException_FromSerialization() - { - await using var client = TestClient.CreateForDaprClient(c => - { - c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions); - }); - - var request = await client.CaptureHttpRequestAsync(async daprClient => - { - var request = daprClient.CreateInvokeMethodRequest("test-app", "test"); - return await daprClient.InvokeMethodAsync(request); - }); - - var response = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent("{ \"invalid\": true", Encoding.UTF8, "application/json") - }; - var thrown = await Assert.ThrowsAsync(async () => await request.CompleteAsync(response)); - Assert.Equal("test-app", thrown.AppId); - Assert.Equal("test", thrown.MethodName); - Assert.IsType(thrown.InnerException); - Assert.Same(response, thrown.Response); - } - - [Theory] - [InlineData("", "https://test-endpoint:3501/v1.0/invoke/test-app/method/")] - [InlineData("/", "https://test-endpoint:3501/v1.0/invoke/test-app/method/")] - [InlineData("mymethod", "https://test-endpoint:3501/v1.0/invoke/test-app/method/mymethod")] - [InlineData("/mymethod", "https://test-endpoint:3501/v1.0/invoke/test-app/method/mymethod")] - [InlineData("mymethod?key1=value1&key2=value2#fragment", "https://test-endpoint:3501/v1.0/invoke/test-app/method/mymethod?key1=value1&key2=value2#fragment")] - - // garbage in -> garbage out - we don't deeply inspect what you pass. - [InlineData("http://example.com", "https://test-endpoint:3501/v1.0/invoke/test-app/method/http://example.com")] - public async Task CreateInvokeMethodRequest_TransformsUrlCorrectly(string method, string expected) - { - await using var client = TestClient.CreateForDaprClient(c => - { - c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions); - }); - - var request = client.InnerClient.CreateInvokeMethodRequest("test-app", method); - Assert.Equal(new Uri(expected).AbsoluteUri, request.RequestUri.AbsoluteUri); - } - - [Fact] - public async Task CreateInvokeMethodRequest_AppendQueryStringValuesCorrectly() - { - await using var client = TestClient.CreateForDaprClient(c => - { - c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions); - }); - - var request = client.InnerClient.CreateInvokeMethodRequest("test-app", "mymethod", (IReadOnlyCollection>)new List> { new("a", "0"), new("b", "1") }); - Assert.Equal(new Uri("https://test-endpoint:3501/v1.0/invoke/test-app/method/mymethod?a=0&b=1").AbsoluteUri, request.RequestUri.AbsoluteUri); - } - - [Fact] - public async Task CreateInvokeMethodRequest_WithoutApiToken_CreatesHttpRequestWithoutApiTokenHeader() - { - await using var client = TestClient.CreateForDaprClient(c => - { - c - .UseGrpcEndpoint("http://localhost") - .UseHttpEndpoint("https://test-endpoint:3501") - .UseJsonSerializationOptions(this.jsonSerializerOptions) - .UseDaprApiToken(null); - }); - - var request = client.InnerClient.CreateInvokeMethodRequest("test-app", "test"); - Assert.False(request.Headers.TryGetValues("dapr-api-token", out _)); - } - - [Fact] - public async Task CreateInvokeMethodRequest_WithApiToken_CreatesHttpRequestWithApiTokenHeader() - { - await using var client = TestClient.CreateForDaprClient(c => - { - c - .UseGrpcEndpoint("http://localhost") - .UseHttpEndpoint("https://test-endpoint:3501") - .UseJsonSerializationOptions(this.jsonSerializerOptions) - .UseDaprApiToken("test-token"); - }); - - var request = client.InnerClient.CreateInvokeMethodRequest("test-app", "test"); - Assert.True(request.Headers.TryGetValues("dapr-api-token", out var values)); - Assert.Equal("test-token", Assert.Single(values)); - } - - [Fact] - public async Task CreateInvokeMethodRequest_WithoutApiTokenAndWithData_CreatesHttpRequestWithoutApiTokenHeader() - { - await using var client = TestClient.CreateForDaprClient(c => - { - c - .UseGrpcEndpoint("http://localhost") - .UseHttpEndpoint("https://test-endpoint:3501") - .UseJsonSerializationOptions(this.jsonSerializerOptions) - .UseDaprApiToken(null); - }); - - var data = new Widget - { - Color = "red", - }; - - var request = client.InnerClient.CreateInvokeMethodRequest("test-app", "test", data); - Assert.False(request.Headers.TryGetValues("dapr-api-token", out _)); - } - - [Fact] - public async Task CreateInvokeMethodRequest_WithApiTokenAndData_CreatesHttpRequestWithApiTokenHeader() - { - await using var client = TestClient.CreateForDaprClient(c => - { - c - .UseGrpcEndpoint("http://localhost") - .UseHttpEndpoint("https://test-endpoint:3501") - .UseJsonSerializationOptions(this.jsonSerializerOptions) - .UseDaprApiToken("test-token"); - }); - - var data = new Widget - { - Color = "red", - }; - - var request = client.InnerClient.CreateInvokeMethodRequest("test-app", "test", data); - Assert.True(request.Headers.TryGetValues("dapr-api-token", out var values)); - Assert.Equal("test-token", Assert.Single(values)); - } - - [Fact] - public async Task CreateInvokeMethodRequest_WithData_CreatesJsonContent() - { - await using var client = TestClient.CreateForDaprClient(c => - { - c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions); - }); - - var data = new Widget - { - Color = "red", - }; - - var request = client.InnerClient.CreateInvokeMethodRequest("test-app", "test", data); - var content = Assert.IsType(request.Content); - Assert.Equal(typeof(Widget), content.ObjectType); - Assert.Same(data, content.Value); - - // the best way to verify the usage of the correct settings object - var actual = await content.ReadFromJsonAsync(this.jsonSerializerOptions); - Assert.Equal(data.Color, actual.Color); - } - - [Fact] - public async Task CreateInvokeMethodRequest_WithData_CreatesJsonContentWithQueryString() - { - await using var client = TestClient.CreateForDaprClient(c => - { - c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions); - }); - - var data = new Widget - { - Color = "red", - }; - - var request = client.InnerClient.CreateInvokeMethodRequest(HttpMethod.Post, "test-app", "test", new List> { new("a", "0"), new("b", "1") }, data); - - Assert.Equal(new Uri("https://test-endpoint:3501/v1.0/invoke/test-app/method/test?a=0&b=1").AbsoluteUri, request.RequestUri.AbsoluteUri); - - var content = Assert.IsType(request.Content); - Assert.Equal(typeof(Widget), content.ObjectType); - Assert.Same(data, content.Value); - - // the best way to verify the usage of the correct settings object - var actual = await content.ReadFromJsonAsync(this.jsonSerializerOptions); - Assert.Equal(data.Color, actual.Color); - } - - [Fact] - public async Task InvokeMethodWithoutResponse_WithExtraneousHeaders() - { - await using var client = TestClient.CreateForDaprClient(c => - { - c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions); - }); - - var req = await client.CaptureHttpRequestAsync(async DaprClient => - { - var request = client.InnerClient.CreateInvokeMethodRequest(HttpMethod.Get, "test-app", "mymethod"); - request.Headers.Add("test-api-key", "test"); - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "abc123"); - request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - - await DaprClient.InvokeMethodAsync(request); - }); - - req.Dismiss(); - - Assert.NotNull(req); - Assert.True(req.Request.Headers.Contains("test-api-key")); - Assert.Equal("test", req.Request.Headers.GetValues("test-api-key").First()); - Assert.True(req.Request.Headers.Contains("Authorization")); - Assert.Equal("Bearer abc123", req.Request.Headers.GetValues("Authorization").First()); - Assert.Equal("application/json", req.Request.Headers.GetValues("Accept").First()); - } - - [Fact] - public async Task InvokeMethodWithResponseAsync_ReturnsMessageWithoutCheckingStatus() - { - await using var client = TestClient.CreateForDaprClient(c => - { - c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions); - }); - - var request = await client.CaptureHttpRequestAsync(async daprClient => - { - var request = daprClient.CreateInvokeMethodRequest("test-app", "test"); - return await daprClient.InvokeMethodWithResponseAsync(request); - }); - - var response = await request.CompleteAsync(new HttpResponseMessage(HttpStatusCode.BadRequest)); // Non-2xx response - Assert.NotNull(response); - } - - [Fact] - public async Task InvokeMethodWithResponseAsync_WrapsHttpRequestException() - { - await using var client = TestClient.CreateForDaprClient(c => - { - c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions); - }); - - var request = await client.CaptureHttpRequestAsync(async daprClient => - { - var request = daprClient.CreateInvokeMethodRequest("test-app", "test"); - return await daprClient.InvokeMethodWithResponseAsync(request); - }); - - var exception = new HttpRequestException(); - var thrown = await Assert.ThrowsAsync(async () => await request.CompleteWithExceptionAsync(exception)); - Assert.Equal("test-app", thrown.AppId); - Assert.Equal("test", thrown.MethodName); - Assert.Same(exception, thrown.InnerException); - Assert.Null(thrown.Response); - } - - [Fact] - public async Task InvokeMethodWithResponseAsync_PreventsNonDaprRequest() - { - await using var client = TestClient.CreateForDaprClient(c => - { - c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions); - }); - - var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com"); - var ex = await Assert.ThrowsAsync(async () => - { - await client.InnerClient.InvokeMethodWithResponseAsync(request); - }); - - Assert.Equal("The provided request URI is not a Dapr service invocation URI.", ex.Message); - } - - private class Widget - { - public string Color { get; set; } - } + var actual = await request.CompleteWithJsonAsync(expected, jsonSerializerOptions); + Assert.Equal(expected.Color, actual.Color); } -} + + [Fact] + public async Task InvokeMethodAsync_VoidResponseWithHttpMethod_Success() + { + await using var client = TestClient.CreateForDaprClient(c => + { + c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions); + }); + + var request = await client.CaptureHttpRequestAsync(async daprClient => + { + return await daprClient.InvokeMethodAsync(HttpMethod.Put, "app1", "mymethod"); + }); + + Assert.Equal(request.Request.Method, HttpMethod.Put); + Assert.Equal(new Uri("https://test-endpoint:3501/v1.0/invoke/app1/method/mymethod").AbsoluteUri, request.Request.RequestUri.AbsoluteUri); + Assert.Null(request.Request.Content); + + var expected = new Widget() + { + Color = "red", + }; + + var actual = await request.CompleteWithJsonAsync(expected, jsonSerializerOptions); + Assert.Equal(expected.Color, actual.Color); + } + + [Fact] + public async Task InvokeMethodAsync_RequestVoidNoHttpMethod_Success() + { + await using var client = TestClient.CreateForDaprClient(c => + { + c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions); + }); + + var data = new Widget() + { + Color = "red", + }; + + var request = await client.CaptureHttpRequestAsync(async daprClient => + { + await daprClient.InvokeMethodAsync("app1", "mymethod", data); + }); + + // Get Request and validate + Assert.Equal(request.Request.Method, HttpMethod.Post); + Assert.Equal(new Uri("https://test-endpoint:3501/v1.0/invoke/app1/method/mymethod").AbsoluteUri, request.Request.RequestUri.AbsoluteUri); + + var content = Assert.IsType(request.Request.Content); + Assert.Equal(data.GetType(), content.ObjectType); + Assert.Same(data, content.Value); + + await request.CompleteAsync(new HttpResponseMessage()); + } + + [Fact] + public async Task InvokeMethodAsync_RequestVoidWithHttpMethod_Success() + { + await using var client = TestClient.CreateForDaprClient(c => + { + c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions); + }); + + var data = new Widget() + { + Color = "red", + }; + + var request = await client.CaptureHttpRequestAsync(async daprClient => + { + await daprClient.InvokeMethodAsync(HttpMethod.Put, "app1", "mymethod", data); + }); + + // Get Request and validate + Assert.Equal(request.Request.Method, HttpMethod.Put); + Assert.Equal(new Uri("https://test-endpoint:3501/v1.0/invoke/app1/method/mymethod").AbsoluteUri, request.Request.RequestUri.AbsoluteUri); + + var content = Assert.IsType(request.Request.Content); + Assert.Equal(data.GetType(), content.ObjectType); + Assert.Same(data, content.Value); + + await request.CompleteAsync(new HttpResponseMessage()); + } + + [Fact] + public async Task InvokeMethodAsync_RequestResponseNoHttpMethod_Success() + { + await using var client = TestClient.CreateForDaprClient(c => + { + c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions); + }); + + var data = new Widget() + { + Color = "red", + }; + + var request = await client.CaptureHttpRequestAsync(async daprClient => + { + return await daprClient.InvokeMethodAsync("app1", "mymethod", data); + }); + + // Get Request and validate + Assert.Equal(request.Request.Method, HttpMethod.Post); + Assert.Equal(new Uri("https://test-endpoint:3501/v1.0/invoke/app1/method/mymethod").AbsoluteUri, request.Request.RequestUri.AbsoluteUri); + + var content = Assert.IsType(request.Request.Content); + Assert.Equal(data.GetType(), content.ObjectType); + Assert.Same(data, content.Value); + + var actual = await request.CompleteWithJsonAsync(data, jsonSerializerOptions); + Assert.Equal(data.Color, actual.Color); + } + + [Fact] + public async Task InvokeMethodAsync_RequestResponseWithHttpMethod_Success() + { + await using var client = TestClient.CreateForDaprClient(c => + { + c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions); + }); + + var data = new Widget() + { + Color = "red", + }; + + var request = await client.CaptureHttpRequestAsync(async daprClient => + { + return await daprClient.InvokeMethodAsync(HttpMethod.Put, "app1", "mymethod", data); + }); + + // Get Request and validate + Assert.Equal(request.Request.Method, HttpMethod.Put); + Assert.Equal(new Uri("https://test-endpoint:3501/v1.0/invoke/app1/method/mymethod").AbsoluteUri, request.Request.RequestUri.AbsoluteUri); + + var content = Assert.IsType(request.Request.Content); + Assert.Equal(data.GetType(), content.ObjectType); + Assert.Same(data, content.Value); + + var actual = await request.CompleteWithJsonAsync(data, jsonSerializerOptions); + Assert.Equal(data.Color, actual.Color); + } + + [Fact] + public async Task CheckHealthAsync_Success() + { + await using var client = TestClient.CreateForDaprClient(c => + { + c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions); + }); + + var request = await client.CaptureHttpRequestAsync(async daprClient => + await daprClient.CheckHealthAsync()); + + // Get Request and validate + Assert.Equal(request.Request.Method, HttpMethod.Get); + Assert.Equal(new Uri("https://test-endpoint:3501/v1.0/healthz").AbsoluteUri, request.Request.RequestUri.AbsoluteUri); + + var result = await request.CompleteAsync(new HttpResponseMessage()); + Assert.True(result); + } + + [Fact] + public async Task CheckHealthAsync_NotSuccess() + { + await using var client = TestClient.CreateForDaprClient(c => + { + c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions); + }); + + var request = await client.CaptureHttpRequestAsync(async daprClient => + await daprClient.CheckHealthAsync()); + + // Get Request and validate + Assert.Equal(request.Request.Method, HttpMethod.Get); + Assert.Equal(new Uri("https://test-endpoint:3501/v1.0/healthz").AbsoluteUri, request.Request.RequestUri.AbsoluteUri); + + var result = await request.CompleteAsync(new HttpResponseMessage(HttpStatusCode.InternalServerError)); + Assert.False(result); + } + + [Fact] + public async Task CheckHealthAsync_WrapsHttpRequestException() + { + await using var client = TestClient.CreateForDaprClient(c => + { + c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions); + }); + + var request = await client.CaptureHttpRequestAsync(async daprClient => + await daprClient.CheckHealthAsync()); + + Assert.Equal(request.Request.Method, HttpMethod.Get); + Assert.Equal(new Uri("https://test-endpoint:3501/v1.0/healthz").AbsoluteUri, request.Request.RequestUri.AbsoluteUri); + + var exception = new HttpRequestException(); + var result = await request.CompleteWithExceptionAndResultAsync(exception); + Assert.False(result); + } + + [Fact] + public async Task CheckOutboundHealthAsync_Success() + { + await using var client = TestClient.CreateForDaprClient(c => + { + c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions); + }); + var request = await client.CaptureHttpRequestAsync(async daprClient => await daprClient.CheckOutboundHealthAsync()); + + Assert.Equal(request.Request.Method, HttpMethod.Get); + Assert.Equal(new Uri("https://test-endpoint:3501/v1.0/healthz/outbound").AbsoluteUri, request.Request.RequestUri.AbsoluteUri); + + var result = await request.CompleteAsync(new HttpResponseMessage()); + Assert.True(result); + } + + [Fact] + public async Task CheckOutboundHealthAsync_NotSuccess() + { + await using var client = TestClient.CreateForDaprClient(c => + { + c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions); + }); + var request = await client.CaptureHttpRequestAsync(async daprClient => await daprClient.CheckOutboundHealthAsync()); + + Assert.Equal(request.Request.Method, HttpMethod.Get); + Assert.Equal(new Uri("https://test-endpoint:3501/v1.0/healthz/outbound").AbsoluteUri, request.Request.RequestUri.AbsoluteUri); + + var result = await request.CompleteAsync(new HttpResponseMessage(HttpStatusCode.InternalServerError)); + Assert.False(result); + } + + [Fact] + public async Task CheckOutboundHealthAsync_WrapsRequestException() + { + await using var client = TestClient.CreateForDaprClient(c => + { + c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions); + }); + var request = await client.CaptureHttpRequestAsync(async daprClient => await daprClient.CheckOutboundHealthAsync()); + + Assert.Equal(request.Request.Method, HttpMethod.Get); + Assert.Equal(new Uri("https://test-endpoint:3501/v1.0/healthz/outbound").AbsoluteUri, request.Request.RequestUri.AbsoluteUri); + + var result = await request.CompleteWithExceptionAndResultAsync(new HttpRequestException()); + Assert.False(result); + } + + [Fact] + public async Task WaitForSidecarAsync_SuccessWhenSidecarHealthy() + { + await using var client = TestClient.CreateForDaprClient(); + var request = await client.CaptureHttpRequestAsync(async daprClient => await daprClient.WaitForSidecarAsync()); + + // If we don't throw, we're good. + await request.CompleteAsync(new HttpResponseMessage()); + } + + [Fact] + public async Task WaitForSidecarAsync_NotSuccessWhenSidecarNotHealthy() + { + await using var client = TestClient.CreateForDaprClient(); + using var cts = new CancellationTokenSource(); + var waitRequest = await client.CaptureHttpRequestAsync(async daprClient => await daprClient.WaitForSidecarAsync(cts.Token)); + var healthRequest = await client.CaptureHttpRequestAsync(async daprClient => await daprClient.CheckOutboundHealthAsync()); + + cts.Cancel(); + + await healthRequest.CompleteAsync(new HttpResponseMessage(HttpStatusCode.InternalServerError)); + await Assert.ThrowsAsync(async () => await waitRequest.CompleteWithExceptionAsync(new TaskCanceledException())); + } + + [Fact] + public async Task InvokeMethodAsync_WrapsHttpRequestException() + { + await using var client = TestClient.CreateForDaprClient(c => + { + c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions); + }); + + var request = await client.CaptureHttpRequestAsync(async daprClient => + { + var request = daprClient.CreateInvokeMethodRequest("test-app", "test"); + await daprClient.InvokeMethodAsync(request); + }); + + var exception = new HttpRequestException(); + var thrown = await Assert.ThrowsAsync(async () => await request.CompleteWithExceptionAsync(exception)); + Assert.Equal("test-app", thrown.AppId); + Assert.Equal("test", thrown.MethodName); + Assert.Same(exception, thrown.InnerException); + Assert.Null(thrown.Response); + } + + [Fact] + public async Task InvokeMethodAsync_WrapsHttpRequestException_FromEnsureSuccessStatus() + { + await using var client = TestClient.CreateForDaprClient(c => + { + c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions); + }); + + var request = await client.CaptureHttpRequestAsync(async daprClient => + { + var request = daprClient.CreateInvokeMethodRequest("test-app", "test"); + await daprClient.InvokeMethodAsync(request); + }); + + var response = new HttpResponseMessage(HttpStatusCode.NotFound); + var thrown = await Assert.ThrowsAsync(async () => await request.CompleteAsync(response)); + Assert.Equal("test-app", thrown.AppId); + Assert.Equal("test", thrown.MethodName); + Assert.IsType(thrown.InnerException); + Assert.Same(response, thrown.Response); + } + + [Fact] + public async Task InvokeMethodAsync_WithBody_WrapsHttpRequestException() + { + await using var client = TestClient.CreateForDaprClient(c => + { + c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions); + }); + + var request = await client.CaptureHttpRequestAsync(async daprClient => + { + var request = daprClient.CreateInvokeMethodRequest("test-app", "test"); + return await daprClient.InvokeMethodAsync(request); + }); + + var exception = new HttpRequestException(); + var thrown = await Assert.ThrowsAsync(async () => await request.CompleteWithExceptionAsync(exception)); + Assert.Equal("test-app", thrown.AppId); + Assert.Equal("test", thrown.MethodName); + Assert.Same(exception, thrown.InnerException); + Assert.Null(thrown.Response); + } + + [Fact] + public async Task InvokeMethodAsync_WithBody_WrapsHttpRequestException_FromEnsureSuccessStatus() + { + await using var client = TestClient.CreateForDaprClient(c => + { + c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions); + }); + + var request = await client.CaptureHttpRequestAsync(async daprClient => + { + var request = daprClient.CreateInvokeMethodRequest("test-app", "test"); + return await daprClient.InvokeMethodAsync(request); + }); + + + + var response = new HttpResponseMessage(HttpStatusCode.NotFound); + var thrown = await Assert.ThrowsAsync(async () => await request.CompleteAsync(response)); + Assert.Equal("test-app", thrown.AppId); + Assert.Equal("test", thrown.MethodName); + Assert.IsType(thrown.InnerException); + Assert.Same(response, thrown.Response); + } + + [Fact] + public async Task InvokeMethodAsync_WrapsHttpRequestException_FromSerialization() + { + await using var client = TestClient.CreateForDaprClient(c => + { + c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions); + }); + + var request = await client.CaptureHttpRequestAsync(async daprClient => + { + var request = daprClient.CreateInvokeMethodRequest("test-app", "test"); + return await daprClient.InvokeMethodAsync(request); + }); + + var response = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("{ \"invalid\": true", Encoding.UTF8, "application/json") + }; + var thrown = await Assert.ThrowsAsync(async () => await request.CompleteAsync(response)); + Assert.Equal("test-app", thrown.AppId); + Assert.Equal("test", thrown.MethodName); + Assert.IsType(thrown.InnerException); + Assert.Same(response, thrown.Response); + } + + [Theory] + [InlineData("", "https://test-endpoint:3501/v1.0/invoke/test-app/method/")] + [InlineData("/", "https://test-endpoint:3501/v1.0/invoke/test-app/method/")] + [InlineData("mymethod", "https://test-endpoint:3501/v1.0/invoke/test-app/method/mymethod")] + [InlineData("/mymethod", "https://test-endpoint:3501/v1.0/invoke/test-app/method/mymethod")] + [InlineData("mymethod?key1=value1&key2=value2#fragment", "https://test-endpoint:3501/v1.0/invoke/test-app/method/mymethod?key1=value1&key2=value2#fragment")] + + // garbage in -> garbage out - we don't deeply inspect what you pass. + [InlineData("http://example.com", "https://test-endpoint:3501/v1.0/invoke/test-app/method/http://example.com")] + public async Task CreateInvokeMethodRequest_TransformsUrlCorrectly(string method, string expected) + { + await using var client = TestClient.CreateForDaprClient(c => + { + c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions); + }); + + var request = client.InnerClient.CreateInvokeMethodRequest("test-app", method); + Assert.Equal(new Uri(expected).AbsoluteUri, request.RequestUri.AbsoluteUri); + } + + [Fact] + public async Task CreateInvokeMethodRequest_AppendQueryStringValuesCorrectly() + { + await using var client = TestClient.CreateForDaprClient(c => + { + c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions); + }); + + var request = client.InnerClient.CreateInvokeMethodRequest("test-app", "mymethod", (IReadOnlyCollection>)new List> { new("a", "0"), new("b", "1") }); + Assert.Equal(new Uri("https://test-endpoint:3501/v1.0/invoke/test-app/method/mymethod?a=0&b=1").AbsoluteUri, request.RequestUri.AbsoluteUri); + } + + [Fact] + public async Task CreateInvokeMethodRequest_WithoutApiToken_CreatesHttpRequestWithoutApiTokenHeader() + { + await using var client = TestClient.CreateForDaprClient(c => + { + c + .UseGrpcEndpoint("http://localhost") + .UseHttpEndpoint("https://test-endpoint:3501") + .UseJsonSerializationOptions(this.jsonSerializerOptions) + .UseDaprApiToken(null); + }); + + var request = client.InnerClient.CreateInvokeMethodRequest("test-app", "test"); + Assert.False(request.Headers.TryGetValues("dapr-api-token", out _)); + } + + [Fact] + public async Task CreateInvokeMethodRequest_WithApiToken_CreatesHttpRequestWithApiTokenHeader() + { + await using var client = TestClient.CreateForDaprClient(c => + { + c + .UseGrpcEndpoint("http://localhost") + .UseHttpEndpoint("https://test-endpoint:3501") + .UseJsonSerializationOptions(this.jsonSerializerOptions) + .UseDaprApiToken("test-token"); + }); + + var request = client.InnerClient.CreateInvokeMethodRequest("test-app", "test"); + Assert.True(request.Headers.TryGetValues("dapr-api-token", out var values)); + Assert.Equal("test-token", Assert.Single(values)); + } + + [Fact] + public async Task CreateInvokeMethodRequest_WithoutApiTokenAndWithData_CreatesHttpRequestWithoutApiTokenHeader() + { + await using var client = TestClient.CreateForDaprClient(c => + { + c + .UseGrpcEndpoint("http://localhost") + .UseHttpEndpoint("https://test-endpoint:3501") + .UseJsonSerializationOptions(this.jsonSerializerOptions) + .UseDaprApiToken(null); + }); + + var data = new Widget + { + Color = "red", + }; + + var request = client.InnerClient.CreateInvokeMethodRequest("test-app", "test", data); + Assert.False(request.Headers.TryGetValues("dapr-api-token", out _)); + } + + [Fact] + public async Task CreateInvokeMethodRequest_WithApiTokenAndData_CreatesHttpRequestWithApiTokenHeader() + { + await using var client = TestClient.CreateForDaprClient(c => + { + c + .UseGrpcEndpoint("http://localhost") + .UseHttpEndpoint("https://test-endpoint:3501") + .UseJsonSerializationOptions(this.jsonSerializerOptions) + .UseDaprApiToken("test-token"); + }); + + var data = new Widget + { + Color = "red", + }; + + var request = client.InnerClient.CreateInvokeMethodRequest("test-app", "test", data); + Assert.True(request.Headers.TryGetValues("dapr-api-token", out var values)); + Assert.Equal("test-token", Assert.Single(values)); + } + + [Fact] + public async Task CreateInvokeMethodRequest_WithData_CreatesJsonContent() + { + await using var client = TestClient.CreateForDaprClient(c => + { + c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions); + }); + + var data = new Widget + { + Color = "red", + }; + + var request = client.InnerClient.CreateInvokeMethodRequest("test-app", "test", data); + var content = Assert.IsType(request.Content); + Assert.Equal(typeof(Widget), content.ObjectType); + Assert.Same(data, content.Value); + + // the best way to verify the usage of the correct settings object + var actual = await content.ReadFromJsonAsync(this.jsonSerializerOptions); + Assert.Equal(data.Color, actual.Color); + } + + [Fact] + public async Task CreateInvokeMethodRequest_WithData_CreatesJsonContentWithQueryString() + { + await using var client = TestClient.CreateForDaprClient(c => + { + c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions); + }); + + var data = new Widget + { + Color = "red", + }; + + var request = client.InnerClient.CreateInvokeMethodRequest(HttpMethod.Post, "test-app", "test", new List> { new("a", "0"), new("b", "1") }, data); + + Assert.Equal(new Uri("https://test-endpoint:3501/v1.0/invoke/test-app/method/test?a=0&b=1").AbsoluteUri, request.RequestUri.AbsoluteUri); + + var content = Assert.IsType(request.Content); + Assert.Equal(typeof(Widget), content.ObjectType); + Assert.Same(data, content.Value); + + // the best way to verify the usage of the correct settings object + var actual = await content.ReadFromJsonAsync(this.jsonSerializerOptions); + Assert.Equal(data.Color, actual.Color); + } + + [Fact] + public async Task InvokeMethodWithoutResponse_WithExtraneousHeaders() + { + await using var client = TestClient.CreateForDaprClient(c => + { + c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions); + }); + + var req = await client.CaptureHttpRequestAsync(async DaprClient => + { + var request = client.InnerClient.CreateInvokeMethodRequest(HttpMethod.Get, "test-app", "mymethod"); + request.Headers.Add("test-api-key", "test"); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "abc123"); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + + await DaprClient.InvokeMethodAsync(request); + }); + + req.Dismiss(); + + Assert.NotNull(req); + Assert.True(req.Request.Headers.Contains("test-api-key")); + Assert.Equal("test", req.Request.Headers.GetValues("test-api-key").First()); + Assert.True(req.Request.Headers.Contains("Authorization")); + Assert.Equal("Bearer abc123", req.Request.Headers.GetValues("Authorization").First()); + Assert.Equal("application/json", req.Request.Headers.GetValues("Accept").First()); + } + + [Fact] + public async Task InvokeMethodWithResponseAsync_ReturnsMessageWithoutCheckingStatus() + { + await using var client = TestClient.CreateForDaprClient(c => + { + c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions); + }); + + var request = await client.CaptureHttpRequestAsync(async daprClient => + { + var request = daprClient.CreateInvokeMethodRequest("test-app", "test"); + return await daprClient.InvokeMethodWithResponseAsync(request); + }); + + var response = await request.CompleteAsync(new HttpResponseMessage(HttpStatusCode.BadRequest)); // Non-2xx response + Assert.NotNull(response); + } + + [Fact] + public async Task InvokeMethodWithResponseAsync_WrapsHttpRequestException() + { + await using var client = TestClient.CreateForDaprClient(c => + { + c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions); + }); + + var request = await client.CaptureHttpRequestAsync(async daprClient => + { + var request = daprClient.CreateInvokeMethodRequest("test-app", "test"); + return await daprClient.InvokeMethodWithResponseAsync(request); + }); + + var exception = new HttpRequestException(); + var thrown = await Assert.ThrowsAsync(async () => await request.CompleteWithExceptionAsync(exception)); + Assert.Equal("test-app", thrown.AppId); + Assert.Equal("test", thrown.MethodName); + Assert.Same(exception, thrown.InnerException); + Assert.Null(thrown.Response); + } + + [Fact] + public async Task InvokeMethodWithResponseAsync_PreventsNonDaprRequest() + { + await using var client = TestClient.CreateForDaprClient(c => + { + c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions); + }); + + var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com"); + var ex = await Assert.ThrowsAsync(async () => + { + await client.InnerClient.InvokeMethodWithResponseAsync(request); + }); + + Assert.Equal("The provided request URI is not a Dapr service invocation URI.", ex.Message); + } + + private class Widget + { + public string Color { get; set; } + } +} \ No newline at end of file diff --git a/test/Dapr.Client.Test/DaprClientTest.InvokeMethodGrpcAsync.cs b/test/Dapr.Client.Test/DaprClientTest.InvokeMethodGrpcAsync.cs index 3ef0aed6..dba99a72 100644 --- a/test/Dapr.Client.Test/DaprClientTest.InvokeMethodGrpcAsync.cs +++ b/test/Dapr.Client.Test/DaprClientTest.InvokeMethodGrpcAsync.cs @@ -26,467 +26,466 @@ using Xunit; using Request = Dapr.Client.Autogen.Test.Grpc.v1.Request; using Response = Dapr.Client.Autogen.Test.Grpc.v1.Response; -namespace Dapr.Client.Test +namespace Dapr.Client.Test; + +public partial class DaprClientTest { - public partial class DaprClientTest + [Fact] + public async Task InvokeMethodGrpcAsync_WithCancelledToken() { - [Fact] - public async Task InvokeMethodGrpcAsync_WithCancelledToken() + await using var client = TestClient.CreateForDaprClient(c => { - await using var client = TestClient.CreateForDaprClient(c => - { - c.UseJsonSerializationOptions(this.jsonSerializerOptions); - }); + c.UseJsonSerializationOptions(this.jsonSerializerOptions); + }); - var cts = new CancellationTokenSource(); - cts.Cancel(); + var cts = new CancellationTokenSource(); + cts.Cancel(); - await Assert.ThrowsAsync(async () => - { - await client.InnerClient.InvokeMethodGrpcAsync("test", "test", new Request() { RequestParameter = "Hello " }, cancellationToken: cts.Token); - }); - } - - [Fact] - public async Task InvokeMethodGrpcAsync_CanInvokeMethodWithReturnTypeAndData() + await Assert.ThrowsAsync(async () => { - await using var client = TestClient.CreateForDaprClient(c => - { - c.UseJsonSerializationOptions(this.jsonSerializerOptions); - }); + await client.InnerClient.InvokeMethodGrpcAsync("test", "test", new Request() { RequestParameter = "Hello " }, cancellationToken: cts.Token); + }); + } - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.InvokeMethodGrpcAsync("test", "test", new Request() { RequestParameter = "Hello " }); - }); - - // Get Request and validate - var envelope = await request.GetRequestEnvelopeAsync(); - envelope.Id.ShouldBe("test"); - envelope.Message.Method.ShouldBe("test"); - envelope.Message.ContentType.ShouldBe(Constants.ContentTypeApplicationGrpc); - - // Create Response & Respond - var data = new Response() { Name = "Look, I was invoked!" }; - var response = new Autogen.Grpc.v1.InvokeResponse() - { - Data = Any.Pack(data), - }; - - // Validate Response - var invokedResponse = await request.CompleteWithMessageAsync(response); - invokedResponse.Name.ShouldBe("Look, I was invoked!"); - } - - [Fact] - public async Task InvokeMethodGrpcAsync_CanInvokeMethodWithReturnTypeAndData_ThrowsExceptionForNonSuccess() + [Fact] + public async Task InvokeMethodGrpcAsync_CanInvokeMethodWithReturnTypeAndData() + { + await using var client = TestClient.CreateForDaprClient(c => { - var client = new MockClient(); - var data = new Response() { Name = "Look, I was invoked!" }; - var invokeResponse = new InvokeResponse - { - Data = Any.Pack(data), - }; + c.UseJsonSerializationOptions(this.jsonSerializerOptions); + }); - await client.Call() + var request = await client.CaptureGrpcRequestAsync(async daprClient => + { + return await daprClient.InvokeMethodGrpcAsync("test", "test", new Request() { RequestParameter = "Hello " }); + }); + + // Get Request and validate + var envelope = await request.GetRequestEnvelopeAsync(); + envelope.Id.ShouldBe("test"); + envelope.Message.Method.ShouldBe("test"); + envelope.Message.ContentType.ShouldBe(Constants.ContentTypeApplicationGrpc); + + // Create Response & Respond + var data = new Response() { Name = "Look, I was invoked!" }; + var response = new Autogen.Grpc.v1.InvokeResponse() + { + Data = Any.Pack(data), + }; + + // Validate Response + var invokedResponse = await request.CompleteWithMessageAsync(response); + invokedResponse.Name.ShouldBe("Look, I was invoked!"); + } + + [Fact] + public async Task InvokeMethodGrpcAsync_CanInvokeMethodWithReturnTypeAndData_ThrowsExceptionForNonSuccess() + { + var client = new MockClient(); + var data = new Response() { Name = "Look, I was invoked!" }; + var invokeResponse = new InvokeResponse + { + Data = Any.Pack(data), + }; + + await client.Call() + .SetResponse(invokeResponse) + .Build(); + + const string rpcExceptionMessage = "RPC exception"; + const StatusCode rpcStatusCode = StatusCode.Unavailable; + const string rpcStatusDetail = "Non success"; + + var rpcStatus = new Status(rpcStatusCode, rpcStatusDetail); + var rpcException = new RpcException(rpcStatus, new Metadata(), rpcExceptionMessage); + + // Setup the mock client to throw an Rpc Exception with the expected details info + client.Mock + .Setup(m => m.InvokeServiceAsync(It.IsAny(), It.IsAny())) + .Throws(rpcException); + + var ex = await Assert.ThrowsAsync(async () => + { + await client.DaprClient.InvokeMethodGrpcAsync("test", "test", new Request() { RequestParameter = "Hello " }); + }); + Assert.Same(rpcException, ex.InnerException); + } + + [Fact] + public async Task InvokeMethodGrpcAsync_CanInvokeMethodWithReturnTypeNoData() + { + await using var client = TestClient.CreateForDaprClient(c => + { + c.UseJsonSerializationOptions(this.jsonSerializerOptions); + }); + + var request = await client.CaptureGrpcRequestAsync(async daprClient => + { + return await daprClient.InvokeMethodGrpcAsync("test", "test"); + }); + + // Get Request and validate + var envelope = await request.GetRequestEnvelopeAsync(); + envelope.Id.ShouldBe("test"); + envelope.Message.Method.ShouldBe("test"); + envelope.Message.ContentType.ShouldBe(string.Empty); + + // Create Response & Respond + var data = new Response() { Name = "Look, I was invoked!" }; + var response = new Autogen.Grpc.v1.InvokeResponse() + { + Data = Any.Pack(data), + }; + + // Validate Response + var invokedResponse = await request.CompleteWithMessageAsync(response); + invokedResponse.Name.ShouldBe("Look, I was invoked!"); + } + + [Fact] + public async Task InvokeMethodGrpcAsync_CanInvokeMethodWithReturnTypeNoData_ThrowsExceptionNonSuccess() + { + var client = new MockClient(); + var data = new Response() { Name = "Look, I was invoked!" }; + var invokeResponse = new InvokeResponse + { + Data = Any.Pack(data), + }; + + await client.Call() + .SetResponse(invokeResponse) + .Build(); + + + const string rpcExceptionMessage = "RPC exception"; + const StatusCode rpcStatusCode = StatusCode.Unavailable; + const string rpcStatusDetail = "Non success"; + + var rpcStatus = new Status(rpcStatusCode, rpcStatusDetail); + var rpcException = new RpcException(rpcStatus, new Metadata(), rpcExceptionMessage); + + client.Mock + .Setup(m => m.InvokeServiceAsync(It.IsAny(), It.IsAny())) + .Throws(rpcException); + + var ex = await Assert.ThrowsAsync(async () => + { + await client.DaprClient.InvokeMethodGrpcAsync("test", "test"); + }); + Assert.Same(rpcException, ex.InnerException); + } + + [Fact] + public async Task InvokeMethodGrpcAsync_CanInvokeMethodWithNoReturnTypeAndData() + { + var request = new Request() { RequestParameter = "Hello " }; + var client = new MockClient(); + var data = new Response() { Name = "Look, I was invoked!" }; + var invokeResponse = new InvokeResponse + { + Data = Any.Pack(data), + }; + + var response = + client.Call() .SetResponse(invokeResponse) .Build(); - const string rpcExceptionMessage = "RPC exception"; - const StatusCode rpcStatusCode = StatusCode.Unavailable; - const string rpcStatusDetail = "Non success"; + client.Mock + .Setup(m => m.InvokeServiceAsync(It.IsAny(), It.IsAny())) + .Returns(response); - var rpcStatus = new Status(rpcStatusCode, rpcStatusDetail); - var rpcException = new RpcException(rpcStatus, new Metadata(), rpcExceptionMessage); + await Should.NotThrowAsync(async () => await client.DaprClient.InvokeMethodGrpcAsync("test", "test", request)); + } - // Setup the mock client to throw an Rpc Exception with the expected details info - client.Mock - .Setup(m => m.InvokeServiceAsync(It.IsAny(), It.IsAny())) - .Throws(rpcException); - - var ex = await Assert.ThrowsAsync(async () => - { - await client.DaprClient.InvokeMethodGrpcAsync("test", "test", new Request() { RequestParameter = "Hello " }); - }); - Assert.Same(rpcException, ex.InnerException); - } - - [Fact] - public async Task InvokeMethodGrpcAsync_CanInvokeMethodWithReturnTypeNoData() + [Fact] + public async Task InvokeMethodGrpcAsync_CanInvokeMethodWithNoReturnTypeAndData_ThrowsErrorNonSuccess() + { + var client = new MockClient(); + var data = new Response() { Name = "Look, I was invoked!" }; + var invokeResponse = new InvokeResponse { - await using var client = TestClient.CreateForDaprClient(c => - { - c.UseJsonSerializationOptions(this.jsonSerializerOptions); - }); + Data = Any.Pack(data), + }; - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.InvokeMethodGrpcAsync("test", "test"); - }); + await client.Call() + .SetResponse(invokeResponse) + .Build(); - // Get Request and validate - var envelope = await request.GetRequestEnvelopeAsync(); - envelope.Id.ShouldBe("test"); - envelope.Message.Method.ShouldBe("test"); - envelope.Message.ContentType.ShouldBe(string.Empty); - // Create Response & Respond - var data = new Response() { Name = "Look, I was invoked!" }; - var response = new Autogen.Grpc.v1.InvokeResponse() - { - Data = Any.Pack(data), - }; + const string rpcExceptionMessage = "RPC exception"; + const StatusCode rpcStatusCode = StatusCode.Unavailable; + const string rpcStatusDetail = "Non success"; - // Validate Response - var invokedResponse = await request.CompleteWithMessageAsync(response); - invokedResponse.Name.ShouldBe("Look, I was invoked!"); - } + var rpcStatus = new Status(rpcStatusCode, rpcStatusDetail); + var rpcException = new RpcException(rpcStatus, new Metadata(), rpcExceptionMessage); - [Fact] - public async Task InvokeMethodGrpcAsync_CanInvokeMethodWithReturnTypeNoData_ThrowsExceptionNonSuccess() + // Setup the mock client to throw an Rpc Exception with the expected details info + client.Mock + .Setup(m => m.InvokeServiceAsync(It.IsAny(), It.IsAny())) + .Throws(rpcException); + + var ex = await Assert.ThrowsAsync(async () => { - var client = new MockClient(); - var data = new Response() { Name = "Look, I was invoked!" }; - var invokeResponse = new InvokeResponse - { - Data = Any.Pack(data), - }; + await client.DaprClient.InvokeMethodGrpcAsync("test", "test", new Request() { RequestParameter = "Hello " }); + }); + Assert.Same(rpcException, ex.InnerException); + } - await client.Call() - .SetResponse(invokeResponse) - .Build(); - - - const string rpcExceptionMessage = "RPC exception"; - const StatusCode rpcStatusCode = StatusCode.Unavailable; - const string rpcStatusDetail = "Non success"; - - var rpcStatus = new Status(rpcStatusCode, rpcStatusDetail); - var rpcException = new RpcException(rpcStatus, new Metadata(), rpcExceptionMessage); - - client.Mock - .Setup(m => m.InvokeServiceAsync(It.IsAny(), It.IsAny())) - .Throws(rpcException); - - var ex = await Assert.ThrowsAsync(async () => - { - await client.DaprClient.InvokeMethodGrpcAsync("test", "test"); - }); - Assert.Same(rpcException, ex.InnerException); - } - - [Fact] - public async Task InvokeMethodGrpcAsync_CanInvokeMethodWithNoReturnTypeAndData() + [Fact] + public async Task InvokeMethodGrpcAsync_WithNoReturnTypeAndData() + { + await using var client = TestClient.CreateForDaprClient(c => { - var request = new Request() { RequestParameter = "Hello " }; - var client = new MockClient(); - var data = new Response() { Name = "Look, I was invoked!" }; - var invokeResponse = new InvokeResponse - { - Data = Any.Pack(data), - }; + c.UseJsonSerializationOptions(this.jsonSerializerOptions); + }); - var response = - client.Call() - .SetResponse(invokeResponse) - .Build(); - - client.Mock - .Setup(m => m.InvokeServiceAsync(It.IsAny(), It.IsAny())) - .Returns(response); - - await Should.NotThrowAsync(async () => await client.DaprClient.InvokeMethodGrpcAsync("test", "test", request)); - } - - [Fact] - public async Task InvokeMethodGrpcAsync_CanInvokeMethodWithNoReturnTypeAndData_ThrowsErrorNonSuccess() + var invokeRequest = new Request() { RequestParameter = "Hello" }; + var request = await client.CaptureGrpcRequestAsync(async daprClient => { - var client = new MockClient(); - var data = new Response() { Name = "Look, I was invoked!" }; - var invokeResponse = new InvokeResponse - { - Data = Any.Pack(data), - }; + return await daprClient.InvokeMethodGrpcAsync("test", "test", invokeRequest); + }); - await client.Call() - .SetResponse(invokeResponse) - .Build(); + request.Dismiss(); + // Get Request and validate + var envelope = await request.GetRequestEnvelopeAsync(); + envelope.Id.ShouldBe("test"); + envelope.Message.Method.ShouldBe("test"); + envelope.Message.ContentType.ShouldBe(Constants.ContentTypeApplicationGrpc); - const string rpcExceptionMessage = "RPC exception"; - const StatusCode rpcStatusCode = StatusCode.Unavailable; - const string rpcStatusDetail = "Non success"; + var actual = envelope.Message.Data.Unpack(); + Assert.Equal(invokeRequest.RequestParameter, actual.RequestParameter); + } - var rpcStatus = new Status(rpcStatusCode, rpcStatusDetail); - var rpcException = new RpcException(rpcStatus, new Metadata(), rpcExceptionMessage); - - // Setup the mock client to throw an Rpc Exception with the expected details info - client.Mock - .Setup(m => m.InvokeServiceAsync(It.IsAny(), It.IsAny())) - .Throws(rpcException); - - var ex = await Assert.ThrowsAsync(async () => - { - await client.DaprClient.InvokeMethodGrpcAsync("test", "test", new Request() { RequestParameter = "Hello " }); - }); - Assert.Same(rpcException, ex.InnerException); - } - - [Fact] - public async Task InvokeMethodGrpcAsync_WithNoReturnTypeAndData() + [Fact] + public async Task InvokeMethodGrpcAsync_WithReturnTypeAndData() + { + await using var client = TestClient.CreateForDaprClient(c => { - await using var client = TestClient.CreateForDaprClient(c => - { - c.UseJsonSerializationOptions(this.jsonSerializerOptions); - }); + c.UseJsonSerializationOptions(this.jsonSerializerOptions); + }); - var invokeRequest = new Request() { RequestParameter = "Hello" }; - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.InvokeMethodGrpcAsync("test", "test", invokeRequest); - }); - - request.Dismiss(); - - // Get Request and validate - var envelope = await request.GetRequestEnvelopeAsync(); - envelope.Id.ShouldBe("test"); - envelope.Message.Method.ShouldBe("test"); - envelope.Message.ContentType.ShouldBe(Constants.ContentTypeApplicationGrpc); - - var actual = envelope.Message.Data.Unpack(); - Assert.Equal(invokeRequest.RequestParameter, actual.RequestParameter); - } - - [Fact] - public async Task InvokeMethodGrpcAsync_WithReturnTypeAndData() + var invokeRequest = new Request() { RequestParameter = "Hello " }; + var invokeResponse = new Response { Name = "Look, I was invoked!" }; + var request = await client.CaptureGrpcRequestAsync(async daprClient => { - await using var client = TestClient.CreateForDaprClient(c => - { - c.UseJsonSerializationOptions(this.jsonSerializerOptions); - }); + return await daprClient.InvokeMethodGrpcAsync("test", "test", invokeRequest); + }); - var invokeRequest = new Request() { RequestParameter = "Hello " }; - var invokeResponse = new Response { Name = "Look, I was invoked!" }; - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.InvokeMethodGrpcAsync("test", "test", invokeRequest); - }); + // Get Request and validate + var envelope = await request.GetRequestEnvelopeAsync(); + envelope.Id.ShouldBe("test"); + envelope.Message.Method.ShouldBe("test"); + envelope.Message.ContentType.ShouldBe(Constants.ContentTypeApplicationGrpc); - // Get Request and validate - var envelope = await request.GetRequestEnvelopeAsync(); - envelope.Id.ShouldBe("test"); - envelope.Message.Method.ShouldBe("test"); - envelope.Message.ContentType.ShouldBe(Constants.ContentTypeApplicationGrpc); + var actual = envelope.Message.Data.Unpack(); + Assert.Equal(invokeRequest.RequestParameter, actual.RequestParameter); - var actual = envelope.Message.Data.Unpack(); - Assert.Equal(invokeRequest.RequestParameter, actual.RequestParameter); - - // Create Response & Respond - var data = new Response() { Name = "Look, I was invoked!" }; - var response = new Autogen.Grpc.v1.InvokeResponse() - { - Data = Any.Pack(data), - }; - - // Validate Response - var invokedResponse = await request.CompleteWithMessageAsync(response); - invokedResponse.Name.ShouldBe(invokeResponse.Name); - } - - [Fact] - public async Task InvokeMethodGrpcAsync_AppCallback_SayHello() + // Create Response & Respond + var data = new Response() { Name = "Look, I was invoked!" }; + var response = new Autogen.Grpc.v1.InvokeResponse() { - // Configure Client - var httpClient = new AppCallbackClient(new DaprAppCallbackService()); - var daprClient = new DaprClientBuilder() - .UseGrpcChannelOptions(new GrpcChannelOptions() { HttpClient = httpClient, }) - .UseJsonSerializationOptions(this.jsonSerializerOptions) - .Build(); + Data = Any.Pack(data), + }; - var request = new Request() { RequestParameter = "Look, I was invoked!" }; + // Validate Response + var invokedResponse = await request.CompleteWithMessageAsync(response); + invokedResponse.Name.ShouldBe(invokeResponse.Name); + } - var response = await daprClient.InvokeMethodGrpcAsync("test", "SayHello", request); + [Fact] + public async Task InvokeMethodGrpcAsync_AppCallback_SayHello() + { + // Configure Client + var httpClient = new AppCallbackClient(new DaprAppCallbackService()); + var daprClient = new DaprClientBuilder() + .UseGrpcChannelOptions(new GrpcChannelOptions() { HttpClient = httpClient, }) + .UseJsonSerializationOptions(this.jsonSerializerOptions) + .Build(); - response.Name.ShouldBe("Hello Look, I was invoked!"); - } + var request = new Request() { RequestParameter = "Look, I was invoked!" }; - [Fact] - public async Task InvokeMethodGrpcAsync_AppCallback_RepeatedField() + var response = await daprClient.InvokeMethodGrpcAsync("test", "SayHello", request); + + response.Name.ShouldBe("Hello Look, I was invoked!"); + } + + [Fact] + public async Task InvokeMethodGrpcAsync_AppCallback_RepeatedField() + { + // Configure Client + var httpClient = new AppCallbackClient(new DaprAppCallbackService()); + var daprClient = new DaprClientBuilder() + .UseGrpcChannelOptions(new GrpcChannelOptions() { HttpClient = httpClient, }) + .UseJsonSerializationOptions(this.jsonSerializerOptions) + .Build(); + + var testRun = new TestRun(); + testRun.Tests.Add(new TestCase() { Name = "test1" }); + testRun.Tests.Add(new TestCase() { Name = "test2" }); + testRun.Tests.Add(new TestCase() { Name = "test3" }); + + var response = await daprClient.InvokeMethodGrpcAsync("test", "TestRun", testRun); + + response.Tests.Count.ShouldBe(3); + response.Tests[0].Name.ShouldBe("test1"); + response.Tests[1].Name.ShouldBe("test2"); + response.Tests[2].Name.ShouldBe("test3"); + } + + [Fact] + public async Task InvokeMethodGrpcAsync_AppCallback_UnexpectedMethod() + { + // Configure Client + var httpClient = new AppCallbackClient(new DaprAppCallbackService()); + var daprClient = new DaprClientBuilder() + .UseGrpcChannelOptions(new GrpcChannelOptions() { HttpClient = httpClient, }) + .UseJsonSerializationOptions(this.jsonSerializerOptions) + .Build(); + + var request = new Request() { RequestParameter = "Look, I was invoked!" }; + + var response = await daprClient.InvokeMethodGrpcAsync("test", "not-existing", request); + + response.Name.ShouldBe("unexpected"); + } + + + [Fact] + public async Task GetMetadataAsync_WrapsRpcException() + { + var client = new MockClient(); + + const string rpcExceptionMessage = "RPC exception"; + const StatusCode rpcStatusCode = StatusCode.Unavailable; + const string rpcStatusDetail = "Non success"; + + var rpcStatus = new Status(rpcStatusCode, rpcStatusDetail); + var rpcException = new RpcException(rpcStatus, new Metadata(), rpcExceptionMessage); + + client.Mock + .Setup(m => m.GetMetadataAsync(It.IsAny(), It.IsAny())) + .Throws(rpcException); + + var ex = await Assert.ThrowsAsync(async () => { - // Configure Client - var httpClient = new AppCallbackClient(new DaprAppCallbackService()); - var daprClient = new DaprClientBuilder() - .UseGrpcChannelOptions(new GrpcChannelOptions() { HttpClient = httpClient, }) - .UseJsonSerializationOptions(this.jsonSerializerOptions) - .Build(); + await client.DaprClient.GetMetadataAsync(default); + }); + Assert.Same(rpcException, ex.InnerException); + } - var testRun = new TestRun(); - testRun.Tests.Add(new TestCase() { Name = "test1" }); - testRun.Tests.Add(new TestCase() { Name = "test2" }); - testRun.Tests.Add(new TestCase() { Name = "test3" }); - - var response = await daprClient.InvokeMethodGrpcAsync("test", "TestRun", testRun); - - response.Tests.Count.ShouldBe(3); - response.Tests[0].Name.ShouldBe("test1"); - response.Tests[1].Name.ShouldBe("test2"); - response.Tests[2].Name.ShouldBe("test3"); - } - - [Fact] - public async Task InvokeMethodGrpcAsync_AppCallback_UnexpectedMethod() + [Fact] + public async Task GetMetadataAsync_WithReturnTypeAndData() + { + await using var client = TestClient.CreateForDaprClient(c => { - // Configure Client - var httpClient = new AppCallbackClient(new DaprAppCallbackService()); - var daprClient = new DaprClientBuilder() - .UseGrpcChannelOptions(new GrpcChannelOptions() { HttpClient = httpClient, }) - .UseJsonSerializationOptions(this.jsonSerializerOptions) - .Build(); + c.UseJsonSerializationOptions(this.jsonSerializerOptions); + }); - var request = new Request() { RequestParameter = "Look, I was invoked!" }; - - var response = await daprClient.InvokeMethodGrpcAsync("test", "not-existing", request); - - response.Name.ShouldBe("unexpected"); - } - - - [Fact] - public async Task GetMetadataAsync_WrapsRpcException() + var request = await client.CaptureGrpcRequestAsync(async daprClient => { - var client = new MockClient(); + return await daprClient.GetMetadataAsync(default); + }); - const string rpcExceptionMessage = "RPC exception"; - const StatusCode rpcStatusCode = StatusCode.Unavailable; - const string rpcStatusDetail = "Non success"; - var rpcStatus = new Status(rpcStatusCode, rpcStatusDetail); - var rpcException = new RpcException(rpcStatus, new Metadata(), rpcExceptionMessage); - - client.Mock - .Setup(m => m.GetMetadataAsync(It.IsAny(), It.IsAny())) - .Throws(rpcException); - - var ex = await Assert.ThrowsAsync(async () => - { - await client.DaprClient.GetMetadataAsync(default); - }); - Assert.Same(rpcException, ex.InnerException); - } - - [Fact] - public async Task GetMetadataAsync_WithReturnTypeAndData() + // Create Response & Respond + var response = new Autogen.Grpc.v1.GetMetadataResponse() { - await using var client = TestClient.CreateForDaprClient(c => - { - c.UseJsonSerializationOptions(this.jsonSerializerOptions); - }); + ActorRuntime = new(), + Id = "testId", + }; + response.ActorRuntime.ActiveActors.Add(new ActiveActorsCount { Type = "testType", Count = 1 }); + response.RegisteredComponents.Add(new RegisteredComponents { Name = "testName", Type = "testType", Version = "V1" }); + response.ExtendedMetadata.Add("e1", "v1"); - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.GetMetadataAsync(default); - }); + // Validate Response + var metadata = await request.CompleteWithMessageAsync(response); + metadata.Id.ShouldBe("testId"); + metadata.Extended.ShouldContain(new System.Collections.Generic.KeyValuePair("e1", "v1")); + metadata.Actors.ShouldContain(actors => actors.Count == 1 && actors.Type == "testType"); + metadata.Components.ShouldContain(components => components.Name == "testName" && components.Type == "testType" && components.Version == "V1" && components.Capabilities.Length == 0); + } + [Fact] + public async Task SetMetadataAsync_WrapsRpcException() + { + var client = new MockClient(); - // Create Response & Respond - var response = new Autogen.Grpc.v1.GetMetadataResponse() - { - ActorRuntime = new(), - Id = "testId", - }; - response.ActorRuntime.ActiveActors.Add(new ActiveActorsCount { Type = "testType", Count = 1 }); - response.RegisteredComponents.Add(new RegisteredComponents { Name = "testName", Type = "testType", Version = "V1" }); - response.ExtendedMetadata.Add("e1", "v1"); + const string rpcExceptionMessage = "RPC exception"; + const StatusCode rpcStatusCode = StatusCode.Unavailable; + const string rpcStatusDetail = "Non success"; - // Validate Response - var metadata = await request.CompleteWithMessageAsync(response); - metadata.Id.ShouldBe("testId"); - metadata.Extended.ShouldContain(new System.Collections.Generic.KeyValuePair("e1", "v1")); - metadata.Actors.ShouldContain(actors => actors.Count == 1 && actors.Type == "testType"); - metadata.Components.ShouldContain(components => components.Name == "testName" && components.Type == "testType" && components.Version == "V1" && components.Capabilities.Length == 0); - } + var rpcStatus = new Status(rpcStatusCode, rpcStatusDetail); + var rpcException = new RpcException(rpcStatus, new Metadata(), rpcExceptionMessage); - [Fact] - public async Task SetMetadataAsync_WrapsRpcException() + client.Mock + .Setup(m => m.SetMetadataAsync(It.IsAny(), It.IsAny())) + .Throws(rpcException); + + var ex = await Assert.ThrowsAsync(async () => { - var client = new MockClient(); + await client.DaprClient.SetMetadataAsync("testName", "", default); + }); + Assert.Same(rpcException, ex.InnerException); + } - const string rpcExceptionMessage = "RPC exception"; - const StatusCode rpcStatusCode = StatusCode.Unavailable; - const string rpcStatusDetail = "Non success"; - - var rpcStatus = new Status(rpcStatusCode, rpcStatusDetail); - var rpcException = new RpcException(rpcStatus, new Metadata(), rpcExceptionMessage); - - client.Mock - .Setup(m => m.SetMetadataAsync(It.IsAny(), It.IsAny())) - .Throws(rpcException); - - var ex = await Assert.ThrowsAsync(async () => - { - await client.DaprClient.SetMetadataAsync("testName", "", default); - }); - Assert.Same(rpcException, ex.InnerException); - } - - [Fact] - public async Task SetMetadataAsync_WithReturnTypeAndData() + [Fact] + public async Task SetMetadataAsync_WithReturnTypeAndData() + { + await using var client = TestClient.CreateForDaprClient(c => { - await using var client = TestClient.CreateForDaprClient(c => - { - c.UseJsonSerializationOptions(this.jsonSerializerOptions); - }); + c.UseJsonSerializationOptions(this.jsonSerializerOptions); + }); - var request = await client.CaptureGrpcRequestAsync(daprClient => - { - return daprClient.SetMetadataAsync("test", "testv", default); - }); - - // Get Request and validate - var envelope = await request.GetRequestEnvelopeAsync(); - envelope.Key.ShouldBe("test"); - envelope.Value.ShouldBe("testv"); - - await request.CompleteWithMessageAsync(new Empty()); - - } - - // Test implementation of the AppCallback.AppCallbackBase service - private class DaprAppCallbackService : AppCallback.Autogen.Grpc.v1.AppCallback.AppCallbackBase + var request = await client.CaptureGrpcRequestAsync(daprClient => { - public override Task OnInvoke(InvokeRequest request, ServerCallContext context) + return daprClient.SetMetadataAsync("test", "testv", default); + }); + + // Get Request and validate + var envelope = await request.GetRequestEnvelopeAsync(); + envelope.Key.ShouldBe("test"); + envelope.Value.ShouldBe("testv"); + + await request.CompleteWithMessageAsync(new Empty()); + + } + + // Test implementation of the AppCallback.AppCallbackBase service + private class DaprAppCallbackService : AppCallback.Autogen.Grpc.v1.AppCallback.AppCallbackBase + { + public override Task OnInvoke(InvokeRequest request, ServerCallContext context) + { + return request.Method switch { - return request.Method switch + "SayHello" => SayHello(request), + "TestRun" => TestRun(request), + _ => Task.FromResult(new InvokeResponse() { - "SayHello" => SayHello(request), - "TestRun" => TestRun(request), - _ => Task.FromResult(new InvokeResponse() - { - Data = Any.Pack(new Response() { Name = $"unexpected" }), - }), - }; - } + Data = Any.Pack(new Response() { Name = $"unexpected" }), + }), + }; + } - private Task SayHello(InvokeRequest request) + private Task SayHello(InvokeRequest request) + { + var helloRequest = request.Data.Unpack(); + var helloResponse = new Response() { Name = $"Hello {helloRequest.RequestParameter}" }; + + return Task.FromResult(new InvokeResponse() { - var helloRequest = request.Data.Unpack(); - var helloResponse = new Response() { Name = $"Hello {helloRequest.RequestParameter}" }; + Data = Any.Pack(helloResponse), + }); + } - return Task.FromResult(new InvokeResponse() - { - Data = Any.Pack(helloResponse), - }); - } - - private Task TestRun(InvokeRequest request) + private Task TestRun(InvokeRequest request) + { + var echoRequest = request.Data.Unpack(); + return Task.FromResult(new InvokeResponse() { - var echoRequest = request.Data.Unpack(); - return Task.FromResult(new InvokeResponse() - { - Data = Any.Pack(echoRequest), - }); - } + Data = Any.Pack(echoRequest), + }); } } -} +} \ No newline at end of file diff --git a/test/Dapr.Client.Test/DaprClientTest.cs b/test/Dapr.Client.Test/DaprClientTest.cs index e280728c..b825375b 100644 --- a/test/Dapr.Client.Test/DaprClientTest.cs +++ b/test/Dapr.Client.Test/DaprClientTest.cs @@ -15,86 +15,85 @@ using System; using System.Threading.Tasks; using Xunit; -namespace Dapr.Client +namespace Dapr.Client; + +public partial class DaprClientTest { - public partial class DaprClientTest + [Fact] + public void CreateInvokeHttpClient_WithAppId() { - [Fact] - public void CreateInvokeHttpClient_WithAppId() - { - var client = DaprClient.CreateInvokeHttpClient(appId: "bank", daprEndpoint: "http://localhost:3500"); - Assert.Equal("http://bank/", client.BaseAddress.AbsoluteUri); - } - - [Fact] - public void CreateInvokeHttpClient_InvalidAppId() - { - var ex = Assert.Throws(() => - { - // The appId needs to be something that can be used as hostname in a URI. - _ = DaprClient.CreateInvokeHttpClient(appId: ""); - }); - - Assert.Contains("The appId must be a valid hostname.", ex.Message); - Assert.IsType(ex.InnerException); - } - - [Fact] - public void CreateInvokeHttpClient_WithoutAppId() - { - var client = DaprClient.CreateInvokeHttpClient(daprEndpoint: "http://localhost:3500"); - Assert.Null(client.BaseAddress); - } - - [Fact] - public void CreateInvokeHttpClient_InvalidDaprEndpoint_InvalidFormat() - { - Assert.Throws(() => - { - _ = DaprClient.CreateInvokeHttpClient(daprEndpoint: ""); - }); - - // Exception message comes from the runtime, not validating it here. - } - - [Fact] - public void CreateInvokeHttpClient_InvalidDaprEndpoint_InvalidScheme() - { - var ex = Assert.Throws(() => - { - _ = DaprClient.CreateInvokeHttpClient(daprEndpoint: "ftp://localhost:3500"); - }); - - Assert.Contains("The URI scheme of the Dapr endpoint must be http or https.", ex.Message); - } - - [Fact] - public void GetDaprApiTokenHeader_ApiTokenSet_SetsApiTokenHeader() - { - var token = "test_token"; - var entry = DaprClient.GetDaprApiTokenHeader(token); - Assert.NotNull(entry); - Assert.Equal("test_token", entry.Value.Value); - } - - [Fact] - public void GetDaprApiTokenHeader_ApiTokenNotSet_NullApiTokenHeader() - { - var entry = DaprClient.GetDaprApiTokenHeader(null); - Assert.Equal(default, entry); - } - - [Fact] - public async Task TestShutdownApi() - { - await using var client = TestClient.CreateForDaprClient(); - - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - await daprClient.ShutdownSidecarAsync(); - }); - - request.Dismiss(); - } + var client = DaprClient.CreateInvokeHttpClient(appId: "bank", daprEndpoint: "http://localhost:3500"); + Assert.Equal("http://bank/", client.BaseAddress.AbsoluteUri); } -} + + [Fact] + public void CreateInvokeHttpClient_InvalidAppId() + { + var ex = Assert.Throws(() => + { + // The appId needs to be something that can be used as hostname in a URI. + _ = DaprClient.CreateInvokeHttpClient(appId: ""); + }); + + Assert.Contains("The appId must be a valid hostname.", ex.Message); + Assert.IsType(ex.InnerException); + } + + [Fact] + public void CreateInvokeHttpClient_WithoutAppId() + { + var client = DaprClient.CreateInvokeHttpClient(daprEndpoint: "http://localhost:3500"); + Assert.Null(client.BaseAddress); + } + + [Fact] + public void CreateInvokeHttpClient_InvalidDaprEndpoint_InvalidFormat() + { + Assert.Throws(() => + { + _ = DaprClient.CreateInvokeHttpClient(daprEndpoint: ""); + }); + + // Exception message comes from the runtime, not validating it here. + } + + [Fact] + public void CreateInvokeHttpClient_InvalidDaprEndpoint_InvalidScheme() + { + var ex = Assert.Throws(() => + { + _ = DaprClient.CreateInvokeHttpClient(daprEndpoint: "ftp://localhost:3500"); + }); + + Assert.Contains("The URI scheme of the Dapr endpoint must be http or https.", ex.Message); + } + + [Fact] + public void GetDaprApiTokenHeader_ApiTokenSet_SetsApiTokenHeader() + { + var token = "test_token"; + var entry = DaprClient.GetDaprApiTokenHeader(token); + Assert.NotNull(entry); + Assert.Equal("test_token", entry.Value.Value); + } + + [Fact] + public void GetDaprApiTokenHeader_ApiTokenNotSet_NullApiTokenHeader() + { + var entry = DaprClient.GetDaprApiTokenHeader(null); + Assert.Equal(default, entry); + } + + [Fact] + public async Task TestShutdownApi() + { + await using var client = TestClient.CreateForDaprClient(); + + var request = await client.CaptureGrpcRequestAsync(async daprClient => + { + await daprClient.ShutdownSidecarAsync(); + }); + + request.Dismiss(); + } +} \ No newline at end of file diff --git a/test/Dapr.Client.Test/DistributedLockApiTest.cs b/test/Dapr.Client.Test/DistributedLockApiTest.cs index 1935a543..ad5775a9 100644 --- a/test/Dapr.Client.Test/DistributedLockApiTest.cs +++ b/test/Dapr.Client.Test/DistributedLockApiTest.cs @@ -17,80 +17,79 @@ using Xunit; using Shouldly; using System; -namespace Dapr.Client.Test +namespace Dapr.Client.Test; + +[System.Obsolete] +public class DistributedLockApiTest { - [System.Obsolete] - public class DistributedLockApiTest + [Fact] + public async Task TryLockAsync_WithAllValues_ValidateRequest() { - [Fact] - public async Task TryLockAsync_WithAllValues_ValidateRequest() + await using var client = TestClient.CreateForDaprClient(); + string storeName = "redis"; + string resourceId = "resourceId"; + string lockOwner = "owner1"; + Int32 expiryInSeconds = 1000; + var request = await client.CaptureGrpcRequestAsync(async daprClient => { - await using var client = TestClient.CreateForDaprClient(); - string storeName = "redis"; - string resourceId = "resourceId"; - string lockOwner = "owner1"; - Int32 expiryInSeconds = 1000; - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.Lock(storeName, resourceId, lockOwner, expiryInSeconds); - }); + return await daprClient.Lock(storeName, resourceId, lockOwner, expiryInSeconds); + }); - // Get Request and validate - var envelope = await request.GetRequestEnvelopeAsync(); - envelope.StoreName.ShouldBe("redis"); - envelope.ResourceId.ShouldBe("resourceId"); - envelope.LockOwner.ShouldBe("owner1"); - envelope.ExpiryInSeconds.ShouldBe(1000); + // Get Request and validate + var envelope = await request.GetRequestEnvelopeAsync(); + envelope.StoreName.ShouldBe("redis"); + envelope.ResourceId.ShouldBe("resourceId"); + envelope.LockOwner.ShouldBe("owner1"); + envelope.ExpiryInSeconds.ShouldBe(1000); - // Get response and validate - var invokeResponse = new Autogenerated.TryLockResponse{ - Success = true - }; + // Get response and validate + var invokeResponse = new Autogenerated.TryLockResponse{ + Success = true + }; - var domainResponse = await request.CompleteWithMessageAsync(invokeResponse); - domainResponse.Success.ShouldBe(true); - } - - [Fact] - public async Task TryLockAsync_WithAllValues_ArgumentVerifierException() - { - var client = new DaprClientBuilder().Build(); - string storeName = "redis"; - string resourceId = "resourceId"; - string lockOwner = "owner1"; - Int32 expiryInSeconds = 0; - // Get response and validate - await Assert.ThrowsAsync(async () => await client.Lock(storeName, resourceId, lockOwner, expiryInSeconds)); - } - - //Tests For Unlock API - - [Fact] - public async Task UnLockAsync_WithAllValues_ValidateRequest() - { - await using var client = TestClient.CreateForDaprClient(); - string storeName = "redis"; - string resourceId = "resourceId"; - string lockOwner = "owner1"; - - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.Unlock(storeName, resourceId, lockOwner); - }); - - // Get Request and validate - var envelope = await request.GetRequestEnvelopeAsync(); - envelope.StoreName.ShouldBe("redis"); - envelope.ResourceId.ShouldBe("resourceId"); - envelope.LockOwner.ShouldBe("owner1"); - - // Get response and validate - var invokeResponse = new Autogenerated.UnlockResponse{ - Status = Autogenerated.UnlockResponse.Types.Status.LockDoesNotExist - }; - - var domainResponse = await request.CompleteWithMessageAsync(invokeResponse); - domainResponse.status.ShouldBe(LockStatus.LockDoesNotExist); - } + var domainResponse = await request.CompleteWithMessageAsync(invokeResponse); + domainResponse.Success.ShouldBe(true); } -} + + [Fact] + public async Task TryLockAsync_WithAllValues_ArgumentVerifierException() + { + var client = new DaprClientBuilder().Build(); + string storeName = "redis"; + string resourceId = "resourceId"; + string lockOwner = "owner1"; + Int32 expiryInSeconds = 0; + // Get response and validate + await Assert.ThrowsAsync(async () => await client.Lock(storeName, resourceId, lockOwner, expiryInSeconds)); + } + + //Tests For Unlock API + + [Fact] + public async Task UnLockAsync_WithAllValues_ValidateRequest() + { + await using var client = TestClient.CreateForDaprClient(); + string storeName = "redis"; + string resourceId = "resourceId"; + string lockOwner = "owner1"; + + var request = await client.CaptureGrpcRequestAsync(async daprClient => + { + return await daprClient.Unlock(storeName, resourceId, lockOwner); + }); + + // Get Request and validate + var envelope = await request.GetRequestEnvelopeAsync(); + envelope.StoreName.ShouldBe("redis"); + envelope.ResourceId.ShouldBe("resourceId"); + envelope.LockOwner.ShouldBe("owner1"); + + // Get response and validate + var invokeResponse = new Autogenerated.UnlockResponse{ + Status = Autogenerated.UnlockResponse.Types.Status.LockDoesNotExist + }; + + var domainResponse = await request.CompleteWithMessageAsync(invokeResponse); + domainResponse.status.ShouldBe(LockStatus.LockDoesNotExist); + } +} \ No newline at end of file diff --git a/test/Dapr.Client.Test/Extensions/EnumExtensionTest.cs b/test/Dapr.Client.Test/Extensions/EnumExtensionTest.cs index 83c4354f..e2eb395c 100644 --- a/test/Dapr.Client.Test/Extensions/EnumExtensionTest.cs +++ b/test/Dapr.Client.Test/Extensions/EnumExtensionTest.cs @@ -1,38 +1,37 @@ using System.Runtime.Serialization; using Xunit; -namespace Dapr.Client.Test.Extensions +namespace Dapr.Client.Test.Extensions; + +public class EnumExtensionTest { - public class EnumExtensionTest + [Fact] + public void GetValueFromEnumMember_RedResolvesAsExpected() { - [Fact] - public void GetValueFromEnumMember_RedResolvesAsExpected() - { - var value = TestEnum.Red.GetValueFromEnumMember(); - Assert.Equal("red", value); - } - - [Fact] - public void GetValueFromEnumMember_YellowResolvesAsExpected() - { - var value = TestEnum.Yellow.GetValueFromEnumMember(); - Assert.Equal("YELLOW", value); - } - - [Fact] - public void GetValueFromEnumMember_BlueResolvesAsExpected() - { - var value = TestEnum.Blue.GetValueFromEnumMember(); - Assert.Equal("Blue", value); - } + var value = TestEnum.Red.GetValueFromEnumMember(); + Assert.Equal("red", value); } - public enum TestEnum + [Fact] + public void GetValueFromEnumMember_YellowResolvesAsExpected() { - [EnumMember(Value = "red")] - Red, - [EnumMember(Value = "YELLOW")] - Yellow, - Blue + var value = TestEnum.Yellow.GetValueFromEnumMember(); + Assert.Equal("YELLOW", value); + } + + [Fact] + public void GetValueFromEnumMember_BlueResolvesAsExpected() + { + var value = TestEnum.Blue.GetValueFromEnumMember(); + Assert.Equal("Blue", value); } } + +public enum TestEnum +{ + [EnumMember(Value = "red")] + Red, + [EnumMember(Value = "YELLOW")] + Yellow, + Blue +} \ No newline at end of file diff --git a/test/Dapr.Client.Test/Extensions/HttpExtensionTest.cs b/test/Dapr.Client.Test/Extensions/HttpExtensionTest.cs index 7b93c1c9..4f5bdc49 100644 --- a/test/Dapr.Client.Test/Extensions/HttpExtensionTest.cs +++ b/test/Dapr.Client.Test/Extensions/HttpExtensionTest.cs @@ -2,62 +2,61 @@ using System.Net.Http; using Xunit; -namespace Dapr.Client.Test.Extensions +namespace Dapr.Client.Test.Extensions; + +public class HttpExtensionTest { - public class HttpExtensionTest + [Fact] + public void AddQueryParameters_ReturnsEmptyQueryStringWithNullParameters() { - [Fact] - public void AddQueryParameters_ReturnsEmptyQueryStringWithNullParameters() - { - const string uri = "https://localhost/mypath"; - var httpRq = new HttpRequestMessage(HttpMethod.Get, uri); - var updatedUri = httpRq.RequestUri.AddQueryParameters(null); - Assert.Equal(uri, updatedUri.AbsoluteUri); - } - - [Fact] - public void AddQueryParameters_ReturnsOriginalQueryStringWithNullParameters() - { - const string uri = "https://localhost/mypath?a=0&b=1"; - var httpRq = new HttpRequestMessage(HttpMethod.Get, uri); - var updatedUri = httpRq.RequestUri.AddQueryParameters(null); - Assert.Equal(uri, updatedUri.AbsoluteUri); - } - - [Fact] - public void AddQueryParameters_BuildsQueryString() - { - var httpRq = new HttpRequestMessage(HttpMethod.Get, "https://localhost/mypath?a=0"); - var updatedUri = httpRq.RequestUri.AddQueryParameters(new List> - { - new("test", "value") - }); - Assert.Equal("https://localhost/mypath?a=0&test=value", updatedUri.AbsoluteUri); - } - - [Fact] - public void AddQueryParameters_BuildQueryStringWithDuplicateKeys() - { - var httpRq = new HttpRequestMessage(HttpMethod.Get, "https://localhost/mypath"); - var updatedUri = httpRq.RequestUri.AddQueryParameters(new List> - { - new("test", "1"), - new("test", "2"), - new("test", "3") - }); - Assert.Equal("https://localhost/mypath?test=1&test=2&test=3", updatedUri.AbsoluteUri); - } - - [Fact] - public void AddQueryParameters_EscapeSpacesInValues() - { - var httpRq = new HttpRequestMessage(HttpMethod.Get, "https://localhost/mypath"); - var updatedUri = httpRq.RequestUri.AddQueryParameters(new List> - { - new("name1", "John Doe"), - new("name2", "Jane Doe") - }); - Assert.Equal("https://localhost/mypath?name1=John%20Doe&name2=Jane%20Doe", updatedUri.AbsoluteUri); - } + const string uri = "https://localhost/mypath"; + var httpRq = new HttpRequestMessage(HttpMethod.Get, uri); + var updatedUri = httpRq.RequestUri.AddQueryParameters(null); + Assert.Equal(uri, updatedUri.AbsoluteUri); } -} + + [Fact] + public void AddQueryParameters_ReturnsOriginalQueryStringWithNullParameters() + { + const string uri = "https://localhost/mypath?a=0&b=1"; + var httpRq = new HttpRequestMessage(HttpMethod.Get, uri); + var updatedUri = httpRq.RequestUri.AddQueryParameters(null); + Assert.Equal(uri, updatedUri.AbsoluteUri); + } + + [Fact] + public void AddQueryParameters_BuildsQueryString() + { + var httpRq = new HttpRequestMessage(HttpMethod.Get, "https://localhost/mypath?a=0"); + var updatedUri = httpRq.RequestUri.AddQueryParameters(new List> + { + new("test", "value") + }); + Assert.Equal("https://localhost/mypath?a=0&test=value", updatedUri.AbsoluteUri); + } + + [Fact] + public void AddQueryParameters_BuildQueryStringWithDuplicateKeys() + { + var httpRq = new HttpRequestMessage(HttpMethod.Get, "https://localhost/mypath"); + var updatedUri = httpRq.RequestUri.AddQueryParameters(new List> + { + new("test", "1"), + new("test", "2"), + new("test", "3") + }); + Assert.Equal("https://localhost/mypath?test=1&test=2&test=3", updatedUri.AbsoluteUri); + } + + [Fact] + public void AddQueryParameters_EscapeSpacesInValues() + { + var httpRq = new HttpRequestMessage(HttpMethod.Get, "https://localhost/mypath"); + var updatedUri = httpRq.RequestUri.AddQueryParameters(new List> + { + new("name1", "John Doe"), + new("name2", "Jane Doe") + }); + Assert.Equal("https://localhost/mypath?name1=John%20Doe&name2=Jane%20Doe", updatedUri.AbsoluteUri); + } +} \ No newline at end of file diff --git a/test/Dapr.Client.Test/InvocationHandlerTests.cs b/test/Dapr.Client.Test/InvocationHandlerTests.cs index b171ca39..e6cfc223 100644 --- a/test/Dapr.Client.Test/InvocationHandlerTests.cs +++ b/test/Dapr.Client.Test/InvocationHandlerTests.cs @@ -22,230 +22,229 @@ using Xunit; #nullable enable -namespace Dapr.Client +namespace Dapr.Client; + +public class InvocationHandlerTests { - public class InvocationHandlerTests + [Fact] + public void DaprEndpoint_InvalidScheme() { - [Fact] - public void DaprEndpoint_InvalidScheme() + var handler = new InvocationHandler(); + var ex = Assert.Throws(() => { - var handler = new InvocationHandler(); - var ex = Assert.Throws(() => - { - handler.DaprEndpoint = "ftp://localhost:3500"; - }); + handler.DaprEndpoint = "ftp://localhost:3500"; + }); - Assert.Contains("The URI scheme of the Dapr endpoint must be http or https.", ex.Message); + Assert.Contains("The URI scheme of the Dapr endpoint must be http or https.", ex.Message); + } + + [Fact] + public void DaprEndpoint_InvalidUri() + { + var handler = new InvocationHandler(); + Assert.Throws(() => + { + handler.DaprEndpoint = ""; + }); + + // Exception message comes from the runtime, not validating it here + } + + [Fact] + public void TryRewriteUri_FailsForNullUri() + { + var handler = new InvocationHandler(); + Assert.False(handler.TryRewriteUri(null!, out var rewritten)); + Assert.Null(rewritten); + } + + [Fact] + public void TryRewriteUri_FailsForBadScheme() + { + var uri = new Uri("ftp://test", UriKind.Absolute); + + var handler = new InvocationHandler(); + Assert.False(handler.TryRewriteUri(uri, out var rewritten)); + Assert.Null(rewritten); + } + + [Fact] + public void TryRewriteUri_FailsForRelativeUris() + { + var uri = new Uri("test", UriKind.Relative); + + var handler = new InvocationHandler(); + Assert.False(handler.TryRewriteUri(uri, out var rewritten)); + Assert.Null(rewritten); + } + + [Theory] + [InlineData(null, "http://bank", "https://some.host:3499/v1.0/invoke/bank/method/")] + [InlineData("bank", "http://bank", "https://some.host:3499/v1.0/invoke/bank/method/")] + [InlineData("Bank", "http://bank", "https://some.host:3499/v1.0/invoke/Bank/method/")] + [InlineData("invalid", "http://bank", "https://some.host:3499/v1.0/invoke/bank/method/")] + [InlineData(null, "http://Bank", "https://some.host:3499/v1.0/invoke/bank/method/")] + [InlineData("Bank", "http://Bank", "https://some.host:3499/v1.0/invoke/Bank/method/")] + [InlineData("bank", "http://Bank", "https://some.host:3499/v1.0/invoke/bank/method/")] + [InlineData("invalid", "http://Bank", "https://some.host:3499/v1.0/invoke/bank/method/")] + [InlineData(null, "http://bank:3939", "https://some.host:3499/v1.0/invoke/bank/method/")] + [InlineData("bank", "http://bank:3939", "https://some.host:3499/v1.0/invoke/bank/method/")] + [InlineData("invalid", "http://bank:3939", "https://some.host:3499/v1.0/invoke/bank/method/")] + [InlineData(null, "http://Bank:3939", "https://some.host:3499/v1.0/invoke/bank/method/")] + [InlineData("Bank", "http://Bank:3939", "https://some.host:3499/v1.0/invoke/Bank/method/")] + [InlineData("invalid", "http://Bank:3939", "https://some.host:3499/v1.0/invoke/bank/method/")] + [InlineData(null, "http://app-id.with.dots", "https://some.host:3499/v1.0/invoke/app-id.with.dots/method/")] + [InlineData("app-id.with.dots", "http://app-id.with.dots", "https://some.host:3499/v1.0/invoke/app-id.with.dots/method/")] + [InlineData("invalid", "http://app-id.with.dots", "https://some.host:3499/v1.0/invoke/app-id.with.dots/method/")] + [InlineData(null, "http://App-id.with.dots", "https://some.host:3499/v1.0/invoke/app-id.with.dots/method/")] + [InlineData("App-id.with.dots", "http://App-id.with.dots", "https://some.host:3499/v1.0/invoke/App-id.with.dots/method/")] + [InlineData("invalid", "http://App-id.with.dots", "https://some.host:3499/v1.0/invoke/app-id.with.dots/method/")] + [InlineData(null, "http://bank:3939/", "https://some.host:3499/v1.0/invoke/bank/method/")] + [InlineData("bank", "http://bank:3939/", "https://some.host:3499/v1.0/invoke/bank/method/")] + [InlineData("invalid", "http://bank:3939/", "https://some.host:3499/v1.0/invoke/bank/method/")] + [InlineData(null, "http://Bank:3939/", "https://some.host:3499/v1.0/invoke/bank/method/")] + [InlineData("Bank", "http://Bank:3939/", "https://some.host:3499/v1.0/invoke/Bank/method/")] + [InlineData("invalid", "http://Bank:3939/", "https://some.host:3499/v1.0/invoke/bank/method/")] + [InlineData(null, "http://bank:3939/some/path", "https://some.host:3499/v1.0/invoke/bank/method/some/path")] + [InlineData("bank", "http://bank:3939/some/path", "https://some.host:3499/v1.0/invoke/bank/method/some/path")] + [InlineData("invalid", "http://bank:3939/some/path", "https://some.host:3499/v1.0/invoke/bank/method/some/path")] + [InlineData(null, "http://Bank:3939/some/path", "https://some.host:3499/v1.0/invoke/bank/method/some/path")] + [InlineData("Bank", "http://Bank:3939/some/path", "https://some.host:3499/v1.0/invoke/Bank/method/some/path")] + [InlineData("invalid", "http://Bank:3939/some/path", "https://some.host:3499/v1.0/invoke/bank/method/some/path")] + [InlineData(null, "http://bank:3939/some/path?q=test&p=another#fragment", "https://some.host:3499/v1.0/invoke/bank/method/some/path?q=test&p=another#fragment")] + [InlineData("bank", "http://bank:3939/some/path?q=test&p=another#fragment", "https://some.host:3499/v1.0/invoke/bank/method/some/path?q=test&p=another#fragment")] + [InlineData("invalid", "http://bank:3939/some/path?q=test&p=another#fragment", "https://some.host:3499/v1.0/invoke/bank/method/some/path?q=test&p=another#fragment")] + [InlineData(null, "http://Bank:3939/some/path?q=test&p=another#fragment", "https://some.host:3499/v1.0/invoke/bank/method/some/path?q=test&p=another#fragment")] + [InlineData("Bank", "http://Bank:3939/some/path?q=test&p=another#fragment", "https://some.host:3499/v1.0/invoke/Bank/method/some/path?q=test&p=another#fragment")] + [InlineData("invalid", "http://Bank:3939/some/path?q=test&p=another#fragment", "https://some.host:3499/v1.0/invoke/bank/method/some/path?q=test&p=another#fragment")] + public void TryRewriteUri_WithNoAppId_RewritesUriToDaprInvoke(string? appId, string uri, string expected) + { + var handler = new InvocationHandler() + { + DaprEndpoint = "https://some.host:3499", + DefaultAppId = appId, + }; + + Assert.True(handler.TryRewriteUri(new Uri(uri), out var rewritten)); + Assert.Equal(expected, rewritten!.OriginalString); + } + + [Fact] + public async Task SendAsync_InvalidNotSetUri_ThrowsException() + { + var handler = new InvocationHandler(); + var ex = await Assert.ThrowsAsync(async () => + { + await CallSendAsync(handler, new HttpRequestMessage() { }); // No URI set + }); + + Assert.Contains("The request URI '' is not a valid Dapr service invocation destination.", ex.Message); + } + + [Fact] + public async Task SendAsync_RewritesUri() + { + var uri = "http://bank/accounts/17?"; + + var capture = new CaptureHandler(); + var handler = new InvocationHandler() + { + InnerHandler = capture, + + DaprEndpoint = "https://localhost:5000", + DaprApiToken = null, + }; + + var request = new HttpRequestMessage(HttpMethod.Post, uri); + await CallSendAsync(handler, request); + + Assert.Equal("https://localhost:5000/v1.0/invoke/bank/method/accounts/17?", capture.RequestUri?.OriginalString); + Assert.Null(capture.DaprApiToken); + + Assert.Equal(uri, request.RequestUri?.OriginalString); + Assert.False(request.Headers.TryGetValues("dapr-api-token", out _)); + } + + [Fact] + public async Task SendAsync_RewritesUri_AndAppId() + { + var uri = "http://bank/accounts/17?"; + + var capture = new CaptureHandler(); + var handler = new InvocationHandler() + { + InnerHandler = capture, + + DaprEndpoint = "https://localhost:5000", + DaprApiToken = null, + DefaultAppId = "Bank" + }; + + var request = new HttpRequestMessage(HttpMethod.Post, uri); + await CallSendAsync(handler, request); + + Assert.Equal("https://localhost:5000/v1.0/invoke/Bank/method/accounts/17?", capture.RequestUri?.OriginalString); + Assert.Null(capture.DaprApiToken); + + Assert.Equal(uri, request.RequestUri?.OriginalString); + Assert.False(request.Headers.TryGetValues("dapr-api-token", out _)); + } + + [Fact] + public async Task SendAsync_RewritesUri_AndAddsApiToken() + { + var uri = "http://bank/accounts/17?"; + + var capture = new CaptureHandler(); + var handler = new InvocationHandler() + { + InnerHandler = capture, + + DaprEndpoint = "https://localhost:5000", + DaprApiToken = "super-duper-secure", + }; + + var request = new HttpRequestMessage(HttpMethod.Post, uri); + await CallSendAsync(handler, request); + + Assert.Equal("https://localhost:5000/v1.0/invoke/bank/method/accounts/17?", capture.RequestUri?.OriginalString); + Assert.Equal("super-duper-secure", capture.DaprApiToken); + + Assert.Equal(uri, request.RequestUri?.OriginalString); + Assert.False(request.Headers.TryGetValues("dapr-api-token", out _)); + } + + private async Task CallSendAsync(InvocationHandler handler, HttpRequestMessage message, CancellationToken cancellationToken = default) + { + // SendAsync is protected, can't call it directly. + var method = handler.GetType().GetMethod("SendAsync", BindingFlags.Instance | BindingFlags.NonPublic); + Assert.NotNull(method); + + try + { + return await (Task)method!.Invoke(handler, new object[] { message, cancellationToken, })!; } - - [Fact] - public void DaprEndpoint_InvalidUri() + catch (TargetInvocationException tie) // reflection always adds an extra layer of exceptions. { - var handler = new InvocationHandler(); - Assert.Throws(() => - { - handler.DaprEndpoint = ""; - }); - - // Exception message comes from the runtime, not validating it here - } - - [Fact] - public void TryRewriteUri_FailsForNullUri() - { - var handler = new InvocationHandler(); - Assert.False(handler.TryRewriteUri(null!, out var rewritten)); - Assert.Null(rewritten); - } - - [Fact] - public void TryRewriteUri_FailsForBadScheme() - { - var uri = new Uri("ftp://test", UriKind.Absolute); - - var handler = new InvocationHandler(); - Assert.False(handler.TryRewriteUri(uri, out var rewritten)); - Assert.Null(rewritten); - } - - [Fact] - public void TryRewriteUri_FailsForRelativeUris() - { - var uri = new Uri("test", UriKind.Relative); - - var handler = new InvocationHandler(); - Assert.False(handler.TryRewriteUri(uri, out var rewritten)); - Assert.Null(rewritten); - } - - [Theory] - [InlineData(null, "http://bank", "https://some.host:3499/v1.0/invoke/bank/method/")] - [InlineData("bank", "http://bank", "https://some.host:3499/v1.0/invoke/bank/method/")] - [InlineData("Bank", "http://bank", "https://some.host:3499/v1.0/invoke/Bank/method/")] - [InlineData("invalid", "http://bank", "https://some.host:3499/v1.0/invoke/bank/method/")] - [InlineData(null, "http://Bank", "https://some.host:3499/v1.0/invoke/bank/method/")] - [InlineData("Bank", "http://Bank", "https://some.host:3499/v1.0/invoke/Bank/method/")] - [InlineData("bank", "http://Bank", "https://some.host:3499/v1.0/invoke/bank/method/")] - [InlineData("invalid", "http://Bank", "https://some.host:3499/v1.0/invoke/bank/method/")] - [InlineData(null, "http://bank:3939", "https://some.host:3499/v1.0/invoke/bank/method/")] - [InlineData("bank", "http://bank:3939", "https://some.host:3499/v1.0/invoke/bank/method/")] - [InlineData("invalid", "http://bank:3939", "https://some.host:3499/v1.0/invoke/bank/method/")] - [InlineData(null, "http://Bank:3939", "https://some.host:3499/v1.0/invoke/bank/method/")] - [InlineData("Bank", "http://Bank:3939", "https://some.host:3499/v1.0/invoke/Bank/method/")] - [InlineData("invalid", "http://Bank:3939", "https://some.host:3499/v1.0/invoke/bank/method/")] - [InlineData(null, "http://app-id.with.dots", "https://some.host:3499/v1.0/invoke/app-id.with.dots/method/")] - [InlineData("app-id.with.dots", "http://app-id.with.dots", "https://some.host:3499/v1.0/invoke/app-id.with.dots/method/")] - [InlineData("invalid", "http://app-id.with.dots", "https://some.host:3499/v1.0/invoke/app-id.with.dots/method/")] - [InlineData(null, "http://App-id.with.dots", "https://some.host:3499/v1.0/invoke/app-id.with.dots/method/")] - [InlineData("App-id.with.dots", "http://App-id.with.dots", "https://some.host:3499/v1.0/invoke/App-id.with.dots/method/")] - [InlineData("invalid", "http://App-id.with.dots", "https://some.host:3499/v1.0/invoke/app-id.with.dots/method/")] - [InlineData(null, "http://bank:3939/", "https://some.host:3499/v1.0/invoke/bank/method/")] - [InlineData("bank", "http://bank:3939/", "https://some.host:3499/v1.0/invoke/bank/method/")] - [InlineData("invalid", "http://bank:3939/", "https://some.host:3499/v1.0/invoke/bank/method/")] - [InlineData(null, "http://Bank:3939/", "https://some.host:3499/v1.0/invoke/bank/method/")] - [InlineData("Bank", "http://Bank:3939/", "https://some.host:3499/v1.0/invoke/Bank/method/")] - [InlineData("invalid", "http://Bank:3939/", "https://some.host:3499/v1.0/invoke/bank/method/")] - [InlineData(null, "http://bank:3939/some/path", "https://some.host:3499/v1.0/invoke/bank/method/some/path")] - [InlineData("bank", "http://bank:3939/some/path", "https://some.host:3499/v1.0/invoke/bank/method/some/path")] - [InlineData("invalid", "http://bank:3939/some/path", "https://some.host:3499/v1.0/invoke/bank/method/some/path")] - [InlineData(null, "http://Bank:3939/some/path", "https://some.host:3499/v1.0/invoke/bank/method/some/path")] - [InlineData("Bank", "http://Bank:3939/some/path", "https://some.host:3499/v1.0/invoke/Bank/method/some/path")] - [InlineData("invalid", "http://Bank:3939/some/path", "https://some.host:3499/v1.0/invoke/bank/method/some/path")] - [InlineData(null, "http://bank:3939/some/path?q=test&p=another#fragment", "https://some.host:3499/v1.0/invoke/bank/method/some/path?q=test&p=another#fragment")] - [InlineData("bank", "http://bank:3939/some/path?q=test&p=another#fragment", "https://some.host:3499/v1.0/invoke/bank/method/some/path?q=test&p=another#fragment")] - [InlineData("invalid", "http://bank:3939/some/path?q=test&p=another#fragment", "https://some.host:3499/v1.0/invoke/bank/method/some/path?q=test&p=another#fragment")] - [InlineData(null, "http://Bank:3939/some/path?q=test&p=another#fragment", "https://some.host:3499/v1.0/invoke/bank/method/some/path?q=test&p=another#fragment")] - [InlineData("Bank", "http://Bank:3939/some/path?q=test&p=another#fragment", "https://some.host:3499/v1.0/invoke/Bank/method/some/path?q=test&p=another#fragment")] - [InlineData("invalid", "http://Bank:3939/some/path?q=test&p=another#fragment", "https://some.host:3499/v1.0/invoke/bank/method/some/path?q=test&p=another#fragment")] - public void TryRewriteUri_WithNoAppId_RewritesUriToDaprInvoke(string? appId, string uri, string expected) - { - var handler = new InvocationHandler() - { - DaprEndpoint = "https://some.host:3499", - DefaultAppId = appId, - }; - - Assert.True(handler.TryRewriteUri(new Uri(uri), out var rewritten)); - Assert.Equal(expected, rewritten!.OriginalString); - } - - [Fact] - public async Task SendAsync_InvalidNotSetUri_ThrowsException() - { - var handler = new InvocationHandler(); - var ex = await Assert.ThrowsAsync(async () => - { - await CallSendAsync(handler, new HttpRequestMessage() { }); // No URI set - }); - - Assert.Contains("The request URI '' is not a valid Dapr service invocation destination.", ex.Message); - } - - [Fact] - public async Task SendAsync_RewritesUri() - { - var uri = "http://bank/accounts/17?"; - - var capture = new CaptureHandler(); - var handler = new InvocationHandler() - { - InnerHandler = capture, - - DaprEndpoint = "https://localhost:5000", - DaprApiToken = null, - }; - - var request = new HttpRequestMessage(HttpMethod.Post, uri); - await CallSendAsync(handler, request); - - Assert.Equal("https://localhost:5000/v1.0/invoke/bank/method/accounts/17?", capture.RequestUri?.OriginalString); - Assert.Null(capture.DaprApiToken); - - Assert.Equal(uri, request.RequestUri?.OriginalString); - Assert.False(request.Headers.TryGetValues("dapr-api-token", out _)); - } - - [Fact] - public async Task SendAsync_RewritesUri_AndAppId() - { - var uri = "http://bank/accounts/17?"; - - var capture = new CaptureHandler(); - var handler = new InvocationHandler() - { - InnerHandler = capture, - - DaprEndpoint = "https://localhost:5000", - DaprApiToken = null, - DefaultAppId = "Bank" - }; - - var request = new HttpRequestMessage(HttpMethod.Post, uri); - await CallSendAsync(handler, request); - - Assert.Equal("https://localhost:5000/v1.0/invoke/Bank/method/accounts/17?", capture.RequestUri?.OriginalString); - Assert.Null(capture.DaprApiToken); - - Assert.Equal(uri, request.RequestUri?.OriginalString); - Assert.False(request.Headers.TryGetValues("dapr-api-token", out _)); - } - - [Fact] - public async Task SendAsync_RewritesUri_AndAddsApiToken() - { - var uri = "http://bank/accounts/17?"; - - var capture = new CaptureHandler(); - var handler = new InvocationHandler() - { - InnerHandler = capture, - - DaprEndpoint = "https://localhost:5000", - DaprApiToken = "super-duper-secure", - }; - - var request = new HttpRequestMessage(HttpMethod.Post, uri); - await CallSendAsync(handler, request); - - Assert.Equal("https://localhost:5000/v1.0/invoke/bank/method/accounts/17?", capture.RequestUri?.OriginalString); - Assert.Equal("super-duper-secure", capture.DaprApiToken); - - Assert.Equal(uri, request.RequestUri?.OriginalString); - Assert.False(request.Headers.TryGetValues("dapr-api-token", out _)); - } - - private async Task CallSendAsync(InvocationHandler handler, HttpRequestMessage message, CancellationToken cancellationToken = default) - { - // SendAsync is protected, can't call it directly. - var method = handler.GetType().GetMethod("SendAsync", BindingFlags.Instance | BindingFlags.NonPublic); - Assert.NotNull(method); - - try - { - return await (Task)method!.Invoke(handler, new object[] { message, cancellationToken, })!; - } - catch (TargetInvocationException tie) // reflection always adds an extra layer of exceptions. - { - throw tie.InnerException!; - } - } - - private class CaptureHandler : HttpMessageHandler - { - public Uri? RequestUri { get; private set; } - - public string? DaprApiToken { get; private set; } - - protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - RequestUri = request.RequestUri; - if (request.Headers.TryGetValues("dapr-api-token", out var tokens)) - { - DaprApiToken = tokens.SingleOrDefault(); - } - - return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)); - } + throw tie.InnerException!; } } -} + + private class CaptureHandler : HttpMessageHandler + { + public Uri? RequestUri { get; private set; } + + public string? DaprApiToken { get; private set; } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + RequestUri = request.RequestUri; + if (request.Headers.TryGetValues("dapr-api-token", out var tokens)) + { + DaprApiToken = tokens.SingleOrDefault(); + } + + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)); + } + } +} \ No newline at end of file diff --git a/test/Dapr.Client.Test/InvokeBindingApiTest.cs b/test/Dapr.Client.Test/InvokeBindingApiTest.cs index c980e889..d721045d 100644 --- a/test/Dapr.Client.Test/InvokeBindingApiTest.cs +++ b/test/Dapr.Client.Test/InvokeBindingApiTest.cs @@ -11,302 +11,301 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Client.Test +namespace Dapr.Client.Test; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Dapr.Client.Autogen.Grpc.v1; +using Shouldly; +using Google.Protobuf; +using Grpc.Core; +using Moq; +using Xunit; + +public class InvokeBindingApiTest { - using System; - using System.Collections.Generic; - using System.Linq; - using System.Text.Json; - using System.Threading; - using System.Threading.Tasks; - using Dapr.Client.Autogen.Grpc.v1; - using Shouldly; - using Google.Protobuf; - using Grpc.Core; - using Moq; - using Xunit; - - public class InvokeBindingApiTest + [Fact] + public async Task InvokeBindingAsync_ValidateRequest() { - [Fact] - public async Task InvokeBindingAsync_ValidateRequest() + await using var client = TestClient.CreateForDaprClient(); + + var invokeRequest = new InvokeRequest() { RequestParameter = "Hello " }; + var request = await client.CaptureGrpcRequestAsync(async daprClient => { - await using var client = TestClient.CreateForDaprClient(); + await daprClient.InvokeBindingAsync("test", "create", invokeRequest); + }); - var invokeRequest = new InvokeRequest() { RequestParameter = "Hello " }; - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - await daprClient.InvokeBindingAsync("test", "create", invokeRequest); - }); + request.Dismiss(); - request.Dismiss(); - - // Get Request and validate - var envelope = await request.GetRequestEnvelopeAsync(); - envelope.Name.ShouldBe("test"); - envelope.Metadata.Count.ShouldBe(0); - var json = envelope.Data.ToStringUtf8(); - var typeFromRequest = JsonSerializer.Deserialize(json, client.InnerClient.JsonSerializerOptions); - typeFromRequest.RequestParameter.ShouldBe("Hello "); - } - - [Fact] - public async Task InvokeBindingAsync_ValidateRequest_WithMetadata() - { - await using var client = TestClient.CreateForDaprClient(); - - var metadata = new Dictionary - { - { "key1", "value1" }, - { "key2", "value2" } - }; - var invokeRequest = new InvokeRequest() { RequestParameter = "Hello " }; - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - await daprClient.InvokeBindingAsync("test", "create", invokeRequest, metadata); - }); - - request.Dismiss(); - - // Get Request and validate - var envelope = await request.GetRequestEnvelopeAsync(); - envelope.Name.ShouldBe("test"); - envelope.Metadata.Count.ShouldBe(2); - envelope.Metadata.Keys.Contains("key1").ShouldBeTrue(); - envelope.Metadata.Keys.Contains("key2").ShouldBeTrue(); - envelope.Metadata["key1"].ShouldBe("value1"); - envelope.Metadata["key2"].ShouldBe("value2"); - var json = envelope.Data.ToStringUtf8(); - var typeFromRequest = JsonSerializer.Deserialize(json, client.InnerClient.JsonSerializerOptions); - typeFromRequest.RequestParameter.ShouldBe("Hello "); - } - - [Fact] - public async Task InvokeBindingAsync_WithNullPayload_ValidateRequest() - { - await using var client = TestClient.CreateForDaprClient(); - - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - await daprClient.InvokeBindingAsync("test", "create", null); - }); - - request.Dismiss(); - - // Get Request and validate - var envelope = await request.GetRequestEnvelopeAsync(); - envelope.Name.ShouldBe("test"); - envelope.Metadata.Count.ShouldBe(0); - var json = envelope.Data.ToStringUtf8(); - Assert.Equal("null", json); - } - - [Fact] - public async Task InvokeBindingAsync_WithRequest_ValidateRequest() - { - await using var client = TestClient.CreateForDaprClient(); - - var payload = new InvokeRequest() { RequestParameter = "Hello " }; - var bindingRequest = new BindingRequest("test", "create") - { - Data = JsonSerializer.SerializeToUtf8Bytes(payload, client.InnerClient.JsonSerializerOptions), - Metadata = - { - { "key1", "value1" }, - { "key2", "value2" } - } - }; - - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.InvokeBindingAsync(bindingRequest); - }); - - var gRpcResponse = new Autogen.Grpc.v1.InvokeBindingResponse() - { - Data = ByteString.CopyFrom(JsonSerializer.SerializeToUtf8Bytes(new Widget() { Color = "red", }, client.InnerClient.JsonSerializerOptions)), - Metadata = - { - { "anotherkey", "anothervalue" }, - } - }; - var response = await request.CompleteWithMessageAsync(gRpcResponse); - - var envelope = await request.GetRequestEnvelopeAsync(); - envelope.Name.ShouldBe("test"); - envelope.Metadata.Count.ShouldBe(2); - envelope.Metadata.Keys.Contains("key1").ShouldBeTrue(); - envelope.Metadata.Keys.Contains("key2").ShouldBeTrue(); - envelope.Metadata["key1"].ShouldBe("value1"); - envelope.Metadata["key2"].ShouldBe("value2"); - - var json = envelope.Data.ToStringUtf8(); - var typeFromRequest = JsonSerializer.Deserialize(json, client.InnerClient.JsonSerializerOptions); - typeFromRequest.RequestParameter.ShouldBe("Hello "); - - Assert.Same(bindingRequest, response.Request); - Assert.Equal("red", JsonSerializer.Deserialize(response.Data.Span, client.InnerClient.JsonSerializerOptions).Color); - Assert.Collection( - response.Metadata, - kvp => - { - Assert.Equal("anotherkey", kvp.Key); - Assert.Equal("anothervalue", kvp.Value); - }); - } - - - [Fact] - public async Task InvokeBindingAsync_WithCancelledToken() - { - await using var client = TestClient.CreateForDaprClient(); - - var cts = new CancellationTokenSource(); - cts.Cancel(); - - var metadata = new Dictionary - { - { "key1", "value1" }, - { "key2", "value2" } - }; - var invokeRequest = new InvokeRequest() { RequestParameter = "Hello " }; - await Assert.ThrowsAsync(async () => - { - await client.InnerClient.InvokeBindingAsync("test", "create", invokeRequest, metadata, cts.Token); - }); - } - - [Fact] - public async Task InvokeBindingAsync_WrapsRpcException() - { - var client = new MockClient(); - - var rpcStatus = new Status(StatusCode.Internal, "not gonna work"); - var rpcException = new RpcException(rpcStatus, new Metadata(), "not gonna work"); - - client.Mock - .Setup(m => m.InvokeBindingAsync(It.IsAny(), It.IsAny())) - .Throws(rpcException); - - var ex = await Assert.ThrowsAsync(async () => - { - await client.DaprClient.InvokeBindingAsync("test", "test", new InvokeRequest() { RequestParameter = "Hello " }); - }); - Assert.Same(rpcException, ex.InnerException); - } - - [Fact] - public async Task InvokeBindingAsync_WrapsJsonException() - { - await using var client = TestClient.CreateForDaprClient(); - - var response = new Autogen.Grpc.v1.InvokeBindingResponse(); - var bytes = JsonSerializer.SerializeToUtf8Bytes(new Widget(){ Color = "red", }, client.InnerClient.JsonSerializerOptions); - response.Data = ByteString.CopyFrom(bytes.Take(10).ToArray()); // trim it to make invalid JSON blob - - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.InvokeBindingAsync("test", "test", new InvokeRequest() { RequestParameter = "Hello " }); - }); - - await request.GetRequestEnvelopeAsync(); - var ex = await Assert.ThrowsAsync(async () => - { - await request.CompleteWithMessageAsync(response); - }); - Assert.IsType(ex.InnerException); - } - - [Fact] - public async Task InvokeBindingRequest_WithCustomRequest_ValidateRequest() - { - await using var client = TestClient.CreateForDaprClient(); - - var data = new InvokeRequest { RequestParameter = "Test" }; - var metadata = new Dictionary - { - { "key1", "value1" } - }; - var req = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.InvokeBindingAsync("binding", "operation", data, metadata); - }); - - var resp = new InvokeBindingResponse - { - Data = ByteString.CopyFrom(JsonSerializer.SerializeToUtf8Bytes(new Widget() { Color = "red", }, client.InnerClient.JsonSerializerOptions)) - }; - var response = await req.CompleteWithMessageAsync(resp); - - var envelope = await req.GetRequestEnvelopeAsync(); - envelope.Name.ShouldBe("binding"); - envelope.Operation.ShouldBe("operation"); - envelope.Metadata.Count.ShouldBe(1); - envelope.Metadata.Keys.Contains("key1").ShouldBeTrue(); - envelope.Metadata["key1"].ShouldBe("value1"); - - var json = envelope.Data.ToStringUtf8(); - var typeFromRequest = JsonSerializer.Deserialize(json, client.InnerClient.JsonSerializerOptions); - typeFromRequest.RequestParameter.ShouldBe("Test"); - - Assert.Equal("red", response.Color); - } - - [Fact] - public async Task InvokeBindingRequest_WithNullData_ValidateRequest() - { - await using var client = TestClient.CreateForDaprClient(); - - var req = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.InvokeBindingAsync("binding", "operation", null); - }); - - var resp = new InvokeBindingResponse - { - Data = ByteString.CopyFrom(JsonSerializer.SerializeToUtf8Bytes(new Widget() { Color = "red", }, client.InnerClient.JsonSerializerOptions)) - }; - var response = await req.CompleteWithMessageAsync(resp); - - var envelope = await req.GetRequestEnvelopeAsync(); - envelope.Name.ShouldBe("binding"); - envelope.Operation.ShouldBe("operation"); - - Assert.Equal("red", response.Color); - } - - [Fact] - public async Task InvokeBindingRequest_WithBindingNull_CheckException() - { - await using var client = TestClient.CreateForDaprClient(); - - var ex = await Assert.ThrowsAsync(async () => - { - await client.InnerClient.InvokeBindingAsync(null, "operation", null); - }); - Assert.IsType(ex); - } - - [Fact] - public async Task InvokeBindingRequest_WithOperationNull_CheckException() - { - await using var client = TestClient.CreateForDaprClient(); - - var ex = await Assert.ThrowsAsync(async () => - { - await client.InnerClient.InvokeBindingAsync("binding", null, null); - }); - Assert.IsType(ex); - } - - private class InvokeRequest - { - public string RequestParameter { get; set; } - } - - private class Widget - { - public string Color { get; set; } - } + // Get Request and validate + var envelope = await request.GetRequestEnvelopeAsync(); + envelope.Name.ShouldBe("test"); + envelope.Metadata.Count.ShouldBe(0); + var json = envelope.Data.ToStringUtf8(); + var typeFromRequest = JsonSerializer.Deserialize(json, client.InnerClient.JsonSerializerOptions); + typeFromRequest.RequestParameter.ShouldBe("Hello "); } -} + + [Fact] + public async Task InvokeBindingAsync_ValidateRequest_WithMetadata() + { + await using var client = TestClient.CreateForDaprClient(); + + var metadata = new Dictionary + { + { "key1", "value1" }, + { "key2", "value2" } + }; + var invokeRequest = new InvokeRequest() { RequestParameter = "Hello " }; + var request = await client.CaptureGrpcRequestAsync(async daprClient => + { + await daprClient.InvokeBindingAsync("test", "create", invokeRequest, metadata); + }); + + request.Dismiss(); + + // Get Request and validate + var envelope = await request.GetRequestEnvelopeAsync(); + envelope.Name.ShouldBe("test"); + envelope.Metadata.Count.ShouldBe(2); + envelope.Metadata.Keys.Contains("key1").ShouldBeTrue(); + envelope.Metadata.Keys.Contains("key2").ShouldBeTrue(); + envelope.Metadata["key1"].ShouldBe("value1"); + envelope.Metadata["key2"].ShouldBe("value2"); + var json = envelope.Data.ToStringUtf8(); + var typeFromRequest = JsonSerializer.Deserialize(json, client.InnerClient.JsonSerializerOptions); + typeFromRequest.RequestParameter.ShouldBe("Hello "); + } + + [Fact] + public async Task InvokeBindingAsync_WithNullPayload_ValidateRequest() + { + await using var client = TestClient.CreateForDaprClient(); + + var request = await client.CaptureGrpcRequestAsync(async daprClient => + { + await daprClient.InvokeBindingAsync("test", "create", null); + }); + + request.Dismiss(); + + // Get Request and validate + var envelope = await request.GetRequestEnvelopeAsync(); + envelope.Name.ShouldBe("test"); + envelope.Metadata.Count.ShouldBe(0); + var json = envelope.Data.ToStringUtf8(); + Assert.Equal("null", json); + } + + [Fact] + public async Task InvokeBindingAsync_WithRequest_ValidateRequest() + { + await using var client = TestClient.CreateForDaprClient(); + + var payload = new InvokeRequest() { RequestParameter = "Hello " }; + var bindingRequest = new BindingRequest("test", "create") + { + Data = JsonSerializer.SerializeToUtf8Bytes(payload, client.InnerClient.JsonSerializerOptions), + Metadata = + { + { "key1", "value1" }, + { "key2", "value2" } + } + }; + + var request = await client.CaptureGrpcRequestAsync(async daprClient => + { + return await daprClient.InvokeBindingAsync(bindingRequest); + }); + + var gRpcResponse = new Autogen.Grpc.v1.InvokeBindingResponse() + { + Data = ByteString.CopyFrom(JsonSerializer.SerializeToUtf8Bytes(new Widget() { Color = "red", }, client.InnerClient.JsonSerializerOptions)), + Metadata = + { + { "anotherkey", "anothervalue" }, + } + }; + var response = await request.CompleteWithMessageAsync(gRpcResponse); + + var envelope = await request.GetRequestEnvelopeAsync(); + envelope.Name.ShouldBe("test"); + envelope.Metadata.Count.ShouldBe(2); + envelope.Metadata.Keys.Contains("key1").ShouldBeTrue(); + envelope.Metadata.Keys.Contains("key2").ShouldBeTrue(); + envelope.Metadata["key1"].ShouldBe("value1"); + envelope.Metadata["key2"].ShouldBe("value2"); + + var json = envelope.Data.ToStringUtf8(); + var typeFromRequest = JsonSerializer.Deserialize(json, client.InnerClient.JsonSerializerOptions); + typeFromRequest.RequestParameter.ShouldBe("Hello "); + + Assert.Same(bindingRequest, response.Request); + Assert.Equal("red", JsonSerializer.Deserialize(response.Data.Span, client.InnerClient.JsonSerializerOptions).Color); + Assert.Collection( + response.Metadata, + kvp => + { + Assert.Equal("anotherkey", kvp.Key); + Assert.Equal("anothervalue", kvp.Value); + }); + } + + + [Fact] + public async Task InvokeBindingAsync_WithCancelledToken() + { + await using var client = TestClient.CreateForDaprClient(); + + var cts = new CancellationTokenSource(); + cts.Cancel(); + + var metadata = new Dictionary + { + { "key1", "value1" }, + { "key2", "value2" } + }; + var invokeRequest = new InvokeRequest() { RequestParameter = "Hello " }; + await Assert.ThrowsAsync(async () => + { + await client.InnerClient.InvokeBindingAsync("test", "create", invokeRequest, metadata, cts.Token); + }); + } + + [Fact] + public async Task InvokeBindingAsync_WrapsRpcException() + { + var client = new MockClient(); + + var rpcStatus = new Status(StatusCode.Internal, "not gonna work"); + var rpcException = new RpcException(rpcStatus, new Metadata(), "not gonna work"); + + client.Mock + .Setup(m => m.InvokeBindingAsync(It.IsAny(), It.IsAny())) + .Throws(rpcException); + + var ex = await Assert.ThrowsAsync(async () => + { + await client.DaprClient.InvokeBindingAsync("test", "test", new InvokeRequest() { RequestParameter = "Hello " }); + }); + Assert.Same(rpcException, ex.InnerException); + } + + [Fact] + public async Task InvokeBindingAsync_WrapsJsonException() + { + await using var client = TestClient.CreateForDaprClient(); + + var response = new Autogen.Grpc.v1.InvokeBindingResponse(); + var bytes = JsonSerializer.SerializeToUtf8Bytes(new Widget(){ Color = "red", }, client.InnerClient.JsonSerializerOptions); + response.Data = ByteString.CopyFrom(bytes.Take(10).ToArray()); // trim it to make invalid JSON blob + + var request = await client.CaptureGrpcRequestAsync(async daprClient => + { + return await daprClient.InvokeBindingAsync("test", "test", new InvokeRequest() { RequestParameter = "Hello " }); + }); + + await request.GetRequestEnvelopeAsync(); + var ex = await Assert.ThrowsAsync(async () => + { + await request.CompleteWithMessageAsync(response); + }); + Assert.IsType(ex.InnerException); + } + + [Fact] + public async Task InvokeBindingRequest_WithCustomRequest_ValidateRequest() + { + await using var client = TestClient.CreateForDaprClient(); + + var data = new InvokeRequest { RequestParameter = "Test" }; + var metadata = new Dictionary + { + { "key1", "value1" } + }; + var req = await client.CaptureGrpcRequestAsync(async daprClient => + { + return await daprClient.InvokeBindingAsync("binding", "operation", data, metadata); + }); + + var resp = new InvokeBindingResponse + { + Data = ByteString.CopyFrom(JsonSerializer.SerializeToUtf8Bytes(new Widget() { Color = "red", }, client.InnerClient.JsonSerializerOptions)) + }; + var response = await req.CompleteWithMessageAsync(resp); + + var envelope = await req.GetRequestEnvelopeAsync(); + envelope.Name.ShouldBe("binding"); + envelope.Operation.ShouldBe("operation"); + envelope.Metadata.Count.ShouldBe(1); + envelope.Metadata.Keys.Contains("key1").ShouldBeTrue(); + envelope.Metadata["key1"].ShouldBe("value1"); + + var json = envelope.Data.ToStringUtf8(); + var typeFromRequest = JsonSerializer.Deserialize(json, client.InnerClient.JsonSerializerOptions); + typeFromRequest.RequestParameter.ShouldBe("Test"); + + Assert.Equal("red", response.Color); + } + + [Fact] + public async Task InvokeBindingRequest_WithNullData_ValidateRequest() + { + await using var client = TestClient.CreateForDaprClient(); + + var req = await client.CaptureGrpcRequestAsync(async daprClient => + { + return await daprClient.InvokeBindingAsync("binding", "operation", null); + }); + + var resp = new InvokeBindingResponse + { + Data = ByteString.CopyFrom(JsonSerializer.SerializeToUtf8Bytes(new Widget() { Color = "red", }, client.InnerClient.JsonSerializerOptions)) + }; + var response = await req.CompleteWithMessageAsync(resp); + + var envelope = await req.GetRequestEnvelopeAsync(); + envelope.Name.ShouldBe("binding"); + envelope.Operation.ShouldBe("operation"); + + Assert.Equal("red", response.Color); + } + + [Fact] + public async Task InvokeBindingRequest_WithBindingNull_CheckException() + { + await using var client = TestClient.CreateForDaprClient(); + + var ex = await Assert.ThrowsAsync(async () => + { + await client.InnerClient.InvokeBindingAsync(null, "operation", null); + }); + Assert.IsType(ex); + } + + [Fact] + public async Task InvokeBindingRequest_WithOperationNull_CheckException() + { + await using var client = TestClient.CreateForDaprClient(); + + var ex = await Assert.ThrowsAsync(async () => + { + await client.InnerClient.InvokeBindingAsync("binding", null, null); + }); + Assert.IsType(ex); + } + + private class InvokeRequest + { + public string RequestParameter { get; set; } + } + + private class Widget + { + public string Color { get; set; } + } +} \ No newline at end of file diff --git a/test/Dapr.Client.Test/MockClient.cs b/test/Dapr.Client.Test/MockClient.cs index d00a0a69..1ccdf14c 100644 --- a/test/Dapr.Client.Test/MockClient.cs +++ b/test/Dapr.Client.Test/MockClient.cs @@ -11,132 +11,131 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Client +namespace Dapr.Client; + +using System; +using System.Net.Http; +using System.Text.Json; +using System.Threading.Tasks; +using Grpc.Core; +using Grpc.Net.Client; +using Moq; + +public class MockClient { - using System; - using System.Net.Http; - using System.Text.Json; - using System.Threading.Tasks; - using Grpc.Core; - using Grpc.Net.Client; - using Moq; - - public class MockClient + public MockClient() { - public MockClient() - { - Mock = new Mock(MockBehavior.Strict); - DaprClient = new DaprClientGrpc(GrpcChannel.ForAddress("http://localhost"), Mock.Object, new HttpClient(), new Uri("http://localhost:3500"), new JsonSerializerOptions(), default); - } + Mock = new Mock(MockBehavior.Strict); + DaprClient = new DaprClientGrpc(GrpcChannel.ForAddress("http://localhost"), Mock.Object, new HttpClient(), new Uri("http://localhost:3500"), new JsonSerializerOptions(), default); + } - public Mock Mock { get; } + public Mock Mock { get; } - public DaprClient DaprClient { get; } + public DaprClient DaprClient { get; } - public InvokeApiCallBuilder Call() - { - return new InvokeApiCallBuilder(); - } + public InvokeApiCallBuilder Call() + { + return new InvokeApiCallBuilder(); + } - public StateApiCallBuilder CallStateApi() - { - return new StateApiCallBuilder(); - } + public StateApiCallBuilder CallStateApi() + { + return new StateApiCallBuilder(); + } - public class InvokeApiCallBuilder + public class InvokeApiCallBuilder + { + private TResponse response; + private readonly Metadata headers; + private Status status; + private readonly Metadata trailers; + + public InvokeApiCallBuilder() { - private TResponse response; - private readonly Metadata headers; - private Status status; - private readonly Metadata trailers; - - public InvokeApiCallBuilder() - { - headers = new Metadata(); - trailers = new Metadata(); - } - - public AsyncUnaryCall Build() - { - return new AsyncUnaryCall( - Task.FromResult(response), - Task.FromResult(headers), - () => status, - () => trailers, - () => { }); - } - - public InvokeApiCallBuilder SetResponse(TResponse response) - { - this.response = response; - return this; - } - - public InvokeApiCallBuilder SetStatus(Status status) - { - this.status = status; - return this; - } - - public InvokeApiCallBuilder AddHeader(string key, string value) - { - this.headers.Add(key, value); - return this; - } - - public InvokeApiCallBuilder AddTrailer(string key, string value) - { - this.trailers.Add(key, value); - return this; - } + headers = new Metadata(); + trailers = new Metadata(); } - public class StateApiCallBuilder + public AsyncUnaryCall Build() { - private TResponse response; - private readonly Metadata headers; - private Status status; - private readonly Metadata trailers; + return new AsyncUnaryCall( + Task.FromResult(response), + Task.FromResult(headers), + () => status, + () => trailers, + () => { }); + } - public StateApiCallBuilder() - { - headers = new Metadata(); - trailers = new Metadata(); - } + public InvokeApiCallBuilder SetResponse(TResponse response) + { + this.response = response; + return this; + } - public AsyncUnaryCall Build() - { - return new AsyncUnaryCall( - Task.FromResult(response), - Task.FromResult(headers), - () => status, - () => trailers, - () => { }); - } + public InvokeApiCallBuilder SetStatus(Status status) + { + this.status = status; + return this; + } - public StateApiCallBuilder SetResponse(TResponse response) - { - this.response = response; - return this; - } + public InvokeApiCallBuilder AddHeader(string key, string value) + { + this.headers.Add(key, value); + return this; + } - public StateApiCallBuilder SetStatus(Status status) - { - this.status = status; - return this; - } - - public StateApiCallBuilder AddHeader(string key, string value) - { - this.headers.Add(key, value); - return this; - } - - public StateApiCallBuilder AddTrailer(string key, string value) - { - this.trailers.Add(key, value); - return this; - } + public InvokeApiCallBuilder AddTrailer(string key, string value) + { + this.trailers.Add(key, value); + return this; } } -} + + public class StateApiCallBuilder + { + private TResponse response; + private readonly Metadata headers; + private Status status; + private readonly Metadata trailers; + + public StateApiCallBuilder() + { + headers = new Metadata(); + trailers = new Metadata(); + } + + public AsyncUnaryCall Build() + { + return new AsyncUnaryCall( + Task.FromResult(response), + Task.FromResult(headers), + () => status, + () => trailers, + () => { }); + } + + public StateApiCallBuilder SetResponse(TResponse response) + { + this.response = response; + return this; + } + + public StateApiCallBuilder SetStatus(Status status) + { + this.status = status; + return this; + } + + public StateApiCallBuilder AddHeader(string key, string value) + { + this.headers.Add(key, value); + return this; + } + + public StateApiCallBuilder AddTrailer(string key, string value) + { + this.trailers.Add(key, value); + return this; + } + } +} \ No newline at end of file diff --git a/test/Dapr.Client.Test/PublishEventApiTest.cs b/test/Dapr.Client.Test/PublishEventApiTest.cs index 0dac474a..7130fa4d 100644 --- a/test/Dapr.Client.Test/PublishEventApiTest.cs +++ b/test/Dapr.Client.Test/PublishEventApiTest.cs @@ -16,300 +16,299 @@ using System.Net.Http; using System.Text.Json.Serialization; using Grpc.Net.Client; -namespace Dapr.Client.Test +namespace Dapr.Client.Test; + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Dapr.Client.Autogen.Grpc.v1; +using Shouldly; +using Grpc.Core; +using Moq; +using Xunit; + +public class PublishEventApiTest { - using System; - using System.Collections.Generic; - using System.Text.Json; - using System.Threading; - using System.Threading.Tasks; - using Dapr.Client.Autogen.Grpc.v1; - using Shouldly; - using Grpc.Core; - using Moq; - using Xunit; + const string TestPubsubName = "testpubsubname"; - public class PublishEventApiTest + [Fact] + public async Task PublishEventAsync_CanPublishTopicWithData() { - const string TestPubsubName = "testpubsubname"; + await using var client = TestClient.CreateForDaprClient(); - [Fact] - public async Task PublishEventAsync_CanPublishTopicWithData() + var publishData = new PublishData() { PublishObjectParameter = "testparam" }; + var request = await client.CaptureGrpcRequestAsync(async daprClient => { - await using var client = TestClient.CreateForDaprClient(); + await daprClient.PublishEventAsync(TestPubsubName, "test", publishData); + }); - var publishData = new PublishData() { PublishObjectParameter = "testparam" }; - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - await daprClient.PublishEventAsync(TestPubsubName, "test", publishData); - }); + request.Dismiss(); - request.Dismiss(); + var envelope = await request.GetRequestEnvelopeAsync(); + var jsonFromRequest = envelope.Data.ToStringUtf8(); - var envelope = await request.GetRequestEnvelopeAsync(); - var jsonFromRequest = envelope.Data.ToStringUtf8(); - - envelope.DataContentType.ShouldBe("application/json"); - envelope.PubsubName.ShouldBe(TestPubsubName); - envelope.Topic.ShouldBe("test"); - jsonFromRequest.ShouldBe(JsonSerializer.Serialize(publishData, client.InnerClient.JsonSerializerOptions)); - envelope.Metadata.Count.ShouldBe(0); - } - - [Fact] - public async Task PublishEvent_ShouldRespectJsonStringEnumConverter() - { - //The following mimics how the TestClient is built, but adds the JsonStringEnumConverter to the serialization options - var handler = new TestClient.CapturingHandler(); - var httpClient = new HttpClient(handler); - var clientBuilder = new DaprClientBuilder() - .UseJsonSerializationOptions(new JsonSerializerOptions() - { - Converters = {new JsonStringEnumConverter(null, false)} - }) - .UseHttpClientFactory(() => httpClient) - .UseGrpcChannelOptions(new GrpcChannelOptions() - { - HttpClient = httpClient, ThrowOperationCanceledOnCancellation = true - }); - var client = new TestClient(clientBuilder.Build(), handler); - - //Ensure that the JsonStringEnumConverter is registered - client.InnerClient.JsonSerializerOptions.Converters.Count.ShouldBe(1); - client.InnerClient.JsonSerializerOptions.Converters.First().GetType().Name.ShouldMatch(nameof(JsonStringEnumConverter)); - - var publishData = new Widget {Size = "Large", Color = WidgetColor.Red}; - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - await daprClient.PublishEventAsync(TestPubsubName, "test", publishData); - }); - - request.Dismiss(); - - var envelope = await request.GetRequestEnvelopeAsync(); - var jsonFromRequest = envelope.Data.ToStringUtf8(); - jsonFromRequest.ShouldBe(JsonSerializer.Serialize(publishData, client.InnerClient.JsonSerializerOptions)); - jsonFromRequest.ShouldMatch("{\"Size\":\"Large\",\"Color\":\"Red\"}"); - } - - [Fact] - public async Task PublishEventAsync_CanPublishTopicWithData_WithMetadata() - { - await using var client = TestClient.CreateForDaprClient(); - - var metadata = new Dictionary - { - { "key1", "value1" }, - { "key2", "value2" } - }; - - var publishData = new PublishData() { PublishObjectParameter = "testparam" }; - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - await daprClient.PublishEventAsync(TestPubsubName, "test", publishData, metadata); - }); - - request.Dismiss(); - - var envelope = await request.GetRequestEnvelopeAsync(); - var jsonFromRequest = envelope.Data.ToStringUtf8(); - - envelope.DataContentType.ShouldBe("application/json"); - envelope.PubsubName.ShouldBe(TestPubsubName); - envelope.Topic.ShouldBe("test"); - jsonFromRequest.ShouldBe(JsonSerializer.Serialize(publishData, client.InnerClient.JsonSerializerOptions)); - - envelope.Metadata.Count.ShouldBe(2); - envelope.Metadata.Keys.Contains("key1").ShouldBeTrue(); - envelope.Metadata.Keys.Contains("key2").ShouldBeTrue(); - envelope.Metadata["key1"].ShouldBe("value1"); - envelope.Metadata["key2"].ShouldBe("value2"); - } - - [Fact] - public async Task PublishEventAsync_CanPublishTopicWithNoContent() - { - await using var client = TestClient.CreateForDaprClient(); - - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - await daprClient.PublishEventAsync(TestPubsubName, "test"); - }); - - request.Dismiss(); - - var envelope = await request.GetRequestEnvelopeAsync(); - - envelope.PubsubName.ShouldBe(TestPubsubName); - envelope.Topic.ShouldBe("test"); - envelope.Data.Length.ShouldBe(0); - envelope.Metadata.Count.ShouldBe(0); - } - - [Fact] - public async Task PublishEventAsync_CanPublishTopicWithNoContent_WithMetadata() - { - await using var client = TestClient.CreateForDaprClient(); - - var metadata = new Dictionary - { - { "key1", "value1" }, - { "key2", "value2" } - }; - - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - await daprClient.PublishEventAsync(TestPubsubName, "test", metadata); - }); - - request.Dismiss(); - - var envelope = await request.GetRequestEnvelopeAsync(); - envelope.PubsubName.ShouldBe(TestPubsubName); - envelope.Topic.ShouldBe("test"); - envelope.Data.Length.ShouldBe(0); - - envelope.Metadata.Count.ShouldBe(2); - envelope.Metadata.Keys.Contains("key1").ShouldBeTrue(); - envelope.Metadata.Keys.Contains("key2").ShouldBeTrue(); - envelope.Metadata["key1"].ShouldBe("value1"); - envelope.Metadata["key2"].ShouldBe("value2"); - } - - [Fact] - public async Task PublishEventAsync_CanPublishCloudEventWithData() - { - await using var client = TestClient.CreateForDaprClient(); - - var publishData = new PublishData() { PublishObjectParameter = "testparam" }; - var cloudEvent = new CloudEvent(publishData) - { - Source = new Uri("urn:testsource"), - Type = "testtype", - Subject = "testsubject", - }; - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - await daprClient.PublishEventAsync>(TestPubsubName, "test", cloudEvent); - }); - - request.Dismiss(); - - var envelope = await request.GetRequestEnvelopeAsync(); - var jsonFromRequest = envelope.Data.ToStringUtf8(); - - envelope.DataContentType.ShouldBe("application/cloudevents+json"); - envelope.PubsubName.ShouldBe(TestPubsubName); - envelope.Topic.ShouldBe("test"); - jsonFromRequest.ShouldBe(JsonSerializer.Serialize(cloudEvent, client.InnerClient.JsonSerializerOptions)); - envelope.Metadata.Count.ShouldBe(0); - } - - [Fact] - public async Task PublishEventAsync_CanPublishCloudEventWithNoContent() - { - await using var client = TestClient.CreateForDaprClient(); - - var cloudEvent = new CloudEvent - { - Source = new Uri("urn:testsource"), - Type = "testtype", - Subject = "testsubject", - }; - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - await daprClient.PublishEventAsync(TestPubsubName, "test", cloudEvent); - }); - - request.Dismiss(); - - var envelope = await request.GetRequestEnvelopeAsync(); - var jsonFromRequest = envelope.Data.ToStringUtf8(); - - envelope.DataContentType.ShouldBe("application/cloudevents+json"); - envelope.PubsubName.ShouldBe(TestPubsubName); - envelope.Topic.ShouldBe("test"); - jsonFromRequest.ShouldBe(JsonSerializer.Serialize(cloudEvent, client.InnerClient.JsonSerializerOptions)); - envelope.Metadata.Count.ShouldBe(0); - } - - [Fact] - public async Task PublishEventAsync_WithCancelledToken() - { - await using var client = TestClient.CreateForDaprClient(); - - var cts = new CancellationTokenSource(); - cts.Cancel(); - - await Assert.ThrowsAsync(async () => - { - await client.InnerClient.PublishEventAsync(TestPubsubName, "test", cancellationToken: cts.Token); - }); - } - - // All overloads call through a common path that does exception handling. - [Fact] - public async Task PublishEventAsync_WrapsRpcException() - { - var client = new MockClient(); - - var rpcStatus = new Status(StatusCode.Internal, "not gonna work"); - var rpcException = new RpcException(rpcStatus, new Metadata(), "not gonna work"); - - // Setup the mock client to throw an Rpc Exception with the expected details info - client.Mock - .Setup(m => m.PublishEventAsync(It.IsAny(), It.IsAny())) - .Throws(rpcException); - - var ex = await Assert.ThrowsAsync(async () => - { - await client.DaprClient.PublishEventAsync("test", "test"); - }); - Assert.Same(rpcException, ex.InnerException); - } - - [Fact] - public async Task PublishEventAsync_CanPublishWithRawData() - { - await using var client = TestClient.CreateForDaprClient(); - - var publishData = new PublishData() { PublishObjectParameter = "testparam" }; - var publishBytes = JsonSerializer.SerializeToUtf8Bytes(publishData); - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - await daprClient.PublishByteEventAsync(TestPubsubName, "test", publishBytes.AsMemory()); - }); - - request.Dismiss(); - - var envelope = await request.GetRequestEnvelopeAsync(); - var jsonFromRequest = envelope.Data.ToStringUtf8(); - - envelope.DataContentType.ShouldBe("application/json"); - envelope.PubsubName.ShouldBe(TestPubsubName); - envelope.Topic.ShouldBe("test"); - jsonFromRequest.ShouldBe(JsonSerializer.Serialize(publishData)); - // The default serializer forces camel case, so this should be different from our serialization above. - jsonFromRequest.ShouldNotBe(JsonSerializer.Serialize(publishBytes, client.InnerClient.JsonSerializerOptions)); - envelope.Metadata.Count.ShouldBe(0); - } - - private class PublishData - { - public string PublishObjectParameter { get; set; } - } - - private class Widget - { - public string Size { get; set; } - public WidgetColor Color { get; set; } - } - - private enum WidgetColor - { - Red, - Green, - Yellow - } + envelope.DataContentType.ShouldBe("application/json"); + envelope.PubsubName.ShouldBe(TestPubsubName); + envelope.Topic.ShouldBe("test"); + jsonFromRequest.ShouldBe(JsonSerializer.Serialize(publishData, client.InnerClient.JsonSerializerOptions)); + envelope.Metadata.Count.ShouldBe(0); } -} + + [Fact] + public async Task PublishEvent_ShouldRespectJsonStringEnumConverter() + { + //The following mimics how the TestClient is built, but adds the JsonStringEnumConverter to the serialization options + var handler = new TestClient.CapturingHandler(); + var httpClient = new HttpClient(handler); + var clientBuilder = new DaprClientBuilder() + .UseJsonSerializationOptions(new JsonSerializerOptions() + { + Converters = {new JsonStringEnumConverter(null, false)} + }) + .UseHttpClientFactory(() => httpClient) + .UseGrpcChannelOptions(new GrpcChannelOptions() + { + HttpClient = httpClient, ThrowOperationCanceledOnCancellation = true + }); + var client = new TestClient(clientBuilder.Build(), handler); + + //Ensure that the JsonStringEnumConverter is registered + client.InnerClient.JsonSerializerOptions.Converters.Count.ShouldBe(1); + client.InnerClient.JsonSerializerOptions.Converters.First().GetType().Name.ShouldMatch(nameof(JsonStringEnumConverter)); + + var publishData = new Widget {Size = "Large", Color = WidgetColor.Red}; + var request = await client.CaptureGrpcRequestAsync(async daprClient => + { + await daprClient.PublishEventAsync(TestPubsubName, "test", publishData); + }); + + request.Dismiss(); + + var envelope = await request.GetRequestEnvelopeAsync(); + var jsonFromRequest = envelope.Data.ToStringUtf8(); + jsonFromRequest.ShouldBe(JsonSerializer.Serialize(publishData, client.InnerClient.JsonSerializerOptions)); + jsonFromRequest.ShouldMatch("{\"Size\":\"Large\",\"Color\":\"Red\"}"); + } + + [Fact] + public async Task PublishEventAsync_CanPublishTopicWithData_WithMetadata() + { + await using var client = TestClient.CreateForDaprClient(); + + var metadata = new Dictionary + { + { "key1", "value1" }, + { "key2", "value2" } + }; + + var publishData = new PublishData() { PublishObjectParameter = "testparam" }; + var request = await client.CaptureGrpcRequestAsync(async daprClient => + { + await daprClient.PublishEventAsync(TestPubsubName, "test", publishData, metadata); + }); + + request.Dismiss(); + + var envelope = await request.GetRequestEnvelopeAsync(); + var jsonFromRequest = envelope.Data.ToStringUtf8(); + + envelope.DataContentType.ShouldBe("application/json"); + envelope.PubsubName.ShouldBe(TestPubsubName); + envelope.Topic.ShouldBe("test"); + jsonFromRequest.ShouldBe(JsonSerializer.Serialize(publishData, client.InnerClient.JsonSerializerOptions)); + + envelope.Metadata.Count.ShouldBe(2); + envelope.Metadata.Keys.Contains("key1").ShouldBeTrue(); + envelope.Metadata.Keys.Contains("key2").ShouldBeTrue(); + envelope.Metadata["key1"].ShouldBe("value1"); + envelope.Metadata["key2"].ShouldBe("value2"); + } + + [Fact] + public async Task PublishEventAsync_CanPublishTopicWithNoContent() + { + await using var client = TestClient.CreateForDaprClient(); + + var request = await client.CaptureGrpcRequestAsync(async daprClient => + { + await daprClient.PublishEventAsync(TestPubsubName, "test"); + }); + + request.Dismiss(); + + var envelope = await request.GetRequestEnvelopeAsync(); + + envelope.PubsubName.ShouldBe(TestPubsubName); + envelope.Topic.ShouldBe("test"); + envelope.Data.Length.ShouldBe(0); + envelope.Metadata.Count.ShouldBe(0); + } + + [Fact] + public async Task PublishEventAsync_CanPublishTopicWithNoContent_WithMetadata() + { + await using var client = TestClient.CreateForDaprClient(); + + var metadata = new Dictionary + { + { "key1", "value1" }, + { "key2", "value2" } + }; + + var request = await client.CaptureGrpcRequestAsync(async daprClient => + { + await daprClient.PublishEventAsync(TestPubsubName, "test", metadata); + }); + + request.Dismiss(); + + var envelope = await request.GetRequestEnvelopeAsync(); + envelope.PubsubName.ShouldBe(TestPubsubName); + envelope.Topic.ShouldBe("test"); + envelope.Data.Length.ShouldBe(0); + + envelope.Metadata.Count.ShouldBe(2); + envelope.Metadata.Keys.Contains("key1").ShouldBeTrue(); + envelope.Metadata.Keys.Contains("key2").ShouldBeTrue(); + envelope.Metadata["key1"].ShouldBe("value1"); + envelope.Metadata["key2"].ShouldBe("value2"); + } + + [Fact] + public async Task PublishEventAsync_CanPublishCloudEventWithData() + { + await using var client = TestClient.CreateForDaprClient(); + + var publishData = new PublishData() { PublishObjectParameter = "testparam" }; + var cloudEvent = new CloudEvent(publishData) + { + Source = new Uri("urn:testsource"), + Type = "testtype", + Subject = "testsubject", + }; + var request = await client.CaptureGrpcRequestAsync(async daprClient => + { + await daprClient.PublishEventAsync>(TestPubsubName, "test", cloudEvent); + }); + + request.Dismiss(); + + var envelope = await request.GetRequestEnvelopeAsync(); + var jsonFromRequest = envelope.Data.ToStringUtf8(); + + envelope.DataContentType.ShouldBe("application/cloudevents+json"); + envelope.PubsubName.ShouldBe(TestPubsubName); + envelope.Topic.ShouldBe("test"); + jsonFromRequest.ShouldBe(JsonSerializer.Serialize(cloudEvent, client.InnerClient.JsonSerializerOptions)); + envelope.Metadata.Count.ShouldBe(0); + } + + [Fact] + public async Task PublishEventAsync_CanPublishCloudEventWithNoContent() + { + await using var client = TestClient.CreateForDaprClient(); + + var cloudEvent = new CloudEvent + { + Source = new Uri("urn:testsource"), + Type = "testtype", + Subject = "testsubject", + }; + var request = await client.CaptureGrpcRequestAsync(async daprClient => + { + await daprClient.PublishEventAsync(TestPubsubName, "test", cloudEvent); + }); + + request.Dismiss(); + + var envelope = await request.GetRequestEnvelopeAsync(); + var jsonFromRequest = envelope.Data.ToStringUtf8(); + + envelope.DataContentType.ShouldBe("application/cloudevents+json"); + envelope.PubsubName.ShouldBe(TestPubsubName); + envelope.Topic.ShouldBe("test"); + jsonFromRequest.ShouldBe(JsonSerializer.Serialize(cloudEvent, client.InnerClient.JsonSerializerOptions)); + envelope.Metadata.Count.ShouldBe(0); + } + + [Fact] + public async Task PublishEventAsync_WithCancelledToken() + { + await using var client = TestClient.CreateForDaprClient(); + + var cts = new CancellationTokenSource(); + cts.Cancel(); + + await Assert.ThrowsAsync(async () => + { + await client.InnerClient.PublishEventAsync(TestPubsubName, "test", cancellationToken: cts.Token); + }); + } + + // All overloads call through a common path that does exception handling. + [Fact] + public async Task PublishEventAsync_WrapsRpcException() + { + var client = new MockClient(); + + var rpcStatus = new Status(StatusCode.Internal, "not gonna work"); + var rpcException = new RpcException(rpcStatus, new Metadata(), "not gonna work"); + + // Setup the mock client to throw an Rpc Exception with the expected details info + client.Mock + .Setup(m => m.PublishEventAsync(It.IsAny(), It.IsAny())) + .Throws(rpcException); + + var ex = await Assert.ThrowsAsync(async () => + { + await client.DaprClient.PublishEventAsync("test", "test"); + }); + Assert.Same(rpcException, ex.InnerException); + } + + [Fact] + public async Task PublishEventAsync_CanPublishWithRawData() + { + await using var client = TestClient.CreateForDaprClient(); + + var publishData = new PublishData() { PublishObjectParameter = "testparam" }; + var publishBytes = JsonSerializer.SerializeToUtf8Bytes(publishData); + var request = await client.CaptureGrpcRequestAsync(async daprClient => + { + await daprClient.PublishByteEventAsync(TestPubsubName, "test", publishBytes.AsMemory()); + }); + + request.Dismiss(); + + var envelope = await request.GetRequestEnvelopeAsync(); + var jsonFromRequest = envelope.Data.ToStringUtf8(); + + envelope.DataContentType.ShouldBe("application/json"); + envelope.PubsubName.ShouldBe(TestPubsubName); + envelope.Topic.ShouldBe("test"); + jsonFromRequest.ShouldBe(JsonSerializer.Serialize(publishData)); + // The default serializer forces camel case, so this should be different from our serialization above. + jsonFromRequest.ShouldNotBe(JsonSerializer.Serialize(publishBytes, client.InnerClient.JsonSerializerOptions)); + envelope.Metadata.Count.ShouldBe(0); + } + + private class PublishData + { + public string PublishObjectParameter { get; set; } + } + + private class Widget + { + public string Size { get; set; } + public WidgetColor Color { get; set; } + } + + private enum WidgetColor + { + Red, + Green, + Yellow + } +} \ No newline at end of file diff --git a/test/Dapr.Client.Test/SecretApiTest.cs b/test/Dapr.Client.Test/SecretApiTest.cs index 4f7d8aa5..79dac62c 100644 --- a/test/Dapr.Client.Test/SecretApiTest.cs +++ b/test/Dapr.Client.Test/SecretApiTest.cs @@ -11,352 +11,351 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Client.Test +namespace Dapr.Client.Test; + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Shouldly; +using Grpc.Core; +using Moq; +using Xunit; +using Autogenerated = Dapr.Client.Autogen.Grpc.v1; + +public class SecretApiTest { - using System; - using System.Collections.Generic; - using System.Threading; - using System.Threading.Tasks; - using Shouldly; - using Grpc.Core; - using Moq; - using Xunit; - using Autogenerated = Dapr.Client.Autogen.Grpc.v1; - - public class SecretApiTest + [Fact] + public async Task GetSecretAsync_ValidateRequest() { - [Fact] - public async Task GetSecretAsync_ValidateRequest() + await using var client = TestClient.CreateForDaprClient(); + + var metadata = new Dictionary { - await using var client = TestClient.CreateForDaprClient(); - - var metadata = new Dictionary - { - { "key1", "value1" }, - { "key2", "value2" } - }; - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.GetSecretAsync("testStore", "test_key", metadata); - }); - - request.Dismiss(); - - // Get Request and validate - var envelope = await request.GetRequestEnvelopeAsync(); - envelope.StoreName.ShouldBe("testStore"); - envelope.Key.ShouldBe("test_key"); - envelope.Metadata.Count.ShouldBe(2); - envelope.Metadata.Keys.Contains("key1").ShouldBeTrue(); - envelope.Metadata.Keys.Contains("key2").ShouldBeTrue(); - envelope.Metadata["key1"].ShouldBe("value1"); - envelope.Metadata["key2"].ShouldBe("value2"); - } - - [Fact] - public async Task GetSecretAsync_ReturnSingleSecret() + { "key1", "value1" }, + { "key2", "value2" } + }; + var request = await client.CaptureGrpcRequestAsync(async daprClient => { - await using var client = TestClient.CreateForDaprClient(); + return await daprClient.GetSecretAsync("testStore", "test_key", metadata); + }); - var metadata = new Dictionary - { - { "key1", "value1" }, - { "key2", "value2" } - }; - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.GetSecretAsync("testStore", "test_key", metadata); - }); + request.Dismiss(); - request.Dismiss(); - - // Get Request and validate - var envelope = await request.GetRequestEnvelopeAsync(); - envelope.StoreName.ShouldBe("testStore"); - envelope.Key.ShouldBe("test_key"); - envelope.Metadata.Count.ShouldBe(2); - envelope.Metadata.Keys.Contains("key1").ShouldBeTrue(); - envelope.Metadata.Keys.Contains("key2").ShouldBeTrue(); - envelope.Metadata["key1"].ShouldBe("value1"); - envelope.Metadata["key2"].ShouldBe("value2"); - - // Create Response & Respond - var secrets = new Dictionary - { - { "redis_secret", "Guess_Redis" } - }; - var secretsResponse = await SendResponseWithSecrets(secrets, request); - - // Get response and validate - secretsResponse.Count.ShouldBe(1); - secretsResponse.ContainsKey("redis_secret").ShouldBeTrue(); - secretsResponse["redis_secret"].ShouldBe("Guess_Redis"); - } - - [Fact] - public async Task GetSecretAsync_WithSlashesInName() - { - await using var client = TestClient.CreateForDaprClient(); - - var request = await client.CaptureGrpcRequestAsync(async DaprClient => - { - return await DaprClient.GetSecretAsync("testStore", "us-west-1/org/xpto/secretabc"); - }); - - request.Dismiss(); - - //Get Request and validate - var envelope = await request.GetRequestEnvelopeAsync(); - envelope.StoreName.ShouldBe("testStore"); - envelope.Key.ShouldBe("us-west-1/org/xpto/secretabc"); - - var secrets = new Dictionary { { "us-west-1/org/xpto/secretabc", "abc123" } }; - var secretsResponse = await SendResponseWithSecrets(secrets, request); - - //Get response and validate - secretsResponse.Count.ShouldBe(1); - secretsResponse.ContainsKey("us-west-1/org/xpto/secretabc").ShouldBeTrue(); - secretsResponse["us-west-1/org/xpto/secretabc"].ShouldBe("abc123"); - } - - [Fact] - public async Task GetSecretAsync_ReturnMultipleSecrets() - { - await using var client = TestClient.CreateForDaprClient(); - - var metadata = new Dictionary - { - { "key1", "value1" }, - { "key2", "value2" } - }; - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.GetSecretAsync("testStore", "test_key", metadata); - }); - - request.Dismiss(); - - // Get Request and validate - var envelope = await request.GetRequestEnvelopeAsync(); - envelope.StoreName.ShouldBe("testStore"); - envelope.Key.ShouldBe("test_key"); - envelope.Metadata.Count.ShouldBe(2); - envelope.Metadata.Keys.Contains("key1").ShouldBeTrue(); - envelope.Metadata.Keys.Contains("key2").ShouldBeTrue(); - envelope.Metadata["key1"].ShouldBe("value1"); - envelope.Metadata["key2"].ShouldBe("value2"); - - // Create Response & Respond - var secrets = new Dictionary - { - { "redis_secret", "Guess_Redis" }, - { "kafka_secret", "Guess_Kafka" } - }; - var secretsResponse = await SendResponseWithSecrets(secrets, request); - - // Get response and validate - secretsResponse.Count.ShouldBe(2); - secretsResponse.ContainsKey("redis_secret").ShouldBeTrue(); - secretsResponse["redis_secret"].ShouldBe("Guess_Redis"); - secretsResponse.ContainsKey("kafka_secret").ShouldBeTrue(); - secretsResponse["kafka_secret"].ShouldBe("Guess_Kafka"); - } - - [Fact] - public async Task GetSecretAsync_WithCancelledToken() - { - await using var client = TestClient.CreateForDaprClient(); - - var metadata = new Dictionary - { - { "key1", "value1" }, - { "key2", "value2" } - }; - - var cts = new CancellationTokenSource(); - cts.Cancel(); - - await Assert.ThrowsAsync(async () => - { - await client.InnerClient.GetSecretAsync("testStore", "test_key", metadata, cancellationToken: cts.Token); - }); - } - - [Fact] - public async Task GetSecretAsync_WrapsRpcException() - { - var client = new MockClient(); - - var rpcStatus = new Grpc.Core.Status(StatusCode.Internal, "not gonna work"); - var rpcException = new RpcException(rpcStatus, new Metadata(), "not gonna work"); - - // Setup the mock client to throw an Rpc Exception with the expected details info - client.Mock - .Setup(m => m.GetSecretAsync(It.IsAny(), It.IsAny())) - .Throws(rpcException); - - var ex = await Assert.ThrowsAsync(async () => - { - await client.DaprClient.GetSecretAsync("test", "test"); - }); - Assert.Same(rpcException, ex.InnerException); - } - - [Fact] - public async Task GetBulkSecretAsync_ValidateRequest() - { - await using var client = TestClient.CreateForDaprClient(); - - var metadata = new Dictionary(); - metadata.Add("key1", "value1"); - metadata.Add("key2", "value2"); - - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.GetBulkSecretAsync("testStore", metadata); - }); - - request.Dismiss(); - - // Get Request and validate - var envelope = await request.GetRequestEnvelopeAsync(); - envelope.StoreName.ShouldBe("testStore"); - envelope.Metadata.Count.ShouldBe(2); - envelope.Metadata.Keys.Contains("key1").ShouldBeTrue(); - envelope.Metadata.Keys.Contains("key2").ShouldBeTrue(); - envelope.Metadata["key1"].ShouldBe("value1"); - envelope.Metadata["key2"].ShouldBe("value2"); - } - - [Fact] - public async Task GetBulkSecretAsync_ReturnSingleSecret() - { - await using var client = TestClient.CreateForDaprClient(); - - var metadata = new Dictionary(); - metadata.Add("key1", "value1"); - metadata.Add("key2", "value2"); - - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.GetBulkSecretAsync("testStore", metadata); - }); - - // Get Request and validate - var envelope = await request.GetRequestEnvelopeAsync(); - envelope.StoreName.ShouldBe("testStore"); - envelope.Metadata.Count.ShouldBe(2); - envelope.Metadata.Keys.Contains("key1").ShouldBeTrue(); - envelope.Metadata.Keys.Contains("key2").ShouldBeTrue(); - envelope.Metadata["key1"].ShouldBe("value1"); - envelope.Metadata["key2"].ShouldBe("value2"); - - // Create Response & Respond - var secrets = new Dictionary(); - secrets.Add("redis_secret", "Guess_Redis"); - var secretsResponse = await SendBulkResponseWithSecrets(secrets, request); - - // Get response and validate - secretsResponse.Count.ShouldBe(1); - secretsResponse.ContainsKey("redis_secret").ShouldBeTrue(); - secretsResponse["redis_secret"]["redis_secret"].ShouldBe("Guess_Redis"); - } - - [Fact] - public async Task GetBulkSecretAsync_ReturnMultipleSecrets() - { - await using var client = TestClient.CreateForDaprClient(); - - var metadata = new Dictionary(); - metadata.Add("key1", "value1"); - metadata.Add("key2", "value2"); - - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.GetBulkSecretAsync("testStore", metadata); - }); - - - // Get Request and validate - var envelope = await request.GetRequestEnvelopeAsync(); - envelope.StoreName.ShouldBe("testStore"); - envelope.Metadata.Count.ShouldBe(2); - envelope.Metadata.Keys.Contains("key1").ShouldBeTrue(); - envelope.Metadata.Keys.Contains("key2").ShouldBeTrue(); - envelope.Metadata["key1"].ShouldBe("value1"); - envelope.Metadata["key2"].ShouldBe("value2"); - - // Create Response & Respond - var secrets = new Dictionary(); - secrets.Add("redis_secret", "Guess_Redis"); - secrets.Add("kafka_secret", "Guess_Kafka"); - var secretsResponse = await SendBulkResponseWithSecrets(secrets, request); - - // Get response and validate - secretsResponse.Count.ShouldBe(2); - secretsResponse.ContainsKey("redis_secret").ShouldBeTrue(); - secretsResponse["redis_secret"]["redis_secret"].ShouldBe("Guess_Redis"); - secretsResponse.ContainsKey("kafka_secret").ShouldBeTrue(); - secretsResponse["kafka_secret"]["kafka_secret"].ShouldBe("Guess_Kafka"); - } - - [Fact] - public async Task GetBulkSecretAsync_WithCancelledToken() - { - await using var client = TestClient.CreateForDaprClient(); - - var metadata = new Dictionary(); - metadata.Add("key1", "value1"); - metadata.Add("key2", "value2"); - - var cts = new CancellationTokenSource(); - cts.Cancel(); - - await Assert.ThrowsAsync(async () => - { - await client.InnerClient.GetBulkSecretAsync("testStore", metadata, cancellationToken: cts.Token); - }); - } - - [Fact] - public async Task GetBulkSecretAsync_WrapsRpcException() - { - var client = new MockClient(); - - var rpcStatus = new Grpc.Core.Status(StatusCode.Internal, "not gonna work"); - var rpcException = new RpcException(rpcStatus, new Metadata(), "not gonna work"); - - // Setup the mock client to throw an Rpc Exception with the expected details info - client.Mock - .Setup(m => m.GetBulkSecretAsync(It.IsAny(), It.IsAny())) - .Throws(rpcException); - - var ex = await Assert.ThrowsAsync(async () => - { - await client.DaprClient.GetBulkSecretAsync("test"); - }); - Assert.Same(rpcException, ex.InnerException); - } - - private async Task SendResponseWithSecrets(Dictionary secrets, TestClient.TestGrpcRequest request) - { - var secretResponse = new Autogenerated.GetSecretResponse(); - secretResponse.Data.Add(secrets); - - return await request.CompleteWithMessageAsync(secretResponse); - } - - private async Task SendBulkResponseWithSecrets(Dictionary secrets, TestClient.TestGrpcRequest request) - { - var getBulkSecretResponse = new Autogenerated.GetBulkSecretResponse(); - foreach (var secret in secrets) - { - var secretsResponse = new Autogenerated.SecretResponse(); - secretsResponse.Secrets[secret.Key] = secret.Value; - getBulkSecretResponse.Data.Add(secret.Key, secretsResponse); - } - - return await request.CompleteWithMessageAsync(getBulkSecretResponse); - } + // Get Request and validate + var envelope = await request.GetRequestEnvelopeAsync(); + envelope.StoreName.ShouldBe("testStore"); + envelope.Key.ShouldBe("test_key"); + envelope.Metadata.Count.ShouldBe(2); + envelope.Metadata.Keys.Contains("key1").ShouldBeTrue(); + envelope.Metadata.Keys.Contains("key2").ShouldBeTrue(); + envelope.Metadata["key1"].ShouldBe("value1"); + envelope.Metadata["key2"].ShouldBe("value2"); } -} + + [Fact] + public async Task GetSecretAsync_ReturnSingleSecret() + { + await using var client = TestClient.CreateForDaprClient(); + + var metadata = new Dictionary + { + { "key1", "value1" }, + { "key2", "value2" } + }; + var request = await client.CaptureGrpcRequestAsync(async daprClient => + { + return await daprClient.GetSecretAsync("testStore", "test_key", metadata); + }); + + request.Dismiss(); + + // Get Request and validate + var envelope = await request.GetRequestEnvelopeAsync(); + envelope.StoreName.ShouldBe("testStore"); + envelope.Key.ShouldBe("test_key"); + envelope.Metadata.Count.ShouldBe(2); + envelope.Metadata.Keys.Contains("key1").ShouldBeTrue(); + envelope.Metadata.Keys.Contains("key2").ShouldBeTrue(); + envelope.Metadata["key1"].ShouldBe("value1"); + envelope.Metadata["key2"].ShouldBe("value2"); + + // Create Response & Respond + var secrets = new Dictionary + { + { "redis_secret", "Guess_Redis" } + }; + var secretsResponse = await SendResponseWithSecrets(secrets, request); + + // Get response and validate + secretsResponse.Count.ShouldBe(1); + secretsResponse.ContainsKey("redis_secret").ShouldBeTrue(); + secretsResponse["redis_secret"].ShouldBe("Guess_Redis"); + } + + [Fact] + public async Task GetSecretAsync_WithSlashesInName() + { + await using var client = TestClient.CreateForDaprClient(); + + var request = await client.CaptureGrpcRequestAsync(async DaprClient => + { + return await DaprClient.GetSecretAsync("testStore", "us-west-1/org/xpto/secretabc"); + }); + + request.Dismiss(); + + //Get Request and validate + var envelope = await request.GetRequestEnvelopeAsync(); + envelope.StoreName.ShouldBe("testStore"); + envelope.Key.ShouldBe("us-west-1/org/xpto/secretabc"); + + var secrets = new Dictionary { { "us-west-1/org/xpto/secretabc", "abc123" } }; + var secretsResponse = await SendResponseWithSecrets(secrets, request); + + //Get response and validate + secretsResponse.Count.ShouldBe(1); + secretsResponse.ContainsKey("us-west-1/org/xpto/secretabc").ShouldBeTrue(); + secretsResponse["us-west-1/org/xpto/secretabc"].ShouldBe("abc123"); + } + + [Fact] + public async Task GetSecretAsync_ReturnMultipleSecrets() + { + await using var client = TestClient.CreateForDaprClient(); + + var metadata = new Dictionary + { + { "key1", "value1" }, + { "key2", "value2" } + }; + var request = await client.CaptureGrpcRequestAsync(async daprClient => + { + return await daprClient.GetSecretAsync("testStore", "test_key", metadata); + }); + + request.Dismiss(); + + // Get Request and validate + var envelope = await request.GetRequestEnvelopeAsync(); + envelope.StoreName.ShouldBe("testStore"); + envelope.Key.ShouldBe("test_key"); + envelope.Metadata.Count.ShouldBe(2); + envelope.Metadata.Keys.Contains("key1").ShouldBeTrue(); + envelope.Metadata.Keys.Contains("key2").ShouldBeTrue(); + envelope.Metadata["key1"].ShouldBe("value1"); + envelope.Metadata["key2"].ShouldBe("value2"); + + // Create Response & Respond + var secrets = new Dictionary + { + { "redis_secret", "Guess_Redis" }, + { "kafka_secret", "Guess_Kafka" } + }; + var secretsResponse = await SendResponseWithSecrets(secrets, request); + + // Get response and validate + secretsResponse.Count.ShouldBe(2); + secretsResponse.ContainsKey("redis_secret").ShouldBeTrue(); + secretsResponse["redis_secret"].ShouldBe("Guess_Redis"); + secretsResponse.ContainsKey("kafka_secret").ShouldBeTrue(); + secretsResponse["kafka_secret"].ShouldBe("Guess_Kafka"); + } + + [Fact] + public async Task GetSecretAsync_WithCancelledToken() + { + await using var client = TestClient.CreateForDaprClient(); + + var metadata = new Dictionary + { + { "key1", "value1" }, + { "key2", "value2" } + }; + + var cts = new CancellationTokenSource(); + cts.Cancel(); + + await Assert.ThrowsAsync(async () => + { + await client.InnerClient.GetSecretAsync("testStore", "test_key", metadata, cancellationToken: cts.Token); + }); + } + + [Fact] + public async Task GetSecretAsync_WrapsRpcException() + { + var client = new MockClient(); + + var rpcStatus = new Grpc.Core.Status(StatusCode.Internal, "not gonna work"); + var rpcException = new RpcException(rpcStatus, new Metadata(), "not gonna work"); + + // Setup the mock client to throw an Rpc Exception with the expected details info + client.Mock + .Setup(m => m.GetSecretAsync(It.IsAny(), It.IsAny())) + .Throws(rpcException); + + var ex = await Assert.ThrowsAsync(async () => + { + await client.DaprClient.GetSecretAsync("test", "test"); + }); + Assert.Same(rpcException, ex.InnerException); + } + + [Fact] + public async Task GetBulkSecretAsync_ValidateRequest() + { + await using var client = TestClient.CreateForDaprClient(); + + var metadata = new Dictionary(); + metadata.Add("key1", "value1"); + metadata.Add("key2", "value2"); + + var request = await client.CaptureGrpcRequestAsync(async daprClient => + { + return await daprClient.GetBulkSecretAsync("testStore", metadata); + }); + + request.Dismiss(); + + // Get Request and validate + var envelope = await request.GetRequestEnvelopeAsync(); + envelope.StoreName.ShouldBe("testStore"); + envelope.Metadata.Count.ShouldBe(2); + envelope.Metadata.Keys.Contains("key1").ShouldBeTrue(); + envelope.Metadata.Keys.Contains("key2").ShouldBeTrue(); + envelope.Metadata["key1"].ShouldBe("value1"); + envelope.Metadata["key2"].ShouldBe("value2"); + } + + [Fact] + public async Task GetBulkSecretAsync_ReturnSingleSecret() + { + await using var client = TestClient.CreateForDaprClient(); + + var metadata = new Dictionary(); + metadata.Add("key1", "value1"); + metadata.Add("key2", "value2"); + + var request = await client.CaptureGrpcRequestAsync(async daprClient => + { + return await daprClient.GetBulkSecretAsync("testStore", metadata); + }); + + // Get Request and validate + var envelope = await request.GetRequestEnvelopeAsync(); + envelope.StoreName.ShouldBe("testStore"); + envelope.Metadata.Count.ShouldBe(2); + envelope.Metadata.Keys.Contains("key1").ShouldBeTrue(); + envelope.Metadata.Keys.Contains("key2").ShouldBeTrue(); + envelope.Metadata["key1"].ShouldBe("value1"); + envelope.Metadata["key2"].ShouldBe("value2"); + + // Create Response & Respond + var secrets = new Dictionary(); + secrets.Add("redis_secret", "Guess_Redis"); + var secretsResponse = await SendBulkResponseWithSecrets(secrets, request); + + // Get response and validate + secretsResponse.Count.ShouldBe(1); + secretsResponse.ContainsKey("redis_secret").ShouldBeTrue(); + secretsResponse["redis_secret"]["redis_secret"].ShouldBe("Guess_Redis"); + } + + [Fact] + public async Task GetBulkSecretAsync_ReturnMultipleSecrets() + { + await using var client = TestClient.CreateForDaprClient(); + + var metadata = new Dictionary(); + metadata.Add("key1", "value1"); + metadata.Add("key2", "value2"); + + var request = await client.CaptureGrpcRequestAsync(async daprClient => + { + return await daprClient.GetBulkSecretAsync("testStore", metadata); + }); + + + // Get Request and validate + var envelope = await request.GetRequestEnvelopeAsync(); + envelope.StoreName.ShouldBe("testStore"); + envelope.Metadata.Count.ShouldBe(2); + envelope.Metadata.Keys.Contains("key1").ShouldBeTrue(); + envelope.Metadata.Keys.Contains("key2").ShouldBeTrue(); + envelope.Metadata["key1"].ShouldBe("value1"); + envelope.Metadata["key2"].ShouldBe("value2"); + + // Create Response & Respond + var secrets = new Dictionary(); + secrets.Add("redis_secret", "Guess_Redis"); + secrets.Add("kafka_secret", "Guess_Kafka"); + var secretsResponse = await SendBulkResponseWithSecrets(secrets, request); + + // Get response and validate + secretsResponse.Count.ShouldBe(2); + secretsResponse.ContainsKey("redis_secret").ShouldBeTrue(); + secretsResponse["redis_secret"]["redis_secret"].ShouldBe("Guess_Redis"); + secretsResponse.ContainsKey("kafka_secret").ShouldBeTrue(); + secretsResponse["kafka_secret"]["kafka_secret"].ShouldBe("Guess_Kafka"); + } + + [Fact] + public async Task GetBulkSecretAsync_WithCancelledToken() + { + await using var client = TestClient.CreateForDaprClient(); + + var metadata = new Dictionary(); + metadata.Add("key1", "value1"); + metadata.Add("key2", "value2"); + + var cts = new CancellationTokenSource(); + cts.Cancel(); + + await Assert.ThrowsAsync(async () => + { + await client.InnerClient.GetBulkSecretAsync("testStore", metadata, cancellationToken: cts.Token); + }); + } + + [Fact] + public async Task GetBulkSecretAsync_WrapsRpcException() + { + var client = new MockClient(); + + var rpcStatus = new Grpc.Core.Status(StatusCode.Internal, "not gonna work"); + var rpcException = new RpcException(rpcStatus, new Metadata(), "not gonna work"); + + // Setup the mock client to throw an Rpc Exception with the expected details info + client.Mock + .Setup(m => m.GetBulkSecretAsync(It.IsAny(), It.IsAny())) + .Throws(rpcException); + + var ex = await Assert.ThrowsAsync(async () => + { + await client.DaprClient.GetBulkSecretAsync("test"); + }); + Assert.Same(rpcException, ex.InnerException); + } + + private async Task SendResponseWithSecrets(Dictionary secrets, TestClient.TestGrpcRequest request) + { + var secretResponse = new Autogenerated.GetSecretResponse(); + secretResponse.Data.Add(secrets); + + return await request.CompleteWithMessageAsync(secretResponse); + } + + private async Task SendBulkResponseWithSecrets(Dictionary secrets, TestClient.TestGrpcRequest request) + { + var getBulkSecretResponse = new Autogenerated.GetBulkSecretResponse(); + foreach (var secret in secrets) + { + var secretsResponse = new Autogenerated.SecretResponse(); + secretsResponse.Secrets[secret.Key] = secret.Value; + getBulkSecretResponse.Data.Add(secret.Key, secretsResponse); + } + + return await request.CompleteWithMessageAsync(getBulkSecretResponse); + } +} \ No newline at end of file diff --git a/test/Dapr.Client.Test/StateApiTest.cs b/test/Dapr.Client.Test/StateApiTest.cs index 43c7f408..7fcc44a4 100644 --- a/test/Dapr.Client.Test/StateApiTest.cs +++ b/test/Dapr.Client.Test/StateApiTest.cs @@ -29,1470 +29,1468 @@ using System.Threading; using System.Net.Http; using System.Text; -namespace Dapr.Client.Test +namespace Dapr.Client.Test; + +public class StateApiTest { - public class StateApiTest + [Fact] + public async Task GetStateAsync_CanReadState() { - [Fact] - public async Task GetStateAsync_CanReadState() - { - await using var client = TestClient.CreateForDaprClient(); + await using var client = TestClient.CreateForDaprClient(); - var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetStateAsync("testStore", "test")); + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetStateAsync("testStore", "test")); - request.Dismiss(); + request.Dismiss(); - // Create Response & Respond - var data = new Widget() { Size = "small", Color = "yellow", }; - var envelope = MakeGetStateResponse(data); - var state = await request.CompleteWithMessageAsync(envelope); + // Create Response & Respond + var data = new Widget() { Size = "small", Color = "yellow", }; + var envelope = MakeGetStateResponse(data); + var state = await request.CompleteWithMessageAsync(envelope); - // Get response and validate - state.Size.ShouldBe("small"); - state.Color.ShouldBe("yellow"); - } + // Get response and validate + state.Size.ShouldBe("small"); + state.Color.ShouldBe("yellow"); + } - [Fact] - public async Task GetBulkStateAsync_CanReadState() - { - await using var client = TestClient.CreateForDaprClient(); + [Fact] + public async Task GetBulkStateAsync_CanReadState() + { + await using var client = TestClient.CreateForDaprClient(); - const string key = "test"; - var request = await client.CaptureGrpcRequestAsync(async daprClient => 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 data = "value"; - var envelope = MakeGetBulkStateResponse(key, data); - var state = await request.CompleteWithMessageAsync(envelope); + // Create Response & Respond + const string data = "value"; + var envelope = MakeGetBulkStateResponse(key, data); + var state = await request.CompleteWithMessageAsync(envelope); - // Get response and validate - state.Count.ShouldBe(1); - } + // Get response and validate + state.Count.ShouldBe(1); + } - [Fact] - public async Task GetBulkStateAsync_CanReadDeserializedState() - { - await using var client = TestClient.CreateForDaprClient(); + [Fact] + public async Task GetBulkStateAsync_CanReadDeserializedState() + { + await using var client = TestClient.CreateForDaprClient(); - const string key = "test"; - var request = await client.CaptureGrpcRequestAsync(async daprClient => 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"; - const string color = "yellow"; - var data = new Widget() {Size = size, Color = color}; - var envelope = MakeGetBulkStateResponse(key, data); - var state = await request.CompleteWithMessageAsync(envelope); + // Create Response & Respond + const string size = "small"; + const string color = "yellow"; + var data = new Widget() {Size = size, Color = color}; + var envelope = MakeGetBulkStateResponse(key, data); + var state = await request.CompleteWithMessageAsync(envelope); - // Get response and validate - state.Count.ShouldBe(1); - state[0].Value.Size.ShouldMatch(size); - state[0].Value.Color.ShouldMatch(color); - } + // Get response and validate + state.Count.ShouldBe(1); + state[0].Value.Size.ShouldMatch(size); + state[0].Value.Color.ShouldMatch(color); + } - [Fact] - public async Task GetBulkStateAsync_WrapsRpcException() + [Fact] + public async Task GetBulkStateAsync_WrapsRpcException() + { + await using var client = TestClient.CreateForDaprClient(); + + 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 () => { - await using var client = TestClient.CreateForDaprClient(); + await request.CompleteAsync(new HttpResponseMessage(HttpStatusCode.NotAcceptable)); + }); + Assert.IsType(ex.InnerException); + } - const string key = "test"; - var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetBulkStateAsync("testStore", new List() { key }, null)); + [Fact] + public async Task GetBulkStateAsync_ValidateRequest() + { + await using var client = TestClient.CreateForDaprClient(); - // Create Response & Respond - var ex = await Assert.ThrowsAsync(async () => - { - await request.CompleteAsync(new HttpResponseMessage(HttpStatusCode.NotAcceptable)); - }); - Assert.IsType(ex.InnerException); - } - - [Fact] - public async Task GetBulkStateAsync_ValidateRequest() + const string key = "test"; + var metadata = new Dictionary { - await using var client = TestClient.CreateForDaprClient(); + { "partitionKey", "mypartition" } + }; + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetBulkStateAsync("testStore", new List() { key }, null, metadata: metadata)); - const string key = "test"; - var metadata = new Dictionary - { - { "partitionKey", "mypartition" } - }; - var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetBulkStateAsync("testStore", new List() { key }, null, metadata: metadata)); + request.Dismiss(); - request.Dismiss(); + // Create Response & Validate + var envelope = await request.GetRequestEnvelopeAsync(); + envelope.StoreName.ShouldBe("testStore"); + envelope.Metadata.ShouldBe(metadata); + } - // Create Response & Validate - var envelope = await request.GetRequestEnvelopeAsync(); - envelope.StoreName.ShouldBe("testStore"); - envelope.Metadata.ShouldBe(metadata); - } + [Fact] + public async Task GetStateAndEtagAsync_CanReadState() + { + await using var client = TestClient.CreateForDaprClient(); - [Fact] - public async Task GetStateAndEtagAsync_CanReadState() + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetStateAndETagAsync("testStore", "test")); + + // Create Response & Respond + var data = new Widget() { Size = "small", Color = "yellow", }; + var envelope = MakeGetStateResponse(data, "Test_Etag"); + var (state, etag) = await request.CompleteWithMessageAsync(envelope); + + // Get response and validate + state.Size.ShouldBe("small"); + state.Color.ShouldBe("yellow"); + etag.ShouldBe("Test_Etag"); + } + + [Fact] + public async Task GetStateAndETagAsync_WrapsRpcException() + { + await using var client = TestClient.CreateForDaprClient(); + + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetStateAndETagAsync("testStore", "test")); + + // Create Response & Respond + var ex = await Assert.ThrowsAsync(async () => { - await using var client = TestClient.CreateForDaprClient(); + await request.CompleteAsync(new HttpResponseMessage(HttpStatusCode.NotAcceptable)); + }); + Assert.IsType(ex.InnerException); + } - var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetStateAndETagAsync("testStore", "test")); + [Fact] + public async Task GetStateAndETagAsync_WrapsJsonException() + { + await using var client = TestClient.CreateForDaprClient(); - // Create Response & Respond - var data = new Widget() { Size = "small", Color = "yellow", }; - var envelope = MakeGetStateResponse(data, "Test_Etag"); - var (state, etag) = await request.CompleteWithMessageAsync(envelope); + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetStateAndETagAsync("testStore", "test")); - // Get response and validate - state.Size.ShouldBe("small"); - state.Color.ShouldBe("yellow"); - etag.ShouldBe("Test_Etag"); - } - - [Fact] - public async Task GetStateAndETagAsync_WrapsRpcException() + // Create Response & Respond + var envelope = new Autogenerated.GetStateResponse() { - await using var client = TestClient.CreateForDaprClient(); - - var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetStateAndETagAsync("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 GetStateAndETagAsync_WrapsJsonException() + // Totally NOT valid JSON + Data = ByteString.CopyFrom(0x5b, 0x7b, 0x5b, 0x7b), + }; + var ex = await Assert.ThrowsAsync(async () => { - await using var client = TestClient.CreateForDaprClient(); + await request.CompleteWithMessageAsync(envelope); + }); + Assert.IsType(ex.InnerException); + } - var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetStateAndETagAsync("testStore", "test")); + [Fact] + public async Task GetStateAsync_CanReadEmptyState_ReturnsDefault() + { + await using var client = TestClient.CreateForDaprClient(); - // Create Response & Respond - var envelope = new Autogenerated.GetStateResponse() - { - // Totally NOT valid JSON - Data = ByteString.CopyFrom(0x5b, 0x7b, 0x5b, 0x7b), - }; - var ex = await Assert.ThrowsAsync(async () => - { - await request.CompleteWithMessageAsync(envelope); - }); - Assert.IsType(ex.InnerException); - } + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetStateAsync("testStore", "test", ConsistencyMode.Eventual)); - [Fact] - public async Task GetStateAsync_CanReadEmptyState_ReturnsDefault() + // Create Response & Respond + var envelope = MakeGetStateResponse(null); + var state = await request.CompleteWithMessageAsync(envelope); + + // Get response and validate + state.ShouldBeNull(); + } + + [Theory] + [InlineData(ConsistencyMode.Eventual, StateConsistency.ConsistencyEventual)] + [InlineData(ConsistencyMode.Strong, StateConsistency.ConsistencyStrong)] + public async Task GetStateAsync_ValidateRequest(ConsistencyMode consistencyMode, StateConsistency expectedConsistencyMode) + { + await using var client = TestClient.CreateForDaprClient(); + + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetStateAsync("testStore", "test", consistencyMode)); + + // Get Request & Validate + var envelope = await request.GetRequestEnvelopeAsync(); + envelope.StoreName.ShouldBe("testStore"); + envelope.Key.ShouldBe("test"); + envelope.Consistency.ShouldBe(expectedConsistencyMode); + + // Create Response & Respond + var state = await request.CompleteWithMessageAsync(MakeGetStateResponse(null)); + + // Get response and validate + state.ShouldBeNull(); + } + + [Fact] + public async Task GetStateAndEtagAsync_ValidateRequest() + { + await using var client = TestClient.CreateForDaprClient(); + + var metadata = new Dictionary { - await using var client = TestClient.CreateForDaprClient(); + { "partitionKey", "mypartition" } + }; + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetStateAsync("testStore", "test", metadata: metadata)); - var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetStateAsync("testStore", "test", ConsistencyMode.Eventual)); + // Get Request & Validate + var envelope = await request.GetRequestEnvelopeAsync(); + envelope.StoreName.ShouldBe("testStore"); + envelope.Key.ShouldBe("test"); + envelope.Metadata.ShouldBe(metadata); - // Create Response & Respond - var envelope = MakeGetStateResponse(null); - var state = await request.CompleteWithMessageAsync(envelope); + // Create Response & Respond + var state = await request.CompleteWithMessageAsync(MakeGetStateResponse(null)); - // Get response and validate - state.ShouldBeNull(); - } + // Get response and validate + state.ShouldBeNull(); + } - [Theory] - [InlineData(ConsistencyMode.Eventual, StateConsistency.ConsistencyEventual)] - [InlineData(ConsistencyMode.Strong, StateConsistency.ConsistencyStrong)] - public async Task GetStateAsync_ValidateRequest(ConsistencyMode consistencyMode, StateConsistency expectedConsistencyMode) + [Fact] + public async Task GetStateAsync_WrapsRpcException() + { + await using var client = TestClient.CreateForDaprClient(); + + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetStateAsync("testStore", "test")); + + // Create Response & Respond + var ex = await Assert.ThrowsAsync(async () => { - await using var client = TestClient.CreateForDaprClient(); + await request.CompleteAsync(new HttpResponseMessage(HttpStatusCode.NotAcceptable)); + }); + Assert.IsType(ex.InnerException); + } - var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetStateAsync("testStore", "test", consistencyMode)); + [Fact] + public async Task GetStateAsync_WrapsJsonException() + { + await using var client = TestClient.CreateForDaprClient(); - // Get Request & Validate - var envelope = await request.GetRequestEnvelopeAsync(); - envelope.StoreName.ShouldBe("testStore"); - envelope.Key.ShouldBe("test"); - envelope.Consistency.ShouldBe(expectedConsistencyMode); + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetStateAsync("testStore", "test")); - // Create Response & Respond - var state = await request.CompleteWithMessageAsync(MakeGetStateResponse(null)); - - // Get response and validate - state.ShouldBeNull(); - } - - [Fact] - public async Task GetStateAndEtagAsync_ValidateRequest() + // Create Response & Respond + var stateResponse = new Autogenerated.GetStateResponse() { - await using var client = TestClient.CreateForDaprClient(); + // Totally NOT valid JSON + Data = ByteString.CopyFrom(0x5b, 0x7b, 0x5b, 0x7b), + }; - var metadata = new Dictionary - { - { "partitionKey", "mypartition" } - }; - var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetStateAsync("testStore", "test", metadata: metadata)); - - // Get Request & Validate - var envelope = await request.GetRequestEnvelopeAsync(); - envelope.StoreName.ShouldBe("testStore"); - envelope.Key.ShouldBe("test"); - envelope.Metadata.ShouldBe(metadata); - - // Create Response & Respond - var state = await request.CompleteWithMessageAsync(MakeGetStateResponse(null)); - - // Get response and validate - state.ShouldBeNull(); - } - - [Fact] - public async Task GetStateAsync_WrapsRpcException() + var ex = await Assert.ThrowsAsync(async () => { - await using var client = TestClient.CreateForDaprClient(); + await request.CompleteWithMessageAsync(stateResponse); + }); + Assert.IsType(ex.InnerException); + } - var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetStateAsync("testStore", "test")); + [Fact] + public async Task SaveStateAsync_CanSaveState() + { + await using var client = TestClient.CreateForDaprClient(); - // Create Response & Respond - var ex = await Assert.ThrowsAsync(async () => - { - await request.CompleteAsync(new HttpResponseMessage(HttpStatusCode.NotAcceptable)); - }); - Assert.IsType(ex.InnerException); - } - - [Fact] - public async Task GetStateAsync_WrapsJsonException() + var widget = new Widget() { Size = "small", Color = "yellow", }; + var request = await client.CaptureGrpcRequestAsync(async daprClient => { - await using var client = TestClient.CreateForDaprClient(); + await daprClient.SaveStateAsync("testStore", "test", widget); + }); - var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetStateAsync("testStore", "test")); + request.Dismiss(); - // Create Response & Respond - var stateResponse = new Autogenerated.GetStateResponse() - { - // Totally NOT valid JSON - Data = ByteString.CopyFrom(0x5b, 0x7b, 0x5b, 0x7b), - }; + // Get Request and validate + var envelope = await request.GetRequestEnvelopeAsync(); + envelope.StoreName.ShouldBe("testStore"); + envelope.States.Count.ShouldBe(1); + var state = envelope.States[0]; + state.Key.ShouldBe("test"); - var ex = await Assert.ThrowsAsync(async () => - { - await request.CompleteWithMessageAsync(stateResponse); - }); - Assert.IsType(ex.InnerException); - } - - [Fact] - public async Task SaveStateAsync_CanSaveState() - { - await using var client = TestClient.CreateForDaprClient(); - - var widget = new Widget() { Size = "small", Color = "yellow", }; - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - await daprClient.SaveStateAsync("testStore", "test", widget); - }); - - request.Dismiss(); - - // Get Request and validate - var envelope = await request.GetRequestEnvelopeAsync(); - envelope.StoreName.ShouldBe("testStore"); - envelope.States.Count.ShouldBe(1); - var state = envelope.States[0]; - state.Key.ShouldBe("test"); - - var stateJson = state.Value.ToStringUtf8(); - var stateFromRequest = JsonSerializer.Deserialize(stateJson, client.InnerClient.JsonSerializerOptions); - stateFromRequest.Size.ShouldBe(widget.Size); - stateFromRequest.Color.ShouldBe(widget.Color); - } + var stateJson = state.Value.ToStringUtf8(); + var stateFromRequest = JsonSerializer.Deserialize(stateJson, client.InnerClient.JsonSerializerOptions); + stateFromRequest.Size.ShouldBe(widget.Size); + stateFromRequest.Color.ShouldBe(widget.Color); + } - [Fact] - public async Task SaveBulkStateAsync_ValidateRequest() - { - await using var client = TestClient.CreateForDaprClient(); + [Fact] + public async Task SaveBulkStateAsync_ValidateRequest() + { + await using var client = TestClient.CreateForDaprClient(); - var stateItem1 = new SaveStateItem("testKey1", "testValue1", "testEtag1", - new StateOptions { Concurrency = ConcurrencyMode.LastWrite }, - new Dictionary {{ "partitionKey1", "mypartition1" } }); + var stateItem1 = new SaveStateItem("testKey1", "testValue1", "testEtag1", + new StateOptions { Concurrency = ConcurrencyMode.LastWrite }, + new Dictionary {{ "partitionKey1", "mypartition1" } }); - var stateItem2 = new SaveStateItem("testKey2", "testValue2", "testEtag2", - new StateOptions { Concurrency = ConcurrencyMode.LastWrite }, - new Dictionary {{ "partitionKey2", "mypartition2" } }); + var stateItem2 = new SaveStateItem("testKey2", "testValue2", "testEtag2", + new StateOptions { Concurrency = ConcurrencyMode.LastWrite }, + new Dictionary {{ "partitionKey2", "mypartition2" } }); - var stateItem3 = new SaveStateItem("testKey3", "testValue3", null, - new StateOptions { Concurrency = ConcurrencyMode.LastWrite }, - new Dictionary {{ "partitionKey3", "mypartition3" } }); + var stateItem3 = new SaveStateItem("testKey3", "testValue3", null, + new StateOptions { Concurrency = ConcurrencyMode.LastWrite }, + new Dictionary {{ "partitionKey3", "mypartition3" } }); - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - await daprClient.SaveBulkStateAsync("testStore", - new List>() { stateItem1, stateItem2, stateItem3}); - }); + var request = await client.CaptureGrpcRequestAsync(async daprClient => + { + await daprClient.SaveBulkStateAsync("testStore", + new List>() { stateItem1, stateItem2, stateItem3}); + }); - request.Dismiss(); + request.Dismiss(); - // Create Response & Validate - var envelope = await request.GetRequestEnvelopeAsync(); - envelope.StoreName.ShouldBe("testStore"); - envelope.States.Count.ShouldBe(3); + // Create Response & Validate + var envelope = await request.GetRequestEnvelopeAsync(); + envelope.StoreName.ShouldBe("testStore"); + envelope.States.Count.ShouldBe(3); - envelope.States[0].Key.ShouldBe("testKey1"); - envelope.States[0].Value.ShouldBe(ByteString.CopyFromUtf8(JsonSerializer.Serialize("testValue1"))); - envelope.States[0].Metadata.ShouldContainKey("partitionKey1"); + envelope.States[0].Key.ShouldBe("testKey1"); + envelope.States[0].Value.ShouldBe(ByteString.CopyFromUtf8(JsonSerializer.Serialize("testValue1"))); + envelope.States[0].Metadata.ShouldContainKey("partitionKey1"); - envelope.States[1].Key.ShouldBe("testKey2"); - envelope.States[1].Value.ShouldBe(ByteString.CopyFromUtf8(JsonSerializer.Serialize("testValue2"))); - envelope.States[1].Metadata.ShouldContainKey("partitionKey2"); + envelope.States[1].Key.ShouldBe("testKey2"); + envelope.States[1].Value.ShouldBe(ByteString.CopyFromUtf8(JsonSerializer.Serialize("testValue2"))); + envelope.States[1].Metadata.ShouldContainKey("partitionKey2"); - envelope.States[2].Key.ShouldBe("testKey3"); - envelope.States[2].Value.ShouldBe(ByteString.CopyFromUtf8(JsonSerializer.Serialize("testValue3"))); - envelope.States[2].Metadata.ShouldContainKey("partitionKey3"); - } + envelope.States[2].Key.ShouldBe("testKey3"); + envelope.States[2].Value.ShouldBe(ByteString.CopyFromUtf8(JsonSerializer.Serialize("testValue3"))); + envelope.States[2].Metadata.ShouldContainKey("partitionKey3"); + } - [Fact] - public async Task GetStateAsync_WithCancelledToken() + [Fact] + public async Task GetStateAsync_WithCancelledToken() + { + await using var client = TestClient.CreateForDaprClient(); + + var cts = new CancellationTokenSource(); + cts.Cancel(); + + await Assert.ThrowsAsync(async () => { - await using var client = TestClient.CreateForDaprClient(); + await client.InnerClient.GetStateAsync("testStore", "test", cancellationToken: cts.Token); + }); + } - var cts = new CancellationTokenSource(); - cts.Cancel(); + [Fact] + public async Task SaveStateAsync_CanClearState() + { + await using var client = TestClient.CreateForDaprClient(); - await Assert.ThrowsAsync(async () => - { - await client.InnerClient.GetStateAsync("testStore", "test", cancellationToken: cts.Token); - }); - } - - [Fact] - public async Task SaveStateAsync_CanClearState() + var request = await client.CaptureGrpcRequestAsync(async daprClient => { - await using var client = TestClient.CreateForDaprClient(); + await daprClient.SaveStateAsync("testStore", "test", null); + }); - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - await daprClient.SaveStateAsync("testStore", "test", null); - }); + request.Dismiss(); - request.Dismiss(); + // Get Request and validate + var envelope = await request.GetRequestEnvelopeAsync(); - // Get Request and validate - var envelope = await request.GetRequestEnvelopeAsync(); + envelope.StoreName.ShouldBe("testStore"); + envelope.States.Count.ShouldBe(1); + var state = envelope.States[0]; + state.Key.ShouldBe("test"); + state.Value.ShouldBe(ByteString.Empty); + } - envelope.StoreName.ShouldBe("testStore"); - envelope.States.Count.ShouldBe(1); - var state = envelope.States[0]; - state.Key.ShouldBe("test"); - state.Value.ShouldBe(ByteString.Empty); - } + [Fact] + public async Task SaveStateAsync_WithCancelledToken() + { + await using var client = TestClient.CreateForDaprClient(); - [Fact] - public async Task SaveStateAsync_WithCancelledToken() + var cts = new CancellationTokenSource(); + cts.Cancel(); + + await Assert.ThrowsAsync(async () => { - await using var client = TestClient.CreateForDaprClient(); + await client.InnerClient.SaveStateAsync("testStore", "test", null, cancellationToken: cts.Token); + }); + } - var cts = new CancellationTokenSource(); - cts.Cancel(); + [Fact] + public async Task SetStateAsync_ThrowsForNonSuccess() + { + await using var client = TestClient.CreateForDaprClient(); - await Assert.ThrowsAsync(async () => - { - await client.InnerClient.SaveStateAsync("testStore", "test", null, cancellationToken: cts.Token); - }); - } - - [Fact] - public async Task SetStateAsync_ThrowsForNonSuccess() + var widget = new Widget() { Size = "small", Color = "yellow", }; + var request = await client.CaptureGrpcRequestAsync(async daprClient => { - await using var client = TestClient.CreateForDaprClient(); + await daprClient.SaveStateAsync("testStore", "test", widget); + }); - var widget = new Widget() { Size = "small", Color = "yellow", }; - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - await daprClient.SaveStateAsync("testStore", "test", widget); - }); - - // Create Response & Respond - var ex = await Assert.ThrowsAsync(async () => - { - await request.CompleteAsync(new HttpResponseMessage(HttpStatusCode.NotAcceptable)); - }); - Assert.IsType(ex.InnerException); - } - - [Fact] - public async Task ExecuteStateTransactionAsync_CanSaveState() + // Create Response & Respond + var ex = await Assert.ThrowsAsync(async () => { - await using var client = TestClient.CreateForDaprClient(); + await request.CompleteAsync(new HttpResponseMessage(HttpStatusCode.NotAcceptable)); + }); + Assert.IsType(ex.InnerException); + } - var stateValue1 = new Widget() { Size = "small", Color = "yellow", }; - var metadata1 = new Dictionary() - { - {"a", "b" } - }; - var options1 = new StateOptions - { - Concurrency = ConcurrencyMode.LastWrite - }; + [Fact] + public async Task ExecuteStateTransactionAsync_CanSaveState() + { + await using var client = TestClient.CreateForDaprClient(); - var state1 = new StateTransactionRequest("stateKey1", JsonSerializer.SerializeToUtf8Bytes(stateValue1), StateOperationType.Upsert, "testEtag", metadata1, options1); - const int stateValue2 = 100; - var state2 = new StateTransactionRequest("stateKey2", JsonSerializer.SerializeToUtf8Bytes(stateValue2), StateOperationType.Delete); - - const string stateValue3 = "teststring"; - var state3 = new StateTransactionRequest("stateKey3", JsonSerializer.SerializeToUtf8Bytes(stateValue3), StateOperationType.Upsert); - - var states = new List - { - state1, - state2, - state3 - }; - - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - await daprClient.ExecuteStateTransactionAsync("testStore", states); - }); - - request.Dismiss(); - - // Get Request and validate - var envelope = await request.GetRequestEnvelopeAsync(); - - envelope.StoreName.ShouldBe("testStore"); - envelope.Operations.Count.ShouldBe(3); - - var req1 = envelope.Operations[0]; - req1.Request.Key.ShouldBe("stateKey1"); - req1.OperationType.ShouldBe(StateOperationType.Upsert.ToString().ToLower()); - var valueJson1 = req1.Request.Value.ToStringUtf8(); - var value1 = JsonSerializer.Deserialize(valueJson1, client.InnerClient.JsonSerializerOptions); - value1.Size.ShouldBe(stateValue1.Size); - value1.Color.ShouldBe(stateValue1.Color); - req1.Request.Etag.Value.ShouldBe("testEtag"); - req1.Request.Metadata.Count.ShouldBe(1); - req1.Request.Metadata["a"].ShouldBe("b"); - req1.Request.Options.Concurrency.ShouldBe(StateConcurrency.ConcurrencyLastWrite); - - var req2 = envelope.Operations[1]; - req2.Request.Key.ShouldBe("stateKey2"); - req2.OperationType.ShouldBe(StateOperationType.Delete.ToString().ToLower()); - var valueJson2 = req2.Request.Value.ToStringUtf8(); - var value2 = JsonSerializer.Deserialize(valueJson2, client.InnerClient.JsonSerializerOptions); - value2.ShouldBe(100); - - var req3 = envelope.Operations[2]; - req3.Request.Key.ShouldBe("stateKey3"); - req3.OperationType.ShouldBe(StateOperationType.Upsert.ToString().ToLower()); - var valueJson3 = req3.Request.Value.ToStringUtf8(); - var value3 = JsonSerializer.Deserialize(valueJson3, client.InnerClient.JsonSerializerOptions); - value3.ShouldBe("teststring"); - } - - [Fact] - public async Task ExecuteStateTransactionAsync_ThrowsForNonSuccess() + var stateValue1 = new Widget() { Size = "small", Color = "yellow", }; + var metadata1 = new Dictionary() { - await using var client = TestClient.CreateForDaprClient(); - - var widget1 = new Widget() { Size = "small", Color = "yellow", }; - var state1 = new StateTransactionRequest("stateKey1", JsonSerializer.SerializeToUtf8Bytes(widget1), StateOperationType.Upsert); - var states = new List - { - state1 - }; - - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - await daprClient.ExecuteStateTransactionAsync("testStore", states); - }); - - // Create Response & Respond - var ex = await Assert.ThrowsAsync(async () => - { - await request.CompleteAsync(new HttpResponseMessage(HttpStatusCode.NotAcceptable)); - }); - Assert.IsType(ex.InnerException); - } - - [Fact] - public async Task ExecuteStateTransactionAsync_WithCancelledToken() + {"a", "b" } + }; + var options1 = new StateOptions { - await using var client = TestClient.CreateForDaprClient(); + Concurrency = ConcurrencyMode.LastWrite + }; - var operation = new StateTransactionRequest("test", null, StateOperationType.Delete); - var operations = new List - { - operation, - }; + var state1 = new StateTransactionRequest("stateKey1", JsonSerializer.SerializeToUtf8Bytes(stateValue1), StateOperationType.Upsert, "testEtag", metadata1, options1); + const int stateValue2 = 100; + var state2 = new StateTransactionRequest("stateKey2", JsonSerializer.SerializeToUtf8Bytes(stateValue2), StateOperationType.Delete); - var cts = new CancellationTokenSource(); - cts.Cancel(); + const string stateValue3 = "teststring"; + var state3 = new StateTransactionRequest("stateKey3", JsonSerializer.SerializeToUtf8Bytes(stateValue3), StateOperationType.Upsert); - await Assert.ThrowsAsync(async () => - { - await client.InnerClient.ExecuteStateTransactionAsync("testStore", operations, new Dictionary(), cancellationToken: cts.Token); - }); - } - - [Fact] - public async Task DeleteStateAsync_CanDeleteState() + var states = new List { - await using var client = TestClient.CreateForDaprClient(); + state1, + state2, + state3 + }; - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - await daprClient.DeleteStateAsync("testStore", "test"); - }); - - request.Dismiss(); - - var envelope = await request.GetRequestEnvelopeAsync(); - envelope.StoreName.ShouldBe("testStore"); - envelope.Key.ShouldBe("test"); - } - - [Fact] - public async Task DeleteStateAsync_ThrowsForNonSuccess() + var request = await client.CaptureGrpcRequestAsync(async daprClient => { - await using var client = TestClient.CreateForDaprClient(); + await daprClient.ExecuteStateTransactionAsync("testStore", states); + }); - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - await daprClient.DeleteStateAsync("testStore", "test"); - }); + request.Dismiss(); - // Create Response & Respond - var ex = await Assert.ThrowsAsync(async () => - { - await request.CompleteAsync(new HttpResponseMessage(HttpStatusCode.NotAcceptable)); - }); - Assert.IsType(ex.InnerException); - } + // Get Request and validate + var envelope = await request.GetRequestEnvelopeAsync(); - [Fact] - public async Task DeleteStateAsync_WithCancelledToken() + envelope.StoreName.ShouldBe("testStore"); + envelope.Operations.Count.ShouldBe(3); + + var req1 = envelope.Operations[0]; + req1.Request.Key.ShouldBe("stateKey1"); + req1.OperationType.ShouldBe(StateOperationType.Upsert.ToString().ToLower()); + var valueJson1 = req1.Request.Value.ToStringUtf8(); + var value1 = JsonSerializer.Deserialize(valueJson1, client.InnerClient.JsonSerializerOptions); + value1.Size.ShouldBe(stateValue1.Size); + value1.Color.ShouldBe(stateValue1.Color); + req1.Request.Etag.Value.ShouldBe("testEtag"); + req1.Request.Metadata.Count.ShouldBe(1); + req1.Request.Metadata["a"].ShouldBe("b"); + req1.Request.Options.Concurrency.ShouldBe(StateConcurrency.ConcurrencyLastWrite); + + var req2 = envelope.Operations[1]; + req2.Request.Key.ShouldBe("stateKey2"); + req2.OperationType.ShouldBe(StateOperationType.Delete.ToString().ToLower()); + var valueJson2 = req2.Request.Value.ToStringUtf8(); + var value2 = JsonSerializer.Deserialize(valueJson2, client.InnerClient.JsonSerializerOptions); + value2.ShouldBe(100); + + var req3 = envelope.Operations[2]; + req3.Request.Key.ShouldBe("stateKey3"); + req3.OperationType.ShouldBe(StateOperationType.Upsert.ToString().ToLower()); + var valueJson3 = req3.Request.Value.ToStringUtf8(); + var value3 = JsonSerializer.Deserialize(valueJson3, client.InnerClient.JsonSerializerOptions); + value3.ShouldBe("teststring"); + } + + [Fact] + public async Task ExecuteStateTransactionAsync_ThrowsForNonSuccess() + { + await using var client = TestClient.CreateForDaprClient(); + + var widget1 = new Widget() { Size = "small", Color = "yellow", }; + var state1 = new StateTransactionRequest("stateKey1", JsonSerializer.SerializeToUtf8Bytes(widget1), StateOperationType.Upsert); + var states = new List { - await using var client = TestClient.CreateForDaprClient(); + state1 + }; - var cts = new CancellationTokenSource(); - cts.Cancel(); - - await Assert.ThrowsAsync(async () => - { - await client.InnerClient.DeleteStateAsync("testStore", "key", cancellationToken: cts.Token); - }); - } - - [Fact] - public async Task GetStateEntryAsync_CanReadState() + var request = await client.CaptureGrpcRequestAsync(async daprClient => { - await using var client = TestClient.CreateForDaprClient(); + await daprClient.ExecuteStateTransactionAsync("testStore", states); + }); - var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetStateEntryAsync("testStore", "test")); - - // Create Response & Respond - var data = new Widget() { Size = "small", Color = "yellow", }; - var envelope = MakeGetStateResponse(data); - var state = await request.CompleteWithMessageAsync(envelope); - - // Get response and validate - state.Value.Size.ShouldBe("small"); - state.Value.Color.ShouldBe("yellow"); - } - - [Fact] - public async Task GetStateEntryAsync_CanReadEmptyState_ReturnsDefault() + // Create Response & Respond + var ex = await Assert.ThrowsAsync(async () => { - await using var client = TestClient.CreateForDaprClient(); + await request.CompleteAsync(new HttpResponseMessage(HttpStatusCode.NotAcceptable)); + }); + Assert.IsType(ex.InnerException); + } - var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetStateEntryAsync("testStore", "test")); + [Fact] + public async Task ExecuteStateTransactionAsync_WithCancelledToken() + { + await using var client = TestClient.CreateForDaprClient(); - // Create Response & Respond - var envelope = MakeGetStateResponse(null); - var state = await request.CompleteWithMessageAsync(envelope); - - state.Key.ShouldBe("test"); - state.Value.ShouldBeNull(); - } - - [Fact] - public async Task GetStateEntryAsync_CanSaveState() + var operation = new StateTransactionRequest("test", null, StateOperationType.Delete); + var operations = new List { - await using var client = TestClient.CreateForDaprClient(); + operation, + }; - var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetStateEntryAsync("testStore", "test")); + var cts = new CancellationTokenSource(); + cts.Cancel(); - // Create Response & Respond - var data = new Widget() { Size = "small", Color = "yellow", }; - var state = await request.CompleteWithMessageAsync(MakeGetStateResponse(data)); - - state.Key.ShouldBe("test"); - state.Value.Size.ShouldBe("small"); - state.Value.Color.ShouldBe("yellow"); - - // Modify the state and save it - state.Value.Color = "green"; - - var request2 = await client.CaptureGrpcRequestAsync(async _ => - { - await state.SaveAsync(); - }); - - request2.Dismiss(); - - // Get Request and validate - var envelope = await request2.GetRequestEnvelopeAsync(); - - envelope.StoreName.ShouldBe("testStore"); - envelope.States.Count.ShouldBe(1); - var requestState = envelope.States[0]; - requestState.Key.ShouldBe("test"); - - var stateJson = requestState.Value.ToStringUtf8(); - var stateFromRequest = JsonSerializer.Deserialize(stateJson, client.InnerClient.JsonSerializerOptions); - stateFromRequest.Size.ShouldBe("small"); - stateFromRequest.Color.ShouldBe("green"); - } - - [Fact] - public async Task GetStateEntryAsync_CanDeleteState() + await Assert.ThrowsAsync(async () => { - await using var client = TestClient.CreateForDaprClient(); + await client.InnerClient.ExecuteStateTransactionAsync("testStore", operations, new Dictionary(), cancellationToken: cts.Token); + }); + } - var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetStateEntryAsync("testStore", "test")); + [Fact] + public async Task DeleteStateAsync_CanDeleteState() + { + await using var client = TestClient.CreateForDaprClient(); - // Create Response & Respond - var data = new Widget() { Size = "small", Color = "yellow", }; - var state = await request.CompleteWithMessageAsync(MakeGetStateResponse(data)); - - state.Key.ShouldBe("test"); - state.Value.Size.ShouldBe("small"); - state.Value.Color.ShouldBe("yellow"); - - state.Value.Color = "green"; - var request2 = await client.CaptureGrpcRequestAsync(async daprClient => - { - await state.DeleteAsync(); - }); - - request2.Dismiss(); - - // Get Request and validate - var envelope = await request.GetRequestEnvelopeAsync(); - envelope.StoreName.ShouldBe("testStore"); - envelope.Key.ShouldBe("test"); - - } - - [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 SaveStateAsync_ValidateOptions( - ConsistencyMode consistencyMode, - ConcurrencyMode concurrencyMode, - StateConsistency expectedConsistency, - StateConcurrency expectedConcurrency) + var request = await client.CaptureGrpcRequestAsync(async daprClient => { - await using var client = TestClient.CreateForDaprClient(); + await daprClient.DeleteStateAsync("testStore", "test"); + }); - var widget = new Widget() { Size = "small", Color = "yellow", }; - var stateOptions = new StateOptions - { - Concurrency = concurrencyMode, - Consistency = consistencyMode - }; + request.Dismiss(); - var metadata = new Dictionary - { - { "key1", "value1" }, - { "key2", "value2" } - }; + var envelope = await request.GetRequestEnvelopeAsync(); + envelope.StoreName.ShouldBe("testStore"); + envelope.Key.ShouldBe("test"); + } - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - await daprClient.SaveStateAsync("testStore", "test", widget, stateOptions, metadata); - }); + [Fact] + public async Task DeleteStateAsync_ThrowsForNonSuccess() + { + await using var client = TestClient.CreateForDaprClient(); - request.Dismiss(); - - // Get Request and validate - var envelope = await request.GetRequestEnvelopeAsync(); - envelope.StoreName.ShouldBe("testStore"); - envelope.States.Count.ShouldBe(1); - var state = envelope.States[0]; - state.Key.ShouldBe("test"); - state.Metadata.Count.ShouldBe(2); - state.Metadata.Keys.Contains("key1").ShouldBeTrue(); - state.Metadata.Keys.Contains("key2").ShouldBeTrue(); - state.Metadata["key1"].ShouldBe("value1"); - state.Metadata["key2"].ShouldBe("value2"); - state.Options.Concurrency.ShouldBe(expectedConcurrency); - state.Options.Consistency.ShouldBe(expectedConsistency); - - var stateJson = state.Value.ToStringUtf8(); - var stateFromRequest = JsonSerializer.Deserialize(stateJson, client.InnerClient.JsonSerializerOptions); - stateFromRequest.Size.ShouldBe(widget.Size); - stateFromRequest.Color.ShouldBe(widget.Color); - } - - [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 TrySaveStateAsync_ValidateOptions( - ConsistencyMode consistencyMode, - ConcurrencyMode concurrencyMode, - StateConsistency expectedConsistency, - StateConcurrency expectedConcurrency) + var request = await client.CaptureGrpcRequestAsync(async daprClient => { - await using var client = TestClient.CreateForDaprClient(); + await daprClient.DeleteStateAsync("testStore", "test"); + }); - var widget = new Widget() { Size = "small", Color = "yellow", }; - 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.TrySaveStateAsync("testStore", "test", widget, "Test_Etag", stateOptions, metadata)); - - request.Dismiss(); - - // Get Request and validate - var envelope = await request.GetRequestEnvelopeAsync(); - envelope.StoreName.ShouldBe("testStore"); - envelope.States.Count.ShouldBe(1); - var state = envelope.States[0]; - state.Etag.Value.ShouldBe("Test_Etag"); - state.Metadata.Count.ShouldBe(2); - state.Metadata.Keys.Contains("key1").ShouldBeTrue(); - state.Metadata.Keys.Contains("key2").ShouldBeTrue(); - state.Metadata["key1"].ShouldBe("value1"); - state.Metadata["key2"].ShouldBe("value2"); - state.Options.Concurrency.ShouldBe(expectedConcurrency); - state.Options.Consistency.ShouldBe(expectedConsistency); - - var stateJson = state.Value.ToStringUtf8(); - var stateFromRequest = JsonSerializer.Deserialize(stateJson, client.InnerClient.JsonSerializerOptions); - stateFromRequest.Size.ShouldBe(widget.Size); - stateFromRequest.Color.ShouldBe(widget.Color); - } - - [Fact] - public async Task TrySaveStateAsync_ValidateNonETagErrorThrowsException() + // Create Response & Respond + var ex = await Assert.ThrowsAsync(async () => { - var client = new MockClient(); + await request.CompleteAsync(new HttpResponseMessage(HttpStatusCode.NotAcceptable)); + }); + Assert.IsType(ex.InnerException); + } - await client.CallStateApi().Build(); + [Fact] + public async Task DeleteStateAsync_WithCancelledToken() + { + await using var client = TestClient.CreateForDaprClient(); - var rpcException = new RpcException(new Status(StatusCode.Internal, "Network Error")); + var cts = new CancellationTokenSource(); + cts.Cancel(); - // 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.TrySaveStateAsync("test", "test", "testValue", "someETag"); - }); - Assert.Same(rpcException, ex.InnerException); - } - - [Fact] - public async Task TrySaveStateAsync_ValidateETagRelatedExceptionReturnsFalse() + await Assert.ThrowsAsync(async () => { - var client = new MockClient(); + await client.InnerClient.DeleteStateAsync("testStore", "key", cancellationToken: cts.Token); + }); + } - await client.CallStateApi() - .Build(); + [Fact] + public async Task GetStateEntryAsync_CanReadState() + { + await using var client = TestClient.CreateForDaprClient(); - 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 request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetStateEntryAsync("testStore", "test")); - var operationResult = await client.DaprClient.TrySaveStateAsync("testStore", "test", "testValue", "invalidETag"); - Assert.False(operationResult); - } + // Create Response & Respond + var data = new Widget() { Size = "small", Color = "yellow", }; + var envelope = MakeGetStateResponse(data); + var state = await request.CompleteWithMessageAsync(envelope); - [Fact] - public async Task TrySaveStateAsync_NullEtagThrowsArgumentException() + // Get response and validate + state.Value.Size.ShouldBe("small"); + state.Value.Color.ShouldBe("yellow"); + } + + [Fact] + public async Task GetStateEntryAsync_CanReadEmptyState_ReturnsDefault() + { + await using var client = TestClient.CreateForDaprClient(); + + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetStateEntryAsync("testStore", "test")); + + // Create Response & Respond + var envelope = MakeGetStateResponse(null); + var state = await request.CompleteWithMessageAsync(envelope); + + state.Key.ShouldBe("test"); + state.Value.ShouldBeNull(); + } + + [Fact] + public async Task GetStateEntryAsync_CanSaveState() + { + await using var client = TestClient.CreateForDaprClient(); + + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetStateEntryAsync("testStore", "test")); + + // Create Response & Respond + var data = new Widget() { Size = "small", Color = "yellow", }; + var state = await request.CompleteWithMessageAsync(MakeGetStateResponse(data)); + + state.Key.ShouldBe("test"); + state.Value.Size.ShouldBe("small"); + state.Value.Color.ShouldBe("yellow"); + + // Modify the state and save it + state.Value.Color = "green"; + + var request2 = await client.CaptureGrpcRequestAsync(async _ => { - var client = new MockClient(); + await state.SaveAsync(); + }); - await client.CallStateApi() - .Build(); + request2.Dismiss(); - await Should.ThrowAsync(async () => await client.DaprClient.TrySaveStateAsync("test", "test", "testValue", null)); - } + // Get Request and validate + var envelope = await request2.GetRequestEnvelopeAsync(); - [Fact] - public async Task TrySaveStateAsync_EmptyEtagDoesNotThrow() + envelope.StoreName.ShouldBe("testStore"); + envelope.States.Count.ShouldBe(1); + var requestState = envelope.States[0]; + requestState.Key.ShouldBe("test"); + + var stateJson = requestState.Value.ToStringUtf8(); + var stateFromRequest = JsonSerializer.Deserialize(stateJson, client.InnerClient.JsonSerializerOptions); + stateFromRequest.Size.ShouldBe("small"); + stateFromRequest.Color.ShouldBe("green"); + } + + [Fact] + public async Task GetStateEntryAsync_CanDeleteState() + { + await using var client = TestClient.CreateForDaprClient(); + + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetStateEntryAsync("testStore", "test")); + + // Create Response & Respond + var data = new Widget() { Size = "small", Color = "yellow", }; + var state = await request.CompleteWithMessageAsync(MakeGetStateResponse(data)); + + state.Key.ShouldBe("test"); + state.Value.Size.ShouldBe("small"); + state.Value.Color.ShouldBe("yellow"); + + state.Value.Color = "green"; + var request2 = await client.CaptureGrpcRequestAsync(async daprClient => { - var client = new MockClient(); - var response = client.CallStateApi() + await state.DeleteAsync(); + }); + + request2.Dismiss(); + + // Get Request and validate + var envelope = await request.GetRequestEnvelopeAsync(); + envelope.StoreName.ShouldBe("testStore"); + envelope.Key.ShouldBe("test"); + + } + + [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 SaveStateAsync_ValidateOptions( + ConsistencyMode consistencyMode, + ConcurrencyMode concurrencyMode, + StateConsistency expectedConsistency, + StateConcurrency expectedConcurrency) + { + await using var client = TestClient.CreateForDaprClient(); + + var widget = new Widget() { Size = "small", Color = "yellow", }; + 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.SaveStateAsync("testStore", "test", widget, stateOptions, metadata); + }); + + request.Dismiss(); + + // Get Request and validate + var envelope = await request.GetRequestEnvelopeAsync(); + envelope.StoreName.ShouldBe("testStore"); + envelope.States.Count.ShouldBe(1); + var state = envelope.States[0]; + state.Key.ShouldBe("test"); + state.Metadata.Count.ShouldBe(2); + state.Metadata.Keys.Contains("key1").ShouldBeTrue(); + state.Metadata.Keys.Contains("key2").ShouldBeTrue(); + state.Metadata["key1"].ShouldBe("value1"); + state.Metadata["key2"].ShouldBe("value2"); + state.Options.Concurrency.ShouldBe(expectedConcurrency); + state.Options.Consistency.ShouldBe(expectedConsistency); + + var stateJson = state.Value.ToStringUtf8(); + var stateFromRequest = JsonSerializer.Deserialize(stateJson, client.InnerClient.JsonSerializerOptions); + stateFromRequest.Size.ShouldBe(widget.Size); + stateFromRequest.Color.ShouldBe(widget.Color); + } + + [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 TrySaveStateAsync_ValidateOptions( + ConsistencyMode consistencyMode, + ConcurrencyMode concurrencyMode, + StateConsistency expectedConsistency, + StateConcurrency expectedConcurrency) + { + await using var client = TestClient.CreateForDaprClient(); + + var widget = new Widget() { Size = "small", Color = "yellow", }; + 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.TrySaveStateAsync("testStore", "test", widget, "Test_Etag", stateOptions, metadata)); + + request.Dismiss(); + + // Get Request and validate + var envelope = await request.GetRequestEnvelopeAsync(); + envelope.StoreName.ShouldBe("testStore"); + envelope.States.Count.ShouldBe(1); + var state = envelope.States[0]; + state.Etag.Value.ShouldBe("Test_Etag"); + state.Metadata.Count.ShouldBe(2); + state.Metadata.Keys.Contains("key1").ShouldBeTrue(); + state.Metadata.Keys.Contains("key2").ShouldBeTrue(); + state.Metadata["key1"].ShouldBe("value1"); + state.Metadata["key2"].ShouldBe("value2"); + state.Options.Concurrency.ShouldBe(expectedConcurrency); + state.Options.Consistency.ShouldBe(expectedConsistency); + + var stateJson = state.Value.ToStringUtf8(); + var stateFromRequest = JsonSerializer.Deserialize(stateJson, client.InnerClient.JsonSerializerOptions); + stateFromRequest.Size.ShouldBe(widget.Size); + stateFromRequest.Color.ShouldBe(widget.Color); + } + + [Fact] + public async Task TrySaveStateAsync_ValidateNonETagErrorThrowsException() + { + var client = new MockClient(); + + await client.CallStateApi().Build(); + + 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.TrySaveStateAsync("test", "test", "testValue", "someETag"); + }); + Assert.Same(rpcException, ex.InnerException); + } + + [Fact] + public async Task TrySaveStateAsync_ValidateETagRelatedExceptionReturnsFalse() + { + var client = new MockClient(); + + await client.CallStateApi() .Build(); - // Setup the mock client to return success - client.Mock - .Setup(m => m.SaveStateAsync(It.IsAny(), It.IsAny())) - .Returns(response); + 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 result = await client.DaprClient.TrySaveStateAsync("test", "test", "testValue", ""); - Assert.True(result); - } + var operationResult = await client.DaprClient.TrySaveStateAsync("testStore", "test", "testValue", "invalidETag"); + Assert.False(operationResult); + } - [Fact] - public async Task TryDeleteStateAsync_ValidateNonETagErrorThrowsException() - { - var client = new MockClient(); + [Fact] + public async Task TrySaveStateAsync_NullEtagThrowsArgumentException() + { + var client = new MockClient(); - await client.CallStateApi() - .Build(); - - 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.DeleteStateAsync(It.IsAny(), It.IsAny())) - .Throws(rpcException); - - var ex = await Assert.ThrowsAsync(async () => - { - await client.DaprClient.TryDeleteStateAsync("test", "test", "badEtag"); - }); - Assert.Same(rpcException, ex.InnerException); - } - - [Fact] - public async Task TryDeleteStateAsync_NullEtagThrowsArgumentException() - { - var client = new MockClient(); - - await client.CallStateApi() - .Build(); - - await Should.ThrowAsync(async () => await client.DaprClient.TryDeleteStateAsync("test", "test", null)); - } - - [Fact] - public async Task TryDeleteStateAsync_EmptyEtagDoesNotThrow() - { - var client = new MockClient(); - var response = client.CallStateApi() + await client.CallStateApi() .Build(); - // Setup the mock client to return success - client.Mock - .Setup(m => m.DeleteStateAsync(It.IsAny(), It.IsAny())) - .Returns(response); + await Should.ThrowAsync(async () => await client.DaprClient.TrySaveStateAsync("test", "test", "testValue", null)); + } - var result = await client.DaprClient.TryDeleteStateAsync("test", "test", ""); - Assert.True(result); + [Fact] + public async Task TrySaveStateAsync_EmptyEtagDoesNotThrow() + { + var client = new MockClient(); + 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.TrySaveStateAsync("test", "test", "testValue", ""); + Assert.True(result); + } + + [Fact] + public async Task TryDeleteStateAsync_ValidateNonETagErrorThrowsException() + { + var client = new MockClient(); + + await client.CallStateApi() + .Build(); + + 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.DeleteStateAsync(It.IsAny(), It.IsAny())) + .Throws(rpcException); + + var ex = await Assert.ThrowsAsync(async () => + { + await client.DaprClient.TryDeleteStateAsync("test", "test", "badEtag"); + }); + Assert.Same(rpcException, ex.InnerException); + } + + [Fact] + public async Task TryDeleteStateAsync_NullEtagThrowsArgumentException() + { + var client = new MockClient(); + + await client.CallStateApi() + .Build(); + + await Should.ThrowAsync(async () => await client.DaprClient.TryDeleteStateAsync("test", "test", null)); + } + + [Fact] + public async Task TryDeleteStateAsync_EmptyEtagDoesNotThrow() + { + var client = new MockClient(); + var response = client.CallStateApi() + .Build(); + + // Setup the mock client to return success + client.Mock + .Setup(m => m.DeleteStateAsync(It.IsAny(), It.IsAny())) + .Returns(response); + + var result = await client.DaprClient.TryDeleteStateAsync("test", "test", ""); + Assert.True(result); + } + + [Fact] + public async Task TryDeleteStateAsync_ValidateETagRelatedExceptionReturnsFalse() + { + var client = new MockClient(); + + await client.CallStateApi() + .Build(); + + var rpcException = new RpcException(new Status(StatusCode.Aborted, $"failed deleting state with key test")); + // Setup the mock client to throw an Rpc Exception with the expected details info + client.Mock + .Setup(m => m.DeleteStateAsync(It.IsAny(), It.IsAny())) + .Throws(rpcException); + + var operationResult = await client.DaprClient.TryDeleteStateAsync("test", "test", "invalidETag"); + Assert.False(operationResult); + } + + [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 DeleteStateAsync_ValidateOptions( + ConsistencyMode consistencyMode, + ConcurrencyMode concurrencyMode, + StateConsistency expectedConsistency, + StateConcurrency expectedConcurrency) + { + await using var client = TestClient.CreateForDaprClient(); + + var stateOptions = new StateOptions + { + Concurrency = concurrencyMode, + Consistency = consistencyMode + }; + + var request = await client.CaptureGrpcRequestAsync(async daprClient => + { + await daprClient.DeleteStateAsync("testStore", "test", stateOptions); + }); + + request.Dismiss(); + + // Get Request and validate + var envelope = await request.GetRequestEnvelopeAsync(); + envelope.StoreName.ShouldBe("testStore"); + envelope.Key.ShouldBe("test"); + envelope.Options.Concurrency.ShouldBe(expectedConcurrency); + envelope.Options.Consistency.ShouldBe(expectedConsistency); + + } + + [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 TryDeleteStateAsync_ValidateOptions( + ConsistencyMode consistencyMode, + ConcurrencyMode concurrencyMode, + StateConsistency expectedConsistency, + StateConcurrency expectedConcurrency) + { + await using var client = TestClient.CreateForDaprClient(); + + var stateOptions = new StateOptions + { + Concurrency = concurrencyMode, + Consistency = consistencyMode + }; + + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.TryDeleteStateAsync("testStore", "test", "Test_Etag", stateOptions)); + + request.Dismiss(); + + // Get Request and validate + var envelope = await request.GetRequestEnvelopeAsync(); + envelope.StoreName.ShouldBe("testStore"); + envelope.Key.ShouldBe("test"); + envelope.Etag.Value.ShouldBe("Test_Etag"); + envelope.Options.Concurrency.ShouldBe(expectedConcurrency); + envelope.Options.Consistency.ShouldBe(expectedConsistency); + } + + [Fact] + public async Task DeleteBulkStateAsync_ValidateRequest() + { + await using var client = TestClient.CreateForDaprClient(); + + const string key = "test"; + const string etag = "etag"; + var metadata = new Dictionary + { + { "partitionKey", "mypartition" } + }; + var deleteBulkStateItem = new BulkDeleteStateItem(key, etag, null, metadata); + var request = await client.CaptureGrpcRequestAsync(async daprClient => + { + await daprClient.DeleteBulkStateAsync("testStore", new List() { deleteBulkStateItem }); + }); + + request.Dismiss(); + + // Create Response & Validate + var envelope = await request.GetRequestEnvelopeAsync(); + envelope.StoreName.ShouldBe("testStore"); + envelope.States.Count.ShouldBe(1); + envelope.States[0].Key.ShouldBe(key); + envelope.States[0].Metadata.ShouldContainKey("partitionKey"); + } + + [Fact] + public async Task QueryStateAsync_ValidateResult() + { + await using var client = TestClient.CreateForDaprClient(); + + 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(); + envelope.StoreName.ShouldBe("testStore"); + envelope.Query.ShouldBe(queryJson); + envelope.Metadata.ShouldBeEmpty(); + + // Validate response. + var testData = new Widget() { Color = "Green", Size = "Small" }; + var wireResponse = new Autogenerated.QueryStateResponse(); + wireResponse.Results.Add(MakeQueryStateItem("test", testData, "an etag")); + + var response = await request.CompleteWithMessageAsync(wireResponse); + response.Results.Count.ShouldBe(1); + response.Results[0].Key.ShouldBe("test"); + response.Results[0].Data.ShouldBe(testData); + response.Results[0].ETag.ShouldBe("an etag"); + response.Results[0].Error.ShouldBeNullOrEmpty(); + } + + [Fact] + public async Task QueryStateAsync_EncountersError_ValidatePartialResult() + { + await using var client = TestClient.CreateForDaprClient(); + + 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(); + envelope.StoreName.ShouldBe("testStore"); + envelope.Query.ShouldBe(queryJson); + envelope.Metadata.ShouldBeEmpty(); + + // Validate response, we expect to only get the first object as the 2nd will present an error. + var testData1 = new Widget() { Color = "Green", Size = "Small" }; + var testData2 = new Widget() { Color = "Green", Size = "Medium" }; + var testData3 = new Widget() { Color = "Green", Size = "Large" }; + var wireResponse = new Autogenerated.QueryStateResponse(); + + wireResponse.Results.Add(MakeQueryStateItem("test1", testData1)); + wireResponse.Results.Add(MakeQueryStateItem("test2", testData2, string.Empty, "An error!")); + wireResponse.Results.Add(MakeQueryStateItem("test3", testData3)); + + var ex = await Assert.ThrowsAsync>(() => request.CompleteWithMessageAsync(wireResponse)); + ex.Message.ShouldBe("Encountered an error while processing state query results."); + var response = ex.Response; + response.Results.Count.ShouldBe(2); + response.Results[0].Key.ShouldBe("test1"); + response.Results[0].Data.ShouldBe(testData1); + response.Results[0].ETag.ShouldBeNullOrEmpty(); + response.Results[0].Error.ShouldBeNullOrEmpty(); + response.Results[1].Key.ShouldBe("test3"); + response.Results[1].Data.ShouldBe(testData3); + response.Results[1].ETag.ShouldBeNullOrEmpty(); + response.Results[1].Error.ShouldBeNullOrEmpty(); + + var failedKeys = ex.FailedKeys; + failedKeys.Count.ShouldBe(1); + failedKeys[0].ShouldBe("test2"); + } + + private Autogenerated.GetStateResponse MakeGetStateResponse(T state, string etag = null) + { + var data = TypeConverters.ToJsonByteString(state, new JsonSerializerOptions(JsonSerializerDefaults.Web)); + var response = new Autogenerated.GetStateResponse + { + Data = data + }; + + if (etag != null) + { + response.Etag = etag; } - [Fact] - public async Task TryDeleteStateAsync_ValidateETagRelatedExceptionReturnsFalse() + return response; + } + + private Autogenerated.GetBulkStateResponse MakeGetBulkStateResponse(string key, T state) + { + var data = TypeConverters.ToJsonByteString(state, new JsonSerializerOptions(JsonSerializerDefaults.Web)); + var response = new Autogenerated.GetBulkStateResponse { - var client = new MockClient(); - - await client.CallStateApi() - .Build(); - - var rpcException = new RpcException(new Status(StatusCode.Aborted, $"failed deleting state with key test")); - // Setup the mock client to throw an Rpc Exception with the expected details info - client.Mock - .Setup(m => m.DeleteStateAsync(It.IsAny(), It.IsAny())) - .Throws(rpcException); - - var operationResult = await client.DaprClient.TryDeleteStateAsync("test", "test", "invalidETag"); - Assert.False(operationResult); - } - - [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 DeleteStateAsync_ValidateOptions( - ConsistencyMode consistencyMode, - ConcurrencyMode concurrencyMode, - StateConsistency expectedConsistency, - StateConcurrency expectedConcurrency) - { - await using var client = TestClient.CreateForDaprClient(); - - var stateOptions = new StateOptions + Items = { - Concurrency = concurrencyMode, - Consistency = consistencyMode - }; - - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - await daprClient.DeleteStateAsync("testStore", "test", stateOptions); - }); - - request.Dismiss(); - - // Get Request and validate - var envelope = await request.GetRequestEnvelopeAsync(); - envelope.StoreName.ShouldBe("testStore"); - envelope.Key.ShouldBe("test"); - envelope.Options.Concurrency.ShouldBe(expectedConcurrency); - envelope.Options.Consistency.ShouldBe(expectedConsistency); - - } - - [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 TryDeleteStateAsync_ValidateOptions( - ConsistencyMode consistencyMode, - ConcurrencyMode concurrencyMode, - StateConsistency expectedConsistency, - StateConcurrency expectedConcurrency) - { - await using var client = TestClient.CreateForDaprClient(); - - var stateOptions = new StateOptions - { - Concurrency = concurrencyMode, - Consistency = consistencyMode - }; - - var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.TryDeleteStateAsync("testStore", "test", "Test_Etag", stateOptions)); - - request.Dismiss(); - - // Get Request and validate - var envelope = await request.GetRequestEnvelopeAsync(); - envelope.StoreName.ShouldBe("testStore"); - envelope.Key.ShouldBe("test"); - envelope.Etag.Value.ShouldBe("Test_Etag"); - envelope.Options.Concurrency.ShouldBe(expectedConcurrency); - envelope.Options.Consistency.ShouldBe(expectedConsistency); - } - - [Fact] - public async Task DeleteBulkStateAsync_ValidateRequest() - { - await using var client = TestClient.CreateForDaprClient(); - - const string key = "test"; - const string etag = "etag"; - var metadata = new Dictionary - { - { "partitionKey", "mypartition" } - }; - var deleteBulkStateItem = new BulkDeleteStateItem(key, etag, null, metadata); - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - await daprClient.DeleteBulkStateAsync("testStore", new List() { deleteBulkStateItem }); - }); - - request.Dismiss(); - - // Create Response & Validate - var envelope = await request.GetRequestEnvelopeAsync(); - envelope.StoreName.ShouldBe("testStore"); - envelope.States.Count.ShouldBe(1); - envelope.States[0].Key.ShouldBe(key); - envelope.States[0].Metadata.ShouldContainKey("partitionKey"); - } - - [Fact] - public async Task QueryStateAsync_ValidateResult() - { - await using var client = TestClient.CreateForDaprClient(); - - 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(); - envelope.StoreName.ShouldBe("testStore"); - envelope.Query.ShouldBe(queryJson); - envelope.Metadata.ShouldBeEmpty(); - - // Validate response. - var testData = new Widget() { Color = "Green", Size = "Small" }; - var wireResponse = new Autogenerated.QueryStateResponse(); - wireResponse.Results.Add(MakeQueryStateItem("test", testData, "an etag")); - - var response = await request.CompleteWithMessageAsync(wireResponse); - response.Results.Count.ShouldBe(1); - response.Results[0].Key.ShouldBe("test"); - response.Results[0].Data.ShouldBe(testData); - response.Results[0].ETag.ShouldBe("an etag"); - response.Results[0].Error.ShouldBeNullOrEmpty(); - } - - [Fact] - public async Task QueryStateAsync_EncountersError_ValidatePartialResult() - { - await using var client = TestClient.CreateForDaprClient(); - - 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(); - envelope.StoreName.ShouldBe("testStore"); - envelope.Query.ShouldBe(queryJson); - envelope.Metadata.ShouldBeEmpty(); - - // Validate response, we expect to only get the first object as the 2nd will present an error. - var testData1 = new Widget() { Color = "Green", Size = "Small" }; - var testData2 = new Widget() { Color = "Green", Size = "Medium" }; - var testData3 = new Widget() { Color = "Green", Size = "Large" }; - var wireResponse = new Autogenerated.QueryStateResponse(); - - wireResponse.Results.Add(MakeQueryStateItem("test1", testData1)); - wireResponse.Results.Add(MakeQueryStateItem("test2", testData2, string.Empty, "An error!")); - wireResponse.Results.Add(MakeQueryStateItem("test3", testData3)); - - var ex = await Assert.ThrowsAsync>(() => request.CompleteWithMessageAsync(wireResponse)); - ex.Message.ShouldBe("Encountered an error while processing state query results."); - var response = ex.Response; - response.Results.Count.ShouldBe(2); - response.Results[0].Key.ShouldBe("test1"); - response.Results[0].Data.ShouldBe(testData1); - response.Results[0].ETag.ShouldBeNullOrEmpty(); - response.Results[0].Error.ShouldBeNullOrEmpty(); - response.Results[1].Key.ShouldBe("test3"); - response.Results[1].Data.ShouldBe(testData3); - response.Results[1].ETag.ShouldBeNullOrEmpty(); - response.Results[1].Error.ShouldBeNullOrEmpty(); - - var failedKeys = ex.FailedKeys; - failedKeys.Count.ShouldBe(1); - failedKeys[0].ShouldBe("test2"); - } - - private Autogenerated.GetStateResponse MakeGetStateResponse(T state, string etag = null) - { - var data = TypeConverters.ToJsonByteString(state, new JsonSerializerOptions(JsonSerializerDefaults.Web)); - var response = new Autogenerated.GetStateResponse - { - Data = data - }; - - if (etag != null) - { - response.Etag = etag; - } - - return response; - } - - private Autogenerated.GetBulkStateResponse MakeGetBulkStateResponse(string key, T state) - { - var data = TypeConverters.ToJsonByteString(state, new JsonSerializerOptions(JsonSerializerDefaults.Web)); - var response = new Autogenerated.GetBulkStateResponse - { - Items = + new Autogenerated.BulkStateItem() { - new Autogenerated.BulkStateItem() - { - Key = key, - Data = data, - } + Key = key, + Data = data, } - }; + } + }; - return response; - } + return response; + } - private Autogenerated.QueryStateItem MakeQueryStateItem(string key, T data, string etag = default, string error = default) + private Autogenerated.QueryStateItem MakeQueryStateItem(string key, T data, string etag = default, string error = default) + { + var wireItem = new Autogenerated.QueryStateItem { - 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) + 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 { - await using var client = TestClient.CreateForDaprClient(); + Concurrency = concurrencyMode, + Consistency = consistencyMode + }; - 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.ShouldBe("testStore"); - envelope.States.Count.ShouldBe(1); - var state = envelope.States[0]; - state.Key.ShouldBe("test"); - state.Metadata.Count.ShouldBe(2); - state.Metadata.Keys.Contains("key1").ShouldBeTrue(); - state.Metadata.Keys.Contains("key2").ShouldBeTrue(); - state.Metadata["key1"].ShouldBe("value1"); - state.Metadata["key2"].ShouldBe("value2"); - state.Options.Concurrency.ShouldBe(expectedConcurrency); - state.Options.Consistency.ShouldBe(expectedConsistency); - - var stateBinaryData = state.Value.ToStringUtf8(); - stateBinaryData.ShouldBe(data); - } - - [Fact] - public async Task SaveByteStateAsync_CanSaveState() + var metadata = new Dictionary { - await using var client = TestClient.CreateForDaprClient(); + { "key1", "value1" }, + { "key2", "value2" } + }; - 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.ShouldBe("testStore"); - envelope.States.Count.ShouldBe(1); - var state = envelope.States[0]; - state.Key.ShouldBe("test"); - - var stateBinaryData = state.Value.ToStringUtf8(); - stateBinaryData.ShouldBe(data); - } - - [Fact] - public async Task SaveByteStateAsync_CanClearState() + var request = await client.CaptureGrpcRequestAsync(async daprClient => { - await using var client = TestClient.CreateForDaprClient(); + await daprClient.SaveByteStateAsync("testStore", "test", stateBytes.AsMemory(), stateOptions, metadata); + }); - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - await daprClient.SaveByteStateAsync("testStore", "test", null); - }); + request.Dismiss(); - request.Dismiss(); + // Get Request and validate + var envelope = await request.GetRequestEnvelopeAsync(); + envelope.StoreName.ShouldBe("testStore"); + envelope.States.Count.ShouldBe(1); + var state = envelope.States[0]; + state.Key.ShouldBe("test"); + state.Metadata.Count.ShouldBe(2); + state.Metadata.Keys.Contains("key1").ShouldBeTrue(); + state.Metadata.Keys.Contains("key2").ShouldBeTrue(); + state.Metadata["key1"].ShouldBe("value1"); + state.Metadata["key2"].ShouldBe("value2"); + state.Options.Concurrency.ShouldBe(expectedConcurrency); + state.Options.Consistency.ShouldBe(expectedConsistency); - // Get Request and validate - var envelope = await request.GetRequestEnvelopeAsync(); + var stateBinaryData = state.Value.ToStringUtf8(); + stateBinaryData.ShouldBe(data); + } - envelope.StoreName.ShouldBe("testStore"); - envelope.States.Count.ShouldBe(1); - var state = envelope.States[0]; - state.Key.ShouldBe("test"); - state.Value.ShouldBe(ByteString.Empty); - } + [Fact] + public async Task SaveByteStateAsync_CanSaveState() + { + await using var client = TestClient.CreateForDaprClient(); - [Fact] - public async Task SaveByteStateAsync_WithCancelledToken() + const string data = "Test binary data"; + var stateBytes = Encoding.UTF8.GetBytes(data); + var request = await client.CaptureGrpcRequestAsync(async daprClient => { - await using var client = TestClient.CreateForDaprClient(); + await daprClient.SaveByteStateAsync("testStore", "test", stateBytes.AsMemory()); + }); - var cts = new CancellationTokenSource(); - cts.Cancel(); + request.Dismiss(); - await Assert.ThrowsAsync(async () => - { - await client.InnerClient.SaveByteStateAsync("testStore", "test", null, cancellationToken: cts.Token); - }); - } + // Get Request and validate + var envelope = await request.GetRequestEnvelopeAsync(); + envelope.StoreName.ShouldBe("testStore"); + envelope.States.Count.ShouldBe(1); + var state = envelope.States[0]; + state.Key.ShouldBe("test"); - [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) + var stateBinaryData = state.Value.ToStringUtf8(); + stateBinaryData.ShouldBe(data); + } + + [Fact] + public async Task SaveByteStateAsync_CanClearState() + { + await using var client = TestClient.CreateForDaprClient(); + + var request = await client.CaptureGrpcRequestAsync(async daprClient => { - 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 - }; + await daprClient.SaveByteStateAsync("testStore", "test", null); + }); - 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(); - request.Dismiss(); + // Get Request and validate + var envelope = await request.GetRequestEnvelopeAsync(); - // Get Request and validate - var envelope = await request.GetRequestEnvelopeAsync(); - envelope.StoreName.ShouldBe("testStore"); - envelope.States.Count.ShouldBe(1); - var state = envelope.States[0]; - state.Etag.Value.ShouldBe("Test_Etag"); - state.Metadata.Count.ShouldBe(2); - state.Metadata.Keys.Contains("key1").ShouldBeTrue(); - state.Metadata.Keys.Contains("key2").ShouldBeTrue(); - state.Metadata["key1"].ShouldBe("value1"); - state.Metadata["key2"].ShouldBe("value2"); - state.Options.Concurrency.ShouldBe(expectedConcurrency); - state.Options.Consistency.ShouldBe(expectedConsistency); + envelope.StoreName.ShouldBe("testStore"); + envelope.States.Count.ShouldBe(1); + var state = envelope.States[0]; + state.Key.ShouldBe("test"); + state.Value.ShouldBe(ByteString.Empty); + } - var stateBinaryData = state.Value.ToStringUtf8(); - stateBinaryData.ShouldBe(data); - } + [Fact] + public async Task SaveByteStateAsync_WithCancelledToken() + { + await using var client = TestClient.CreateForDaprClient(); - [Fact] - public async Task TrySaveByteStateAsync_ValidateNonETagErrorThrowsException() + var cts = new CancellationTokenSource(); + cts.Cancel(); + + await Assert.ThrowsAsync(async () => { - var client = new MockClient(); + await client.InnerClient.SaveByteStateAsync("testStore", "test", null, cancellationToken: cts.Token); + }); + } - var response = client.CallStateApi() + [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.ShouldBe("testStore"); + envelope.States.Count.ShouldBe(1); + var state = envelope.States[0]; + state.Etag.Value.ShouldBe("Test_Etag"); + state.Metadata.Count.ShouldBe(2); + state.Metadata.Keys.Contains("key1").ShouldBeTrue(); + state.Metadata.Keys.Contains("key2").ShouldBeTrue(); + state.Metadata["key1"].ShouldBe("value1"); + state.Metadata["key2"].ShouldBe("value2"); + state.Options.Concurrency.ShouldBe(expectedConcurrency); + state.Options.Consistency.ShouldBe(expectedConsistency); + + var stateBinaryData = state.Value.ToStringUtf8(); + stateBinaryData.ShouldBe(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")); + 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); + // 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 ex = await Assert.ThrowsAsync(async () => { - var client = new MockClient(); + await client.DaprClient.TrySaveByteStateAsync("test", "test", stateBytes.AsMemory(), "someETag"); + }); + Assert.Same(rpcException, ex.InnerException); + } - var response = client.CallStateApi() + [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); + 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); - } + 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() + [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 Should.ThrowAsync(async () => await client.DaprClient.TrySaveByteStateAsync("test", "test", stateBytes.AsMemory(), null)); - } + await Should.ThrowAsync(async () => await client.DaprClient.TrySaveByteStateAsync("test", "test", stateBytes.AsMemory(), null)); + } - [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() + [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); + // 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() + 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().ShouldBeEmpty(); + } + + [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.ShouldBe("testStore"); + envelope.Key.ShouldBe("test"); + envelope.Consistency.ShouldBe(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.ShouldBeEquivalentTo(binaryData); + } + + [Fact] + public async Task GetByteStateAndEtagAsync_ValidateRequest() + { + await using var client = TestClient.CreateForDaprClient(); + + var metadata = new Dictionary { - await using var client = TestClient.CreateForDaprClient(); + { "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.ShouldBe("testStore"); + envelope.Key.ShouldBe("test"); + envelope.Metadata.ShouldBe(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.ShouldBe(binaryData); + } - var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetByteStateAsync("testStore", "test", ConsistencyMode.Eventual)); + [Fact] + public async Task GetByteStateAsync_WrapsRpcException() + { + await using var client = TestClient.CreateForDaprClient(); - // Create Response & Respond to request - var envelope = MakeGetByteStateResponse(null); - var state = await request.CompleteWithMessageAsync(envelope); + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetByteStateAsync("testStore", "test")); - // Get response and validate - state.ToArray().ShouldBeEmpty(); + // 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.ShouldBeEquivalentTo(binaryData); + etag.ShouldBe("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); } - [Theory] - [InlineData(ConsistencyMode.Eventual, StateConsistency.ConsistencyEventual)] - [InlineData(ConsistencyMode.Strong, StateConsistency.ConsistencyStrong)] - public async Task GetByteStateAsync_ValidateRequest(ConsistencyMode consistencyMode, StateConsistency expectedConsistencyMode) + if (etag != null) { - 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.ShouldBe("testStore"); - envelope.Key.ShouldBe("test"); - envelope.Consistency.ShouldBe(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.ShouldBeEquivalentTo(binaryData); + response.Etag = etag; } - [Fact] - public async Task GetByteStateAndEtagAsync_ValidateRequest() - { - await using var client = TestClient.CreateForDaprClient(); + return response; + } + private class Widget + { + public string Size { get; set; } - 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.ShouldBe("testStore"); - envelope.Key.ShouldBe("test"); - envelope.Metadata.ShouldBe(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.ShouldBe(binaryData); + public string Color { get; set; } + + public override bool Equals(object obj) + { + return obj is Widget widget && + Size == widget.Size && + Color == widget.Color; } - [Fact] - public async Task GetByteStateAsync_WrapsRpcException() + public override int GetHashCode() { - 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.ShouldBeEquivalentTo(binaryData); - etag.ShouldBe("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; } - - public string Color { get; set; } - - public override bool Equals(object obj) - { - return obj is Widget widget && - Size == widget.Size && - Color == widget.Color; - } - - public override int GetHashCode() - { - return HashCode.Combine(Size, Color); - } + return HashCode.Combine(Size, Color); } } } - diff --git a/test/Dapr.Client.Test/TryLockResponseTest.cs b/test/Dapr.Client.Test/TryLockResponseTest.cs index f9b6f524..fd7489f5 100644 --- a/test/Dapr.Client.Test/TryLockResponseTest.cs +++ b/test/Dapr.Client.Test/TryLockResponseTest.cs @@ -17,55 +17,54 @@ using Xunit; using Shouldly; using System; -namespace Dapr.Client.Test +namespace Dapr.Client.Test; + +[System.Obsolete] +public class TryLockResponseTest { - [System.Obsolete] - public class TryLockResponseTest + [Fact] + public async Task TryLockAsync_WithAllValues_ValidateRequest() { - [Fact] - public async Task TryLockAsync_WithAllValues_ValidateRequest() + await using var client = TestClient.CreateForDaprClient(); + string storeName = "redis"; + string resourceId = "resourceId"; + string lockOwner = "owner1"; + Int32 expiryInSeconds = 1000; + var request = await client.CaptureGrpcRequestAsync(async daprClient => { - await using var client = TestClient.CreateForDaprClient(); - string storeName = "redis"; - string resourceId = "resourceId"; - string lockOwner = "owner1"; - Int32 expiryInSeconds = 1000; - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.Lock(storeName, resourceId, lockOwner, expiryInSeconds); - }); + return await daprClient.Lock(storeName, resourceId, lockOwner, expiryInSeconds); + }); - // Get Request and validate - var envelope = await request.GetRequestEnvelopeAsync(); - envelope.StoreName.ShouldBe("redis"); - envelope.ResourceId.ShouldBe("resourceId"); - envelope.LockOwner.ShouldBe("owner1"); - envelope.ExpiryInSeconds.ShouldBe(1000); + // Get Request and validate + var envelope = await request.GetRequestEnvelopeAsync(); + envelope.StoreName.ShouldBe("redis"); + envelope.ResourceId.ShouldBe("resourceId"); + envelope.LockOwner.ShouldBe("owner1"); + envelope.ExpiryInSeconds.ShouldBe(1000); - // Get response and validate - var invokeResponse = new Autogenerated.TryLockResponse{ - Success = true - }; + // Get response and validate + var invokeResponse = new Autogenerated.TryLockResponse{ + Success = true + }; - await request.CompleteWithMessageAsync(invokeResponse); + await request.CompleteWithMessageAsync(invokeResponse); - //testing unlocking + //testing unlocking - var unlockRequest = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.Unlock(storeName, resourceId, lockOwner); - }); - var unlockEnvelope = await unlockRequest.GetRequestEnvelopeAsync(); - unlockEnvelope.StoreName.ShouldBe("redis"); - unlockEnvelope.ResourceId.ShouldBe("resourceId"); - unlockEnvelope.LockOwner.ShouldBe("owner1"); + var unlockRequest = await client.CaptureGrpcRequestAsync(async daprClient => + { + return await daprClient.Unlock(storeName, resourceId, lockOwner); + }); + var unlockEnvelope = await unlockRequest.GetRequestEnvelopeAsync(); + unlockEnvelope.StoreName.ShouldBe("redis"); + unlockEnvelope.ResourceId.ShouldBe("resourceId"); + unlockEnvelope.LockOwner.ShouldBe("owner1"); - var invokeUnlockResponse = new Autogenerated.UnlockResponse{ - Status = Autogenerated.UnlockResponse.Types.Status.LockDoesNotExist - }; + var invokeUnlockResponse = new Autogenerated.UnlockResponse{ + Status = Autogenerated.UnlockResponse.Types.Status.LockDoesNotExist + }; - var domainUnlockResponse = await unlockRequest.CompleteWithMessageAsync(invokeUnlockResponse); - domainUnlockResponse.status.ShouldBe(LockStatus.LockDoesNotExist); - } + var domainUnlockResponse = await unlockRequest.CompleteWithMessageAsync(invokeUnlockResponse); + domainUnlockResponse.status.ShouldBe(LockStatus.LockDoesNotExist); } -} +} \ No newline at end of file diff --git a/test/Dapr.Client.Test/TypeConvertersTest.cs b/test/Dapr.Client.Test/TypeConvertersTest.cs index ff73de4c..a8b19c1f 100644 --- a/test/Dapr.Client.Test/TypeConvertersTest.cs +++ b/test/Dapr.Client.Test/TypeConvertersTest.cs @@ -11,35 +11,34 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Client.Test +namespace Dapr.Client.Test; + +using System.Text.Json; +using Shouldly; +using Xunit; + +public class TypeConvertersTest { - using System.Text.Json; - using Shouldly; - using Xunit; - - public class TypeConvertersTest + [Fact] + public void AnyConversion_JSON_Serialization_Deserialization() { - [Fact] - public void AnyConversion_JSON_Serialization_Deserialization() + var response = new Response() { - var response = new Response() - { - Name = "test" - }; + Name = "test" + }; - var options = new JsonSerializerOptions(JsonSerializerDefaults.Web); + var options = new JsonSerializerOptions(JsonSerializerDefaults.Web); - var any = TypeConverters.ToJsonAny(response, options); - var type = TypeConverters.FromJsonAny(any, options); + var any = TypeConverters.ToJsonAny(response, options); + var type = TypeConverters.FromJsonAny(any, options); - type.ShouldBeEquivalentTo(response); - any.TypeUrl.ShouldBe(string.Empty); - type.Name.ShouldBe("test"); - } - - private class Response - { - public string Name { get; set; } - } + type.ShouldBeEquivalentTo(response); + any.TypeUrl.ShouldBe(string.Empty); + type.Name.ShouldBe("test"); } -} + + private class Response + { + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/test/Dapr.Common.Test/DaprExtendedErrorInfoTest.cs b/test/Dapr.Common.Test/DaprExtendedErrorInfoTest.cs index b215bb60..f2d3a274 100644 --- a/test/Dapr.Common.Test/DaprExtendedErrorInfoTest.cs +++ b/test/Dapr.Common.Test/DaprExtendedErrorInfoTest.cs @@ -6,685 +6,684 @@ using Google.Rpc; using Grpc.Core; using Xunit; -namespace Dapr.Common.Test +namespace Dapr.Common.Test; + +public class DaprExtendedErrorInfoTest { - public class DaprExtendedErrorInfoTest + private static int statusCode = 1; + private static string statusMessage = "Status Message"; + + [Fact] + public void DaprExendedErrorInfo_ThrowsRpcDaprException_ExtendedErrorInfoReturnsTrueAndNotNull() { - private static int statusCode = 1; - private static string statusMessage = "Status Message"; - - [Fact] - public void DaprExendedErrorInfo_ThrowsRpcDaprException_ExtendedErrorInfoReturnsTrueAndNotNull() + // Arrange + DaprExtendedErrorInfo result = null; + var metadataEntry = new Google.Rpc.Status() { - // Arrange - DaprExtendedErrorInfo result = null; - var metadataEntry = new Google.Rpc.Status() - { - Code = statusCode, - Message = statusMessage, - }; + Code = statusCode, + Message = statusMessage, + }; - BadRequest badRequest = new(); + BadRequest badRequest = new(); - metadataEntry.Details.Add(new Any() { TypeUrl = "type.googleapis.com/Google.rpc.BadRequest", Value = badRequest.ToByteString() }); + metadataEntry.Details.Add(new Any() { TypeUrl = "type.googleapis.com/Google.rpc.BadRequest", Value = badRequest.ToByteString() }); - Metadata trailers = new() - { - { DaprExtendedErrorConstants.GrpcDetails, metadataEntry.ToByteArray() } - }; + Metadata trailers = new() + { + { DaprExtendedErrorConstants.GrpcDetails, metadataEntry.ToByteArray() } + }; - var rpcEx = new RpcException(status: new Grpc.Core.Status(StatusCode.Aborted, "BadRequest"), trailers: trailers); + var rpcEx = new RpcException(status: new Grpc.Core.Status(StatusCode.Aborted, "BadRequest"), trailers: trailers); - // Act, Assert - try - { - ThrowsRpcBasedDaprException(rpcEx); - } - - catch (DaprException daprEx) - { - Assert.True(daprEx.TryGetExtendedErrorInfo(out result)); - } - - Assert.NotNull(result); + // Act, Assert + try + { + ThrowsRpcBasedDaprException(rpcEx); } - [Fact] - public void DaprExendedErrorInfo_ThrowsNonRpcDaprException_ExtendedErrorInfoReturnsFalseAndIsNull() + catch (DaprException daprEx) { - // Arrange - DaprExtendedErrorInfo result = null; - - // Act, Assert - try - { - throw new DaprException("Non-Rpc based Dapr exception"); - } - - catch (DaprException daprEx) - { - Assert.False(daprEx.TryGetExtendedErrorInfo(out result)); - } - - Assert.Null(result); + Assert.True(daprEx.TryGetExtendedErrorInfo(out result)); } - [Fact] - public void DaprExendedErrorInfo_ThrowsRpcDaprException_NoTrailers_ExtendedErrorInfoIsNull() - { - // Arrange - DaprExtendedErrorInfo result = null; - - var rpcEx = new RpcException(status: new Grpc.Core.Status()); - - // Act, Assert - try - { - ThrowsRpcBasedDaprException(rpcEx); - } - - catch (DaprException daprEx) - { - Assert.False(daprEx.TryGetExtendedErrorInfo(out result)); - } - - Assert.Null(result); - } - - [Fact] - public void DaprExtendedErrorInfo_ThrowsBadRequestRpcException_ShouldGetSingleDaprBadRequestDetail() - { - // Arrange - DaprExtendedErrorInfo result = null; - var metadataEntry = new Google.Rpc.Status() - { - Code = statusCode, - Message = statusMessage, - }; - - BadRequest badRequest = new(); - - var violationDescription = "Violation Description"; - var violationField = "Violation Field"; - - badRequest.FieldViolations.Add(new BadRequest.Types.FieldViolation() - { - Description = violationDescription, - Field = violationField, - }); - - metadataEntry.Details.Add(new Any() { TypeUrl = "type.googleapis.com/Google.rpc.BadRequest", Value = badRequest.ToByteString() }); - - Metadata trailers = new() - { - { DaprExtendedErrorConstants.GrpcDetails, metadataEntry.ToByteArray() } - }; - - var rpcEx = new RpcException(status: new Grpc.Core.Status(StatusCode.Aborted, "BadRequest"), trailers: trailers); - - // Act, Assert - try - { - ThrowsRpcBasedDaprException(rpcEx); - } - - catch (DaprException daprEx) - { - Assert.True(daprEx.TryGetExtendedErrorInfo(out result)); - } - - Assert.NotNull(result); - var detail = Assert.Single(result.Details); - - Assert.Equal(DaprExtendedErrorType.BadRequest, detail.ErrorType); - var badRequestDetail = Assert.IsType(detail); - - var violation = Assert.Single(badRequestDetail.FieldViolations); - - Assert.Equal(violation.Description, violationDescription); - Assert.Equal(violation.Field, violationField); - } - - [Fact] - public void DaprExtendedErrorInfo_ThrowsLocalizedMessageRpcException_ShouldGetSingleDaprLocalizedMessageDetail() - { - // Arrange - DaprExtendedErrorInfo result = null; - var metadataEntry = new Google.Rpc.Status() - { - Code = statusCode, - Message = statusMessage, - }; - - var localizedMessage = "Localized Message"; - var locale = "locale"; - - LocalizedMessage badRequest = new() - { - Message = localizedMessage, - Locale = locale, - }; - - metadataEntry.Details.Add(new Any() { TypeUrl = "type.googleapis.com/Google.rpc.LocalizedMessage", Value = badRequest.ToByteString() }); - - Metadata trailers = new() - { - { DaprExtendedErrorConstants.GrpcDetails, metadataEntry.ToByteArray() } - }; - - var rpcEx = new RpcException(status: new Grpc.Core.Status(StatusCode.Aborted, "BadRequest"), trailers: trailers); - - // Act, Assert - try - { - ThrowsRpcBasedDaprException(rpcEx); - } - - catch (DaprException daprEx) - { - Assert.True(daprEx.TryGetExtendedErrorInfo(out result)); - } - - Assert.NotNull(result); - var detail = Assert.Single(result.Details); - Assert.Equal(DaprExtendedErrorType.LocalizedMessage, detail.ErrorType); - - var localizedMessageDetail = Assert.IsType(detail); - - Assert.Equal(localizedMessage, localizedMessageDetail.Message); - Assert.Equal(locale, localizedMessageDetail.Locale); - } - - [Fact] - public void DaprExtendedErrorInfo_ThrowRetryInfoRpcException_ShouldGetSingleDaprRetryInfoDetail() - { - // Arrange - DaprExtendedErrorInfo result = null; - var metadataEntry = new Google.Rpc.Status() - { - Code = statusCode, - Message = statusMessage, - }; - - RetryInfo retryInfo = new(); - - retryInfo.RetryDelay = new Duration() - { - Seconds = 1, - Nanos = 0, - }; - - metadataEntry.Details.Add(new Any() { TypeUrl = "type.googleapis.com/Google.rpc.RetryInfo", Value = retryInfo.ToByteString() }); - - Metadata trailers = new() - { - { DaprExtendedErrorConstants.GrpcDetails, metadataEntry.ToByteArray() } - }; - - var rpcEx = new RpcException(status: new Grpc.Core.Status(StatusCode.FailedPrecondition, "RetryInfo"), trailers: trailers); - - // Act, Assert - try - { - ThrowsRpcBasedDaprException(rpcEx); - } - - catch (DaprException daprEx) - { - Assert.True(daprEx.TryGetExtendedErrorInfo(out result)); - } - - Assert.NotNull(result); - var detail = Assert.Single(result.Details); - Assert.Equal(DaprExtendedErrorType.RetryInfo, detail.ErrorType); - - var retryInfoDetail = Assert.IsType(detail); - - Assert.NotNull(retryInfoDetail); - Assert.Equal(1, retryInfoDetail.Delay.Seconds); - Assert.Equal(0, retryInfoDetail.Delay.Nanos); - } - - [Fact] - public void DaprExtendedErrorInfo_ThrowsDebugInfoRpcException_ShouldGetSingleDaprDebugInfoDetail() - { - // Arrange - DaprExtendedErrorInfo result = null; - var metadataEntry = new Google.Rpc.Status() - { - Code = statusCode, - Message = statusMessage, - }; - - Google.Rpc.DebugInfo debugInfo = new(); - - debugInfo.Detail = "Debug Detail"; - debugInfo.StackEntries.Add("Stack Entry"); - - metadataEntry.Details.Add(new Any() { TypeUrl = "type.googleapis.com/Google.rpc.DebugInfo", Value = debugInfo.ToByteString() }); - - Grpc.Core.Metadata trailers = new() - { - { DaprExtendedErrorConstants.GrpcDetails, metadataEntry.ToByteArray() } - }; - - var rpcEx = new RpcException(status: new Grpc.Core.Status(Grpc.Core.StatusCode.FailedPrecondition, "DebugInfo"), trailers: trailers); - - // Act, Assert - try - { - ThrowsRpcBasedDaprException(rpcEx); - } - - catch (DaprException daprEx) - { - Assert.True(daprEx.TryGetExtendedErrorInfo(out result)); - } - - Assert.NotNull(result); - var detail = Assert.Single(result.Details); - - Assert.Equal(DaprExtendedErrorType.DebugInfo, detail.ErrorType); - - var daprDebugInfoDetail = Assert.IsType(detail); - - Assert.Equal("Debug Detail", daprDebugInfoDetail.Detail); - var entry = Assert.Single(daprDebugInfoDetail.StackEntries); - Assert.Equal("Stack Entry", entry); - } - - [Fact] - public void DaprExtendedErrorInfo_ThrowsPreconditionFailureRpcException_ShouldGetSingleDaprPreconditionFailureDetail() - { - // Arrange - DaprExtendedErrorInfo result = null; - var metadataEntry = new Google.Rpc.Status() - { - Code = statusCode, - Message = statusMessage, - }; - - Google.Rpc.PreconditionFailure failure = new(); - - var violationDesc = "Violation Description"; - var violationSubject = "Violation Subject"; - var violationType = "Violation Type"; - - failure.Violations.Add(new Google.Rpc.PreconditionFailure.Types.Violation() - { - Description = violationDesc, - Subject = violationSubject, - Type = violationType - }); - - metadataEntry.Details.Add(new Any() { TypeUrl = "type.googleapis.com/Google.rpc.PreconditionFailure", Value = failure.ToByteString() }); - - Metadata trailers = new() - { - { DaprExtendedErrorConstants.GrpcDetails, metadataEntry.ToByteArray() } - }; - - var rpcEx = new RpcException(status: new Grpc.Core.Status(Grpc.Core.StatusCode.FailedPrecondition, "PrecondtionFailure"), trailers: trailers); - - // Act, Assert - try - { - ThrowsRpcBasedDaprException(rpcEx); - } - - catch (DaprException daprEx) - { - Assert.True(daprEx.TryGetExtendedErrorInfo(out result)); - } - - Assert.NotNull(result); - - var detail = Assert.Single(result.Details); - Assert.Equal(DaprExtendedErrorType.PreconditionFailure, detail.ErrorType); - var preconditionFailureDetail = Assert.IsType(detail); - - var violation = Assert.Single(preconditionFailureDetail.Violations); - - Assert.Equal(violation.Description, violationDesc); - Assert.Equal(violation.Subject, violationSubject); - Assert.Equal(violation.Type, violationType); - } - - [Fact] - public void DaprExtendedErrorInfo_ThrowsHelpRpcException_ShouldGetSingleDaprHelpDetail() - { - // Arrange - DaprExtendedErrorInfo result = null; - var metadataEntry = new Google.Rpc.Status() - { - Code = statusCode, - Message = statusMessage, - }; - - Help help = new(); - - var helpDesc = "Help Description"; - - var helpUrl = "help-link.com"; - - help.Links.Add(new Help.Types.Link() - { - Description = helpDesc, - Url = helpUrl, - }); - - metadataEntry.Details.Add(new Any() { TypeUrl = "type.googleapis.com/Google.rpc.Help", Value = help.ToByteString() }); - - Metadata trailers = new() - { - { DaprExtendedErrorConstants.GrpcDetails, metadataEntry.ToByteArray() } - }; - - var rpcEx = new RpcException(status: new Grpc.Core.Status(StatusCode.Aborted, "Help"), trailers: trailers); - - // Act, Assert - try - { - ThrowsRpcBasedDaprException(rpcEx); - } - - catch (DaprException daprEx) - { - Assert.True(daprEx.TryGetExtendedErrorInfo(out result)); - } - - Assert.NotNull(result); - var detail = Assert.Single(result.Details); - - Assert.IsAssignableFrom(detail); - Assert.Equal(DaprExtendedErrorType.Help, detail.ErrorType); - - var helpDetail = Assert.IsType(detail); - - var link = Assert.Single(helpDetail.Links); - - Assert.Equal(helpDesc, link.Description); - Assert.Equal(helpUrl, link.Url); - } - - [Fact] - public void DaprExtendedErrorInfo_ThrowsResourceInfoRpcException_ShouldGetSingleDaprResourceInfoDetail() - { - // Arrange - DaprExtendedErrorInfo result = null; - var metadataEntry = new Google.Rpc.Status() - { - Code = statusCode, - Message = statusMessage, - }; - - var resourceInfoDesc = "Description"; - var resourceInfoOwner = "Owner"; - var resourceInfoName = "Name"; - var resourceInfoType = "Type"; - - ResourceInfo resourceInfo = new() - { - Description = resourceInfoDesc, - Owner = resourceInfoOwner, - ResourceName = resourceInfoName, - ResourceType = resourceInfoType, - }; - - - metadataEntry.Details.Add(new Any() { TypeUrl = "type.googleapis.com/Google.rpc.ResourceInfo", Value = resourceInfo.ToByteString() }); - - Metadata trailers = new() - { - { DaprExtendedErrorConstants.GrpcDetails, metadataEntry.ToByteArray() } - }; - - var rpcEx = new RpcException(status: new Grpc.Core.Status(StatusCode.Aborted, "ResourceInfo"), trailers: trailers); - - // Act, Assert - try - { - ThrowsRpcBasedDaprException(rpcEx); - } - - catch (DaprException daprEx) - { - Assert.True(daprEx.TryGetExtendedErrorInfo(out result)); - } - - Assert.NotNull(result); - - var detail = Assert.Single(result.Details); - Assert.IsAssignableFrom(detail); - Assert.Equal(DaprExtendedErrorType.ResourceInfo, detail.ErrorType); - - var daprResourceInfo = Assert.IsType(detail); - - Assert.Equal(resourceInfoDesc, daprResourceInfo.Description); - Assert.Equal(resourceInfoName, daprResourceInfo.ResourceName); - Assert.Equal(resourceInfoType, daprResourceInfo.ResourceType); - Assert.Equal(resourceInfoOwner, daprResourceInfo.Owner); - } - - [Fact] - public void DaprExtendedErrorInfo_ThrowsQuotaFailureRpcException_ShouldGetSingleDaprQuotaFailureDetail() - { - // Arrange - DaprExtendedErrorInfo result = null; - var metadataEntry = new Google.Rpc.Status() - { - Code = statusCode, - Message = statusMessage, - }; - - var quotaFailureDesc = "Description"; - var quotaFailureSubject = "Subject"; - - QuotaFailure quotaFailure = new(); - - quotaFailure.Violations.Add(new QuotaFailure.Types.Violation() - { - Description = quotaFailureDesc, - Subject = quotaFailureSubject, - }); - - metadataEntry.Details.Add(new Any() { TypeUrl = "type.googleapis.com/Google.rpc.QuotaFailure", Value = quotaFailure.ToByteString() }); - - Metadata trailers = new() - { - { DaprExtendedErrorConstants.GrpcDetails, metadataEntry.ToByteArray() } - }; - - var rpcEx = new RpcException(status: new Grpc.Core.Status(StatusCode.Aborted, "QuotaFailure"), trailers: trailers); - - // Act, Assert - try - { - ThrowsRpcBasedDaprException(rpcEx); - } - - catch (DaprException daprEx) - { - Assert.True(daprEx.TryGetExtendedErrorInfo(out result)); - } - - Assert.NotNull(result); - - var detail = Assert.Single(result.Details); - - Assert.IsAssignableFrom(detail); - Assert.Equal(DaprExtendedErrorType.QuotaFailure, detail.ErrorType); - - var quotaFailureDetail = Assert.IsType(detail); - - var violation = Assert.Single(quotaFailureDetail.Violations); - Assert.Equal(quotaFailureDesc, violation.Description); - Assert.Equal(quotaFailureSubject, violation.Subject); - } - - [Fact] - public void DaprExtendedErrorInfo_ThrowsErrorInfoRpcException_ShouldGetSingleDaprErrorInfoDetail() - { - // Arrange - DaprExtendedErrorInfo result = null; - var metadataEntry = new Google.Rpc.Status() - { - Code = statusCode, - Message = statusMessage, - }; - - var errorInfoDomain = "Domain"; - var errorInfoReason = "Reason"; - var errorInfoMetadataKey = "Key"; - var errorInfoMetadataValue = "Value"; - var errorInfoMetadata = new Dictionary() - { - { errorInfoMetadataKey, errorInfoMetadataValue } - }; - - Google.Rpc.ErrorInfo errorInfo = new() - { - Domain = errorInfoDomain, - Reason = errorInfoReason, - }; - - errorInfo.Metadata.Add(errorInfoMetadata); - - metadataEntry.Details.Add(new Any() { TypeUrl = "type.googleapis.com/Google.rpc.ErrorInfo", Value = errorInfo.ToByteString() }); - - Metadata trailers = new() - { - { DaprExtendedErrorConstants.GrpcDetails, metadataEntry.ToByteArray() } - }; - - var rpcEx = new RpcException(status: new Grpc.Core.Status(StatusCode.Aborted, "ErrorInfo"), trailers: trailers); - - // Act, Assert - try - { - ThrowsRpcBasedDaprException(rpcEx); - } - - catch (DaprException daprEx) - { - Assert.True(daprEx.TryGetExtendedErrorInfo(out result)); - } - - Assert.NotNull(result); - - var detail = Assert.Single(result.Details); - - Assert.Equal(DaprExtendedErrorType.ErrorInfo, detail.ErrorType); - - var errorInfoDetail = Assert.IsType(detail); - - Assert.Equal(errorInfoDomain, errorInfoDetail.Domain); - Assert.Equal(errorInfoReason, errorInfoDetail.Reason); - - var metadata = Assert.Single(errorInfoDetail.Metadata); - Assert.Equal(errorInfoMetadataKey, metadata.Key); - Assert.Equal(errorInfoMetadataValue, metadata.Value); - } - - [Fact] - public void DaprExtendedErrorInfo_ThrowsRequestInfoRpcException_ShouldGetSingleDaprRequestInfoDetail() - { - // Arrange - DaprExtendedErrorInfo result = null; - var metadataEntry = new Google.Rpc.Status() - { - Code = statusCode, - Message = statusMessage, - }; - - var requestInfoId = "RequestId"; - var requestInfoServingData = "Serving Data"; - - RequestInfo requestInfo = new() - { - RequestId = requestInfoId, - ServingData = requestInfoServingData, - }; - - metadataEntry.Details.Add(new Any() { TypeUrl = "type.googleapis.com/Google.rpc.RequestInfo", Value = requestInfo.ToByteString() }); - - Metadata trailers = new() - { - { DaprExtendedErrorConstants.GrpcDetails, metadataEntry.ToByteArray() } - }; - - var rpcEx = new RpcException(status: new Grpc.Core.Status(StatusCode.Aborted, "RequestInfo"), trailers: trailers); - - // Act, Assert - try - { - ThrowsRpcBasedDaprException(rpcEx); - } - - catch (DaprException daprEx) - { - Assert.True(daprEx.TryGetExtendedErrorInfo(out result)); - } - - Assert.NotNull(result); - var detail = Assert.Single(result.Details); - - Assert.Equal(DaprExtendedErrorType.RequestInfo, detail.ErrorType); - var requestInfoDetail = Assert.IsType(detail); - - Assert.Equal(requestInfoId, requestInfoDetail.RequestId); - Assert.Equal(requestInfoServingData, requestInfoDetail.ServingData); - } - - [Fact] - public void DaprExtendedErrorInfo_ThrowsRequestInfoRpcException_ShouldGetMultipleDetails() - { - // Arrange - DaprExtendedErrorInfo result = null; - var metadataEntry = new Google.Rpc.Status() - { - Code = statusCode, - Message = statusMessage, - }; - - List expectedDetailTypes = new() - { - typeof(DaprResourceInfoDetail), - typeof(DaprRequestInfoDetail), - }; - - RequestInfo requestInfo = new(); - - metadataEntry.Details.Add(new Any() { TypeUrl = "type.googleapis.com/Google.rpc.RequestInfo", Value = requestInfo.ToByteString() }); - - ResourceInfo resourceInfo = new(); - - metadataEntry.Details.Add(new Any() { TypeUrl = "type.googleapis.com/Google.rpc.ResourceInfo", Value = resourceInfo.ToByteString() }); - - Metadata trailers = new() - { - { DaprExtendedErrorConstants.GrpcDetails, metadataEntry.ToByteArray() } - }; - - var rpcEx = new RpcException(status: new Grpc.Core.Status(StatusCode.Aborted, "RequestInfo"), trailers: trailers); - - // Act, Assert - try - { - ThrowsRpcBasedDaprException(rpcEx); - } - - catch (DaprException daprEx) - { - Assert.True(daprEx.TryGetExtendedErrorInfo(out result)); - } - - Assert.Collection(result.Details, - detail => Assert.Contains(detail.GetType(), expectedDetailTypes), - detail => Assert.Contains(detail.GetType(), expectedDetailTypes) - ); - } - - private static void ThrowsRpcBasedDaprException(RpcException ex) => throw new DaprException("A Dapr exception", ex); + Assert.NotNull(result); } -} + + [Fact] + public void DaprExendedErrorInfo_ThrowsNonRpcDaprException_ExtendedErrorInfoReturnsFalseAndIsNull() + { + // Arrange + DaprExtendedErrorInfo result = null; + + // Act, Assert + try + { + throw new DaprException("Non-Rpc based Dapr exception"); + } + + catch (DaprException daprEx) + { + Assert.False(daprEx.TryGetExtendedErrorInfo(out result)); + } + + Assert.Null(result); + } + + [Fact] + public void DaprExendedErrorInfo_ThrowsRpcDaprException_NoTrailers_ExtendedErrorInfoIsNull() + { + // Arrange + DaprExtendedErrorInfo result = null; + + var rpcEx = new RpcException(status: new Grpc.Core.Status()); + + // Act, Assert + try + { + ThrowsRpcBasedDaprException(rpcEx); + } + + catch (DaprException daprEx) + { + Assert.False(daprEx.TryGetExtendedErrorInfo(out result)); + } + + Assert.Null(result); + } + + [Fact] + public void DaprExtendedErrorInfo_ThrowsBadRequestRpcException_ShouldGetSingleDaprBadRequestDetail() + { + // Arrange + DaprExtendedErrorInfo result = null; + var metadataEntry = new Google.Rpc.Status() + { + Code = statusCode, + Message = statusMessage, + }; + + BadRequest badRequest = new(); + + var violationDescription = "Violation Description"; + var violationField = "Violation Field"; + + badRequest.FieldViolations.Add(new BadRequest.Types.FieldViolation() + { + Description = violationDescription, + Field = violationField, + }); + + metadataEntry.Details.Add(new Any() { TypeUrl = "type.googleapis.com/Google.rpc.BadRequest", Value = badRequest.ToByteString() }); + + Metadata trailers = new() + { + { DaprExtendedErrorConstants.GrpcDetails, metadataEntry.ToByteArray() } + }; + + var rpcEx = new RpcException(status: new Grpc.Core.Status(StatusCode.Aborted, "BadRequest"), trailers: trailers); + + // Act, Assert + try + { + ThrowsRpcBasedDaprException(rpcEx); + } + + catch (DaprException daprEx) + { + Assert.True(daprEx.TryGetExtendedErrorInfo(out result)); + } + + Assert.NotNull(result); + var detail = Assert.Single(result.Details); + + Assert.Equal(DaprExtendedErrorType.BadRequest, detail.ErrorType); + var badRequestDetail = Assert.IsType(detail); + + var violation = Assert.Single(badRequestDetail.FieldViolations); + + Assert.Equal(violation.Description, violationDescription); + Assert.Equal(violation.Field, violationField); + } + + [Fact] + public void DaprExtendedErrorInfo_ThrowsLocalizedMessageRpcException_ShouldGetSingleDaprLocalizedMessageDetail() + { + // Arrange + DaprExtendedErrorInfo result = null; + var metadataEntry = new Google.Rpc.Status() + { + Code = statusCode, + Message = statusMessage, + }; + + var localizedMessage = "Localized Message"; + var locale = "locale"; + + LocalizedMessage badRequest = new() + { + Message = localizedMessage, + Locale = locale, + }; + + metadataEntry.Details.Add(new Any() { TypeUrl = "type.googleapis.com/Google.rpc.LocalizedMessage", Value = badRequest.ToByteString() }); + + Metadata trailers = new() + { + { DaprExtendedErrorConstants.GrpcDetails, metadataEntry.ToByteArray() } + }; + + var rpcEx = new RpcException(status: new Grpc.Core.Status(StatusCode.Aborted, "BadRequest"), trailers: trailers); + + // Act, Assert + try + { + ThrowsRpcBasedDaprException(rpcEx); + } + + catch (DaprException daprEx) + { + Assert.True(daprEx.TryGetExtendedErrorInfo(out result)); + } + + Assert.NotNull(result); + var detail = Assert.Single(result.Details); + Assert.Equal(DaprExtendedErrorType.LocalizedMessage, detail.ErrorType); + + var localizedMessageDetail = Assert.IsType(detail); + + Assert.Equal(localizedMessage, localizedMessageDetail.Message); + Assert.Equal(locale, localizedMessageDetail.Locale); + } + + [Fact] + public void DaprExtendedErrorInfo_ThrowRetryInfoRpcException_ShouldGetSingleDaprRetryInfoDetail() + { + // Arrange + DaprExtendedErrorInfo result = null; + var metadataEntry = new Google.Rpc.Status() + { + Code = statusCode, + Message = statusMessage, + }; + + RetryInfo retryInfo = new(); + + retryInfo.RetryDelay = new Duration() + { + Seconds = 1, + Nanos = 0, + }; + + metadataEntry.Details.Add(new Any() { TypeUrl = "type.googleapis.com/Google.rpc.RetryInfo", Value = retryInfo.ToByteString() }); + + Metadata trailers = new() + { + { DaprExtendedErrorConstants.GrpcDetails, metadataEntry.ToByteArray() } + }; + + var rpcEx = new RpcException(status: new Grpc.Core.Status(StatusCode.FailedPrecondition, "RetryInfo"), trailers: trailers); + + // Act, Assert + try + { + ThrowsRpcBasedDaprException(rpcEx); + } + + catch (DaprException daprEx) + { + Assert.True(daprEx.TryGetExtendedErrorInfo(out result)); + } + + Assert.NotNull(result); + var detail = Assert.Single(result.Details); + Assert.Equal(DaprExtendedErrorType.RetryInfo, detail.ErrorType); + + var retryInfoDetail = Assert.IsType(detail); + + Assert.NotNull(retryInfoDetail); + Assert.Equal(1, retryInfoDetail.Delay.Seconds); + Assert.Equal(0, retryInfoDetail.Delay.Nanos); + } + + [Fact] + public void DaprExtendedErrorInfo_ThrowsDebugInfoRpcException_ShouldGetSingleDaprDebugInfoDetail() + { + // Arrange + DaprExtendedErrorInfo result = null; + var metadataEntry = new Google.Rpc.Status() + { + Code = statusCode, + Message = statusMessage, + }; + + Google.Rpc.DebugInfo debugInfo = new(); + + debugInfo.Detail = "Debug Detail"; + debugInfo.StackEntries.Add("Stack Entry"); + + metadataEntry.Details.Add(new Any() { TypeUrl = "type.googleapis.com/Google.rpc.DebugInfo", Value = debugInfo.ToByteString() }); + + Grpc.Core.Metadata trailers = new() + { + { DaprExtendedErrorConstants.GrpcDetails, metadataEntry.ToByteArray() } + }; + + var rpcEx = new RpcException(status: new Grpc.Core.Status(Grpc.Core.StatusCode.FailedPrecondition, "DebugInfo"), trailers: trailers); + + // Act, Assert + try + { + ThrowsRpcBasedDaprException(rpcEx); + } + + catch (DaprException daprEx) + { + Assert.True(daprEx.TryGetExtendedErrorInfo(out result)); + } + + Assert.NotNull(result); + var detail = Assert.Single(result.Details); + + Assert.Equal(DaprExtendedErrorType.DebugInfo, detail.ErrorType); + + var daprDebugInfoDetail = Assert.IsType(detail); + + Assert.Equal("Debug Detail", daprDebugInfoDetail.Detail); + var entry = Assert.Single(daprDebugInfoDetail.StackEntries); + Assert.Equal("Stack Entry", entry); + } + + [Fact] + public void DaprExtendedErrorInfo_ThrowsPreconditionFailureRpcException_ShouldGetSingleDaprPreconditionFailureDetail() + { + // Arrange + DaprExtendedErrorInfo result = null; + var metadataEntry = new Google.Rpc.Status() + { + Code = statusCode, + Message = statusMessage, + }; + + Google.Rpc.PreconditionFailure failure = new(); + + var violationDesc = "Violation Description"; + var violationSubject = "Violation Subject"; + var violationType = "Violation Type"; + + failure.Violations.Add(new Google.Rpc.PreconditionFailure.Types.Violation() + { + Description = violationDesc, + Subject = violationSubject, + Type = violationType + }); + + metadataEntry.Details.Add(new Any() { TypeUrl = "type.googleapis.com/Google.rpc.PreconditionFailure", Value = failure.ToByteString() }); + + Metadata trailers = new() + { + { DaprExtendedErrorConstants.GrpcDetails, metadataEntry.ToByteArray() } + }; + + var rpcEx = new RpcException(status: new Grpc.Core.Status(Grpc.Core.StatusCode.FailedPrecondition, "PrecondtionFailure"), trailers: trailers); + + // Act, Assert + try + { + ThrowsRpcBasedDaprException(rpcEx); + } + + catch (DaprException daprEx) + { + Assert.True(daprEx.TryGetExtendedErrorInfo(out result)); + } + + Assert.NotNull(result); + + var detail = Assert.Single(result.Details); + Assert.Equal(DaprExtendedErrorType.PreconditionFailure, detail.ErrorType); + var preconditionFailureDetail = Assert.IsType(detail); + + var violation = Assert.Single(preconditionFailureDetail.Violations); + + Assert.Equal(violation.Description, violationDesc); + Assert.Equal(violation.Subject, violationSubject); + Assert.Equal(violation.Type, violationType); + } + + [Fact] + public void DaprExtendedErrorInfo_ThrowsHelpRpcException_ShouldGetSingleDaprHelpDetail() + { + // Arrange + DaprExtendedErrorInfo result = null; + var metadataEntry = new Google.Rpc.Status() + { + Code = statusCode, + Message = statusMessage, + }; + + Help help = new(); + + var helpDesc = "Help Description"; + + var helpUrl = "help-link.com"; + + help.Links.Add(new Help.Types.Link() + { + Description = helpDesc, + Url = helpUrl, + }); + + metadataEntry.Details.Add(new Any() { TypeUrl = "type.googleapis.com/Google.rpc.Help", Value = help.ToByteString() }); + + Metadata trailers = new() + { + { DaprExtendedErrorConstants.GrpcDetails, metadataEntry.ToByteArray() } + }; + + var rpcEx = new RpcException(status: new Grpc.Core.Status(StatusCode.Aborted, "Help"), trailers: trailers); + + // Act, Assert + try + { + ThrowsRpcBasedDaprException(rpcEx); + } + + catch (DaprException daprEx) + { + Assert.True(daprEx.TryGetExtendedErrorInfo(out result)); + } + + Assert.NotNull(result); + var detail = Assert.Single(result.Details); + + Assert.IsAssignableFrom(detail); + Assert.Equal(DaprExtendedErrorType.Help, detail.ErrorType); + + var helpDetail = Assert.IsType(detail); + + var link = Assert.Single(helpDetail.Links); + + Assert.Equal(helpDesc, link.Description); + Assert.Equal(helpUrl, link.Url); + } + + [Fact] + public void DaprExtendedErrorInfo_ThrowsResourceInfoRpcException_ShouldGetSingleDaprResourceInfoDetail() + { + // Arrange + DaprExtendedErrorInfo result = null; + var metadataEntry = new Google.Rpc.Status() + { + Code = statusCode, + Message = statusMessage, + }; + + var resourceInfoDesc = "Description"; + var resourceInfoOwner = "Owner"; + var resourceInfoName = "Name"; + var resourceInfoType = "Type"; + + ResourceInfo resourceInfo = new() + { + Description = resourceInfoDesc, + Owner = resourceInfoOwner, + ResourceName = resourceInfoName, + ResourceType = resourceInfoType, + }; + + + metadataEntry.Details.Add(new Any() { TypeUrl = "type.googleapis.com/Google.rpc.ResourceInfo", Value = resourceInfo.ToByteString() }); + + Metadata trailers = new() + { + { DaprExtendedErrorConstants.GrpcDetails, metadataEntry.ToByteArray() } + }; + + var rpcEx = new RpcException(status: new Grpc.Core.Status(StatusCode.Aborted, "ResourceInfo"), trailers: trailers); + + // Act, Assert + try + { + ThrowsRpcBasedDaprException(rpcEx); + } + + catch (DaprException daprEx) + { + Assert.True(daprEx.TryGetExtendedErrorInfo(out result)); + } + + Assert.NotNull(result); + + var detail = Assert.Single(result.Details); + Assert.IsAssignableFrom(detail); + Assert.Equal(DaprExtendedErrorType.ResourceInfo, detail.ErrorType); + + var daprResourceInfo = Assert.IsType(detail); + + Assert.Equal(resourceInfoDesc, daprResourceInfo.Description); + Assert.Equal(resourceInfoName, daprResourceInfo.ResourceName); + Assert.Equal(resourceInfoType, daprResourceInfo.ResourceType); + Assert.Equal(resourceInfoOwner, daprResourceInfo.Owner); + } + + [Fact] + public void DaprExtendedErrorInfo_ThrowsQuotaFailureRpcException_ShouldGetSingleDaprQuotaFailureDetail() + { + // Arrange + DaprExtendedErrorInfo result = null; + var metadataEntry = new Google.Rpc.Status() + { + Code = statusCode, + Message = statusMessage, + }; + + var quotaFailureDesc = "Description"; + var quotaFailureSubject = "Subject"; + + QuotaFailure quotaFailure = new(); + + quotaFailure.Violations.Add(new QuotaFailure.Types.Violation() + { + Description = quotaFailureDesc, + Subject = quotaFailureSubject, + }); + + metadataEntry.Details.Add(new Any() { TypeUrl = "type.googleapis.com/Google.rpc.QuotaFailure", Value = quotaFailure.ToByteString() }); + + Metadata trailers = new() + { + { DaprExtendedErrorConstants.GrpcDetails, metadataEntry.ToByteArray() } + }; + + var rpcEx = new RpcException(status: new Grpc.Core.Status(StatusCode.Aborted, "QuotaFailure"), trailers: trailers); + + // Act, Assert + try + { + ThrowsRpcBasedDaprException(rpcEx); + } + + catch (DaprException daprEx) + { + Assert.True(daprEx.TryGetExtendedErrorInfo(out result)); + } + + Assert.NotNull(result); + + var detail = Assert.Single(result.Details); + + Assert.IsAssignableFrom(detail); + Assert.Equal(DaprExtendedErrorType.QuotaFailure, detail.ErrorType); + + var quotaFailureDetail = Assert.IsType(detail); + + var violation = Assert.Single(quotaFailureDetail.Violations); + Assert.Equal(quotaFailureDesc, violation.Description); + Assert.Equal(quotaFailureSubject, violation.Subject); + } + + [Fact] + public void DaprExtendedErrorInfo_ThrowsErrorInfoRpcException_ShouldGetSingleDaprErrorInfoDetail() + { + // Arrange + DaprExtendedErrorInfo result = null; + var metadataEntry = new Google.Rpc.Status() + { + Code = statusCode, + Message = statusMessage, + }; + + var errorInfoDomain = "Domain"; + var errorInfoReason = "Reason"; + var errorInfoMetadataKey = "Key"; + var errorInfoMetadataValue = "Value"; + var errorInfoMetadata = new Dictionary() + { + { errorInfoMetadataKey, errorInfoMetadataValue } + }; + + Google.Rpc.ErrorInfo errorInfo = new() + { + Domain = errorInfoDomain, + Reason = errorInfoReason, + }; + + errorInfo.Metadata.Add(errorInfoMetadata); + + metadataEntry.Details.Add(new Any() { TypeUrl = "type.googleapis.com/Google.rpc.ErrorInfo", Value = errorInfo.ToByteString() }); + + Metadata trailers = new() + { + { DaprExtendedErrorConstants.GrpcDetails, metadataEntry.ToByteArray() } + }; + + var rpcEx = new RpcException(status: new Grpc.Core.Status(StatusCode.Aborted, "ErrorInfo"), trailers: trailers); + + // Act, Assert + try + { + ThrowsRpcBasedDaprException(rpcEx); + } + + catch (DaprException daprEx) + { + Assert.True(daprEx.TryGetExtendedErrorInfo(out result)); + } + + Assert.NotNull(result); + + var detail = Assert.Single(result.Details); + + Assert.Equal(DaprExtendedErrorType.ErrorInfo, detail.ErrorType); + + var errorInfoDetail = Assert.IsType(detail); + + Assert.Equal(errorInfoDomain, errorInfoDetail.Domain); + Assert.Equal(errorInfoReason, errorInfoDetail.Reason); + + var metadata = Assert.Single(errorInfoDetail.Metadata); + Assert.Equal(errorInfoMetadataKey, metadata.Key); + Assert.Equal(errorInfoMetadataValue, metadata.Value); + } + + [Fact] + public void DaprExtendedErrorInfo_ThrowsRequestInfoRpcException_ShouldGetSingleDaprRequestInfoDetail() + { + // Arrange + DaprExtendedErrorInfo result = null; + var metadataEntry = new Google.Rpc.Status() + { + Code = statusCode, + Message = statusMessage, + }; + + var requestInfoId = "RequestId"; + var requestInfoServingData = "Serving Data"; + + RequestInfo requestInfo = new() + { + RequestId = requestInfoId, + ServingData = requestInfoServingData, + }; + + metadataEntry.Details.Add(new Any() { TypeUrl = "type.googleapis.com/Google.rpc.RequestInfo", Value = requestInfo.ToByteString() }); + + Metadata trailers = new() + { + { DaprExtendedErrorConstants.GrpcDetails, metadataEntry.ToByteArray() } + }; + + var rpcEx = new RpcException(status: new Grpc.Core.Status(StatusCode.Aborted, "RequestInfo"), trailers: trailers); + + // Act, Assert + try + { + ThrowsRpcBasedDaprException(rpcEx); + } + + catch (DaprException daprEx) + { + Assert.True(daprEx.TryGetExtendedErrorInfo(out result)); + } + + Assert.NotNull(result); + var detail = Assert.Single(result.Details); + + Assert.Equal(DaprExtendedErrorType.RequestInfo, detail.ErrorType); + var requestInfoDetail = Assert.IsType(detail); + + Assert.Equal(requestInfoId, requestInfoDetail.RequestId); + Assert.Equal(requestInfoServingData, requestInfoDetail.ServingData); + } + + [Fact] + public void DaprExtendedErrorInfo_ThrowsRequestInfoRpcException_ShouldGetMultipleDetails() + { + // Arrange + DaprExtendedErrorInfo result = null; + var metadataEntry = new Google.Rpc.Status() + { + Code = statusCode, + Message = statusMessage, + }; + + List expectedDetailTypes = new() + { + typeof(DaprResourceInfoDetail), + typeof(DaprRequestInfoDetail), + }; + + RequestInfo requestInfo = new(); + + metadataEntry.Details.Add(new Any() { TypeUrl = "type.googleapis.com/Google.rpc.RequestInfo", Value = requestInfo.ToByteString() }); + + ResourceInfo resourceInfo = new(); + + metadataEntry.Details.Add(new Any() { TypeUrl = "type.googleapis.com/Google.rpc.ResourceInfo", Value = resourceInfo.ToByteString() }); + + Metadata trailers = new() + { + { DaprExtendedErrorConstants.GrpcDetails, metadataEntry.ToByteArray() } + }; + + var rpcEx = new RpcException(status: new Grpc.Core.Status(StatusCode.Aborted, "RequestInfo"), trailers: trailers); + + // Act, Assert + try + { + ThrowsRpcBasedDaprException(rpcEx); + } + + catch (DaprException daprEx) + { + Assert.True(daprEx.TryGetExtendedErrorInfo(out result)); + } + + Assert.Collection(result.Details, + detail => Assert.Contains(detail.GetType(), expectedDetailTypes), + detail => Assert.Contains(detail.GetType(), expectedDetailTypes) + ); + } + + private static void ThrowsRpcBasedDaprException(RpcException ex) => throw new DaprException("A Dapr exception", ex); +} \ No newline at end of file diff --git a/test/Dapr.E2E.Test.Actors/ExceptionTesting/IExceptionActor.cs b/test/Dapr.E2E.Test.Actors/ExceptionTesting/IExceptionActor.cs index 76778529..f8a85bfa 100644 --- a/test/Dapr.E2E.Test.Actors/ExceptionTesting/IExceptionActor.cs +++ b/test/Dapr.E2E.Test.Actors/ExceptionTesting/IExceptionActor.cs @@ -13,10 +13,9 @@ using Dapr.Actors; using System.Threading.Tasks; -namespace Dapr.E2E.Test.Actors.ExceptionTesting +namespace Dapr.E2E.Test.Actors.ExceptionTesting; + +public interface IExceptionActor : IPingActor, IActor { - public interface IExceptionActor : IPingActor, IActor - { - Task ExceptionExample(); - } + Task ExceptionExample(); } \ No newline at end of file diff --git a/test/Dapr.E2E.Test.Actors/IPingActor.cs b/test/Dapr.E2E.Test.Actors/IPingActor.cs index 8bbe2508..17631217 100644 --- a/test/Dapr.E2E.Test.Actors/IPingActor.cs +++ b/test/Dapr.E2E.Test.Actors/IPingActor.cs @@ -14,10 +14,9 @@ using System.Threading.Tasks; using Dapr.Actors; -namespace Dapr.E2E.Test.Actors +namespace Dapr.E2E.Test.Actors; + +public interface IPingActor : IActor { - public interface IPingActor : IActor - { - Task Ping(); - } -} + Task Ping(); +} \ No newline at end of file diff --git a/test/Dapr.E2E.Test.Actors/ISerializationActor.cs b/test/Dapr.E2E.Test.Actors/ISerializationActor.cs index 46455c28..a1c3e047 100644 --- a/test/Dapr.E2E.Test.Actors/ISerializationActor.cs +++ b/test/Dapr.E2E.Test.Actors/ISerializationActor.cs @@ -6,19 +6,18 @@ using System.Threading; using System.Threading.Tasks; using Dapr.Actors; -namespace Dapr.E2E.Test.Actors +namespace Dapr.E2E.Test.Actors; + +public interface ISerializationActor : IActor, IPingActor { - public interface ISerializationActor : IActor, IPingActor - { - Task SendAsync(string name, SerializationPayload payload, CancellationToken cancellationToken = default); - Task AnotherMethod(DateTime payload); - } - - public record SerializationPayload(string Message) - { - public JsonElement Value { get; set; } - - [JsonExtensionData] - public Dictionary ExtensionData { get; set; } - } + Task SendAsync(string name, SerializationPayload payload, CancellationToken cancellationToken = default); + Task AnotherMethod(DateTime payload); } + +public record SerializationPayload(string Message) +{ + public JsonElement Value { get; set; } + + [JsonExtensionData] + public Dictionary ExtensionData { get; set; } +} \ No newline at end of file diff --git a/test/Dapr.E2E.Test.Actors/Reentrancy/CallRecord.cs b/test/Dapr.E2E.Test.Actors/Reentrancy/CallRecord.cs index 211cf2a4..946f87aa 100644 --- a/test/Dapr.E2E.Test.Actors/Reentrancy/CallRecord.cs +++ b/test/Dapr.E2E.Test.Actors/Reentrancy/CallRecord.cs @@ -13,13 +13,12 @@ using System; -namespace Dapr.E2E.Test.Actors.Reentrancy -{ - public class CallRecord - { - public bool IsEnter { get; set; } - public DateTime Timestamp { get; set; } +namespace Dapr.E2E.Test.Actors.Reentrancy; - public int CallNumber { get; set; } - } -} +public class CallRecord +{ + public bool IsEnter { get; set; } + public DateTime Timestamp { get; set; } + + public int CallNumber { get; set; } +} \ No newline at end of file diff --git a/test/Dapr.E2E.Test.Actors/Reentrancy/IReentrantActor.cs b/test/Dapr.E2E.Test.Actors/Reentrancy/IReentrantActor.cs index 0e662b48..3b91ee76 100644 --- a/test/Dapr.E2E.Test.Actors/Reentrancy/IReentrantActor.cs +++ b/test/Dapr.E2E.Test.Actors/Reentrancy/IReentrantActor.cs @@ -14,12 +14,11 @@ using System.Threading.Tasks; using Dapr.Actors; -namespace Dapr.E2E.Test.Actors.Reentrancy -{ - public interface IReentrantActor : IPingActor, IActor - { - Task ReentrantCall(ReentrantCallOptions callOptions); +namespace Dapr.E2E.Test.Actors.Reentrancy; - Task GetState(int callNumber); - } +public interface IReentrantActor : IPingActor, IActor +{ + Task ReentrantCall(ReentrantCallOptions callOptions); + + Task GetState(int callNumber); } \ No newline at end of file diff --git a/test/Dapr.E2E.Test.Actors/Reentrancy/ReentrantCallOptions.cs b/test/Dapr.E2E.Test.Actors/Reentrancy/ReentrantCallOptions.cs index f66e20f7..0e7d032d 100644 --- a/test/Dapr.E2E.Test.Actors/Reentrancy/ReentrantCallOptions.cs +++ b/test/Dapr.E2E.Test.Actors/Reentrancy/ReentrantCallOptions.cs @@ -11,12 +11,11 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.E2E.Test.Actors.Reentrancy -{ - public class ReentrantCallOptions - { - public int CallsRemaining { get; set; } +namespace Dapr.E2E.Test.Actors.Reentrancy; - public int CallNumber { get; set; } - } +public class ReentrantCallOptions +{ + public int CallsRemaining { get; set; } + + public int CallNumber { get; set; } } \ No newline at end of file diff --git a/test/Dapr.E2E.Test.Actors/Reentrancy/State.cs b/test/Dapr.E2E.Test.Actors/Reentrancy/State.cs index 4a4c7a8c..ec29cc7c 100644 --- a/test/Dapr.E2E.Test.Actors/Reentrancy/State.cs +++ b/test/Dapr.E2E.Test.Actors/Reentrancy/State.cs @@ -13,10 +13,9 @@ using System.Collections.Generic; -namespace Dapr.E2E.Test.Actors.Reentrancy +namespace Dapr.E2E.Test.Actors.Reentrancy; + +public class State { - public class State - { - public List Records { get; set; } = new List(); - } -} + public List Records { get; set; } = new List(); +} \ No newline at end of file diff --git a/test/Dapr.E2E.Test.Actors/RegressionActor/IRegression762Actor.cs b/test/Dapr.E2E.Test.Actors/RegressionActor/IRegression762Actor.cs index 471dbf23..4a78f2fe 100644 --- a/test/Dapr.E2E.Test.Actors/RegressionActor/IRegression762Actor.cs +++ b/test/Dapr.E2E.Test.Actors/RegressionActor/IRegression762Actor.cs @@ -19,14 +19,13 @@ using Dapr.Actors; /// /// https://github.com/dapr/dotnet-sdk/issues/762 /// -namespace Dapr.E2E.Test.Actors.ErrorTesting +namespace Dapr.E2E.Test.Actors.ErrorTesting; + +public interface IRegression762Actor : IPingActor, IActor { - public interface IRegression762Actor : IPingActor, IActor - { - Task GetState(string id); + Task GetState(string id); - Task SaveState(StateCall call); + Task SaveState(StateCall call); - Task RemoveState(string id); - } -} + Task RemoveState(string id); +} \ No newline at end of file diff --git a/test/Dapr.E2E.Test.Actors/RegressionActor/StateCall.cs b/test/Dapr.E2E.Test.Actors/RegressionActor/StateCall.cs index 9dd55947..96ce19b5 100644 --- a/test/Dapr.E2E.Test.Actors/RegressionActor/StateCall.cs +++ b/test/Dapr.E2E.Test.Actors/RegressionActor/StateCall.cs @@ -11,12 +11,11 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.E2E.Test.Actors.ErrorTesting +namespace Dapr.E2E.Test.Actors.ErrorTesting; + +public class StateCall { - public class StateCall - { - public string Key { get; set; } - public string Value { get; set; } - public string Operation { get; set; } - } -} + public string Key { get; set; } + public string Value { get; set; } + public string Operation { get; set; } +} \ No newline at end of file diff --git a/test/Dapr.E2E.Test.Actors/Reminders/IReminderActor.cs b/test/Dapr.E2E.Test.Actors/Reminders/IReminderActor.cs index c0e3f86a..53bc4b92 100644 --- a/test/Dapr.E2E.Test.Actors/Reminders/IReminderActor.cs +++ b/test/Dapr.E2E.Test.Actors/Reminders/IReminderActor.cs @@ -15,20 +15,19 @@ using System; using System.Threading.Tasks; using Dapr.Actors; -namespace Dapr.E2E.Test.Actors.Reminders +namespace Dapr.E2E.Test.Actors.Reminders; + +public interface IReminderActor : IPingActor, IActor { - public interface IReminderActor : IPingActor, IActor - { - Task StartReminder(StartReminderOptions options); + Task StartReminder(StartReminderOptions options); - Task StartReminderWithTtl(TimeSpan ttl); + Task StartReminderWithTtl(TimeSpan ttl); - Task StartReminderWithRepetitions(int repetitions); + Task StartReminderWithRepetitions(int repetitions); - Task StartReminderWithTtlAndRepetitions(TimeSpan ttl, int repetitions); + Task StartReminderWithTtlAndRepetitions(TimeSpan ttl, int repetitions); - Task GetState(); + Task GetState(); - Task GetReminder(); - } -} + Task GetReminder(); +} \ No newline at end of file diff --git a/test/Dapr.E2E.Test.Actors/Reminders/StartReminderOptions.cs b/test/Dapr.E2E.Test.Actors/Reminders/StartReminderOptions.cs index b50d8ec2..8e13f5d7 100644 --- a/test/Dapr.E2E.Test.Actors/Reminders/StartReminderOptions.cs +++ b/test/Dapr.E2E.Test.Actors/Reminders/StartReminderOptions.cs @@ -11,10 +11,9 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.E2E.Test.Actors.Reminders +namespace Dapr.E2E.Test.Actors.Reminders; + +public class StartReminderOptions { - public class StartReminderOptions - { - public int Total { get; set; } - } -} + public int Total { get; set; } +} \ No newline at end of file diff --git a/test/Dapr.E2E.Test.Actors/Reminders/State.cs b/test/Dapr.E2E.Test.Actors/Reminders/State.cs index 04d45d41..f8a979c2 100644 --- a/test/Dapr.E2E.Test.Actors/Reminders/State.cs +++ b/test/Dapr.E2E.Test.Actors/Reminders/State.cs @@ -13,14 +13,13 @@ using System; -namespace Dapr.E2E.Test.Actors.Reminders +namespace Dapr.E2E.Test.Actors.Reminders; + +public class State { - public class State - { - public int Count { get; set; } + public int Count { get; set; } - public bool IsReminderRunning { get; set; } + public bool IsReminderRunning { get; set; } - public DateTime Timestamp { get; set; } - } -} + public DateTime Timestamp { get; set; } +} \ No newline at end of file diff --git a/test/Dapr.E2E.Test.Actors/State/IStateActor.cs b/test/Dapr.E2E.Test.Actors/State/IStateActor.cs index f1912210..3b63984a 100644 --- a/test/Dapr.E2E.Test.Actors/State/IStateActor.cs +++ b/test/Dapr.E2E.Test.Actors/State/IStateActor.cs @@ -15,12 +15,11 @@ using System; using System.Threading.Tasks; using Dapr.Actors; -namespace Dapr.E2E.Test.Actors.State -{ - public interface IStateActor : IPingActor, IActor - { - Task GetState(string key); +namespace Dapr.E2E.Test.Actors.State; - Task SetState(string key, string value, TimeSpan? ttl); - } -} +public interface IStateActor : IPingActor, IActor +{ + Task GetState(string key); + + Task SetState(string key, string value, TimeSpan? ttl); +} \ No newline at end of file diff --git a/test/Dapr.E2E.Test.Actors/Timers/ITimerActor.cs b/test/Dapr.E2E.Test.Actors/Timers/ITimerActor.cs index b02688fb..5b0dbf96 100644 --- a/test/Dapr.E2E.Test.Actors/Timers/ITimerActor.cs +++ b/test/Dapr.E2E.Test.Actors/Timers/ITimerActor.cs @@ -1,28 +1,27 @@ -// ------------------------------------------------------------------------ -// 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. -// 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. -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ +// 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. +// 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.Threading.Tasks; using Dapr.Actors; -namespace Dapr.E2E.Test.Actors.Timers +namespace Dapr.E2E.Test.Actors.Timers; + +public interface ITimerActor : IPingActor, IActor { - public interface ITimerActor : IPingActor, IActor - { - Task StartTimer(StartTimerOptions options); + Task StartTimer(StartTimerOptions options); - Task StartTimerWithTtl(TimeSpan ttl); + Task StartTimerWithTtl(TimeSpan ttl); - Task GetState(); - } -} + Task GetState(); +} \ No newline at end of file diff --git a/test/Dapr.E2E.Test.Actors/Timers/StartTimerOptions.cs b/test/Dapr.E2E.Test.Actors/Timers/StartTimerOptions.cs index d226a048..2d61f2f3 100644 --- a/test/Dapr.E2E.Test.Actors/Timers/StartTimerOptions.cs +++ b/test/Dapr.E2E.Test.Actors/Timers/StartTimerOptions.cs @@ -11,10 +11,9 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.E2E.Test.Actors.Timers +namespace Dapr.E2E.Test.Actors.Timers; + +public class StartTimerOptions { - public class StartTimerOptions - { - public int Total { get; set; } - } -} + public int Total { get; set; } +} \ No newline at end of file diff --git a/test/Dapr.E2E.Test.Actors/Timers/State.cs b/test/Dapr.E2E.Test.Actors/Timers/State.cs index d4705652..e0e7fa81 100644 --- a/test/Dapr.E2E.Test.Actors/Timers/State.cs +++ b/test/Dapr.E2E.Test.Actors/Timers/State.cs @@ -13,14 +13,13 @@ using System; -namespace Dapr.E2E.Test.Actors.Timers +namespace Dapr.E2E.Test.Actors.Timers; + +public class State { - public class State - { - public int Count { get; set; } + public int Count { get; set; } - public bool IsTimerRunning { get; set; } + public bool IsTimerRunning { get; set; } - public DateTime Timestamp { get; set; } - } -} + public DateTime Timestamp { get; set; } +} \ No newline at end of file diff --git a/test/Dapr.E2E.Test.Actors/WeaklyTypedTesting/DerivedResponse.cs b/test/Dapr.E2E.Test.Actors/WeaklyTypedTesting/DerivedResponse.cs index c11f547e..e019a53f 100644 --- a/test/Dapr.E2E.Test.Actors/WeaklyTypedTesting/DerivedResponse.cs +++ b/test/Dapr.E2E.Test.Actors/WeaklyTypedTesting/DerivedResponse.cs @@ -11,10 +11,9 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.E2E.Test.Actors.WeaklyTypedTesting +namespace Dapr.E2E.Test.Actors.WeaklyTypedTesting; + +public class DerivedResponse : ResponseBase { - public class DerivedResponse : ResponseBase - { - public string DerivedProperty { get; set; } - } -} + public string DerivedProperty { get; set; } +} \ No newline at end of file diff --git a/test/Dapr.E2E.Test.Actors/WeaklyTypedTesting/IWeaklyTypedTestingActor.cs b/test/Dapr.E2E.Test.Actors/WeaklyTypedTesting/IWeaklyTypedTestingActor.cs index 2c5d1e82..111ae971 100644 --- a/test/Dapr.E2E.Test.Actors/WeaklyTypedTesting/IWeaklyTypedTestingActor.cs +++ b/test/Dapr.E2E.Test.Actors/WeaklyTypedTesting/IWeaklyTypedTestingActor.cs @@ -14,12 +14,11 @@ using System.Threading.Tasks; using Dapr.Actors; -namespace Dapr.E2E.Test.Actors.WeaklyTypedTesting -{ - public interface IWeaklyTypedTestingActor : IPingActor, IActor - { - Task GetPolymorphicResponse(); +namespace Dapr.E2E.Test.Actors.WeaklyTypedTesting; - Task GetNullResponse(); - } -} +public interface IWeaklyTypedTestingActor : IPingActor, IActor +{ + Task GetPolymorphicResponse(); + + Task GetNullResponse(); +} \ No newline at end of file diff --git a/test/Dapr.E2E.Test.Actors/WeaklyTypedTesting/ResponseBase.cs b/test/Dapr.E2E.Test.Actors/WeaklyTypedTesting/ResponseBase.cs index 45736d80..bd697790 100644 --- a/test/Dapr.E2E.Test.Actors/WeaklyTypedTesting/ResponseBase.cs +++ b/test/Dapr.E2E.Test.Actors/WeaklyTypedTesting/ResponseBase.cs @@ -13,13 +13,11 @@ using System.Text.Json.Serialization; -namespace Dapr.E2E.Test.Actors.WeaklyTypedTesting -{ +namespace Dapr.E2E.Test.Actors.WeaklyTypedTesting; #if NET7_0_OR_GREATER - [JsonDerivedType(typeof(DerivedResponse), typeDiscriminator: nameof(DerivedResponse))] +[JsonDerivedType(typeof(DerivedResponse), typeDiscriminator: nameof(DerivedResponse))] #endif - public class ResponseBase - { - public string BasePropeprty { get; set; } - } -} +public class ResponseBase +{ + public string BasePropeprty { get; set; } +} \ No newline at end of file diff --git a/test/Dapr.E2E.Test.App.Grpc/MessageRepository.cs b/test/Dapr.E2E.Test.App.Grpc/MessageRepository.cs index fd70a11c..5de6bde9 100644 --- a/test/Dapr.E2E.Test.App.Grpc/MessageRepository.cs +++ b/test/Dapr.E2E.Test.App.Grpc/MessageRepository.cs @@ -13,36 +13,35 @@ using System.Collections.Concurrent; -namespace Dapr.E2E.Test +namespace Dapr.E2E.Test; + +public class MessageRepository { - public class MessageRepository + private readonly ConcurrentDictionary> messages; + + public MessageRepository() { - private readonly ConcurrentDictionary> messages; + this.messages = new ConcurrentDictionary>(); + } - public MessageRepository() + public void AddMessage(string recipient, string message) + { + if (!this.messages.ContainsKey(recipient)) { - this.messages = new ConcurrentDictionary>(); + this.messages.TryAdd(recipient, new ConcurrentQueue()); } + this.messages[recipient].Enqueue(message); + } - public void AddMessage(string recipient, string message) + public string GetMessage(string recipient) + { + if (this.messages.TryGetValue(recipient, out var messages) && !messages.IsEmpty) { - if (!this.messages.ContainsKey(recipient)) + if (messages.TryDequeue(out var message)) { - this.messages.TryAdd(recipient, new ConcurrentQueue()); + return message; } - this.messages[recipient].Enqueue(message); - } - - public string GetMessage(string recipient) - { - if (this.messages.TryGetValue(recipient, out var messages) && !messages.IsEmpty) - { - if (messages.TryDequeue(out var message)) - { - return message; - } - } - return string.Empty; } + return string.Empty; } } \ No newline at end of file diff --git a/test/Dapr.E2E.Test.App.Grpc/Program.cs b/test/Dapr.E2E.Test.App.Grpc/Program.cs index 9ea64dc2..7d7543a0 100644 --- a/test/Dapr.E2E.Test.App.Grpc/Program.cs +++ b/test/Dapr.E2E.Test.App.Grpc/Program.cs @@ -1,41 +1,40 @@ -// ------------------------------------------------------------------------ -// 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. -// 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. -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ +// 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. +// 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.Security.Authentication; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.Hosting; -namespace Dapr.E2E.Test.App.Grpc -{ - public class Program - { - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } +namespace Dapr.E2E.Test.App.Grpc; - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseKestrel(options => { - options.ListenLocalhost(int.Parse(args[0]), o => o.Protocols = HttpProtocols.Http2); - options.ConfigureHttpsDefaults(httpOptions => { - httpOptions.SslProtocols = SslProtocols.Tls12; - }); - }); - webBuilder.UseStartup(); - }); +public class Program +{ + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); } -} + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseKestrel(options => { + options.ListenLocalhost(int.Parse(args[0]), o => o.Protocols = HttpProtocols.Http2); + options.ConfigureHttpsDefaults(httpOptions => { + httpOptions.SslProtocols = SslProtocols.Tls12; + }); + }); + webBuilder.UseStartup(); + }); +} \ No newline at end of file diff --git a/test/Dapr.E2E.Test.App.Grpc/Services/MessagerService.cs b/test/Dapr.E2E.Test.App.Grpc/Services/MessagerService.cs index 95066893..cfa6e043 100644 --- a/test/Dapr.E2E.Test.App.Grpc/Services/MessagerService.cs +++ b/test/Dapr.E2E.Test.App.Grpc/Services/MessagerService.cs @@ -16,40 +16,39 @@ using System.Threading.Tasks; using Google.Protobuf.WellKnownTypes; using Grpc.Core; -namespace Dapr.E2E.Test +namespace Dapr.E2E.Test; + +public class MessagerService : Messager.MessagerBase { - public class MessagerService : Messager.MessagerBase + private readonly MessageRepository repository; + + public MessagerService(MessageRepository repository) { - private readonly MessageRepository repository; + this.repository = repository; + } - public MessagerService(MessageRepository repository) - { - this.repository = repository; - } + public override Task SendMessage(SendMessageRequest request, ServerCallContext context) + { + this.repository.AddMessage(request.Recipient, request.Message); + return Task.FromResult(new Empty()); + } - public override Task SendMessage(SendMessageRequest request, ServerCallContext context) - { - this.repository.AddMessage(request.Recipient, request.Message); - return Task.FromResult(new Empty()); - } + public override Task GetMessage(GetMessageRequest request, ServerCallContext context) + { + return Task.FromResult(new MessageResponse { Message = this.repository.GetMessage(request.Recipient) }); + } - public override Task GetMessage(GetMessageRequest request, ServerCallContext context) + public override async Task StreamBroadcast(IAsyncStreamReader requestStream, IServerStreamWriter responseStream, ServerCallContext context) + { + await foreach(var request in requestStream.ReadAllAsync()) { - return Task.FromResult(new MessageResponse { Message = this.repository.GetMessage(request.Recipient) }); - } - - public override async Task StreamBroadcast(IAsyncStreamReader requestStream, IServerStreamWriter responseStream, ServerCallContext context) - { - await foreach(var request in requestStream.ReadAllAsync()) - { - await responseStream.WriteAsync(new MessageResponse { Message = request.Message }); - } - } - - public override async Task DelayedResponse(Empty request, ServerCallContext context) - { - await Task.Delay(TimeSpan.FromSeconds(2)); - return new Empty(); + await responseStream.WriteAsync(new MessageResponse { Message = request.Message }); } } + + public override async Task DelayedResponse(Empty request, ServerCallContext context) + { + await Task.Delay(TimeSpan.FromSeconds(2)); + return new Empty(); + } } \ No newline at end of file diff --git a/test/Dapr.E2E.Test.App.Grpc/Startup.cs b/test/Dapr.E2E.Test.App.Grpc/Startup.cs index 651f0c76..aac2dc70 100644 --- a/test/Dapr.E2E.Test.App.Grpc/Startup.cs +++ b/test/Dapr.E2E.Test.App.Grpc/Startup.cs @@ -16,32 +16,31 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -namespace Dapr.E2E.Test.App.Grpc +namespace Dapr.E2E.Test.App.Grpc; + +public class Startup { - public class Startup + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) { - // This method gets called by the runtime. Use this method to add services to the container. - // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 - public void ConfigureServices(IServiceCollection services) - { - services.AddGrpc(); - services.AddSingleton(); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.UseRouting(); - - app.UseEndpoints(endpoints => - { - endpoints.MapGrpcService(); - }); - } + services.AddGrpc(); + services.AddSingleton(); } -} + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseRouting(); + + app.UseEndpoints(endpoints => + { + endpoints.MapGrpcService(); + }); + } +} \ No newline at end of file diff --git a/test/Dapr.E2E.Test.App.ReentrantActor/Actors/ReentrantActor.cs b/test/Dapr.E2E.Test.App.ReentrantActor/Actors/ReentrantActor.cs index 58776fe2..f59d96a5 100644 --- a/test/Dapr.E2E.Test.App.ReentrantActor/Actors/ReentrantActor.cs +++ b/test/Dapr.E2E.Test.App.ReentrantActor/Actors/ReentrantActor.cs @@ -14,57 +14,56 @@ using System.Threading.Tasks; using Dapr.Actors.Runtime; -namespace Dapr.E2E.Test.Actors.Reentrancy +namespace Dapr.E2E.Test.Actors.Reentrancy; + +public class ReentrantActor : Actor, IReentrantActor { - public class ReentrantActor : Actor, IReentrantActor + public ReentrantActor(ActorHost host) + : base(host) { - public ReentrantActor(ActorHost host) - : base(host) - { + } + + public Task Ping() + { + return Task.CompletedTask; + } + + // An actor method that exercises reentrancy by calling more methods in the same actor. + // Can be configured to different reentrant depths via the ReentrantCallOptions but will + // always make at least one additional call. + public async Task ReentrantCall(ReentrantCallOptions callOptions) + { + await UpdateState(true, callOptions.CallNumber); + var actor = this.ProxyFactory.CreateActorProxy(this.Id, "ReentrantActor"); + if (callOptions == null || callOptions.CallsRemaining <= 1) + { + await actor.Ping(); } - - public Task Ping() + else { - return Task.CompletedTask; + await actor.ReentrantCall(new ReentrantCallOptions + { + CallsRemaining = callOptions.CallsRemaining - 1, + CallNumber = callOptions.CallNumber + 1, + }); } + await UpdateState(false, callOptions.CallNumber); + } - // An actor method that exercises reentrancy by calling more methods in the same actor. - // Can be configured to different reentrant depths via the ReentrantCallOptions but will - // always make at least one additional call. - public async Task ReentrantCall(ReentrantCallOptions callOptions) + public Task GetState(int callNumber) + { + return this.StateManager.GetOrAddStateAsync($"reentrant-record{callNumber}", new State()); + } + + private async Task UpdateState(bool isEnter, int callNumber) + { + var state = await this.StateManager.GetOrAddStateAsync($"reentrant-record{callNumber}", new State()); + state.Records.Add(new CallRecord { IsEnter = isEnter, Timestamp = System.DateTime.Now, CallNumber = callNumber }); + await this.StateManager.SetStateAsync($"reentrant-record{callNumber}", state); + + if (!isEnter) { - await UpdateState(true, callOptions.CallNumber); - var actor = this.ProxyFactory.CreateActorProxy(this.Id, "ReentrantActor"); - if (callOptions == null || callOptions.CallsRemaining <= 1) - { - await actor.Ping(); - } - else - { - await actor.ReentrantCall(new ReentrantCallOptions - { - CallsRemaining = callOptions.CallsRemaining - 1, - CallNumber = callOptions.CallNumber + 1, - }); - } - await UpdateState(false, callOptions.CallNumber); - } - - public Task GetState(int callNumber) - { - return this.StateManager.GetOrAddStateAsync($"reentrant-record{callNumber}", new State()); - } - - private async Task UpdateState(bool isEnter, int callNumber) - { - var state = await this.StateManager.GetOrAddStateAsync($"reentrant-record{callNumber}", new State()); - state.Records.Add(new CallRecord { IsEnter = isEnter, Timestamp = System.DateTime.Now, CallNumber = callNumber }); - await this.StateManager.SetStateAsync($"reentrant-record{callNumber}", state); - - if (!isEnter) - { - await this.StateManager.SaveStateAsync(); - } + await this.StateManager.SaveStateAsync(); } } -} +} \ No newline at end of file diff --git a/test/Dapr.E2E.Test.App.ReentrantActor/Program.cs b/test/Dapr.E2E.Test.App.ReentrantActor/Program.cs index 7914b695..b6af22e6 100644 --- a/test/Dapr.E2E.Test.App.ReentrantActor/Program.cs +++ b/test/Dapr.E2E.Test.App.ReentrantActor/Program.cs @@ -14,20 +14,19 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; -namespace Dapr.E2E.Test.App.ReentrantActors -{ - public class Program - { - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } +namespace Dapr.E2E.Test.App.ReentrantActors; - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }); +public class Program +{ + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); } -} + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); +} \ No newline at end of file diff --git a/test/Dapr.E2E.Test.App.ReentrantActor/Startup.cs b/test/Dapr.E2E.Test.App.ReentrantActor/Startup.cs index 89d07e23..bffc6591 100644 --- a/test/Dapr.E2E.Test.App.ReentrantActor/Startup.cs +++ b/test/Dapr.E2E.Test.App.ReentrantActor/Startup.cs @@ -1,14 +1,14 @@ -// ------------------------------------------------------------------------ -// 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. -// 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. +// ------------------------------------------------------------------------ +// 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. +// 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.E2E.Test.Actors.Reentrancy; @@ -17,42 +17,41 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -namespace Dapr.E2E.Test.App.ReentrantActors +namespace Dapr.E2E.Test.App.ReentrantActors; + +public class Startup { - public class Startup + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) { - // This method gets called by the runtime. Use this method to add services to the container. - // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 - public void ConfigureServices(IServiceCollection services) + services.AddActors(options => { - services.AddActors(options => - { - // We force this to use a per-actor config as an easy way to validate that's working. - options.ReentrancyConfig = new() { Enabled = false }; - options.Actors.RegisterActor(typeOptions: new() - { - ReentrancyConfig = new() - { - Enabled = true, - } - }); - }); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - if (env.IsDevelopment()) + // We force this to use a per-actor config as an easy way to validate that's working. + options.ReentrancyConfig = new() { Enabled = false }; + options.Actors.RegisterActor(typeOptions: new() { - app.UseDeveloperExceptionPage(); - } - - app.UseRouting(); - - app.UseEndpoints(endpoints => - { - endpoints.MapActorsHandlers(); + ReentrancyConfig = new() + { + Enabled = true, + } }); - } + }); } -} + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseRouting(); + + app.UseEndpoints(endpoints => + { + endpoints.MapActorsHandlers(); + }); + } +} \ No newline at end of file diff --git a/test/Dapr.E2E.Test.App/Account.cs b/test/Dapr.E2E.Test.App/Account.cs index 7ba1ce11..3e789fe0 100644 --- a/test/Dapr.E2E.Test.App/Account.cs +++ b/test/Dapr.E2E.Test.App/Account.cs @@ -11,21 +11,20 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.E2E.Test +namespace Dapr.E2E.Test; + +/// +/// Class representing an Account for samples. +/// +public class Account { /// - /// Class representing an Account for samples. + /// Gets or sets account id. /// - public class Account - { - /// - /// Gets or sets account id. - /// - public string Id { get; set; } + public string Id { get; set; } - /// - /// Gets or sets account balance. - /// - public decimal Balance { get; set; } - } + /// + /// Gets or sets account balance. + /// + public decimal Balance { get; set; } } \ No newline at end of file diff --git a/test/Dapr.E2E.Test.App/Actors/ExceptionActor.cs b/test/Dapr.E2E.Test.App/Actors/ExceptionActor.cs index 7dbb5bb8..d2f1ed8b 100644 --- a/test/Dapr.E2E.Test.App/Actors/ExceptionActor.cs +++ b/test/Dapr.E2E.Test.App/Actors/ExceptionActor.cs @@ -13,23 +13,22 @@ using Dapr.Actors.Runtime; using System.Threading.Tasks; -namespace Dapr.E2E.Test.Actors.ExceptionTesting +namespace Dapr.E2E.Test.Actors.ExceptionTesting; + +public class ExceptionActor : Actor, IExceptionActor { - public class ExceptionActor : Actor, IExceptionActor + public ExceptionActor(ActorHost host) + : base(host) { - public ExceptionActor(ActorHost host) - : base(host) - { - } + } - public Task Ping() - { - return Task.CompletedTask; - } + public Task Ping() + { + return Task.CompletedTask; + } - public Task ExceptionExample() - { - throw new System.Exception(); - } + public Task ExceptionExample() + { + throw new System.Exception(); } } \ No newline at end of file diff --git a/test/Dapr.E2E.Test.App/Actors/Regression762Actor.cs b/test/Dapr.E2E.Test.App/Actors/Regression762Actor.cs index 8e752475..602e6598 100644 --- a/test/Dapr.E2E.Test.App/Actors/Regression762Actor.cs +++ b/test/Dapr.E2E.Test.App/Actors/Regression762Actor.cs @@ -16,50 +16,49 @@ using System.Threading.Tasks; using Dapr.Actors.Runtime; using Dapr.E2E.Test.Actors.ErrorTesting; -namespace Dapr.E2E.Test.App.ErrorTesting +namespace Dapr.E2E.Test.App.ErrorTesting; + +public class Regression762Actor : Actor, IRegression762Actor { - public class Regression762Actor : Actor, IRegression762Actor + public Regression762Actor(ActorHost host) : base(host) { - public Regression762Actor(ActorHost host) : base(host) + } + + public Task Ping() + { + return Task.CompletedTask; + } + + public async Task GetState(string id) + { + var data = await this.StateManager.TryGetStateAsync(id); + + if (data.HasValue) { + return data.Value; } + return string.Empty; + } - public Task Ping() + public async Task RemoveState(string id) + { + await this.StateManager.TryRemoveStateAsync(id); + } + + public async Task SaveState(StateCall call) + { + if (call.Operation == "ThrowException") { - return Task.CompletedTask; + await this.StateManager.SetStateAsync(call.Key, call.Value); + throw new NotImplementedException(); } - - public async Task GetState(string id) + else if (call.Operation == "SetState") { - var data = await this.StateManager.TryGetStateAsync(id); - - if (data.HasValue) - { - return data.Value; - } - return string.Empty; - } - - public async Task RemoveState(string id) - { - await this.StateManager.TryRemoveStateAsync(id); + await this.StateManager.SetStateAsync(call.Key, call.Value); } - - public async Task SaveState(StateCall call) + else if (call.Operation == "SaveState") { - if (call.Operation == "ThrowException") - { - await this.StateManager.SetStateAsync(call.Key, call.Value); - throw new NotImplementedException(); - } - else if (call.Operation == "SetState") - { - await this.StateManager.SetStateAsync(call.Key, call.Value); - } - else if (call.Operation == "SaveState") - { - await this.StateManager.SaveStateAsync(); - } + await this.StateManager.SaveStateAsync(); } } -} +} \ No newline at end of file diff --git a/test/Dapr.E2E.Test.App/Actors/ReminderActor.cs b/test/Dapr.E2E.Test.App/Actors/ReminderActor.cs index 57536377..33ebd81f 100644 --- a/test/Dapr.E2E.Test.App/Actors/ReminderActor.cs +++ b/test/Dapr.E2E.Test.App/Actors/ReminderActor.cs @@ -16,93 +16,92 @@ using System.Text.Json; using System.Threading.Tasks; using Dapr.Actors.Runtime; -namespace Dapr.E2E.Test.Actors.Reminders +namespace Dapr.E2E.Test.Actors.Reminders; + +public class ReminderActor : Actor, IReminderActor, IRemindable { - public class ReminderActor : Actor, IReminderActor, IRemindable + public ReminderActor(ActorHost host) + : base(host) { - public ReminderActor(ActorHost host) - : base(host) - { - } - - public Task Ping() - { - return Task.CompletedTask; - } - - public Task GetState() - { - return this.StateManager.GetOrAddStateAsync("reminder-state", new State()); - } - - // Starts a reminder that will fire N times before stopping itself - public async Task StartReminder(StartReminderOptions options) - { - var bytes = JsonSerializer.SerializeToUtf8Bytes(options, this.Host.JsonSerializerOptions); - await this.RegisterReminderAsync("test-reminder", bytes, dueTime: TimeSpan.Zero, period: TimeSpan.FromMilliseconds(50)); - - await this.StateManager.SetStateAsync("reminder-state", new State(){ IsReminderRunning = true, }); - } - - public async Task GetReminder(){ - var reminder = await this.GetReminderAsync("test-reminder"); - var reminderString = JsonSerializer.Serialize(reminder, this.Host.JsonSerializerOptions); - return reminderString; - } - - public async Task StartReminderWithTtl(TimeSpan ttl) - { - var options = new StartReminderOptions() - { - Total = 100, - }; - var bytes = JsonSerializer.SerializeToUtf8Bytes(options, this.Host.JsonSerializerOptions); - await this.RegisterReminderAsync("test-reminder-ttl", bytes, dueTime: TimeSpan.Zero, period: TimeSpan.FromSeconds(1), ttl: ttl); - await this.StateManager.SetStateAsync("reminder-state", new State() { IsReminderRunning = true, }); - } - - public async Task StartReminderWithRepetitions(int repetitions) - { - var options = new StartReminderOptions() - { - Total = 100, - }; - var bytes = JsonSerializer.SerializeToUtf8Bytes(options, this.Host.JsonSerializerOptions); - await this.RegisterReminderAsync("test-reminder-repetition", bytes, dueTime: TimeSpan.Zero, - period: TimeSpan.FromSeconds(1), repetitions: repetitions); - await this.StateManager.SetStateAsync("reminder-state", new State() - { IsReminderRunning = true, }); - } - - public async Task StartReminderWithTtlAndRepetitions(TimeSpan ttl, int repetitions) - { - var options = new StartReminderOptions() - { - Total = 100, - }; - var bytes = JsonSerializer.SerializeToUtf8Bytes(options, this.Host.JsonSerializerOptions); - await this.RegisterReminderAsync("test-reminder-ttl-repetition", bytes, dueTime: TimeSpan.Zero, - period: TimeSpan.FromSeconds(1), repetitions: repetitions, ttl: ttl); - await this.StateManager.SetStateAsync("reminder-state", new State() - { IsReminderRunning = true, }); - } - - public async Task ReceiveReminderAsync(string reminderName, byte[] bytes, TimeSpan dueTime, TimeSpan period) - { - if (!reminderName.StartsWith("test-reminder")) - { - return; - } - var options = JsonSerializer.Deserialize(bytes, this.Host.JsonSerializerOptions); - var state = await this.StateManager.GetStateAsync("reminder-state"); - - if (++state.Count == options.Total) - { - await this.UnregisterReminderAsync("test-reminder"); - state.IsReminderRunning = false; - } - state.Timestamp = DateTime.Now; - await this.StateManager.SetStateAsync("reminder-state", state); - } } -} + + public Task Ping() + { + return Task.CompletedTask; + } + + public Task GetState() + { + return this.StateManager.GetOrAddStateAsync("reminder-state", new State()); + } + + // Starts a reminder that will fire N times before stopping itself + public async Task StartReminder(StartReminderOptions options) + { + var bytes = JsonSerializer.SerializeToUtf8Bytes(options, this.Host.JsonSerializerOptions); + await this.RegisterReminderAsync("test-reminder", bytes, dueTime: TimeSpan.Zero, period: TimeSpan.FromMilliseconds(50)); + + await this.StateManager.SetStateAsync("reminder-state", new State(){ IsReminderRunning = true, }); + } + + public async Task GetReminder(){ + var reminder = await this.GetReminderAsync("test-reminder"); + var reminderString = JsonSerializer.Serialize(reminder, this.Host.JsonSerializerOptions); + return reminderString; + } + + public async Task StartReminderWithTtl(TimeSpan ttl) + { + var options = new StartReminderOptions() + { + Total = 100, + }; + var bytes = JsonSerializer.SerializeToUtf8Bytes(options, this.Host.JsonSerializerOptions); + await this.RegisterReminderAsync("test-reminder-ttl", bytes, dueTime: TimeSpan.Zero, period: TimeSpan.FromSeconds(1), ttl: ttl); + await this.StateManager.SetStateAsync("reminder-state", new State() { IsReminderRunning = true, }); + } + + public async Task StartReminderWithRepetitions(int repetitions) + { + var options = new StartReminderOptions() + { + Total = 100, + }; + var bytes = JsonSerializer.SerializeToUtf8Bytes(options, this.Host.JsonSerializerOptions); + await this.RegisterReminderAsync("test-reminder-repetition", bytes, dueTime: TimeSpan.Zero, + period: TimeSpan.FromSeconds(1), repetitions: repetitions); + await this.StateManager.SetStateAsync("reminder-state", new State() + { IsReminderRunning = true, }); + } + + public async Task StartReminderWithTtlAndRepetitions(TimeSpan ttl, int repetitions) + { + var options = new StartReminderOptions() + { + Total = 100, + }; + var bytes = JsonSerializer.SerializeToUtf8Bytes(options, this.Host.JsonSerializerOptions); + await this.RegisterReminderAsync("test-reminder-ttl-repetition", bytes, dueTime: TimeSpan.Zero, + period: TimeSpan.FromSeconds(1), repetitions: repetitions, ttl: ttl); + await this.StateManager.SetStateAsync("reminder-state", new State() + { IsReminderRunning = true, }); + } + + public async Task ReceiveReminderAsync(string reminderName, byte[] bytes, TimeSpan dueTime, TimeSpan period) + { + if (!reminderName.StartsWith("test-reminder")) + { + return; + } + var options = JsonSerializer.Deserialize(bytes, this.Host.JsonSerializerOptions); + var state = await this.StateManager.GetStateAsync("reminder-state"); + + if (++state.Count == options.Total) + { + await this.UnregisterReminderAsync("test-reminder"); + state.IsReminderRunning = false; + } + state.Timestamp = DateTime.Now; + await this.StateManager.SetStateAsync("reminder-state", state); + } +} \ No newline at end of file diff --git a/test/Dapr.E2E.Test.App/Actors/SerializationActor.cs b/test/Dapr.E2E.Test.App/Actors/SerializationActor.cs index 09e650fa..1614eb06 100644 --- a/test/Dapr.E2E.Test.App/Actors/SerializationActor.cs +++ b/test/Dapr.E2E.Test.App/Actors/SerializationActor.cs @@ -4,28 +4,27 @@ using System.Threading; using System.Threading.Tasks; using Dapr.Actors.Runtime; -namespace Dapr.E2E.Test.Actors.Serialization +namespace Dapr.E2E.Test.Actors.Serialization; + +public class SerializationActor : Actor, ISerializationActor { - public class SerializationActor : Actor, ISerializationActor + public SerializationActor(ActorHost host) + : base(host) { - public SerializationActor(ActorHost host) - : base(host) - { - } - - public Task Ping() - { - return Task.CompletedTask; - } - - public Task SendAsync(string name, - SerializationPayload payload, CancellationToken cancellationToken = default) - { - return Task.FromResult(payload); - } - - public Task AnotherMethod(DateTime payload){ - return Task.FromResult(payload); - } } -} + + public Task Ping() + { + return Task.CompletedTask; + } + + public Task SendAsync(string name, + SerializationPayload payload, CancellationToken cancellationToken = default) + { + return Task.FromResult(payload); + } + + public Task AnotherMethod(DateTime payload){ + return Task.FromResult(payload); + } +} \ No newline at end of file diff --git a/test/Dapr.E2E.Test.App/Actors/StateActor.cs b/test/Dapr.E2E.Test.App/Actors/StateActor.cs index 76e9745f..22e91314 100644 --- a/test/Dapr.E2E.Test.App/Actors/StateActor.cs +++ b/test/Dapr.E2E.Test.App/Actors/StateActor.cs @@ -15,32 +15,31 @@ using System; using System.Threading.Tasks; using Dapr.Actors.Runtime; -namespace Dapr.E2E.Test.Actors.State +namespace Dapr.E2E.Test.Actors.State; + +public class StateActor : Actor, IStateActor { - public class StateActor : Actor, IStateActor + public StateActor(ActorHost host) + : base(host) { - public StateActor(ActorHost host) - : base(host) - { - } - - public Task Ping() - { - return Task.CompletedTask; - } - - public Task GetState(string key) - { - return this.StateManager.GetStateAsync(key); - } - - public Task SetState(string key, string value, TimeSpan? ttl) - { - if (ttl.HasValue) - { - return this.StateManager.SetStateAsync(key, value, ttl: ttl.Value); - } - return this.StateManager.SetStateAsync(key, value); - } } -} + + public Task Ping() + { + return Task.CompletedTask; + } + + public Task GetState(string key) + { + return this.StateManager.GetStateAsync(key); + } + + public Task SetState(string key, string value, TimeSpan? ttl) + { + if (ttl.HasValue) + { + return this.StateManager.SetStateAsync(key, value, ttl: ttl.Value); + } + return this.StateManager.SetStateAsync(key, value); + } +} \ No newline at end of file diff --git a/test/Dapr.E2E.Test.App/Actors/TimerActor.cs b/test/Dapr.E2E.Test.App/Actors/TimerActor.cs index 14b9fef4..76b4fe89 100644 --- a/test/Dapr.E2E.Test.App/Actors/TimerActor.cs +++ b/test/Dapr.E2E.Test.App/Actors/TimerActor.cs @@ -16,57 +16,56 @@ using System.Text.Json; using System.Threading.Tasks; using Dapr.Actors.Runtime; -namespace Dapr.E2E.Test.Actors.Timers +namespace Dapr.E2E.Test.Actors.Timers; + +public class TimerActor : Actor, ITimerActor { - public class TimerActor : Actor, ITimerActor + public TimerActor(ActorHost host) + : base(host) { - public TimerActor(ActorHost host) - : base(host) - { - } - - public Task Ping() - { - return Task.CompletedTask; - } - - public Task GetState() - { - return this.StateManager.GetOrAddStateAsync("timer-state", new State()); - } - - // Starts a timer that will fire N times before stopping itself - public async Task StartTimer(StartTimerOptions options) - { - var bytes = JsonSerializer.SerializeToUtf8Bytes(options, this.Host.JsonSerializerOptions); - await this.RegisterTimerAsync("test-timer", nameof(Tick), bytes, dueTime: TimeSpan.Zero, period: TimeSpan.FromMilliseconds(100)); - - await this.StateManager.SetStateAsync("timer-state", new State(){ IsTimerRunning = true, }); - } - - public async Task StartTimerWithTtl(TimeSpan ttl) - { - var options = new StartTimerOptions() - { - Total = 100, - }; - var bytes = JsonSerializer.SerializeToUtf8Bytes(options, this.Host.JsonSerializerOptions); - await this.RegisterTimerAsync("test-timer-ttl", nameof(Tick), bytes, TimeSpan.Zero, TimeSpan.FromSeconds(1), ttl); - await this.StateManager.SetStateAsync("timer-state", new State() { IsTimerRunning = true, }); - } - - private async Task Tick(byte[] bytes) - { - var options = JsonSerializer.Deserialize(bytes, this.Host.JsonSerializerOptions); - var state = await this.StateManager.GetStateAsync("timer-state"); - - if (++state.Count == options.Total) - { - await this.UnregisterTimerAsync("test-timer"); - state.IsTimerRunning = false; - } - state.Timestamp = DateTime.Now; - await this.StateManager.SetStateAsync("timer-state", state); - } } -} + + public Task Ping() + { + return Task.CompletedTask; + } + + public Task GetState() + { + return this.StateManager.GetOrAddStateAsync("timer-state", new State()); + } + + // Starts a timer that will fire N times before stopping itself + public async Task StartTimer(StartTimerOptions options) + { + var bytes = JsonSerializer.SerializeToUtf8Bytes(options, this.Host.JsonSerializerOptions); + await this.RegisterTimerAsync("test-timer", nameof(Tick), bytes, dueTime: TimeSpan.Zero, period: TimeSpan.FromMilliseconds(100)); + + await this.StateManager.SetStateAsync("timer-state", new State(){ IsTimerRunning = true, }); + } + + public async Task StartTimerWithTtl(TimeSpan ttl) + { + var options = new StartTimerOptions() + { + Total = 100, + }; + var bytes = JsonSerializer.SerializeToUtf8Bytes(options, this.Host.JsonSerializerOptions); + await this.RegisterTimerAsync("test-timer-ttl", nameof(Tick), bytes, TimeSpan.Zero, TimeSpan.FromSeconds(1), ttl); + await this.StateManager.SetStateAsync("timer-state", new State() { IsTimerRunning = true, }); + } + + private async Task Tick(byte[] bytes) + { + var options = JsonSerializer.Deserialize(bytes, this.Host.JsonSerializerOptions); + var state = await this.StateManager.GetStateAsync("timer-state"); + + if (++state.Count == options.Total) + { + await this.UnregisterTimerAsync("test-timer"); + state.IsTimerRunning = false; + } + state.Timestamp = DateTime.Now; + await this.StateManager.SetStateAsync("timer-state", state); + } +} \ No newline at end of file diff --git a/test/Dapr.E2E.Test.App/Actors/WeaklyTypedTestingActor.cs b/test/Dapr.E2E.Test.App/Actors/WeaklyTypedTestingActor.cs index 3cbe20ba..23015f99 100644 --- a/test/Dapr.E2E.Test.App/Actors/WeaklyTypedTestingActor.cs +++ b/test/Dapr.E2E.Test.App/Actors/WeaklyTypedTestingActor.cs @@ -14,34 +14,33 @@ using System.Threading.Tasks; using Dapr.Actors.Runtime; -namespace Dapr.E2E.Test.Actors.WeaklyTypedTesting +namespace Dapr.E2E.Test.Actors.WeaklyTypedTesting; + +public class WeaklyTypedTestingActor : Actor, IWeaklyTypedTestingActor { - public class WeaklyTypedTestingActor : Actor, IWeaklyTypedTestingActor + public WeaklyTypedTestingActor(ActorHost host) + : base(host) { - public WeaklyTypedTestingActor(ActorHost host) - : base(host) - { - } - - public Task GetNullResponse() - { - return Task.FromResult(null); - } - - public Task GetPolymorphicResponse() - { - var response = new DerivedResponse - { - BasePropeprty = "Base property value", - DerivedProperty = "Derived property value" - }; - - return Task.FromResult(response); - } - - public Task Ping() - { - return Task.CompletedTask; - } } -} + + public Task GetNullResponse() + { + return Task.FromResult(null); + } + + public Task GetPolymorphicResponse() + { + var response = new DerivedResponse + { + BasePropeprty = "Base property value", + DerivedProperty = "Derived property value" + }; + + return Task.FromResult(response); + } + + public Task Ping() + { + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/test/Dapr.E2E.Test.App/Controllers/TestController.cs b/test/Dapr.E2E.Test.App/Controllers/TestController.cs index 1f9274d0..628b4b71 100644 --- a/test/Dapr.E2E.Test.App/Controllers/TestController.cs +++ b/test/Dapr.E2E.Test.App/Controllers/TestController.cs @@ -11,66 +11,65 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.E2E.Test +namespace Dapr.E2E.Test; + +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +/// +/// Test App invoked by the end-to-end tests +/// +[ApiController] +public class TestController : ControllerBase { - using System; - using System.Threading.Tasks; - using Microsoft.AspNetCore.Authorization; - using Microsoft.AspNetCore.Mvc; - using Microsoft.Extensions.Logging; + /// + /// TestController Constructor with logger injection + /// + /// + public TestController(ILogger logger) + { + this.logger = logger; + } + + private readonly ILogger logger; /// - /// Test App invoked by the end-to-end tests + /// Returns the account details /// - [ApiController] - public class TestController : ControllerBase + /// Transaction to process. + /// Account + [HttpPost("accountDetails")] + public ActionResult AccountDetails(Transaction transaction) { - /// - /// TestController Constructor with logger injection - /// - /// - public TestController(ILogger logger) + var account = new Account() { - this.logger = logger; - } - - private readonly ILogger logger; - - /// - /// Returns the account details - /// - /// Transaction to process. - /// Account - [HttpPost("accountDetails")] - public ActionResult AccountDetails(Transaction transaction) - { - var account = new Account() - { - Id = transaction.Id, - Balance = transaction.Amount + 100 - }; - return account; - } - - [Authorize("Dapr")] - [HttpPost("accountDetails-requires-api-token")] - public ActionResult AccountDetailsRequiresApiToken(Transaction transaction) - { - var account = new Account() - { - Id = transaction.Id, - Balance = transaction.Amount + 100 - }; - return account; - } - - [Authorize("Dapr")] - [HttpGet("DelayedResponse")] - public async Task DelayedResponse() - { - await Task.Delay(TimeSpan.FromSeconds(2)); - return Ok(); - } - + Id = transaction.Id, + Balance = transaction.Amount + 100 + }; + return account; } -} + + [Authorize("Dapr")] + [HttpPost("accountDetails-requires-api-token")] + public ActionResult AccountDetailsRequiresApiToken(Transaction transaction) + { + var account = new Account() + { + Id = transaction.Id, + Balance = transaction.Amount + 100 + }; + return account; + } + + [Authorize("Dapr")] + [HttpGet("DelayedResponse")] + public async Task DelayedResponse() + { + await Task.Delay(TimeSpan.FromSeconds(2)); + return Ok(); + } + +} \ No newline at end of file diff --git a/test/Dapr.E2E.Test.App/Program.cs b/test/Dapr.E2E.Test.App/Program.cs index 5713129d..9616f5c8 100644 --- a/test/Dapr.E2E.Test.App/Program.cs +++ b/test/Dapr.E2E.Test.App/Program.cs @@ -10,24 +10,23 @@ // See the License for the specific language governing permissions and // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.E2E.Test -{ - using Microsoft.AspNetCore.Hosting; - using Microsoft.Extensions.Hosting; - using Serilog; - public class Program - { - public static void Main(string[] args) - { - Log.Logger = new LoggerConfiguration().WriteTo.File("log.txt").CreateLogger(); - CreateHostBuilder(args).Build().Run(); - } +namespace Dapr.E2E.Test; - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }).UseSerilog(); +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; +using Serilog; +public class Program +{ + public static void Main(string[] args) + { + Log.Logger = new LoggerConfiguration().WriteTo.File("log.txt").CreateLogger(); + CreateHostBuilder(args).Build().Run(); } -} + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }).UseSerilog(); +} \ No newline at end of file diff --git a/test/Dapr.E2E.Test.App/Startup.cs b/test/Dapr.E2E.Test.App/Startup.cs index 19de7971..56a4358a 100644 --- a/test/Dapr.E2E.Test.App/Startup.cs +++ b/test/Dapr.E2E.Test.App/Startup.cs @@ -11,158 +11,157 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.E2E.Test +namespace Dapr.E2E.Test; + +using Dapr.E2E.Test.Actors.Reminders; +using Dapr.E2E.Test.Actors.Timers; +using Dapr.E2E.Test.Actors.State; +using Dapr.E2E.Test.Actors.ExceptionTesting; +using Dapr.E2E.Test.Actors.Serialization; +using Dapr.E2E.Test.Actors.WeaklyTypedTesting; +using Dapr.E2E.Test.App.ErrorTesting; +using Dapr.Workflow; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Serilog; +using Grpc.Net.Client; + +/// +/// Startup class. +/// +public class Startup { - using Dapr.E2E.Test.Actors.Reminders; - using Dapr.E2E.Test.Actors.Timers; - using Dapr.E2E.Test.Actors.State; - using Dapr.E2E.Test.Actors.ExceptionTesting; - using Dapr.E2E.Test.Actors.Serialization; - using Dapr.E2E.Test.Actors.WeaklyTypedTesting; - using Dapr.E2E.Test.App.ErrorTesting; - using Dapr.Workflow; - using Microsoft.AspNetCore.Authentication; - using Microsoft.AspNetCore.Authorization; - using Microsoft.AspNetCore.Builder; - using Microsoft.AspNetCore.Hosting; - using Microsoft.Extensions.Configuration; - using Microsoft.Extensions.DependencyInjection; - using Microsoft.Extensions.Hosting; - using System.Threading.Tasks; - using Microsoft.Extensions.Logging; - using Serilog; - using Grpc.Net.Client; + bool JsonSerializationEnabled => + System.Linq.Enumerable.Contains(System.Environment.GetCommandLineArgs(), "--json-serialization"); /// - /// Startup class. + /// Initializes a new instance of the class. /// - public class Startup + /// Configuration. + public Startup(IConfiguration configuration) { - bool JsonSerializationEnabled => - System.Linq.Enumerable.Contains(System.Environment.GetCommandLineArgs(), "--json-serialization"); - - /// - /// Initializes a new instance of the class. - /// - /// Configuration. - public Startup(IConfiguration configuration) - { - this.Configuration = configuration; - } - - /// - /// Gets the configuration. - /// - public IConfiguration Configuration { get; } - - /// - /// Configures Services. - /// - /// Service Collection. - public void ConfigureServices(IServiceCollection services) - { - services.AddAuthentication().AddDapr(); - services.AddAuthorization(o => o.AddDapr()); - services.AddControllers().AddDapr(); - services.AddLogging(builder => - { - builder.AddConsole(); - }); - // Register a workflow and associated activity - services.AddDaprWorkflow(options => - { - // Example of registering a "PlaceOrder" workflow function - options.RegisterWorkflow("PlaceOrder", implementation: async (context, input) => - { - - var itemToPurchase = input; - - // There are 5 of the same event to test that multiple similarly-named events can be raised in parallel - await context.WaitForExternalEventAsync("ChangePurchaseItem"); - await context.WaitForExternalEventAsync("ChangePurchaseItem"); - await context.WaitForExternalEventAsync("ChangePurchaseItem"); - await context.WaitForExternalEventAsync("ChangePurchaseItem"); - itemToPurchase = await context.WaitForExternalEventAsync("ChangePurchaseItem"); - - // In real life there are other steps related to placing an order, like reserving - // inventory and charging the customer credit card etc. But let's keep it simple ;) - await context.CallActivityAsync("ShipProduct", itemToPurchase); - - return itemToPurchase; - }); - - // Example of registering a "ShipProduct" workflow activity function - options.RegisterActivity("ShipProduct", implementation: (context, input) => - { - return Task.FromResult($"We are shipping {input} to the customer using our hoard of drones!"); - }); - }); - services.AddDaprWorkflow(options => - { - // Example of registering a "StartOrder" workflow function - options.RegisterWorkflow("StartLargeOrder", implementation: async (context, input) => - { - var itemToPurchase = input; - itemToPurchase = await context.WaitForExternalEventAsync("FinishLargeOrder"); - return itemToPurchase; - }); - options.RegisterActivity("FinishLargeOrder", implementation: (context, input) => - { - return Task.FromResult($"We are finishing, it's huge!"); - }); - options.UseGrpcChannelOptions(new GrpcChannelOptions - { - MaxReceiveMessageSize = 32 * 1024 * 1024, - MaxSendMessageSize = 32 * 1024 * 1024 - }); - }); - services.AddActors(options => - { - options.UseJsonSerialization = JsonSerializationEnabled; - options.Actors.RegisterActor(); - options.Actors.RegisterActor(); - options.Actors.RegisterActor(); - options.Actors.RegisterActor(); - options.Actors.RegisterActor(); - options.Actors.RegisterActor(); - options.Actors.RegisterActor(); - }); - } - - /// - /// Configures Application Builder and WebHost environment. - /// - /// Application builder. - /// Webhost environment. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - var logger = new LoggerConfiguration().WriteTo.File("log.txt").CreateLogger(); - - LoggerFactory.Create(builder => - { - builder.AddSerilog(logger); - }); - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.UseSerilogRequestLogging(); - - app.UseRouting(); - - app.UseAuthentication(); - - app.UseAuthorization(); - - app.UseCloudEvents(); - - app.UseEndpoints(endpoints => - { - endpoints.MapSubscribeHandler(); - endpoints.MapActorsHandlers(); - endpoints.MapControllers(); - }); - } + this.Configuration = configuration; } -} + + /// + /// Gets the configuration. + /// + public IConfiguration Configuration { get; } + + /// + /// Configures Services. + /// + /// Service Collection. + public void ConfigureServices(IServiceCollection services) + { + services.AddAuthentication().AddDapr(); + services.AddAuthorization(o => o.AddDapr()); + services.AddControllers().AddDapr(); + services.AddLogging(builder => + { + builder.AddConsole(); + }); + // Register a workflow and associated activity + services.AddDaprWorkflow(options => + { + // Example of registering a "PlaceOrder" workflow function + options.RegisterWorkflow("PlaceOrder", implementation: async (context, input) => + { + + var itemToPurchase = input; + + // There are 5 of the same event to test that multiple similarly-named events can be raised in parallel + await context.WaitForExternalEventAsync("ChangePurchaseItem"); + await context.WaitForExternalEventAsync("ChangePurchaseItem"); + await context.WaitForExternalEventAsync("ChangePurchaseItem"); + await context.WaitForExternalEventAsync("ChangePurchaseItem"); + itemToPurchase = await context.WaitForExternalEventAsync("ChangePurchaseItem"); + + // In real life there are other steps related to placing an order, like reserving + // inventory and charging the customer credit card etc. But let's keep it simple ;) + await context.CallActivityAsync("ShipProduct", itemToPurchase); + + return itemToPurchase; + }); + + // Example of registering a "ShipProduct" workflow activity function + options.RegisterActivity("ShipProduct", implementation: (context, input) => + { + return Task.FromResult($"We are shipping {input} to the customer using our hoard of drones!"); + }); + }); + services.AddDaprWorkflow(options => + { + // Example of registering a "StartOrder" workflow function + options.RegisterWorkflow("StartLargeOrder", implementation: async (context, input) => + { + var itemToPurchase = input; + itemToPurchase = await context.WaitForExternalEventAsync("FinishLargeOrder"); + return itemToPurchase; + }); + options.RegisterActivity("FinishLargeOrder", implementation: (context, input) => + { + return Task.FromResult($"We are finishing, it's huge!"); + }); + options.UseGrpcChannelOptions(new GrpcChannelOptions + { + MaxReceiveMessageSize = 32 * 1024 * 1024, + MaxSendMessageSize = 32 * 1024 * 1024 + }); + }); + services.AddActors(options => + { + options.UseJsonSerialization = JsonSerializationEnabled; + options.Actors.RegisterActor(); + options.Actors.RegisterActor(); + options.Actors.RegisterActor(); + options.Actors.RegisterActor(); + options.Actors.RegisterActor(); + options.Actors.RegisterActor(); + options.Actors.RegisterActor(); + }); + } + + /// + /// Configures Application Builder and WebHost environment. + /// + /// Application builder. + /// Webhost environment. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + var logger = new LoggerConfiguration().WriteTo.File("log.txt").CreateLogger(); + + LoggerFactory.Create(builder => + { + builder.AddSerilog(logger); + }); + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseSerilogRequestLogging(); + + app.UseRouting(); + + app.UseAuthentication(); + + app.UseAuthorization(); + + app.UseCloudEvents(); + + app.UseEndpoints(endpoints => + { + endpoints.MapSubscribeHandler(); + endpoints.MapActorsHandlers(); + endpoints.MapControllers(); + }); + } +} \ No newline at end of file diff --git a/test/Dapr.E2E.Test.App/Transaction.cs b/test/Dapr.E2E.Test.App/Transaction.cs index a24c818c..f7978f6b 100644 --- a/test/Dapr.E2E.Test.App/Transaction.cs +++ b/test/Dapr.E2E.Test.App/Transaction.cs @@ -11,25 +11,24 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.E2E.Test +namespace Dapr.E2E.Test; + +using System.ComponentModel.DataAnnotations; + +/// +/// Represents a transaction used by sample code. +/// +public class Transaction { - using System.ComponentModel.DataAnnotations; + /// + /// Gets or sets account id for the transaction. + /// + [Required] + public string Id { get; set; } /// - /// Represents a transaction used by sample code. + /// Gets or sets amount for the transaction. /// - public class Transaction - { - /// - /// Gets or sets account id for the transaction. - /// - [Required] - public string Id { get; set; } - - /// - /// Gets or sets amount for the transaction. - /// - [Range(0, double.MaxValue)] - public decimal Amount { get; set; } - } + [Range(0, double.MaxValue)] + public decimal Amount { get; set; } } \ No newline at end of file diff --git a/test/Dapr.E2E.Test/AVeryCoolWorkAroundTest.cs b/test/Dapr.E2E.Test/AVeryCoolWorkAroundTest.cs index b7fd4ebd..30286ac8 100644 --- a/test/Dapr.E2E.Test/AVeryCoolWorkAroundTest.cs +++ b/test/Dapr.E2E.Test/AVeryCoolWorkAroundTest.cs @@ -13,16 +13,15 @@ // This test has been added as a workaround for https://github.com/NasAmin/trx-parser/issues/111 // to report dummy test results in Dapr.E2E.Test.dll // Can delete this once this bug is fixed -namespace Dapr.E2E.WorkAround -{ - using Xunit; +namespace Dapr.E2E.WorkAround; - public class WorkAroundTests +using Xunit; + +public class WorkAroundTests +{ + [Fact] + public void AVeryCoolWorkAroundTest() { - [Fact] - public void AVeryCoolWorkAroundTest() - { - Assert.True(true); - } + Assert.True(true); } } \ No newline at end of file diff --git a/test/Dapr.E2E.Test/ActorRuntimeChecker.cs b/test/Dapr.E2E.Test/ActorRuntimeChecker.cs index 57c46dc9..244e2e8b 100644 --- a/test/Dapr.E2E.Test/ActorRuntimeChecker.cs +++ b/test/Dapr.E2E.Test/ActorRuntimeChecker.cs @@ -17,28 +17,27 @@ using System.Threading.Tasks; using Dapr.E2E.Test.Actors; using Xunit.Abstractions; -namespace Dapr.E2E.Test -{ - public static class ActorRuntimeChecker - { - public static async Task WaitForActorRuntimeAsync(string appId, ITestOutputHelper output, IPingActor proxy, CancellationToken cancellationToken) - { - while (true) - { - output.WriteLine($"Waiting for actor to be ready in: {appId}"); - cancellationToken.ThrowIfCancellationRequested(); +namespace Dapr.E2E.Test; - try - { - await proxy.Ping(); - output.WriteLine($"Found actor in: {appId}"); - break; - } - catch (DaprApiException) - { - await Task.Delay(TimeSpan.FromMilliseconds(250)); - } +public static class ActorRuntimeChecker +{ + public static async Task WaitForActorRuntimeAsync(string appId, ITestOutputHelper output, IPingActor proxy, CancellationToken cancellationToken) + { + while (true) + { + output.WriteLine($"Waiting for actor to be ready in: {appId}"); + cancellationToken.ThrowIfCancellationRequested(); + + try + { + await proxy.Ping(); + output.WriteLine($"Found actor in: {appId}"); + break; + } + catch (DaprApiException) + { + await Task.Delay(TimeSpan.FromMilliseconds(250)); } } } -} +} \ No newline at end of file diff --git a/test/Dapr.E2E.Test/Actors/E2ETests.CustomSerializerTests.cs b/test/Dapr.E2E.Test/Actors/E2ETests.CustomSerializerTests.cs index 26f01fb5..d0f12315 100644 --- a/test/Dapr.E2E.Test/Actors/E2ETests.CustomSerializerTests.cs +++ b/test/Dapr.E2E.Test/Actors/E2ETests.CustomSerializerTests.cs @@ -10,106 +10,105 @@ // See the License for the specific language governing permissions and // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.E2E.Test +namespace Dapr.E2E.Test; + +using System; +using System.Diagnostics; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Dapr.Actors; +using Dapr.Actors.Client; +using Dapr.E2E.Test.Actors; +using Xunit; +using Xunit.Abstractions; + +public class CustomSerializerTests : DaprTestAppLifecycle { - using System; - using System.Diagnostics; - using System.Text.Json; - using System.Threading; - using System.Threading.Tasks; - using Dapr.Actors; - using Dapr.Actors.Client; - using Dapr.E2E.Test.Actors; - using Xunit; - using Xunit.Abstractions; + private readonly Lazy proxyFactory; + private IActorProxyFactory ProxyFactory => this.HttpEndpoint == null ? null : this.proxyFactory.Value; - public class CustomSerializerTests : DaprTestAppLifecycle + public CustomSerializerTests(ITestOutputHelper output, DaprTestAppFixture fixture) : base(output, fixture) { - private readonly Lazy proxyFactory; - private IActorProxyFactory ProxyFactory => this.HttpEndpoint == null ? null : this.proxyFactory.Value; - - public CustomSerializerTests(ITestOutputHelper output, DaprTestAppFixture fixture) : base(output, fixture) + base.Configuration = new DaprRunConfiguration { - base.Configuration = new DaprRunConfiguration - { - UseAppPort = true, - AppId = "serializerapp", - AppJsonSerialization = true, - TargetProject = "./../../../../../test/Dapr.E2E.Test.App/Dapr.E2E.Test.App.csproj" - }; + UseAppPort = true, + AppId = "serializerapp", + AppJsonSerialization = true, + TargetProject = "./../../../../../test/Dapr.E2E.Test.App/Dapr.E2E.Test.App.csproj" + }; - this.proxyFactory = new Lazy(() => - { - Debug.Assert(this.HttpEndpoint != null); - return new ActorProxyFactory(new ActorProxyOptions() { - HttpEndpoint = this.HttpEndpoint, - JsonSerializerOptions = new JsonSerializerOptions() - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - PropertyNameCaseInsensitive = true, - WriteIndented = true, - }, - UseJsonSerialization = true, - }); - }); - } - - [Fact] - public async Task ActorCanSupportCustomSerializer() + this.proxyFactory = new Lazy(() => { - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); - var proxy = this.ProxyFactory.CreateActorProxy(ActorId.CreateRandom(), "SerializationActor"); - - await ActorRuntimeChecker.WaitForActorRuntimeAsync(this.AppId, this.Output, proxy, cts.Token); - - var payload = new SerializationPayload("hello world") - { - Value = JsonSerializer.SerializeToElement(new { foo = "bar" }), - ExtensionData = new System.Collections.Generic.Dictionary() + Debug.Assert(this.HttpEndpoint != null); + return new ActorProxyFactory(new ActorProxyOptions() { + HttpEndpoint = this.HttpEndpoint, + JsonSerializerOptions = new JsonSerializerOptions() { - { "baz", "qux" }, - { "count", 42 }, - } - }; + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = true, + WriteIndented = true, + }, + UseJsonSerialization = true, + }); + }); + } + + [Fact] + public async Task ActorCanSupportCustomSerializer() + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + var proxy = this.ProxyFactory.CreateActorProxy(ActorId.CreateRandom(), "SerializationActor"); - var result = await proxy.SendAsync("test", payload, CancellationToken.None); + await ActorRuntimeChecker.WaitForActorRuntimeAsync(this.AppId, this.Output, proxy, cts.Token); - Assert.Equal(payload.Message, result.Message); - Assert.Equal(payload.Value.GetRawText(), result.Value.GetRawText()); - Assert.Equal(payload.ExtensionData.Count, result.ExtensionData.Count); - - foreach (var kvp in payload.ExtensionData) - { - Assert.True(result.ExtensionData.TryGetValue(kvp.Key, out var value)); - Assert.Equal(JsonSerializer.Serialize(kvp.Value), JsonSerializer.Serialize(value)); - } - } - - /// - /// This was actually a problem that is why the test exists. - /// It just checks, if the interface of the actor has more than one method defined, - /// that if can call it and serialize the payload correctly. - /// - /// - /// More than one methods means here, that in the exact interface must be two methods defined. - /// That excludes hirachies. - /// So wouldn't count here, because it's not directly defined in - /// . (it's defined in the base of it.) - /// That why was created, - /// so there are now more then one method. - /// - [Fact] - public async Task ActorCanSupportCustomSerializerAndCallMoreThenOneDefinedMethod() + var payload = new SerializationPayload("hello world") { - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); - var proxy = this.ProxyFactory.CreateActorProxy(ActorId.CreateRandom(), "SerializationActor"); + Value = JsonSerializer.SerializeToElement(new { foo = "bar" }), + ExtensionData = new System.Collections.Generic.Dictionary() + { + { "baz", "qux" }, + { "count", 42 }, + } + }; - await ActorRuntimeChecker.WaitForActorRuntimeAsync(this.AppId, this.Output, proxy, cts.Token); + var result = await proxy.SendAsync("test", payload, CancellationToken.None); - var payload = DateTime.MinValue; - var result = await proxy.AnotherMethod(payload); + Assert.Equal(payload.Message, result.Message); + Assert.Equal(payload.Value.GetRawText(), result.Value.GetRawText()); + Assert.Equal(payload.ExtensionData.Count, result.ExtensionData.Count); - Assert.Equal(payload, result); + foreach (var kvp in payload.ExtensionData) + { + Assert.True(result.ExtensionData.TryGetValue(kvp.Key, out var value)); + Assert.Equal(JsonSerializer.Serialize(kvp.Value), JsonSerializer.Serialize(value)); } } -} + + /// + /// This was actually a problem that is why the test exists. + /// It just checks, if the interface of the actor has more than one method defined, + /// that if can call it and serialize the payload correctly. + /// + /// + /// More than one methods means here, that in the exact interface must be two methods defined. + /// That excludes hirachies. + /// So wouldn't count here, because it's not directly defined in + /// . (it's defined in the base of it.) + /// That why was created, + /// so there are now more then one method. + /// + [Fact] + public async Task ActorCanSupportCustomSerializerAndCallMoreThenOneDefinedMethod() + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + var proxy = this.ProxyFactory.CreateActorProxy(ActorId.CreateRandom(), "SerializationActor"); + + await ActorRuntimeChecker.WaitForActorRuntimeAsync(this.AppId, this.Output, proxy, cts.Token); + + var payload = DateTime.MinValue; + var result = await proxy.AnotherMethod(payload); + + Assert.Equal(payload, result); + } +} \ No newline at end of file diff --git a/test/Dapr.E2E.Test/Actors/E2ETests.ExceptionTests.cs b/test/Dapr.E2E.Test/Actors/E2ETests.ExceptionTests.cs index fc036d15..ee4645dc 100644 --- a/test/Dapr.E2E.Test/Actors/E2ETests.ExceptionTests.cs +++ b/test/Dapr.E2E.Test/Actors/E2ETests.ExceptionTests.cs @@ -11,27 +11,26 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.E2E.Test -{ - using System; - using System.Threading; - using System.Threading.Tasks; - using Dapr.Actors; - using Dapr.E2E.Test.Actors.ExceptionTesting; - using Xunit; - public partial class E2ETests : IAsyncLifetime - { - [Fact] - public async Task ActorCanProvideExceptionDetails() - { - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); +namespace Dapr.E2E.Test; - var proxy = this.ProxyFactory.CreateActorProxy(ActorId.CreateRandom(), "ExceptionActor"); - await WaitForActorRuntimeAsync(proxy, cts.Token); - ActorMethodInvocationException ex = await Assert.ThrowsAsync(async () => await proxy.ExceptionExample()); - Assert.Contains("Remote Actor Method Exception", ex.Message); - Assert.Contains("ExceptionExample", ex.Message); - Assert.Contains("32", ex.Message); - } +using System; +using System.Threading; +using System.Threading.Tasks; +using Dapr.Actors; +using Dapr.E2E.Test.Actors.ExceptionTesting; +using Xunit; +public partial class E2ETests : IAsyncLifetime +{ + [Fact] + public async Task ActorCanProvideExceptionDetails() + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + + var proxy = this.ProxyFactory.CreateActorProxy(ActorId.CreateRandom(), "ExceptionActor"); + await WaitForActorRuntimeAsync(proxy, cts.Token); + ActorMethodInvocationException ex = await Assert.ThrowsAsync(async () => await proxy.ExceptionExample()); + Assert.Contains("Remote Actor Method Exception", ex.Message); + Assert.Contains("ExceptionExample", ex.Message); + Assert.Contains("32", ex.Message); } } \ No newline at end of file diff --git a/test/Dapr.E2E.Test/Actors/E2ETests.ReentrantTests.cs b/test/Dapr.E2E.Test/Actors/E2ETests.ReentrantTests.cs index 30b19a45..3b31e766 100644 --- a/test/Dapr.E2E.Test/Actors/E2ETests.ReentrantTests.cs +++ b/test/Dapr.E2E.Test/Actors/E2ETests.ReentrantTests.cs @@ -10,70 +10,69 @@ // See the License for the specific language governing permissions and // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.E2E.Test +namespace Dapr.E2E.Test; + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Dapr.Actors; +using Dapr.Actors.Client; +using Dapr.E2E.Test.Actors.Reentrancy; +using Xunit; +using Xunit.Abstractions; + +public class ReentrantTests : DaprTestAppLifecycle { - using System; - using System.Collections.Generic; - using System.Diagnostics; - using System.Threading; - using System.Threading.Tasks; - using Dapr.Actors; - using Dapr.Actors.Client; - using Dapr.E2E.Test.Actors.Reentrancy; - using Xunit; - using Xunit.Abstractions; + private static readonly int NumCalls = 10; + private readonly Lazy proxyFactory; + private IActorProxyFactory ProxyFactory => this.HttpEndpoint == null ? null : this.proxyFactory.Value; - public class ReentrantTests : DaprTestAppLifecycle + public ReentrantTests(ITestOutputHelper output, DaprTestAppFixture fixture) : base(output, fixture) { - private static readonly int NumCalls = 10; - private readonly Lazy proxyFactory; - private IActorProxyFactory ProxyFactory => this.HttpEndpoint == null ? null : this.proxyFactory.Value; - - public ReentrantTests(ITestOutputHelper output, DaprTestAppFixture fixture) : base(output, fixture) + base.Configuration = new DaprRunConfiguration { - base.Configuration = new DaprRunConfiguration - { - UseAppPort = true, - AppId = "reentrantapp", - TargetProject = "./../../../../../test/Dapr.E2E.Test.App.ReentrantActor/Dapr.E2E.Test.App.ReentrantActors.csproj", - }; + UseAppPort = true, + AppId = "reentrantapp", + TargetProject = "./../../../../../test/Dapr.E2E.Test.App.ReentrantActor/Dapr.E2E.Test.App.ReentrantActors.csproj", + }; - this.proxyFactory = new Lazy(() => - { - Debug.Assert(this.HttpEndpoint != null); - return new ActorProxyFactory(new ActorProxyOptions() { HttpEndpoint = this.HttpEndpoint, }); - }); + this.proxyFactory = new Lazy(() => + { + Debug.Assert(this.HttpEndpoint != null); + return new ActorProxyFactory(new ActorProxyOptions() { HttpEndpoint = this.HttpEndpoint, }); + }); + } + + [Fact] + public async Task ActorCanPerformReentrantCalls() + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + var proxy = this.ProxyFactory.CreateActorProxy(ActorId.CreateRandom(), "ReentrantActor"); + + await ActorRuntimeChecker.WaitForActorRuntimeAsync(this.AppId, this.Output, proxy, cts.Token); + + await proxy.ReentrantCall(new ReentrantCallOptions(){ CallsRemaining = NumCalls, }); + var records = new List(); + for (int i = 0; i < NumCalls; i++) + { + var state = await proxy.GetState(i); + records.AddRange(state.Records); } - [Fact] - public async Task ActorCanPerformReentrantCalls() - { - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); - var proxy = this.ProxyFactory.CreateActorProxy(ActorId.CreateRandom(), "ReentrantActor"); - - await ActorRuntimeChecker.WaitForActorRuntimeAsync(this.AppId, this.Output, proxy, cts.Token); - - await proxy.ReentrantCall(new ReentrantCallOptions(){ CallsRemaining = NumCalls, }); - var records = new List(); - for (int i = 0; i < NumCalls; i++) - { - var state = await proxy.GetState(i); - records.AddRange(state.Records); - } - - var enterRecords = records.FindAll(record => record.IsEnter); - var exitRecords = records.FindAll(record => !record.IsEnter); + var enterRecords = records.FindAll(record => record.IsEnter); + var exitRecords = records.FindAll(record => !record.IsEnter); - this.Output.WriteLine($"Got {records.Count} records."); - Assert.True(records.Count == NumCalls * 2); - for (int i = 0; i < NumCalls; i++) + this.Output.WriteLine($"Got {records.Count} records."); + Assert.True(records.Count == NumCalls * 2); + for (int i = 0; i < NumCalls; i++) + { + for (int j = 0; j < NumCalls; j++) { - for (int j = 0; j < NumCalls; j++) - { - // Assert all the enters happen before the exits. - Assert.True(enterRecords[i].Timestamp < exitRecords[j].Timestamp); - } + // Assert all the enters happen before the exits. + Assert.True(enterRecords[i].Timestamp < exitRecords[j].Timestamp); } } } -} +} \ No newline at end of file diff --git a/test/Dapr.E2E.Test/Actors/E2ETests.Regression762Tests.cs b/test/Dapr.E2E.Test/Actors/E2ETests.Regression762Tests.cs index 12d6c736..c4aa669b 100644 --- a/test/Dapr.E2E.Test/Actors/E2ETests.Regression762Tests.cs +++ b/test/Dapr.E2E.Test/Actors/E2ETests.Regression762Tests.cs @@ -18,99 +18,98 @@ using Dapr.Actors; using Dapr.E2E.Test.Actors.ErrorTesting; using Xunit; -namespace Dapr.E2E.Test +namespace Dapr.E2E.Test; + +public partial class E2ETests : IAsyncLifetime { - public partial class E2ETests : IAsyncLifetime + [Fact] + public async Task ActorSuccessfullyClearsStateAfterErrorWithRemoting() { - [Fact] - public async Task ActorSuccessfullyClearsStateAfterErrorWithRemoting() + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + var proxy = this.ProxyFactory.CreateActorProxy(ActorId.CreateRandom(), "Regression762Actor"); + + await WaitForActorRuntimeAsync(proxy, cts.Token); + + var key = Guid.NewGuid().ToString(); + var throwingCall = new StateCall { - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); - var proxy = this.ProxyFactory.CreateActorProxy(ActorId.CreateRandom(), "Regression762Actor"); + Key = key, + Value = "Throw value", + Operation = "ThrowException" + }; - await WaitForActorRuntimeAsync(proxy, cts.Token); - - var key = Guid.NewGuid().ToString(); - var throwingCall = new StateCall - { - Key = key, - Value = "Throw value", - Operation = "ThrowException" - }; - - var setCall = new StateCall() - { - Key = key, - Value = "Real value", - Operation = "SetState" - }; - - var savingCall = new StateCall() - { - Operation = "SaveState" - }; - - // We attempt to delete it on the unlikely chance it's already there. - await proxy.RemoveState(throwingCall.Key); - - // Initiate a call that will set the state, then throw. - await Assert.ThrowsAsync(async () => await proxy.SaveState(throwingCall)); - - // Save the state and assert that the old value was not persisted. - await proxy.SaveState(savingCall); - var errorResp = await proxy.GetState(key); - Assert.Equal(string.Empty, errorResp); - - // Persist normally and ensure it works. - await proxy.SaveState(setCall); - var resp = await proxy.GetState(key); - Assert.Equal("Real value", resp); - } - - [Fact] - public async Task ActorSuccessfullyClearsStateAfterErrorWithoutRemoting() + var setCall = new StateCall() { - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); - var pingProxy = this.ProxyFactory.CreateActorProxy(ActorId.CreateRandom(), "Regression762Actor"); - var proxy = this.ProxyFactory.Create(ActorId.CreateRandom(), "Regression762Actor"); + Key = key, + Value = "Real value", + Operation = "SetState" + }; - await WaitForActorRuntimeAsync(pingProxy, cts.Token); + var savingCall = new StateCall() + { + Operation = "SaveState" + }; - var key = Guid.NewGuid().ToString(); - var throwingCall = new StateCall - { - Key = key, - Value = "Throw value", - Operation = "ThrowException" - }; + // We attempt to delete it on the unlikely chance it's already there. + await proxy.RemoveState(throwingCall.Key); - var setCall = new StateCall() - { - Key = key, - Value = "Real value", - Operation = "SetState" - }; + // Initiate a call that will set the state, then throw. + await Assert.ThrowsAsync(async () => await proxy.SaveState(throwingCall)); - var savingCall = new StateCall() - { - Operation = "SaveState" - }; + // Save the state and assert that the old value was not persisted. + await proxy.SaveState(savingCall); + var errorResp = await proxy.GetState(key); + Assert.Equal(string.Empty, errorResp); - // We attempt to delete it on the unlikely chance it's already there. - await proxy.InvokeMethodAsync("RemoveState", throwingCall.Key); - - // Initiate a call that will set the state, then throw. - await Assert.ThrowsAsync(async () => await proxy.InvokeMethodAsync("SaveState", throwingCall)); - - // Save the state and assert that the old value was not persisted. - await proxy.InvokeMethodAsync("SaveState", savingCall); - var errorResp = await proxy.InvokeMethodAsync("GetState", key); - Assert.Equal(string.Empty, errorResp); - - // Persist normally and ensure it works. - await proxy.InvokeMethodAsync("SaveState", setCall); - var resp = await proxy.InvokeMethodAsync("GetState", key); - Assert.Equal("Real value", resp); - } + // Persist normally and ensure it works. + await proxy.SaveState(setCall); + var resp = await proxy.GetState(key); + Assert.Equal("Real value", resp); } -} + + [Fact] + public async Task ActorSuccessfullyClearsStateAfterErrorWithoutRemoting() + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + var pingProxy = this.ProxyFactory.CreateActorProxy(ActorId.CreateRandom(), "Regression762Actor"); + var proxy = this.ProxyFactory.Create(ActorId.CreateRandom(), "Regression762Actor"); + + await WaitForActorRuntimeAsync(pingProxy, cts.Token); + + var key = Guid.NewGuid().ToString(); + var throwingCall = new StateCall + { + Key = key, + Value = "Throw value", + Operation = "ThrowException" + }; + + var setCall = new StateCall() + { + Key = key, + Value = "Real value", + Operation = "SetState" + }; + + var savingCall = new StateCall() + { + Operation = "SaveState" + }; + + // We attempt to delete it on the unlikely chance it's already there. + await proxy.InvokeMethodAsync("RemoveState", throwingCall.Key); + + // Initiate a call that will set the state, then throw. + await Assert.ThrowsAsync(async () => await proxy.InvokeMethodAsync("SaveState", throwingCall)); + + // Save the state and assert that the old value was not persisted. + await proxy.InvokeMethodAsync("SaveState", savingCall); + var errorResp = await proxy.InvokeMethodAsync("GetState", key); + Assert.Equal(string.Empty, errorResp); + + // Persist normally and ensure it works. + await proxy.InvokeMethodAsync("SaveState", setCall); + var resp = await proxy.InvokeMethodAsync("GetState", key); + Assert.Equal("Real value", resp); + } +} \ No newline at end of file diff --git a/test/Dapr.E2E.Test/Actors/E2ETests.ReminderTests.cs b/test/Dapr.E2E.Test/Actors/E2ETests.ReminderTests.cs index ff39cce8..378d96d2 100644 --- a/test/Dapr.E2E.Test/Actors/E2ETests.ReminderTests.cs +++ b/test/Dapr.E2E.Test/Actors/E2ETests.ReminderTests.cs @@ -10,170 +10,169 @@ // See the License for the specific language governing permissions and // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.E2E.Test +namespace Dapr.E2E.Test; + +using System; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Dapr.Actors; +using Dapr.E2E.Test.Actors.Reminders; +using Xunit; + +public partial class E2ETests : IAsyncLifetime { - using System; - using System.Text.Json; - using System.Threading; - using System.Threading.Tasks; - using Dapr.Actors; - using Dapr.E2E.Test.Actors.Reminders; - using Xunit; - - public partial class E2ETests : IAsyncLifetime + [Fact] + public async Task ActorCanStartAndStopReminder() { - [Fact] - public async Task ActorCanStartAndStopReminder() + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + var proxy = this.ProxyFactory.CreateActorProxy(ActorId.CreateRandom(), "ReminderActor"); + + await WaitForActorRuntimeAsync(proxy, cts.Token); + + // Start reminder, to count up to 10 + await proxy.StartReminder(new StartReminderOptions(){ Total = 10, }); + + State state; + while (true) { - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); - var proxy = this.ProxyFactory.CreateActorProxy(ActorId.CreateRandom(), "ReminderActor"); + cts.Token.ThrowIfCancellationRequested(); - await WaitForActorRuntimeAsync(proxy, cts.Token); - - // Start reminder, to count up to 10 - await proxy.StartReminder(new StartReminderOptions(){ Total = 10, }); - - State state; - while (true) + state = await proxy.GetState(); + this.Output.WriteLine($"Got Count: {state.Count} IsReminderRunning: {state.IsReminderRunning}"); + if (!state.IsReminderRunning) { - cts.Token.ThrowIfCancellationRequested(); - - state = await proxy.GetState(); - this.Output.WriteLine($"Got Count: {state.Count} IsReminderRunning: {state.IsReminderRunning}"); - if (!state.IsReminderRunning) - { - break; - } + break; } - - // Should count up to exactly 10 - Assert.Equal(10, state.Count); } - [Fact] - public async Task ActorCanStartAndStopAndGetReminder() - { - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); - var proxy = this.ProxyFactory.CreateActorProxy(ActorId.CreateRandom(), "ReminderActor"); - - await WaitForActorRuntimeAsync(proxy, cts.Token); - - // Get reminder before starting it, should return null. - var reminder = await proxy.GetReminder(); - Assert.Equal("null", reminder); - - // Start reminder, to count up to 10 - await proxy.StartReminder(new StartReminderOptions(){ Total = 10, }); - - State state = new State(); - var countGetReminder = 0; - while (true) - { - cts.Token.ThrowIfCancellationRequested(); - - reminder = await proxy.GetReminder(); - Assert.NotNull(reminder); - - // If reminder is null then it means the reminder has been stopped. - if (reminder != "null") - { - countGetReminder++; - var reminderJson = JsonSerializer.Deserialize(reminder); - var name = reminderJson.GetProperty("name").ToString(); - var period = reminderJson.GetProperty("period").ToString(); - var dueTime = reminderJson.GetProperty("dueTime").ToString(); - - Assert.Equal("test-reminder", name); - Assert.Equal(TimeSpan.FromMilliseconds(50).ToString(), period); - Assert.Equal(TimeSpan.Zero.ToString(), dueTime); - } - - state = await proxy.GetState(); - this.Output.WriteLine($"Got Count: {state.Count} IsReminderRunning: {state.IsReminderRunning}"); - if (!state.IsReminderRunning) - { - break; - } - } - - // Should count up to exactly 10 - Assert.Equal(10, state.Count); - // Should be able to Get Reminder at least once. - Assert.True(countGetReminder > 0); - } - - [Fact] - public async Task ActorCanStartReminderWithRepetitions() - { - int repetitions = 5; - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); - var proxy = this.ProxyFactory.CreateActorProxy(ActorId.CreateRandom(), "ReminderActor"); - - await WaitForActorRuntimeAsync(proxy, cts.Token); - - // Reminder that should fire 5 times (repetitions) at an interval of 1s - await proxy.StartReminderWithRepetitions(repetitions); - var start = DateTime.Now; - - await Task.Delay(TimeSpan.FromSeconds(7)); - - var state = await proxy.GetState(); - - // Make sure the reminder fired 5 times (repetitions) - Assert.Equal(repetitions, state.Count); - - // Make sure the reminder has fired and that it didn't fire within the past second since it should have expired. - Assert.True(state.Timestamp.Subtract(start) > TimeSpan.Zero, "Reminder may not have triggered."); - Assert.True(DateTime.Now.Subtract(state.Timestamp) > TimeSpan.FromSeconds(1), - $"Reminder triggered too recently. {DateTime.Now} - {state.Timestamp}"); - } - - [Fact] - public async Task ActorCanStartReminderWithTtlAndRepetitions() - { - int repetitions = 2; - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); - var proxy = this.ProxyFactory.CreateActorProxy(ActorId.CreateRandom(), "ReminderActor"); - - await WaitForActorRuntimeAsync(proxy, cts.Token); - - // Reminder that should fire 2 times (repetitions) at an interval of 1s - await proxy.StartReminderWithTtlAndRepetitions(TimeSpan.FromSeconds(5), repetitions); - var start = DateTime.Now; - - await Task.Delay(TimeSpan.FromSeconds(5)); - - var state = await proxy.GetState(); - - // Make sure the reminder fired 2 times (repetitions) whereas the ttl was 5 seconds. - Assert.Equal(repetitions, state.Count); - - // Make sure the reminder has fired and that it didn't fire within the past second since it should have expired. - Assert.True(state.Timestamp.Subtract(start) > TimeSpan.Zero, "Reminder may not have triggered."); - Assert.True(DateTime.Now.Subtract(state.Timestamp) > TimeSpan.FromSeconds(1), - $"Reminder triggered too recently. {DateTime.Now} - {state.Timestamp}"); - } - - [Fact] - public async Task ActorCanStartReminderWithTtl() - { - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); - var proxy = this.ProxyFactory.CreateActorProxy(ActorId.CreateRandom(), "ReminderActor"); - - await WaitForActorRuntimeAsync(proxy, cts.Token); - - // Reminder that should fire 3 times (at 0, 1, and 2 seconds) - await proxy.StartReminderWithTtl(TimeSpan.FromSeconds(2)); - - // Record the start time and wait for longer than the reminder should exist for. - var start = DateTime.Now; - await Task.Delay(TimeSpan.FromSeconds(5)); - - var state = await proxy.GetState(); - - // Make sure the reminder has fired and that it didn't fire within the past second since it should have expired. - Assert.True(state.Timestamp.Subtract(start) > TimeSpan.Zero, "Reminder may not have triggered."); - Assert.True(DateTime.Now.Subtract(state.Timestamp) > TimeSpan.FromSeconds(1), $"Reminder triggered too recently. {DateTime.Now} - {state.Timestamp}"); - } + // Should count up to exactly 10 + Assert.Equal(10, state.Count); } -} + + [Fact] + public async Task ActorCanStartAndStopAndGetReminder() + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + var proxy = this.ProxyFactory.CreateActorProxy(ActorId.CreateRandom(), "ReminderActor"); + + await WaitForActorRuntimeAsync(proxy, cts.Token); + + // Get reminder before starting it, should return null. + var reminder = await proxy.GetReminder(); + Assert.Equal("null", reminder); + + // Start reminder, to count up to 10 + await proxy.StartReminder(new StartReminderOptions(){ Total = 10, }); + + State state = new State(); + var countGetReminder = 0; + while (true) + { + cts.Token.ThrowIfCancellationRequested(); + + reminder = await proxy.GetReminder(); + Assert.NotNull(reminder); + + // If reminder is null then it means the reminder has been stopped. + if (reminder != "null") + { + countGetReminder++; + var reminderJson = JsonSerializer.Deserialize(reminder); + var name = reminderJson.GetProperty("name").ToString(); + var period = reminderJson.GetProperty("period").ToString(); + var dueTime = reminderJson.GetProperty("dueTime").ToString(); + + Assert.Equal("test-reminder", name); + Assert.Equal(TimeSpan.FromMilliseconds(50).ToString(), period); + Assert.Equal(TimeSpan.Zero.ToString(), dueTime); + } + + state = await proxy.GetState(); + this.Output.WriteLine($"Got Count: {state.Count} IsReminderRunning: {state.IsReminderRunning}"); + if (!state.IsReminderRunning) + { + break; + } + } + + // Should count up to exactly 10 + Assert.Equal(10, state.Count); + // Should be able to Get Reminder at least once. + Assert.True(countGetReminder > 0); + } + + [Fact] + public async Task ActorCanStartReminderWithRepetitions() + { + int repetitions = 5; + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + var proxy = this.ProxyFactory.CreateActorProxy(ActorId.CreateRandom(), "ReminderActor"); + + await WaitForActorRuntimeAsync(proxy, cts.Token); + + // Reminder that should fire 5 times (repetitions) at an interval of 1s + await proxy.StartReminderWithRepetitions(repetitions); + var start = DateTime.Now; + + await Task.Delay(TimeSpan.FromSeconds(7)); + + var state = await proxy.GetState(); + + // Make sure the reminder fired 5 times (repetitions) + Assert.Equal(repetitions, state.Count); + + // Make sure the reminder has fired and that it didn't fire within the past second since it should have expired. + Assert.True(state.Timestamp.Subtract(start) > TimeSpan.Zero, "Reminder may not have triggered."); + Assert.True(DateTime.Now.Subtract(state.Timestamp) > TimeSpan.FromSeconds(1), + $"Reminder triggered too recently. {DateTime.Now} - {state.Timestamp}"); + } + + [Fact] + public async Task ActorCanStartReminderWithTtlAndRepetitions() + { + int repetitions = 2; + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + var proxy = this.ProxyFactory.CreateActorProxy(ActorId.CreateRandom(), "ReminderActor"); + + await WaitForActorRuntimeAsync(proxy, cts.Token); + + // Reminder that should fire 2 times (repetitions) at an interval of 1s + await proxy.StartReminderWithTtlAndRepetitions(TimeSpan.FromSeconds(5), repetitions); + var start = DateTime.Now; + + await Task.Delay(TimeSpan.FromSeconds(5)); + + var state = await proxy.GetState(); + + // Make sure the reminder fired 2 times (repetitions) whereas the ttl was 5 seconds. + Assert.Equal(repetitions, state.Count); + + // Make sure the reminder has fired and that it didn't fire within the past second since it should have expired. + Assert.True(state.Timestamp.Subtract(start) > TimeSpan.Zero, "Reminder may not have triggered."); + Assert.True(DateTime.Now.Subtract(state.Timestamp) > TimeSpan.FromSeconds(1), + $"Reminder triggered too recently. {DateTime.Now} - {state.Timestamp}"); + } + + [Fact] + public async Task ActorCanStartReminderWithTtl() + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + var proxy = this.ProxyFactory.CreateActorProxy(ActorId.CreateRandom(), "ReminderActor"); + + await WaitForActorRuntimeAsync(proxy, cts.Token); + + // Reminder that should fire 3 times (at 0, 1, and 2 seconds) + await proxy.StartReminderWithTtl(TimeSpan.FromSeconds(2)); + + // Record the start time and wait for longer than the reminder should exist for. + var start = DateTime.Now; + await Task.Delay(TimeSpan.FromSeconds(5)); + + var state = await proxy.GetState(); + + // Make sure the reminder has fired and that it didn't fire within the past second since it should have expired. + Assert.True(state.Timestamp.Subtract(start) > TimeSpan.Zero, "Reminder may not have triggered."); + Assert.True(DateTime.Now.Subtract(state.Timestamp) > TimeSpan.FromSeconds(1), $"Reminder triggered too recently. {DateTime.Now} - {state.Timestamp}"); + } +} \ No newline at end of file diff --git a/test/Dapr.E2E.Test/Actors/E2ETests.StateTests.cs b/test/Dapr.E2E.Test/Actors/E2ETests.StateTests.cs index 7a1408a0..6c2a5023 100644 --- a/test/Dapr.E2E.Test/Actors/E2ETests.StateTests.cs +++ b/test/Dapr.E2E.Test/Actors/E2ETests.StateTests.cs @@ -10,113 +10,112 @@ // See the License for the specific language governing permissions and // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.E2E.Test +namespace Dapr.E2E.Test; + +using System; +using System.Threading; +using System.Threading.Tasks; +using Dapr.Actors; +using Dapr.E2E.Test.Actors.State; +using Xunit; + +public partial class E2ETests : IAsyncLifetime { - using System; - using System.Threading; - using System.Threading.Tasks; - using Dapr.Actors; - using Dapr.E2E.Test.Actors.State; - using Xunit; - - public partial class E2ETests : IAsyncLifetime + [Fact] + public async Task ActorCanSaveStateWithTTL() { - [Fact] - public async Task ActorCanSaveStateWithTTL() - { - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); - var proxy = this.ProxyFactory.CreateActorProxy(ActorId.CreateRandom(), "StateActor"); + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + var proxy = this.ProxyFactory.CreateActorProxy(ActorId.CreateRandom(), "StateActor"); - await WaitForActorRuntimeAsync(proxy, cts.Token); + await WaitForActorRuntimeAsync(proxy, cts.Token); - await proxy.SetState("key", "value", TimeSpan.FromSeconds(2)); + await proxy.SetState("key", "value", TimeSpan.FromSeconds(2)); - var resp = await proxy.GetState("key"); - Assert.Equal("value", resp); + var resp = await proxy.GetState("key"); + Assert.Equal("value", resp); - await Task.Delay(TimeSpan.FromSeconds(2.5)); + await Task.Delay(TimeSpan.FromSeconds(2.5)); - // Assert key no longer exists. - await Assert.ThrowsAsync(() => proxy.GetState("key")); + // Assert key no longer exists. + await Assert.ThrowsAsync(() => proxy.GetState("key")); - // Can create key again - await proxy.SetState("key", "new-value", null); - resp = await proxy.GetState("key"); - Assert.Equal("new-value", resp); - } - - [Fact] - public async Task ActorStateTTLOverridesExisting() - { - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); - var proxy = this.ProxyFactory.CreateActorProxy(ActorId.CreateRandom(), "StateActor"); - - await WaitForActorRuntimeAsync(proxy, cts.Token); - - // TLL 4 seconds - await proxy.SetState("key", "value", TimeSpan.FromSeconds(4)); - - var resp = await proxy.GetState("key"); - Assert.Equal("value", resp); - - // TLL 2 seconds - await Task.Delay(TimeSpan.FromSeconds(2)); - resp = await proxy.GetState("key"); - Assert.Equal("value", resp); - - // TLL 4 seconds - await proxy.SetState("key", "value", TimeSpan.FromSeconds(4)); - - // TLL 2 seconds - await Task.Delay(TimeSpan.FromSeconds(2)); - resp = await proxy.GetState("key"); - Assert.Equal("value", resp); - - // TLL 0 seconds - await Task.Delay(TimeSpan.FromSeconds(2.5)); - - // Assert key no longer exists. - await Assert.ThrowsAsync(() => proxy.GetState("key")); - } - - [Fact] - public async Task ActorStateTTLRemoveTTL() - { - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); - var proxy = this.ProxyFactory.CreateActorProxy(ActorId.CreateRandom(), "StateActor"); - - await WaitForActorRuntimeAsync(proxy, cts.Token); - - // Can remove TTL and then add again - await proxy.SetState("key", "value", TimeSpan.FromSeconds(2)); - await proxy.SetState("key", "value", null); - await Task.Delay(TimeSpan.FromSeconds(2)); - var resp = await proxy.GetState("key"); - Assert.Equal("value", resp); - await proxy.SetState("key", "value", TimeSpan.FromSeconds(2)); - await Task.Delay(TimeSpan.FromSeconds(2.5)); - await Assert.ThrowsAsync(() => proxy.GetState("key")); - } - - [Fact] - public async Task ActorStateBetweenProxies() - { - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); - var actorId = ActorId.CreateRandom(); - var proxy1 = this.ProxyFactory.CreateActorProxy(actorId, "StateActor"); - var proxy2 = this.ProxyFactory.CreateActorProxy(actorId, "StateActor"); - - await WaitForActorRuntimeAsync(proxy1, cts.Token); - - await proxy1.SetState("key", "value", TimeSpan.FromSeconds(2)); - var resp = await proxy1.GetState("key"); - Assert.Equal("value", resp); - resp = await proxy2.GetState("key"); - Assert.Equal("value", resp); - - await Task.Delay(TimeSpan.FromSeconds(2.5)); - await Assert.ThrowsAsync(() => proxy1.GetState("key")); - await Assert.ThrowsAsync(() => proxy2.GetState("key")); - } + // Can create key again + await proxy.SetState("key", "new-value", null); + resp = await proxy.GetState("key"); + Assert.Equal("new-value", resp); } -} + + [Fact] + public async Task ActorStateTTLOverridesExisting() + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + var proxy = this.ProxyFactory.CreateActorProxy(ActorId.CreateRandom(), "StateActor"); + + await WaitForActorRuntimeAsync(proxy, cts.Token); + + // TLL 4 seconds + await proxy.SetState("key", "value", TimeSpan.FromSeconds(4)); + + var resp = await proxy.GetState("key"); + Assert.Equal("value", resp); + + // TLL 2 seconds + await Task.Delay(TimeSpan.FromSeconds(2)); + resp = await proxy.GetState("key"); + Assert.Equal("value", resp); + + // TLL 4 seconds + await proxy.SetState("key", "value", TimeSpan.FromSeconds(4)); + + // TLL 2 seconds + await Task.Delay(TimeSpan.FromSeconds(2)); + resp = await proxy.GetState("key"); + Assert.Equal("value", resp); + + // TLL 0 seconds + await Task.Delay(TimeSpan.FromSeconds(2.5)); + + // Assert key no longer exists. + await Assert.ThrowsAsync(() => proxy.GetState("key")); + } + + [Fact] + public async Task ActorStateTTLRemoveTTL() + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + var proxy = this.ProxyFactory.CreateActorProxy(ActorId.CreateRandom(), "StateActor"); + + await WaitForActorRuntimeAsync(proxy, cts.Token); + + // Can remove TTL and then add again + await proxy.SetState("key", "value", TimeSpan.FromSeconds(2)); + await proxy.SetState("key", "value", null); + await Task.Delay(TimeSpan.FromSeconds(2)); + var resp = await proxy.GetState("key"); + Assert.Equal("value", resp); + await proxy.SetState("key", "value", TimeSpan.FromSeconds(2)); + await Task.Delay(TimeSpan.FromSeconds(2.5)); + await Assert.ThrowsAsync(() => proxy.GetState("key")); + } + + [Fact] + public async Task ActorStateBetweenProxies() + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + var actorId = ActorId.CreateRandom(); + var proxy1 = this.ProxyFactory.CreateActorProxy(actorId, "StateActor"); + var proxy2 = this.ProxyFactory.CreateActorProxy(actorId, "StateActor"); + + await WaitForActorRuntimeAsync(proxy1, cts.Token); + + await proxy1.SetState("key", "value", TimeSpan.FromSeconds(2)); + var resp = await proxy1.GetState("key"); + Assert.Equal("value", resp); + resp = await proxy2.GetState("key"); + Assert.Equal("value", resp); + + await Task.Delay(TimeSpan.FromSeconds(2.5)); + await Assert.ThrowsAsync(() => proxy1.GetState("key")); + await Assert.ThrowsAsync(() => proxy2.GetState("key")); + } +} \ No newline at end of file diff --git a/test/Dapr.E2E.Test/Actors/E2ETests.TimerTests.cs b/test/Dapr.E2E.Test/Actors/E2ETests.TimerTests.cs index d9aba386..61e16d8d 100644 --- a/test/Dapr.E2E.Test/Actors/E2ETests.TimerTests.cs +++ b/test/Dapr.E2E.Test/Actors/E2ETests.TimerTests.cs @@ -10,65 +10,64 @@ // See the License for the specific language governing permissions and // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.E2E.Test +namespace Dapr.E2E.Test; + +using System; +using System.Threading; +using System.Threading.Tasks; +using Dapr.Actors; +using Dapr.E2E.Test.Actors.Timers; +using Xunit; + +public partial class E2ETests : IAsyncLifetime { - using System; - using System.Threading; - using System.Threading.Tasks; - using Dapr.Actors; - using Dapr.E2E.Test.Actors.Timers; - using Xunit; - - public partial class E2ETests : IAsyncLifetime + [Fact] + public async Task ActorCanStartAndStopTimer() { - [Fact] - public async Task ActorCanStartAndStopTimer() + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + var proxy = this.ProxyFactory.CreateActorProxy(ActorId.CreateRandom(), "TimerActor"); + + await WaitForActorRuntimeAsync(proxy, cts.Token); + + // Start timer, to count up to 10 + await proxy.StartTimer(new StartTimerOptions(){ Total = 10, }); + + State state; + while (true) { - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); - var proxy = this.ProxyFactory.CreateActorProxy(ActorId.CreateRandom(), "TimerActor"); + cts.Token.ThrowIfCancellationRequested(); - await WaitForActorRuntimeAsync(proxy, cts.Token); - - // Start timer, to count up to 10 - await proxy.StartTimer(new StartTimerOptions(){ Total = 10, }); - - State state; - while (true) + state = await proxy.GetState(); + this.Output.WriteLine($"Got Count: {state.Count} IsTimerRunning: {state.IsTimerRunning}"); + if (!state.IsTimerRunning) { - cts.Token.ThrowIfCancellationRequested(); - - state = await proxy.GetState(); - this.Output.WriteLine($"Got Count: {state.Count} IsTimerRunning: {state.IsTimerRunning}"); - if (!state.IsTimerRunning) - { - break; - } + break; } - - // Should count up to exactly 10 - Assert.Equal(10, state.Count); } - [Fact] - public async Task ActorCanStartTimerWithTtl() - { - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); - var proxy = this.ProxyFactory.CreateActorProxy(ActorId.CreateRandom(), "TimerActor"); - - await WaitForActorRuntimeAsync(proxy, cts.Token); - - // Reminder that should fire 3 times (at 0, 1, and 2 seconds) - await proxy.StartTimerWithTtl(TimeSpan.FromSeconds(2)); - - // Record the start time and wait for longer than the reminder should exist for. - var start = DateTime.Now; - await Task.Delay(TimeSpan.FromSeconds(5)); - - var state = await proxy.GetState(); - - // Make sure the reminder has fired and that it didn't fire within the past second since it should have expired. - Assert.True(state.Timestamp.Subtract(start) > TimeSpan.Zero, "Timer may not have fired."); - Assert.True(DateTime.Now.Subtract(state.Timestamp) > TimeSpan.FromSeconds(1), $"Timer fired too recently. {DateTime.Now} - {state.Timestamp}"); - } + // Should count up to exactly 10 + Assert.Equal(10, state.Count); } -} + + [Fact] + public async Task ActorCanStartTimerWithTtl() + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + var proxy = this.ProxyFactory.CreateActorProxy(ActorId.CreateRandom(), "TimerActor"); + + await WaitForActorRuntimeAsync(proxy, cts.Token); + + // Reminder that should fire 3 times (at 0, 1, and 2 seconds) + await proxy.StartTimerWithTtl(TimeSpan.FromSeconds(2)); + + // Record the start time and wait for longer than the reminder should exist for. + var start = DateTime.Now; + await Task.Delay(TimeSpan.FromSeconds(5)); + + var state = await proxy.GetState(); + + // Make sure the reminder has fired and that it didn't fire within the past second since it should have expired. + Assert.True(state.Timestamp.Subtract(start) > TimeSpan.Zero, "Timer may not have fired."); + Assert.True(DateTime.Now.Subtract(state.Timestamp) > TimeSpan.FromSeconds(1), $"Timer fired too recently. {DateTime.Now} - {state.Timestamp}"); + } +} \ No newline at end of file diff --git a/test/Dapr.E2E.Test/Actors/E2ETests.WeaklyTypedTests.cs b/test/Dapr.E2E.Test/Actors/E2ETests.WeaklyTypedTests.cs index 5518f32b..22040a15 100644 --- a/test/Dapr.E2E.Test/Actors/E2ETests.WeaklyTypedTests.cs +++ b/test/Dapr.E2E.Test/Actors/E2ETests.WeaklyTypedTests.cs @@ -10,32 +10,32 @@ // See the License for the specific language governing permissions and // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.E2E.Test +namespace Dapr.E2E.Test; + +using System; +using System.Threading; +using System.Threading.Tasks; +using Dapr.Actors; +using Dapr.E2E.Test.Actors.WeaklyTypedTesting; +using Shouldly; +using Xunit; + +public partial class E2ETests : IAsyncLifetime { - using System; - using System.Threading; - using System.Threading.Tasks; - using Dapr.Actors; - using Dapr.E2E.Test.Actors.WeaklyTypedTesting; - using Shouldly; - using Xunit; - - public partial class E2ETests : IAsyncLifetime - { #if NET8_0_OR_GREATER - [Fact] - public async Task WeaklyTypedActorCanReturnPolymorphicResponse() - { - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); - var pingProxy = this.ProxyFactory.CreateActorProxy(ActorId.CreateRandom(), "WeaklyTypedTestingActor"); - var proxy = this.ProxyFactory.Create(ActorId.CreateRandom(), "WeaklyTypedTestingActor"); + [Fact] + public async Task WeaklyTypedActorCanReturnPolymorphicResponse() + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + var pingProxy = this.ProxyFactory.CreateActorProxy(ActorId.CreateRandom(), "WeaklyTypedTestingActor"); + var proxy = this.ProxyFactory.Create(ActorId.CreateRandom(), "WeaklyTypedTestingActor"); - await WaitForActorRuntimeAsync(pingProxy, cts.Token); + await WaitForActorRuntimeAsync(pingProxy, cts.Token); - var result = await proxy.InvokeMethodAsync(nameof(IWeaklyTypedTestingActor.GetPolymorphicResponse)); + var result = await proxy.InvokeMethodAsync(nameof(IWeaklyTypedTestingActor.GetPolymorphicResponse)); - result.ShouldBeOfType().DerivedProperty.ShouldNotBeNullOrWhiteSpace(); - } + result.ShouldBeOfType().DerivedProperty.ShouldNotBeNullOrWhiteSpace(); + } #else [Fact] public async Task WeaklyTypedActorCanReturnDerivedResponse() @@ -51,18 +51,17 @@ namespace Dapr.E2E.Test result.ShouldBeOfType().DerivedProperty.ShouldNotBeNullOrWhiteSpace(); } #endif - [Fact] - public async Task WeaklyTypedActorCanReturnNullResponse() - { - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); - var pingProxy = this.ProxyFactory.CreateActorProxy(ActorId.CreateRandom(), "WeaklyTypedTestingActor"); - var proxy = this.ProxyFactory.Create(ActorId.CreateRandom(), "WeaklyTypedTestingActor"); + [Fact] + public async Task WeaklyTypedActorCanReturnNullResponse() + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + var pingProxy = this.ProxyFactory.CreateActorProxy(ActorId.CreateRandom(), "WeaklyTypedTestingActor"); + var proxy = this.ProxyFactory.Create(ActorId.CreateRandom(), "WeaklyTypedTestingActor"); - await WaitForActorRuntimeAsync(pingProxy, cts.Token); + await WaitForActorRuntimeAsync(pingProxy, cts.Token); - var result = await proxy.InvokeMethodAsync(nameof(IWeaklyTypedTestingActor.GetNullResponse)); + var result = await proxy.InvokeMethodAsync(nameof(IWeaklyTypedTestingActor.GetNullResponse)); - result.ShouldBeNull(); - } + result.ShouldBeNull(); } -} +} \ No newline at end of file diff --git a/test/Dapr.E2E.Test/DaprCommand.cs b/test/Dapr.E2E.Test/DaprCommand.cs index 21e31365..52a36934 100644 --- a/test/Dapr.E2E.Test/DaprCommand.cs +++ b/test/Dapr.E2E.Test/DaprCommand.cs @@ -11,177 +11,176 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.E2E.Test +namespace Dapr.E2E.Test; + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Drawing; +using System.Linq; +using System.Threading; +using Xunit.Abstractions; + +public class DaprCommand { - using System; - using System.Collections.Generic; - using System.Diagnostics; - using System.Drawing; - using System.Linq; - using System.Threading; - using Xunit.Abstractions; + private readonly ITestOutputHelper output; + private readonly CircularBuffer logBuffer = new CircularBuffer(1000); - public class DaprCommand + public DaprCommand(ITestOutputHelper output) { - private readonly ITestOutputHelper output; - private readonly CircularBuffer logBuffer = new CircularBuffer(1000); + this.output = output; + } - public DaprCommand(ITestOutputHelper output) + private EventWaitHandle outputReceived = new EventWaitHandle(false, EventResetMode.ManualReset); + public string DaprBinaryName { get; set; } + public string Command { get; set; } + public Dictionary EnvironmentVariables { get; set; } = new Dictionary(); + public string[] OutputToMatch { get; set; } + public TimeSpan Timeout { get; set; } + + public void Run() + { + Console.WriteLine($@"Running command: {this.Command}"); + var escapedArgs = Command.Replace("\"", "\\\""); + var process = new Process() { - this.output = output; + StartInfo = new ProcessStartInfo + { + FileName = this.DaprBinaryName, + Arguments = escapedArgs, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + } + }; + foreach (var (key, value) in EnvironmentVariables) + { + process.StartInfo.EnvironmentVariables.Add(key, value); } + process.OutputDataReceived += CheckOutput; + process.ErrorDataReceived += CheckOutput; - private EventWaitHandle outputReceived = new EventWaitHandle(false, EventResetMode.ManualReset); - public string DaprBinaryName { get; set; } - public string Command { get; set; } - public Dictionary EnvironmentVariables { get; set; } = new Dictionary(); - public string[] OutputToMatch { get; set; } - public TimeSpan Timeout { get; set; } + process.Start(); + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); - public void Run() + var done = outputReceived.WaitOne(this.Timeout); + if (!done) { - Console.WriteLine($@"Running command: {this.Command}"); - var escapedArgs = Command.Replace("\"", "\\\""); - var process = new Process() - { - StartInfo = new ProcessStartInfo - { - FileName = this.DaprBinaryName, - Arguments = escapedArgs, - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true, - } - }; - foreach (var (key, value) in EnvironmentVariables) - { - process.StartInfo.EnvironmentVariables.Add(key, value); - } - process.OutputDataReceived += CheckOutput; - process.ErrorDataReceived += CheckOutput; - - process.Start(); - process.BeginOutputReadLine(); - process.BeginErrorReadLine(); - - var done = outputReceived.WaitOne(this.Timeout); - if (!done) - { - var ex = new Exception($"Command: \"{this.Command}\" timed out while waiting for output: \"{this.OutputToMatch}\"{System.Environment.NewLine}" + - "This could also mean the E2E app had a startup error. For more details see the Data property of this exception."); - // we add here the log buffer of the last 1000 lines, of the application log - // to make it easier to debug failing tests - ex.Data.Add("log", this.logBuffer.ToArray()); - throw ex; - } - } - - private void CheckOutput(object sendingProcess, DataReceivedEventArgs e) - { - if (e.Data == null) - { - return; - } - - try - { - WriteLine(e.Data); - } - catch (InvalidOperationException) - { - } - - if (this.OutputToMatch.Any(o => e.Data.Contains(o))) - { - outputReceived.Set(); - } - } - - private void OnErrorOutput(object sender, DataReceivedEventArgs e) - { - if (e.Data == null) - { - return; - } - - try - { - WriteLine(e.Data); - } - catch (InvalidOperationException) - { - } - } - - private void WriteLine(string message) - { - // see: https://github.com/xunit/xunit/issues/2146 - var formattedMessage = message.TrimEnd(Environment.NewLine.ToCharArray()); - this.output.WriteLine(formattedMessage); - this.logBuffer.Add(formattedMessage); + var ex = new Exception($"Command: \"{this.Command}\" timed out while waiting for output: \"{this.OutputToMatch}\"{System.Environment.NewLine}" + + "This could also mean the E2E app had a startup error. For more details see the Data property of this exception."); + // we add here the log buffer of the last 1000 lines, of the application log + // to make it easier to debug failing tests + ex.Data.Add("log", this.logBuffer.ToArray()); + throw ex; } } - /// - /// A circular buffer that can be used to store a fixed number of items. - /// When the buffer is full, the oldest item is overwritten. - /// The buffer can be read in the same order as the items were added. - /// More information can be found here. - /// - /// - /// The buffer gets initialized by the call to the constructor and will allocate, - /// the memory for the buffer. The buffer is not resizable. - /// That means be carefull with , because it can cause an . - /// - /// The type of what the cicular buffer is off. - internal class CircularBuffer{ - private readonly int size; - private readonly T[] buffer; - private int readPosition = 0; - private int writePosition = 0; - /// - /// Initialize the buffer with the buffer size of . - /// - /// - /// The size the buffer will have - /// - public CircularBuffer(int size) + private void CheckOutput(object sendingProcess, DataReceivedEventArgs e) + { + if (e.Data == null) { - this.size = size; - buffer = new T[size]; + return; } - /// - /// Adds an item and move the write position to the next value - /// - /// The item that should be written. - public void Add(T item) + + try { - buffer[writePosition] = item; - writePosition = (writePosition + 1) % size; + WriteLine(e.Data); } - /// - /// Reads on value and move the position to the next value - /// - /// - public T Read(){ - var value = buffer[readPosition]; - readPosition = (readPosition + 1) % size; - return value; - } - /// - /// Read the full buffer. - /// While the buffer is read, the read position is moved to the next value - /// - /// - public T[] ToArray() + catch (InvalidOperationException) { - var result = new T[size]; - for (int i = 0; i < size; i++) - { - result[i] = Read(); - } - return result; } + + if (this.OutputToMatch.Any(o => e.Data.Contains(o))) + { + outputReceived.Set(); + } + } + + private void OnErrorOutput(object sender, DataReceivedEventArgs e) + { + if (e.Data == null) + { + return; + } + + try + { + WriteLine(e.Data); + } + catch (InvalidOperationException) + { + } + } + + private void WriteLine(string message) + { + // see: https://github.com/xunit/xunit/issues/2146 + var formattedMessage = message.TrimEnd(Environment.NewLine.ToCharArray()); + this.output.WriteLine(formattedMessage); + this.logBuffer.Add(formattedMessage); } } + +/// +/// A circular buffer that can be used to store a fixed number of items. +/// When the buffer is full, the oldest item is overwritten. +/// The buffer can be read in the same order as the items were added. +/// More information can be found here. +/// +/// +/// The buffer gets initialized by the call to the constructor and will allocate, +/// the memory for the buffer. The buffer is not resizable. +/// That means be carefull with , because it can cause an . +/// +/// The type of what the cicular buffer is off. +internal class CircularBuffer{ + private readonly int size; + private readonly T[] buffer; + private int readPosition = 0; + private int writePosition = 0; + /// + /// Initialize the buffer with the buffer size of . + /// + /// + /// The size the buffer will have + /// + public CircularBuffer(int size) + { + this.size = size; + buffer = new T[size]; + } + /// + /// Adds an item and move the write position to the next value + /// + /// The item that should be written. + public void Add(T item) + { + buffer[writePosition] = item; + writePosition = (writePosition + 1) % size; + } + /// + /// Reads on value and move the position to the next value + /// + /// + public T Read(){ + var value = buffer[readPosition]; + readPosition = (readPosition + 1) % size; + return value; + } + /// + /// Read the full buffer. + /// While the buffer is read, the read position is moved to the next value + /// + /// + public T[] ToArray() + { + var result = new T[size]; + for (int i = 0; i < size; i++) + { + result[i] = Read(); + } + return result; + } +} \ No newline at end of file diff --git a/test/Dapr.E2E.Test/DaprRunConfiguration.cs b/test/Dapr.E2E.Test/DaprRunConfiguration.cs index fccfbcdd..964cc6b2 100644 --- a/test/Dapr.E2E.Test/DaprRunConfiguration.cs +++ b/test/Dapr.E2E.Test/DaprRunConfiguration.cs @@ -11,20 +11,19 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.E2E.Test +namespace Dapr.E2E.Test; + +public class DaprRunConfiguration { - public class DaprRunConfiguration - { - public bool UseAppPort { get; set; } + public bool UseAppPort { get; set; } - public string AppId { get; set; } + public string AppId { get; set; } - public string AppProtocol { get; set; } + public string AppProtocol { get; set; } - public bool AppJsonSerialization { get; set; } + public bool AppJsonSerialization { get; set; } - public string ConfigurationPath { get; set; } + public string ConfigurationPath { get; set; } - public string TargetProject { get; set; } - } -} + public string TargetProject { get; set; } +} \ No newline at end of file diff --git a/test/Dapr.E2E.Test/DaprTestApp.cs b/test/Dapr.E2E.Test/DaprTestApp.cs index 8776a43d..408dca3b 100644 --- a/test/Dapr.E2E.Test/DaprTestApp.cs +++ b/test/Dapr.E2E.Test/DaprTestApp.cs @@ -22,147 +22,146 @@ using System.Runtime.Versioning; using Xunit.Abstractions; using static System.IO.Path; -namespace Dapr.E2E.Test +namespace Dapr.E2E.Test; + +public class DaprTestApp { - public class DaprTestApp + static string daprBinaryName = "dapr"; + private string appId; + private readonly string[] outputToMatchOnStart = new string[] { "dapr initialized. Status: Running.", }; + private readonly string[] outputToMatchOnStop = new string[] { "app stopped successfully", "failed to stop app id", }; + + private ITestOutputHelper testOutput; + + public DaprTestApp(ITestOutputHelper output, string appId) { - static string daprBinaryName = "dapr"; - private string appId; - private readonly string[] outputToMatchOnStart = new string[] { "dapr initialized. Status: Running.", }; - private readonly string[] outputToMatchOnStop = new string[] { "app stopped successfully", "failed to stop app id", }; - - private ITestOutputHelper testOutput; - - public DaprTestApp(ITestOutputHelper output, string appId) - { - this.appId = appId; - this.testOutput = output; - } - - public string AppId => this.appId; - - public (string httpEndpoint, string grpcEndpoint) Start(DaprRunConfiguration configuration) - { - var (appPort, httpPort, grpcPort, metricsPort) = GetFreePorts(); - - var resourcesPath = Combine(".", "..", "..", "..", "..", "..", "test", "Dapr.E2E.Test", "components"); - var configPath = Combine(".", "..", "..", "..", "..", "..", "test", "Dapr.E2E.Test", "configuration", "featureconfig.yaml"); - var arguments = new List() - { - // `dapr run` args - "run", - "--app-id", configuration.AppId, - "--dapr-http-port", httpPort.ToString(CultureInfo.InvariantCulture), - "--dapr-grpc-port", grpcPort.ToString(CultureInfo.InvariantCulture), - "--metrics-port", metricsPort.ToString(CultureInfo.InvariantCulture), - "--resources-path", resourcesPath, - "--config", configPath, - "--log-level", "debug", - "--max-body-size", "8Mi" - }; - - if (configuration.UseAppPort) - { - arguments.AddRange(new[] { "--app-port", appPort.ToString(CultureInfo.InvariantCulture), }); - } - - if (!string.IsNullOrEmpty(configuration.AppProtocol)) - { - arguments.AddRange(new[] { "--app-protocol", configuration.AppProtocol }); - } - - arguments.AddRange(new[] - { - // separator - "--", - - // `dotnet run` args - "dotnet", "run", - "--project", configuration.TargetProject, - "--framework", GetTargetFrameworkName(), - }); - - if (configuration.UseAppPort) - { - // The first argument is the port, if the application needs it. - arguments.AddRange(new[] { "--", $"{appPort.ToString(CultureInfo.InvariantCulture)}" }); - arguments.AddRange(new[] { "--urls", $"http://localhost:{appPort.ToString(CultureInfo.InvariantCulture)}", }); - } - - if (configuration.AppJsonSerialization) - { - arguments.AddRange(new[] { "--json-serialization" }); - } - - // TODO: we don't do any quoting right now because our paths are guaranteed not to contain spaces - var daprStart = new DaprCommand(this.testOutput) - { - DaprBinaryName = DaprTestApp.daprBinaryName, - Command = string.Join(" ", arguments), - OutputToMatch = outputToMatchOnStart, - Timeout = TimeSpan.FromSeconds(30), - EnvironmentVariables = new Dictionary - { - { "APP_API_TOKEN", "abcdefg" } - } - }; - - daprStart.Run(); - - testOutput.WriteLine($"Dapr app: {appId} started successfully"); - var httpEndpoint = $"http://localhost:{httpPort}"; - var grpcEndpoint = $"http://localhost:{grpcPort}"; - return (httpEndpoint, grpcEndpoint); - } - - public void Stop() - { - var daprStopCommand = $" stop --app-id {appId}"; - var daprStop = new DaprCommand(this.testOutput) - { - DaprBinaryName = DaprTestApp.daprBinaryName, - Command = daprStopCommand, - OutputToMatch = outputToMatchOnStop, - Timeout = TimeSpan.FromSeconds(30), - }; - daprStop.Run(); - testOutput.WriteLine($"Dapr app: {appId} stopped successfully"); - } - - private static string GetTargetFrameworkName() - { - var targetFrameworkName = ((TargetFrameworkAttribute)Assembly.GetExecutingAssembly().GetCustomAttributes(typeof(TargetFrameworkAttribute), false).FirstOrDefault()).FrameworkName; - - return targetFrameworkName switch - { - ".NETCoreApp,Version=v6.0" => "net6", - ".NETCoreApp,Version=v7.0" => "net7", - ".NETCoreApp,Version=v8.0" => "net8", - ".NETCoreApp,Version=v9.0" => "net9", - _ => throw new InvalidOperationException($"Unsupported target framework: {targetFrameworkName}") - }; - } - - private static (int, int, int, int) GetFreePorts() - { - const int NUM_LISTENERS = 4; - IPAddress ip = IPAddress.Loopback; - var listeners = new TcpListener[NUM_LISTENERS]; - var ports = new int[NUM_LISTENERS]; - // Note: Starting only one listener at a time might end up returning the - // same port each time. - for (int i = 0; i < NUM_LISTENERS; i++) - { - listeners[i] = new TcpListener(ip, 0); - listeners[i].Start(); - ports[i] = ((IPEndPoint)listeners[i].LocalEndpoint).Port; - } - - for (int i = 0; i < NUM_LISTENERS; i++) - { - listeners[i].Stop(); - } - return (ports[0], ports[1], ports[2], ports[3]); - } + this.appId = appId; + this.testOutput = output; } -} + + public string AppId => this.appId; + + public (string httpEndpoint, string grpcEndpoint) Start(DaprRunConfiguration configuration) + { + var (appPort, httpPort, grpcPort, metricsPort) = GetFreePorts(); + + var resourcesPath = Combine(".", "..", "..", "..", "..", "..", "test", "Dapr.E2E.Test", "components"); + var configPath = Combine(".", "..", "..", "..", "..", "..", "test", "Dapr.E2E.Test", "configuration", "featureconfig.yaml"); + var arguments = new List() + { + // `dapr run` args + "run", + "--app-id", configuration.AppId, + "--dapr-http-port", httpPort.ToString(CultureInfo.InvariantCulture), + "--dapr-grpc-port", grpcPort.ToString(CultureInfo.InvariantCulture), + "--metrics-port", metricsPort.ToString(CultureInfo.InvariantCulture), + "--resources-path", resourcesPath, + "--config", configPath, + "--log-level", "debug", + "--max-body-size", "8Mi" + }; + + if (configuration.UseAppPort) + { + arguments.AddRange(new[] { "--app-port", appPort.ToString(CultureInfo.InvariantCulture), }); + } + + if (!string.IsNullOrEmpty(configuration.AppProtocol)) + { + arguments.AddRange(new[] { "--app-protocol", configuration.AppProtocol }); + } + + arguments.AddRange(new[] + { + // separator + "--", + + // `dotnet run` args + "dotnet", "run", + "--project", configuration.TargetProject, + "--framework", GetTargetFrameworkName(), + }); + + if (configuration.UseAppPort) + { + // The first argument is the port, if the application needs it. + arguments.AddRange(new[] { "--", $"{appPort.ToString(CultureInfo.InvariantCulture)}" }); + arguments.AddRange(new[] { "--urls", $"http://localhost:{appPort.ToString(CultureInfo.InvariantCulture)}", }); + } + + if (configuration.AppJsonSerialization) + { + arguments.AddRange(new[] { "--json-serialization" }); + } + + // TODO: we don't do any quoting right now because our paths are guaranteed not to contain spaces + var daprStart = new DaprCommand(this.testOutput) + { + DaprBinaryName = DaprTestApp.daprBinaryName, + Command = string.Join(" ", arguments), + OutputToMatch = outputToMatchOnStart, + Timeout = TimeSpan.FromSeconds(30), + EnvironmentVariables = new Dictionary + { + { "APP_API_TOKEN", "abcdefg" } + } + }; + + daprStart.Run(); + + testOutput.WriteLine($"Dapr app: {appId} started successfully"); + var httpEndpoint = $"http://localhost:{httpPort}"; + var grpcEndpoint = $"http://localhost:{grpcPort}"; + return (httpEndpoint, grpcEndpoint); + } + + public void Stop() + { + var daprStopCommand = $" stop --app-id {appId}"; + var daprStop = new DaprCommand(this.testOutput) + { + DaprBinaryName = DaprTestApp.daprBinaryName, + Command = daprStopCommand, + OutputToMatch = outputToMatchOnStop, + Timeout = TimeSpan.FromSeconds(30), + }; + daprStop.Run(); + testOutput.WriteLine($"Dapr app: {appId} stopped successfully"); + } + + private static string GetTargetFrameworkName() + { + var targetFrameworkName = ((TargetFrameworkAttribute)Assembly.GetExecutingAssembly().GetCustomAttributes(typeof(TargetFrameworkAttribute), false).FirstOrDefault()).FrameworkName; + + return targetFrameworkName switch + { + ".NETCoreApp,Version=v6.0" => "net6", + ".NETCoreApp,Version=v7.0" => "net7", + ".NETCoreApp,Version=v8.0" => "net8", + ".NETCoreApp,Version=v9.0" => "net9", + _ => throw new InvalidOperationException($"Unsupported target framework: {targetFrameworkName}") + }; + } + + private static (int, int, int, int) GetFreePorts() + { + const int NUM_LISTENERS = 4; + IPAddress ip = IPAddress.Loopback; + var listeners = new TcpListener[NUM_LISTENERS]; + var ports = new int[NUM_LISTENERS]; + // Note: Starting only one listener at a time might end up returning the + // same port each time. + for (int i = 0; i < NUM_LISTENERS; i++) + { + listeners[i] = new TcpListener(ip, 0); + listeners[i].Start(); + ports[i] = ((IPEndPoint)listeners[i].LocalEndpoint).Port; + } + + for (int i = 0; i < NUM_LISTENERS; i++) + { + listeners[i].Stop(); + } + return (ports[0], ports[1], ports[2], ports[3]); + } +} \ No newline at end of file diff --git a/test/Dapr.E2E.Test/DaprTestAppFixture.cs b/test/Dapr.E2E.Test/DaprTestAppFixture.cs index 4e788945..6017d641 100644 --- a/test/Dapr.E2E.Test/DaprTestAppFixture.cs +++ b/test/Dapr.E2E.Test/DaprTestAppFixture.cs @@ -14,103 +14,102 @@ using System; using System.Threading.Tasks; using Xunit.Abstractions; -namespace Dapr.E2E.Test +namespace Dapr.E2E.Test; + +// Guarantees we run the app a single time for the whole test class. +public class DaprTestAppFixture : IDisposable { - // Guarantees we run the app a single time for the whole test class. - public class DaprTestAppFixture : IDisposable + private readonly object @lock = new object(); + private Task task; + + public void Dispose() { - private readonly object @lock = new object(); - private Task task; - - public void Dispose() + Task task; + lock (@lock) { - Task task; - lock (@lock) + if (this.task is null) { - if (this.task is null) - { - // App didn't start, do nothing. - return; - } - - task = this.task; - } - - State state; - try - { - state = this.task.GetAwaiter().GetResult(); - } - catch - { - // App failed during start, do nothing. + // App didn't start, do nothing. return; } + task = this.task; + } + + State state; + try + { + state = this.task.GetAwaiter().GetResult(); + } + catch + { + // App failed during start, do nothing. + return; + } + + try + { + state.App?.Stop(); + } + catch (Exception ex) + { try { - state.App?.Stop(); + // see: https://github.com/xunit/xunit/issues/2146 + state.Output.WriteLine("Failed to shut down app: " + ex); } - catch (Exception ex) + catch (InvalidOperationException) { - try - { - // see: https://github.com/xunit/xunit/issues/2146 - state.Output.WriteLine("Failed to shut down app: " + ex); - } - catch (InvalidOperationException) - { - } } } - - public Task StartAsync(ITestOutputHelper output, DaprRunConfiguration configuration) - { - lock (@lock) - { - if (this.task is null) - { - this.task = Task.Run(() => Launch(output, configuration)); - } - - return this.task; - } - } - - private State Launch(ITestOutputHelper output, DaprRunConfiguration configuration) - { - var app = new DaprTestApp(output, configuration.AppId); - try - { - var (httpEndpoint, grpcEndpoint) = app.Start(configuration); - return new State() - { - App = app, - HttpEndpoint = httpEndpoint, - GrpcEndpoint = grpcEndpoint, - Output = output, - }; - } - catch (Exception startException) - { - try - { - app.Stop(); - throw; - } - catch (Exception stopException) - { - throw new AggregateException(startException, stopException); - } - } - } - - public class State - { - public string HttpEndpoint; - public string GrpcEndpoint; - public DaprTestApp App; - public ITestOutputHelper Output; - } } -} + + public Task StartAsync(ITestOutputHelper output, DaprRunConfiguration configuration) + { + lock (@lock) + { + if (this.task is null) + { + this.task = Task.Run(() => Launch(output, configuration)); + } + + return this.task; + } + } + + private State Launch(ITestOutputHelper output, DaprRunConfiguration configuration) + { + var app = new DaprTestApp(output, configuration.AppId); + try + { + var (httpEndpoint, grpcEndpoint) = app.Start(configuration); + return new State() + { + App = app, + HttpEndpoint = httpEndpoint, + GrpcEndpoint = grpcEndpoint, + Output = output, + }; + } + catch (Exception startException) + { + try + { + app.Stop(); + throw; + } + catch (Exception stopException) + { + throw new AggregateException(startException, stopException); + } + } + } + + public class State + { + public string HttpEndpoint; + public string GrpcEndpoint; + public DaprTestApp App; + public ITestOutputHelper Output; + } +} \ No newline at end of file diff --git a/test/Dapr.E2E.Test/DaprTestAppLifecycle.cs b/test/Dapr.E2E.Test/DaprTestAppLifecycle.cs index 98103cda..07b62b6a 100644 --- a/test/Dapr.E2E.Test/DaprTestAppLifecycle.cs +++ b/test/Dapr.E2E.Test/DaprTestAppLifecycle.cs @@ -18,57 +18,56 @@ using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; -namespace Dapr.E2E.Test +namespace Dapr.E2E.Test; + +public class DaprTestAppLifecycle : IClassFixture, IAsyncLifetime { - public class DaprTestAppLifecycle : IClassFixture, IAsyncLifetime + + private readonly ITestOutputHelper output; + private readonly DaprTestAppFixture fixture; + private DaprTestAppFixture.State state; + + public DaprTestAppLifecycle(ITestOutputHelper output, DaprTestAppFixture fixture) { + this.output = output; + this.fixture = fixture; + } - private readonly ITestOutputHelper output; - private readonly DaprTestAppFixture fixture; - private DaprTestAppFixture.State state; + public DaprRunConfiguration Configuration { get; set; } - public DaprTestAppLifecycle(ITestOutputHelper output, DaprTestAppFixture fixture) + public string AppId => this.state?.App.AppId; + + public string HttpEndpoint => this.state?.HttpEndpoint; + + public string GrpcEndpoint => this.state?.GrpcEndpoint; + + public ITestOutputHelper Output => this.output; + + public async Task InitializeAsync() + { + this.state = await this.fixture.StartAsync(this.output, this.Configuration); + + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); + using var client = new HttpClient(); + + while (!cts.IsCancellationRequested) { - this.output = output; - this.fixture = fixture; - } + cts.Token.ThrowIfCancellationRequested(); - public DaprRunConfiguration Configuration { get; set; } - - public string AppId => this.state?.App.AppId; - - public string HttpEndpoint => this.state?.HttpEndpoint; - - public string GrpcEndpoint => this.state?.GrpcEndpoint; - - public ITestOutputHelper Output => this.output; - - public async Task InitializeAsync() - { - this.state = await this.fixture.StartAsync(this.output, this.Configuration); - - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); - using var client = new HttpClient(); - - while (!cts.IsCancellationRequested) + var response = await client.GetAsync($"{HttpEndpoint}/v1.0/healthz"); + if (response.IsSuccessStatusCode) { - cts.Token.ThrowIfCancellationRequested(); - - var response = await client.GetAsync($"{HttpEndpoint}/v1.0/healthz"); - if (response.IsSuccessStatusCode) - { - return; - } - - await Task.Delay(TimeSpan.FromMilliseconds(250)); + return; } - throw new TimeoutException("Timed out waiting for daprd health check"); + await Task.Delay(TimeSpan.FromMilliseconds(250)); } - public Task DisposeAsync() - { - return Task.CompletedTask; - } + throw new TimeoutException("Timed out waiting for daprd health check"); + } + + public Task DisposeAsync() + { + return Task.CompletedTask; } } \ No newline at end of file diff --git a/test/Dapr.E2E.Test/E2ETests.cs b/test/Dapr.E2E.Test/E2ETests.cs index bc469f71..45974a33 100644 --- a/test/Dapr.E2E.Test/E2ETests.cs +++ b/test/Dapr.E2E.Test/E2ETests.cs @@ -22,78 +22,77 @@ using Xunit.Abstractions; [assembly: CollectionBehavior(DisableTestParallelization = true)] -namespace Dapr.E2E.Test +namespace Dapr.E2E.Test; + +// We're using IClassFixture to manage the state we need across tests. +// +// So for that reason we need all of the E2E tests to be one big class. +// We're using partials to organize the functionality. +public partial class E2ETests : IClassFixture, IAsyncLifetime { - // We're using IClassFixture to manage the state we need across tests. - // - // So for that reason we need all of the E2E tests to be one big class. - // We're using partials to organize the functionality. - public partial class E2ETests : IClassFixture, IAsyncLifetime + private readonly Lazy proxyFactory; + private readonly DaprTestAppFixture fixture; + private DaprTestAppFixture.State state; + + public E2ETests(ITestOutputHelper output, DaprTestAppFixture fixture) { - private readonly Lazy proxyFactory; - private readonly DaprTestAppFixture fixture; - private DaprTestAppFixture.State state; + this.Output = output; + this.fixture = fixture; - public E2ETests(ITestOutputHelper output, DaprTestAppFixture fixture) + this.proxyFactory = new Lazy(() => { - this.Output = output; - this.fixture = fixture; + Debug.Assert(this.HttpEndpoint != null); + return new ActorProxyFactory(new ActorProxyOptions(){ HttpEndpoint = this.HttpEndpoint, }); + }); + } - this.proxyFactory = new Lazy(() => + protected ITestOutputHelper Output { get; } + + public DaprRunConfiguration Configuration { get; set; } = new DaprRunConfiguration + { + UseAppPort = true, + AppId = "testapp", + TargetProject = "./../../../../../test/Dapr.E2E.Test.App/Dapr.E2E.Test.App.csproj" + }; + + public string AppId => this.state?.App.AppId; + + public string HttpEndpoint => this.state?.HttpEndpoint; + + public string GrpcEndpoint => this.state?.GrpcEndpoint; + + public IActorProxyFactory ProxyFactory => this.HttpEndpoint == null ? null : this.proxyFactory.Value; + + public async Task InitializeAsync() + { + this.state = await this.fixture.StartAsync(this.Output, this.Configuration); + + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); + using var client = new HttpClient(); + + while (!cts.IsCancellationRequested) + { + cts.Token.ThrowIfCancellationRequested(); + + var response = await client.GetAsync($"{HttpEndpoint}/v1.0/healthz"); + if (response.IsSuccessStatusCode) { - Debug.Assert(this.HttpEndpoint != null); - return new ActorProxyFactory(new ActorProxyOptions(){ HttpEndpoint = this.HttpEndpoint, }); - }); - } - - protected ITestOutputHelper Output { get; } - - public DaprRunConfiguration Configuration { get; set; } = new DaprRunConfiguration - { - UseAppPort = true, - AppId = "testapp", - TargetProject = "./../../../../../test/Dapr.E2E.Test.App/Dapr.E2E.Test.App.csproj" - }; - - public string AppId => this.state?.App.AppId; - - public string HttpEndpoint => this.state?.HttpEndpoint; - - public string GrpcEndpoint => this.state?.GrpcEndpoint; - - public IActorProxyFactory ProxyFactory => this.HttpEndpoint == null ? null : this.proxyFactory.Value; - - public async Task InitializeAsync() - { - this.state = await this.fixture.StartAsync(this.Output, this.Configuration); - - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); - using var client = new HttpClient(); - - while (!cts.IsCancellationRequested) - { - cts.Token.ThrowIfCancellationRequested(); - - var response = await client.GetAsync($"{HttpEndpoint}/v1.0/healthz"); - if (response.IsSuccessStatusCode) - { - return; - } - - await Task.Delay(TimeSpan.FromMilliseconds(250)); + return; } - throw new TimeoutException("Timed out waiting for daprd health check"); + await Task.Delay(TimeSpan.FromMilliseconds(250)); } - public Task DisposeAsync() - { - return Task.CompletedTask; - } - - protected async Task WaitForActorRuntimeAsync(IPingActor proxy, CancellationToken cancellationToken) - { - await ActorRuntimeChecker.WaitForActorRuntimeAsync(this.AppId, this.Output, proxy, cancellationToken); - } + throw new TimeoutException("Timed out waiting for daprd health check"); } -} + + public Task DisposeAsync() + { + return Task.CompletedTask; + } + + protected async Task WaitForActorRuntimeAsync(IPingActor proxy, CancellationToken cancellationToken) + { + await ActorRuntimeChecker.WaitForActorRuntimeAsync(this.AppId, this.Output, proxy, cancellationToken); + } +} \ No newline at end of file diff --git a/test/Dapr.E2E.Test/HttpAssert.cs b/test/Dapr.E2E.Test/HttpAssert.cs index 5c3fd959..6958fd81 100644 --- a/test/Dapr.E2E.Test/HttpAssert.cs +++ b/test/Dapr.E2E.Test/HttpAssert.cs @@ -16,40 +16,39 @@ using System.Text.Json; using System.Threading.Tasks; using Xunit.Sdk; -namespace Dapr.E2E.Test -{ - // Assertion methods for HTTP requests - public static class HttpAssert - { - public static async Task<(TValue value, string json)> AssertJsonResponseAsync(HttpResponseMessage response) - { - if (response.Content is null && !response.IsSuccessStatusCode) - { - var message = $"The response had status code {response.StatusCode} and no body."; - throw new XunitException(message); - } - else if (!response.IsSuccessStatusCode) - { - var text = await response.Content.ReadAsStringAsync(); - var message = $"The response had status code {response.StatusCode} and body: " + Environment.NewLine + text; - throw new XunitException(message); - } - else - { - // Use the string to read from JSON so we can include it in the error if it fails. - var text = await response.Content.ReadAsStringAsync(); +namespace Dapr.E2E.Test; - try - { - var value = JsonSerializer.Deserialize(text, new JsonSerializerOptions(JsonSerializerDefaults.Web)); - return (value, text); - } - catch (JsonException ex) - { - var message = $"The response failed to deserialize to a {typeof(TValue).Name} from JSON with error: '{ex.Message}' and body: " + Environment.NewLine + text; - throw new XunitException(message); - } +// Assertion methods for HTTP requests +public static class HttpAssert +{ + public static async Task<(TValue value, string json)> AssertJsonResponseAsync(HttpResponseMessage response) + { + if (response.Content is null && !response.IsSuccessStatusCode) + { + var message = $"The response had status code {response.StatusCode} and no body."; + throw new XunitException(message); + } + else if (!response.IsSuccessStatusCode) + { + var text = await response.Content.ReadAsStringAsync(); + var message = $"The response had status code {response.StatusCode} and body: " + Environment.NewLine + text; + throw new XunitException(message); + } + else + { + // Use the string to read from JSON so we can include it in the error if it fails. + var text = await response.Content.ReadAsStringAsync(); + + try + { + var value = JsonSerializer.Deserialize(text, new JsonSerializerOptions(JsonSerializerDefaults.Web)); + return (value, text); + } + catch (JsonException ex) + { + var message = $"The response failed to deserialize to a {typeof(TValue).Name} from JSON with error: '{ex.Message}' and body: " + Environment.NewLine + text; + throw new XunitException(message); } } } -} +} \ No newline at end of file diff --git a/test/Dapr.E2E.Test/ServiceInvocation/E2ETests.GrpcProxyInvocationTests.cs b/test/Dapr.E2E.Test/ServiceInvocation/E2ETests.GrpcProxyInvocationTests.cs index 3b6f31e8..de77f739 100644 --- a/test/Dapr.E2E.Test/ServiceInvocation/E2ETests.GrpcProxyInvocationTests.cs +++ b/test/Dapr.E2E.Test/ServiceInvocation/E2ETests.GrpcProxyInvocationTests.cs @@ -20,79 +20,78 @@ using Grpc.Core; using Xunit; using Xunit.Abstractions; -namespace Dapr.E2E.Test +namespace Dapr.E2E.Test; + +public class GrpcProxyTests : DaprTestAppLifecycle { - public class GrpcProxyTests : DaprTestAppLifecycle + // Grpc proxying requires a specific setup so we can't use the built-in standard for the E2ETest partial class. + public GrpcProxyTests(ITestOutputHelper output, DaprTestAppFixture fixture) : base(output, fixture) { - // Grpc proxying requires a specific setup so we can't use the built-in standard for the E2ETest partial class. - public GrpcProxyTests(ITestOutputHelper output, DaprTestAppFixture fixture) : base(output, fixture) + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); + base.Configuration = new DaprRunConfiguration { - AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); - base.Configuration = new DaprRunConfiguration + UseAppPort = true, + AppId = "grpcapp", + AppProtocol = "grpc", + TargetProject = "./../../../../../test/Dapr.E2E.Test.App.Grpc/Dapr.E2E.Test.App.Grpc.csproj", + }; + } + + [Fact] + public async Task TestGrpcProxyMessageSendAndReceive() + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20)); + var invoker = DaprClient.CreateInvocationInvoker(appId: this.AppId, daprEndpoint: this.GrpcEndpoint); + var client = new Messager.MessagerClient(invoker); + + await client.SendMessageAsync(new SendMessageRequest { Recipient = "Client", Message = "Hello"}, cancellationToken: cts.Token); + + var response = await client.GetMessageAsync(new GetMessageRequest { Recipient = "Client" }, cancellationToken: cts.Token); + + Assert.Equal("Hello", response.Message); + } + + [Fact] + public async Task TestGrpcProxyStreamingBroadcast() + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20)); + var invoker = DaprClient.CreateInvocationInvoker(appId: this.AppId, daprEndpoint: this.GrpcEndpoint); + var client = new Messager.MessagerClient(invoker); + + var messageCount = 10; + var messageReceived = 0; + using (var call = client.StreamBroadcast(cancellationToken: cts.Token)) { + var responseTask = Task.Run(async () => { - UseAppPort = true, - AppId = "grpcapp", - AppProtocol = "grpc", - TargetProject = "./../../../../../test/Dapr.E2E.Test.App.Grpc/Dapr.E2E.Test.App.Grpc.csproj", - }; - } - - [Fact] - public async Task TestGrpcProxyMessageSendAndReceive() - { - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20)); - var invoker = DaprClient.CreateInvocationInvoker(appId: this.AppId, daprEndpoint: this.GrpcEndpoint); - var client = new Messager.MessagerClient(invoker); - - await client.SendMessageAsync(new SendMessageRequest { Recipient = "Client", Message = "Hello"}, cancellationToken: cts.Token); - - var response = await client.GetMessageAsync(new GetMessageRequest { Recipient = "Client" }, cancellationToken: cts.Token); - - Assert.Equal("Hello", response.Message); - } - - [Fact] - public async Task TestGrpcProxyStreamingBroadcast() - { - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20)); - var invoker = DaprClient.CreateInvocationInvoker(appId: this.AppId, daprEndpoint: this.GrpcEndpoint); - var client = new Messager.MessagerClient(invoker); - - var messageCount = 10; - var messageReceived = 0; - using (var call = client.StreamBroadcast(cancellationToken: cts.Token)) { - var responseTask = Task.Run(async () => + await foreach (var response in call.ResponseStream.ReadAllAsync()) { - await foreach (var response in call.ResponseStream.ReadAllAsync()) - { - Assert.Equal($"Hello: {messageReceived++}", response.Message); - } - }); - - for (var i = 0; i < messageCount; i++) - { - await call.RequestStream.WriteAsync(new Broadcast { Message = $"Hello: {i}" }); + Assert.Equal($"Hello: {messageReceived++}", response.Message); } - await call.RequestStream.CompleteAsync(); + }); - await responseTask; + for (var i = 0; i < messageCount; i++) + { + await call.RequestStream.WriteAsync(new Broadcast { Message = $"Hello: {i}" }); } - } + await call.RequestStream.CompleteAsync(); - [Fact] - public async Task TestGrpcServiceInvocationWithTimeout() - { - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20)); - var invoker = DaprClient.CreateInvocationInvoker(appId: this.AppId, daprEndpoint: this.GrpcEndpoint); - var client = new Messager.MessagerClient(invoker); - - var options = new CallOptions(cancellationToken: cts.Token, deadline: DateTime.UtcNow.AddSeconds(1)); - var ex = await Assert.ThrowsAsync(async () => - { - await client.DelayedResponseAsync(new Empty(), options); - }); - - Assert.Equal(StatusCode.DeadlineExceeded, ex.StatusCode); + await responseTask; } } + + [Fact] + public async Task TestGrpcServiceInvocationWithTimeout() + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20)); + var invoker = DaprClient.CreateInvocationInvoker(appId: this.AppId, daprEndpoint: this.GrpcEndpoint); + var client = new Messager.MessagerClient(invoker); + + var options = new CallOptions(cancellationToken: cts.Token, deadline: DateTime.UtcNow.AddSeconds(1)); + var ex = await Assert.ThrowsAsync(async () => + { + await client.DelayedResponseAsync(new Empty(), options); + }); + + Assert.Equal(StatusCode.DeadlineExceeded, ex.StatusCode); + } } \ No newline at end of file diff --git a/test/Dapr.E2E.Test/ServiceInvocation/E2ETests.ServiceInvocationTests.cs b/test/Dapr.E2E.Test/ServiceInvocation/E2ETests.ServiceInvocationTests.cs index 78fed3d3..af94981c 100644 --- a/test/Dapr.E2E.Test/ServiceInvocation/E2ETests.ServiceInvocationTests.cs +++ b/test/Dapr.E2E.Test/ServiceInvocation/E2ETests.ServiceInvocationTests.cs @@ -10,85 +10,84 @@ // See the License for the specific language governing permissions and // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.E2E.Test +namespace Dapr.E2E.Test; + +using System; +using System.Net.Http; +using System.Net.Http.Json; +using System.Threading; +using System.Threading.Tasks; +using Dapr.Client; +using Xunit; + +public partial class E2ETests { - using System; - using System.Net.Http; - using System.Net.Http.Json; - using System.Threading; - using System.Threading.Tasks; - using Dapr.Client; - using Xunit; - - public partial class E2ETests + [Fact] + public async Task TestServiceInvocation() { - [Fact] - public async Task TestServiceInvocation() + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20)); + + using var client = DaprClient.CreateInvokeHttpClient(appId: this.AppId, daprEndpoint: this.HttpEndpoint); + var transaction = new Transaction() { - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20)); + Id = "1", + Amount = 50 + }; - using var client = DaprClient.CreateInvokeHttpClient(appId: this.AppId, daprEndpoint: this.HttpEndpoint); - var transaction = new Transaction() - { - Id = "1", - Amount = 50 - }; + var response = await client.PostAsJsonAsync("/accountDetails", transaction, cts.Token); + var (account, _) = await HttpAssert.AssertJsonResponseAsync(response); - var response = await client.PostAsJsonAsync("/accountDetails", transaction, cts.Token); - var (account, _) = await HttpAssert.AssertJsonResponseAsync(response); - - Assert.Equal("1", account.Id); - Assert.Equal(150, account.Balance); - } - - [Fact] - public async Task TestServiceInvocationRequiresApiToken() - { - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20)); - - using var client = DaprClient.CreateInvokeHttpClient(appId: this.AppId, daprEndpoint: this.HttpEndpoint); - - var transaction = new Transaction() - { - Id = "1", - Amount = 50 - }; - var response = await client.PostAsJsonAsync("/accountDetails-requires-api-token", transaction, cts.Token); - var (account, _) = await HttpAssert.AssertJsonResponseAsync(response); - - Assert.Equal("1", account.Id); - Assert.Equal(150, account.Balance); - } - - [Fact] - public async Task TestHttpServiceInvocationWithTimeout() - { - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20)); - using var client = new DaprClientBuilder() - .UseHttpEndpoint(this.HttpEndpoint) - .UseTimeout(TimeSpan.FromSeconds(1)) - .Build(); - - await Assert.ThrowsAsync(async () => - { - await client.InvokeMethodAsync( - appId: this.AppId, - methodName: "DelayedResponse", - httpMethod: new HttpMethod("GET"), - cancellationToken: cts.Token); - }); - } + Assert.Equal("1", account.Id); + Assert.Equal(150, account.Balance); } - internal class Transaction + [Fact] + public async Task TestServiceInvocationRequiresApiToken() { - public string Id { get; set; } - public decimal Amount { get; set; } + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20)); + + using var client = DaprClient.CreateInvokeHttpClient(appId: this.AppId, daprEndpoint: this.HttpEndpoint); + + var transaction = new Transaction() + { + Id = "1", + Amount = 50 + }; + var response = await client.PostAsJsonAsync("/accountDetails-requires-api-token", transaction, cts.Token); + var (account, _) = await HttpAssert.AssertJsonResponseAsync(response); + + Assert.Equal("1", account.Id); + Assert.Equal(150, account.Balance); } - internal class Account + [Fact] + public async Task TestHttpServiceInvocationWithTimeout() { - public string Id { get; set; } - public decimal Balance { get; set; } + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20)); + using var client = new DaprClientBuilder() + .UseHttpEndpoint(this.HttpEndpoint) + .UseTimeout(TimeSpan.FromSeconds(1)) + .Build(); + + await Assert.ThrowsAsync(async () => + { + await client.InvokeMethodAsync( + appId: this.AppId, + methodName: "DelayedResponse", + httpMethod: new HttpMethod("GET"), + cancellationToken: cts.Token); + }); } } + +internal class Transaction +{ + public string Id { get; set; } + public decimal Amount { get; set; } +} + +internal class Account +{ + public string Id { get; set; } + public decimal Balance { get; set; } +} \ No newline at end of file diff --git a/test/Dapr.Extensions.Configuration.Test/DaprConfigurationStoreProviderTest.cs b/test/Dapr.Extensions.Configuration.Test/DaprConfigurationStoreProviderTest.cs index ee86a206..a1cb299f 100644 --- a/test/Dapr.Extensions.Configuration.Test/DaprConfigurationStoreProviderTest.cs +++ b/test/Dapr.Extensions.Configuration.Test/DaprConfigurationStoreProviderTest.cs @@ -8,201 +8,200 @@ using Microsoft.Extensions.Configuration; using Xunit; using Autogenerated = Dapr.Client.Autogen.Grpc.v1; -namespace Dapr.Extensions.Configuration.Test +namespace Dapr.Extensions.Configuration.Test; + +public class DaprConfigurationStoreProviderTest { - public class DaprConfigurationStoreProviderTest + [Fact] + public void TestConfigurationStoreExtension_ThrowsWithNullStore() { - [Fact] - public void TestConfigurationStoreExtension_ThrowsWithNullStore() + var daprClient = new DaprClientBuilder() + .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = new TestHttpClient() }) + .Build(); + + Assert.Throws(() => { - var daprClient = new DaprClientBuilder() - .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = new TestHttpClient() }) - .Build(); - - Assert.Throws(() => - { - new ConfigurationBuilder().AddDaprConfigurationStore(null, new List(), daprClient, TimeSpan.FromSeconds(5)); - }); - } - - [Fact] - public void TestConfigurationStoreExtension_ThrowsWithEmptyStore() - { - var daprClient = new DaprClientBuilder() - .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = new TestHttpClient() }) - .Build(); - - Assert.Throws(() => - { - new ConfigurationBuilder().AddDaprConfigurationStore(string.Empty, new List(), daprClient, TimeSpan.FromSeconds(5)); - }); - } - - [Fact] - public void TestConfigurationStoreExtension_ThrowsWithNullKeys() - { - var daprClient = new DaprClientBuilder() - .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = new TestHttpClient() }) - .Build(); - - Assert.Throws(() => - { - new ConfigurationBuilder().AddDaprConfigurationStore("configstore", null, daprClient, TimeSpan.FromSeconds(5)); - }); - } - - [Fact] - public void TestConfigurationStoreExtension_ThrowsWithNullClient() - { - Assert.Throws(() => - { - new ConfigurationBuilder().AddDaprConfigurationStore("configstore", new List(), null, TimeSpan.FromSeconds(5)); - }); - } - - [Fact] - public void TestSubscribeConfigurationStoreExtension_ThrowsWithNullStore() - { - var daprClient = new DaprClientBuilder() - .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = new TestHttpClient() }) - .Build(); - - Assert.Throws(() => - { - new ConfigurationBuilder().AddStreamingDaprConfigurationStore(null, new List(), daprClient, TimeSpan.FromSeconds(5)); - }); - } - - [Fact] - public void TestSubscribeConfigurationStoreExtension_ThrowsWithEmptyStore() - { - var daprClient = new DaprClientBuilder() - .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = new TestHttpClient() }) - .Build(); - - Assert.Throws(() => - { - new ConfigurationBuilder().AddStreamingDaprConfigurationStore(string.Empty, new List(), daprClient, TimeSpan.FromSeconds(5)); - }); - } - - [Fact] - public void TestSubscribeConfigurationStoreExtension_ThrowsWithNullKeys() - { - var daprClient = new DaprClientBuilder() - .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = new TestHttpClient() }) - .Build(); - - Assert.Throws(() => - { - new ConfigurationBuilder().AddStreamingDaprConfigurationStore("configstore", null, daprClient, TimeSpan.FromSeconds(5)); - }); - } - - [Fact] - public void TestSubscribeConfigurationStoreExtension_ThrowsWithNullClient() - { - Assert.Throws(() => - { - new ConfigurationBuilder().AddStreamingDaprConfigurationStore("configstore", new List(), null, TimeSpan.FromSeconds(5)); - }); - } - - [Fact] - public async Task TestConfigurationStoreExtension_ProperlyStoresValues() - { - // Sample item. - var item = new ConfigurationItem("testValue", "v1", null); - - // Configure Client - var httpClient = new TestHttpClient() - { - Handler = async (entry) => - { - var items = new Dictionary(); - items["testKey"] = item; - await SendResponseWithConfiguration(items, entry); - } - }; - - var daprClient = new DaprClientBuilder() - .UseHttpClientFactory(() => httpClient) - .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = httpClient }) - .Build(); - - var config = new ConfigurationBuilder() - .AddDaprConfigurationStore("store", new List(), daprClient, TimeSpan.FromSeconds(5)) - .Build(); - - await Task.Delay(TimeSpan.FromMilliseconds(500)); - - Assert.Equal(item.Value, config["testKey"]); - } - - [Fact] - public async Task TestStreamingConfigurationStoreExtension_ProperlyStoresValues() - { - // Sample item. - var item = new ConfigurationItem("testValue", "v1", null); - - // Configure Client - var httpClient = new TestHttpClient() - { - Handler = async (entry) => - { - var items = new Dictionary(); - items["testKey"] = item; - await SendStreamingResponseWithConfiguration(items, entry); - } - }; - - var daprClient = new DaprClientBuilder() - .UseHttpClientFactory(() => httpClient) - .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = httpClient }) - .Build(); - - var config = new ConfigurationBuilder() - .AddStreamingDaprConfigurationStore("store", new List(), daprClient, TimeSpan.FromSeconds(5)) - .Build(); - - await Task.Delay(TimeSpan.FromMilliseconds(500)); - - Assert.Equal(item.Value, config["testKey"]); - } - - private async Task SendResponseWithConfiguration(Dictionary items, TestHttpClient.Entry entry) - { - var configurationResponse = new Autogenerated.GetConfigurationResponse(); - foreach (var item in items) - { - configurationResponse.Items[item.Key] = new Autogenerated.ConfigurationItem() - { - Value = item.Value.Value, - Version = item.Value.Version - }; - } - - var streamContent = await GrpcUtils.CreateResponseContent(configurationResponse); - var response = GrpcUtils.CreateResponse(HttpStatusCode.OK, streamContent); - entry.Completion.SetResult(response); - } - - private async Task SendStreamingResponseWithConfiguration(Dictionary items, TestHttpClient.Entry entry) - { - var streamResponse = new Autogenerated.SubscribeConfigurationResponse(); - streamResponse.Id = "testId"; - foreach (var item in items) - { - streamResponse.Items[item.Key] = new Autogenerated.ConfigurationItem() - { - Value = item.Value.Value, - Version = item.Value.Version - }; - } - - var streamContent = await GrpcUtils.CreateResponseContent(streamResponse); - var response = GrpcUtils.CreateResponse(HttpStatusCode.OK, streamContent); - entry.Completion.SetResult(response); - } + new ConfigurationBuilder().AddDaprConfigurationStore(null, new List(), daprClient, TimeSpan.FromSeconds(5)); + }); } -} + + [Fact] + public void TestConfigurationStoreExtension_ThrowsWithEmptyStore() + { + var daprClient = new DaprClientBuilder() + .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = new TestHttpClient() }) + .Build(); + + Assert.Throws(() => + { + new ConfigurationBuilder().AddDaprConfigurationStore(string.Empty, new List(), daprClient, TimeSpan.FromSeconds(5)); + }); + } + + [Fact] + public void TestConfigurationStoreExtension_ThrowsWithNullKeys() + { + var daprClient = new DaprClientBuilder() + .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = new TestHttpClient() }) + .Build(); + + Assert.Throws(() => + { + new ConfigurationBuilder().AddDaprConfigurationStore("configstore", null, daprClient, TimeSpan.FromSeconds(5)); + }); + } + + [Fact] + public void TestConfigurationStoreExtension_ThrowsWithNullClient() + { + Assert.Throws(() => + { + new ConfigurationBuilder().AddDaprConfigurationStore("configstore", new List(), null, TimeSpan.FromSeconds(5)); + }); + } + + [Fact] + public void TestSubscribeConfigurationStoreExtension_ThrowsWithNullStore() + { + var daprClient = new DaprClientBuilder() + .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = new TestHttpClient() }) + .Build(); + + Assert.Throws(() => + { + new ConfigurationBuilder().AddStreamingDaprConfigurationStore(null, new List(), daprClient, TimeSpan.FromSeconds(5)); + }); + } + + [Fact] + public void TestSubscribeConfigurationStoreExtension_ThrowsWithEmptyStore() + { + var daprClient = new DaprClientBuilder() + .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = new TestHttpClient() }) + .Build(); + + Assert.Throws(() => + { + new ConfigurationBuilder().AddStreamingDaprConfigurationStore(string.Empty, new List(), daprClient, TimeSpan.FromSeconds(5)); + }); + } + + [Fact] + public void TestSubscribeConfigurationStoreExtension_ThrowsWithNullKeys() + { + var daprClient = new DaprClientBuilder() + .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = new TestHttpClient() }) + .Build(); + + Assert.Throws(() => + { + new ConfigurationBuilder().AddStreamingDaprConfigurationStore("configstore", null, daprClient, TimeSpan.FromSeconds(5)); + }); + } + + [Fact] + public void TestSubscribeConfigurationStoreExtension_ThrowsWithNullClient() + { + Assert.Throws(() => + { + new ConfigurationBuilder().AddStreamingDaprConfigurationStore("configstore", new List(), null, TimeSpan.FromSeconds(5)); + }); + } + + [Fact] + public async Task TestConfigurationStoreExtension_ProperlyStoresValues() + { + // Sample item. + var item = new ConfigurationItem("testValue", "v1", null); + + // Configure Client + var httpClient = new TestHttpClient() + { + Handler = async (entry) => + { + var items = new Dictionary(); + items["testKey"] = item; + await SendResponseWithConfiguration(items, entry); + } + }; + + var daprClient = new DaprClientBuilder() + .UseHttpClientFactory(() => httpClient) + .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = httpClient }) + .Build(); + + var config = new ConfigurationBuilder() + .AddDaprConfigurationStore("store", new List(), daprClient, TimeSpan.FromSeconds(5)) + .Build(); + + await Task.Delay(TimeSpan.FromMilliseconds(500)); + + Assert.Equal(item.Value, config["testKey"]); + } + + [Fact] + public async Task TestStreamingConfigurationStoreExtension_ProperlyStoresValues() + { + // Sample item. + var item = new ConfigurationItem("testValue", "v1", null); + + // Configure Client + var httpClient = new TestHttpClient() + { + Handler = async (entry) => + { + var items = new Dictionary(); + items["testKey"] = item; + await SendStreamingResponseWithConfiguration(items, entry); + } + }; + + var daprClient = new DaprClientBuilder() + .UseHttpClientFactory(() => httpClient) + .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = httpClient }) + .Build(); + + var config = new ConfigurationBuilder() + .AddStreamingDaprConfigurationStore("store", new List(), daprClient, TimeSpan.FromSeconds(5)) + .Build(); + + await Task.Delay(TimeSpan.FromMilliseconds(500)); + + Assert.Equal(item.Value, config["testKey"]); + } + + private async Task SendResponseWithConfiguration(Dictionary items, TestHttpClient.Entry entry) + { + var configurationResponse = new Autogenerated.GetConfigurationResponse(); + foreach (var item in items) + { + configurationResponse.Items[item.Key] = new Autogenerated.ConfigurationItem() + { + Value = item.Value.Value, + Version = item.Value.Version + }; + } + + var streamContent = await GrpcUtils.CreateResponseContent(configurationResponse); + var response = GrpcUtils.CreateResponse(HttpStatusCode.OK, streamContent); + entry.Completion.SetResult(response); + } + + private async Task SendStreamingResponseWithConfiguration(Dictionary items, TestHttpClient.Entry entry) + { + var streamResponse = new Autogenerated.SubscribeConfigurationResponse(); + streamResponse.Id = "testId"; + foreach (var item in items) + { + streamResponse.Items[item.Key] = new Autogenerated.ConfigurationItem() + { + Value = item.Value.Value, + Version = item.Value.Version + }; + } + + var streamContent = await GrpcUtils.CreateResponseContent(streamResponse); + var response = GrpcUtils.CreateResponse(HttpStatusCode.OK, streamContent); + entry.Completion.SetResult(response); + } +} \ No newline at end of file diff --git a/test/Dapr.Extensions.Configuration.Test/DaprSecretStoreConfigurationProviderTest.cs b/test/Dapr.Extensions.Configuration.Test/DaprSecretStoreConfigurationProviderTest.cs index d92fc46b..53cb4fc6 100644 --- a/test/Dapr.Extensions.Configuration.Test/DaprSecretStoreConfigurationProviderTest.cs +++ b/test/Dapr.Extensions.Configuration.Test/DaprSecretStoreConfigurationProviderTest.cs @@ -23,916 +23,915 @@ using Shouldly; using Xunit; using Autogenerated = Dapr.Client.Autogen.Grpc.v1; -namespace Dapr.Extensions.Configuration.Test +namespace Dapr.Extensions.Configuration.Test; + +// These tests use the outdated TestHttpClient infrastructure because they need to +// support testing with synchronous HTTP requests. +// +// Don't copy this pattern elsewhere. +public class DaprSecretStoreConfigurationProviderTest { - // These tests use the outdated TestHttpClient infrastructure because they need to - // support testing with synchronous HTTP requests. - // - // Don't copy this pattern elsewhere. - public class DaprSecretStoreConfigurationProviderTest + + [Fact] + public void AddDaprSecretStore_UsingDescriptors_WithoutStore_ReportsError() { + var daprClient = new DaprClientBuilder() + .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = new TestHttpClient() }) + .Build(); - [Fact] - public void AddDaprSecretStore_UsingDescriptors_WithoutStore_ReportsError() + var ex = Assert.Throws(() => { - var daprClient = new DaprClientBuilder() - .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = new TestHttpClient() }) + CreateBuilder() + .AddDaprSecretStore(null, new DaprSecretDescriptor[] { new DaprSecretDescriptor("secretName") }, daprClient) .Build(); + }); - var ex = Assert.Throws(() => - { - CreateBuilder() - .AddDaprSecretStore(null, new DaprSecretDescriptor[] { new DaprSecretDescriptor("secretName") }, daprClient) - .Build(); - }); + Assert.Contains("store", ex.Message); + } - Assert.Contains("store", ex.Message); - } + [Fact] + public void AddDaprSecretStore_UsingDescriptors_WithEmptyStore_ReportsError() + { + var daprClient = new DaprClientBuilder() + .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = new TestHttpClient() }) + .Build(); - [Fact] - public void AddDaprSecretStore_UsingDescriptors_WithEmptyStore_ReportsError() + var ex = Assert.Throws(() => { - var daprClient = new DaprClientBuilder() - .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = new TestHttpClient() }) + CreateBuilder() + .AddDaprSecretStore(string.Empty, new DaprSecretDescriptor[] { new DaprSecretDescriptor("secretName") }, daprClient) .Build(); + }); - var ex = Assert.Throws(() => - { - CreateBuilder() - .AddDaprSecretStore(string.Empty, new DaprSecretDescriptor[] { new DaprSecretDescriptor("secretName") }, daprClient) - .Build(); - }); + Assert.Contains("The value cannot be null or empty", ex.Message); + } - Assert.Contains("The value cannot be null or empty", ex.Message); - } + [Fact] + public void AddDaprSecretStore_UsingDescriptors_WithoutSecretDescriptors_ReportsError() + { + var daprClient = new DaprClientBuilder() + .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = new TestHttpClient() }) + .Build(); - [Fact] - public void AddDaprSecretStore_UsingDescriptors_WithoutSecretDescriptors_ReportsError() + var ex = Assert.Throws(() => { - var daprClient = new DaprClientBuilder() - .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = new TestHttpClient() }) + CreateBuilder() + .AddDaprSecretStore("store", (DaprSecretDescriptor[])null, daprClient) .Build(); + }); - var ex = Assert.Throws(() => - { - CreateBuilder() - .AddDaprSecretStore("store", (DaprSecretDescriptor[])null, daprClient) - .Build(); - }); + Assert.Contains("secretDescriptors", ex.Message); + } - Assert.Contains("secretDescriptors", ex.Message); - } - - [Fact] - public void AddDaprSecretStore_UsingDescriptors_WithoutClient_ReportsError() + [Fact] + public void AddDaprSecretStore_UsingDescriptors_WithoutClient_ReportsError() + { + var ex = Assert.Throws(() => { - var ex = Assert.Throws(() => - { - CreateBuilder() - .AddDaprSecretStore("store", new DaprSecretDescriptor[] { new DaprSecretDescriptor("secretName") }, null) - .Build(); - }); - - Assert.Contains("client", ex.Message); - } - - [Fact] - public void AddDaprSecretStore_UsingDescriptors_WithZeroSecretDescriptors_ReportsError() - { - var daprClient = new DaprClientBuilder() - .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = new TestHttpClient() }) + CreateBuilder() + .AddDaprSecretStore("store", new DaprSecretDescriptor[] { new DaprSecretDescriptor("secretName") }, null) .Build(); + }); - var ex = Assert.Throws(() => - { - CreateBuilder() - .AddDaprSecretStore("store", new DaprSecretDescriptor[] { }, daprClient) - .Build(); - }); + Assert.Contains("client", ex.Message); + } - Assert.Contains("No secret descriptor was provided", ex.Message); - } + [Fact] + public void AddDaprSecretStore_UsingDescriptors_WithZeroSecretDescriptors_ReportsError() + { + var daprClient = new DaprClientBuilder() + .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = new TestHttpClient() }) + .Build(); - [Fact] - public void AddDaprSecretStore_UsingDescriptors_DuplicateSecret_ReportsError() + var ex = Assert.Throws(() => { - var httpClient = new TestHttpClient() - { - Handler = async (entry) => - { - var secrets = new Dictionary() { { "secretName", "secret" }, { "SecretName", "secret" } }; - await SendResponseWithSecrets(secrets, entry); - } - }; - - var daprClient = new DaprClientBuilder() - .UseHttpClientFactory(() => httpClient) - .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = httpClient }) + CreateBuilder() + .AddDaprSecretStore("store", new DaprSecretDescriptor[] { }, daprClient) .Build(); + }); - var ex = Assert.Throws(() => - { - CreateBuilder() - .AddDaprSecretStore("store", new DaprSecretDescriptor[] { new DaprSecretDescriptor("secretName") }, daprClient) - .Build(); - }); + Assert.Contains("No secret descriptor was provided", ex.Message); + } - Assert.Contains("Please remove any duplicates from your secret store.", ex.Message); - } - - [Fact] - public void LoadSecrets_FromSecretStoreThatReturnsOneValue() + [Fact] + public void AddDaprSecretStore_UsingDescriptors_DuplicateSecret_ReportsError() + { + var httpClient = new TestHttpClient() { - var storeName = "store"; - var secretKey = "secretName"; - var secretValue = "secret"; - - var secretDescriptors = new[] + Handler = async (entry) => { - new DaprSecretDescriptor(secretKey), - }; + var secrets = new Dictionary() { { "secretName", "secret" }, { "SecretName", "secret" } }; + await SendResponseWithSecrets(secrets, entry); + } + }; - var daprClient = new Mock(); + var daprClient = new DaprClientBuilder() + .UseHttpClientFactory(() => httpClient) + .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = httpClient }) + .Build(); - daprClient.Setup(c => c.GetSecretAsync(storeName, secretKey, - It.IsAny>(), default)) - .ReturnsAsync(new Dictionary { { secretKey, secretValue } }); - - var config = CreateBuilder() - .AddDaprSecretStore(storeName, secretDescriptors, daprClient.Object) - .Build(); - - config["secretName"].ShouldBe("secret"); - } - - [Fact] - public void LoadSecrets_FromSecretStoreThatCanReturnsMultipleValues() + var ex = Assert.Throws(() => { - var storeName = "store"; - var firstSecretKey = "first_secret"; - var secondSecretKey = "second_secret"; - var firstSecretValue = "secret1"; - var secondSecretValue = "secret2"; - - var secretDescriptors = new[] - { - new DaprSecretDescriptor(firstSecretKey), - new DaprSecretDescriptor(secondSecretKey), - }; - - var daprClient = new Mock(); - - daprClient.Setup(c => c.GetSecretAsync(storeName, firstSecretKey, - It.IsAny>(), default)) - .ReturnsAsync(new Dictionary { { firstSecretKey, firstSecretValue } }); - - daprClient.Setup(c => c.GetSecretAsync(storeName, secondSecretKey, - It.IsAny>(), default)) - .ReturnsAsync(new Dictionary { { secondSecretKey, secondSecretValue } }); - - var config = CreateBuilder() - .AddDaprSecretStore(storeName, secretDescriptors, daprClient.Object) + CreateBuilder() + .AddDaprSecretStore("store", new DaprSecretDescriptor[] { new DaprSecretDescriptor("secretName") }, daprClient) .Build(); + }); - config[firstSecretKey].ShouldBe(firstSecretValue); - config[secondSecretKey].ShouldBe(secondSecretValue); - } + Assert.Contains("Please remove any duplicates from your secret store.", ex.Message); + } - [Fact] - public void LoadSecrets_FromSecretStoreThatCanReturnsMultivaluedValues() + [Fact] + public void LoadSecrets_FromSecretStoreThatReturnsOneValue() + { + var storeName = "store"; + var secretKey = "secretName"; + var secretValue = "secret"; + + var secretDescriptors = new[] { - var storeName = "store"; - var parentSecretKey = "connectionStrings"; - var firstSecretKey = "first_secret"; - var secondSecretKey = "second_secret"; - var firstSecretValue = "secret1"; - var secondSecretValue = "secret2"; + new DaprSecretDescriptor(secretKey), + }; - var secretDescriptors = new[] - { - new DaprSecretDescriptor(parentSecretKey) - }; + var daprClient = new Mock(); - var daprClient = new Mock(); + daprClient.Setup(c => c.GetSecretAsync(storeName, secretKey, + It.IsAny>(), default)) + .ReturnsAsync(new Dictionary { { secretKey, secretValue } }); - daprClient.Setup(c => c.GetSecretAsync(storeName, parentSecretKey, - It.IsAny>(), default)) - .ReturnsAsync(new Dictionary { { firstSecretKey, firstSecretValue }, { secondSecretKey, secondSecretValue } }); + var config = CreateBuilder() + .AddDaprSecretStore(storeName, secretDescriptors, daprClient.Object) + .Build(); - var config = CreateBuilder() - .AddDaprSecretStore(storeName, secretDescriptors, daprClient.Object) - .Build(); + config["secretName"].ShouldBe("secret"); + } - config[firstSecretKey].ShouldBe(firstSecretValue); - config[secondSecretKey].ShouldBe(secondSecretValue); - } + [Fact] + public void LoadSecrets_FromSecretStoreThatCanReturnsMultipleValues() + { + var storeName = "store"; + var firstSecretKey = "first_secret"; + var secondSecretKey = "second_secret"; + var firstSecretValue = "secret1"; + var secondSecretValue = "secret2"; - [Fact] - public void LoadSecrets_FromSecretStoreWithADifferentSecretKeyAndName() + var secretDescriptors = new[] { - var storeName = "store"; - var secretKey = "Microsservice-DatabaseConnStr"; - var secretName = "ConnectionStrings:DatabaseConnStr"; - var secretValue = "secret1"; + new DaprSecretDescriptor(firstSecretKey), + new DaprSecretDescriptor(secondSecretKey), + }; - var secretDescriptors = new[] - { - new DaprSecretDescriptor(secretName, new Dictionary(), true, - secretKey) - }; + var daprClient = new Mock(); - var daprClient = new Mock(); + daprClient.Setup(c => c.GetSecretAsync(storeName, firstSecretKey, + It.IsAny>(), default)) + .ReturnsAsync(new Dictionary { { firstSecretKey, firstSecretValue } }); - daprClient.Setup(c => c.GetSecretAsync(storeName, secretKey, - It.IsAny>(), default)) - .ReturnsAsync(new Dictionary { { secretKey, secretValue } }); + daprClient.Setup(c => c.GetSecretAsync(storeName, secondSecretKey, + It.IsAny>(), default)) + .ReturnsAsync(new Dictionary { { secondSecretKey, secondSecretValue } }); - var config = CreateBuilder() - .AddDaprSecretStore(storeName, secretDescriptors, daprClient.Object) - .Build(); + var config = CreateBuilder() + .AddDaprSecretStore(storeName, secretDescriptors, daprClient.Object) + .Build(); - config[secretName].ShouldBe(secretValue); - } + config[firstSecretKey].ShouldBe(firstSecretValue); + config[secondSecretKey].ShouldBe(secondSecretValue); + } - [Fact] - public void LoadSecrets_FromSecretStoreNotRequiredAndDoesNotExist_ShouldNotThrowException() + [Fact] + public void LoadSecrets_FromSecretStoreThatCanReturnsMultivaluedValues() + { + var storeName = "store"; + var parentSecretKey = "connectionStrings"; + var firstSecretKey = "first_secret"; + var secondSecretKey = "second_secret"; + var firstSecretValue = "secret1"; + var secondSecretValue = "secret2"; + + var secretDescriptors = new[] { - var storeName = "store"; - var secretName = "ConnectionStrings:DatabaseConnStr"; + new DaprSecretDescriptor(parentSecretKey) + }; - var secretDescriptors = new[] + var daprClient = new Mock(); + + daprClient.Setup(c => c.GetSecretAsync(storeName, parentSecretKey, + It.IsAny>(), default)) + .ReturnsAsync(new Dictionary { { firstSecretKey, firstSecretValue }, { secondSecretKey, secondSecretValue } }); + + var config = CreateBuilder() + .AddDaprSecretStore(storeName, secretDescriptors, daprClient.Object) + .Build(); + + config[firstSecretKey].ShouldBe(firstSecretValue); + config[secondSecretKey].ShouldBe(secondSecretValue); + } + + [Fact] + public void LoadSecrets_FromSecretStoreWithADifferentSecretKeyAndName() + { + var storeName = "store"; + var secretKey = "Microsservice-DatabaseConnStr"; + var secretName = "ConnectionStrings:DatabaseConnStr"; + var secretValue = "secret1"; + + var secretDescriptors = new[] + { + new DaprSecretDescriptor(secretName, new Dictionary(), true, + secretKey) + }; + + var daprClient = new Mock(); + + daprClient.Setup(c => c.GetSecretAsync(storeName, secretKey, + It.IsAny>(), default)) + .ReturnsAsync(new Dictionary { { secretKey, secretValue } }); + + var config = CreateBuilder() + .AddDaprSecretStore(storeName, secretDescriptors, daprClient.Object) + .Build(); + + config[secretName].ShouldBe(secretValue); + } + + [Fact] + public void LoadSecrets_FromSecretStoreNotRequiredAndDoesNotExist_ShouldNotThrowException() + { + var storeName = "store"; + var secretName = "ConnectionStrings:DatabaseConnStr"; + + var secretDescriptors = new[] + { + new DaprSecretDescriptor(secretName, new Dictionary(), false) + }; + + var httpClient = new TestHttpClient + { + Handler = async entry => { - new DaprSecretDescriptor(secretName, new Dictionary(), false) - }; + await SendEmptyResponse(entry); + } + }; - var httpClient = new TestHttpClient + var daprClient = new DaprClientBuilder() + .UseHttpClientFactory(() => httpClient) + .Build(); + + var config = CreateBuilder() + .AddDaprSecretStore(storeName, secretDescriptors, daprClient) + .Build(); + + config[secretName].ShouldBeNull(); + } + + [Fact] + public void LoadSecrets_FromSecretStoreRequiredAndDoesNotExist_ShouldThrowException() + { + var storeName = "store"; + var secretName = "ConnectionStrings:DatabaseConnStr"; + + var secretDescriptors = new[] + { + new DaprSecretDescriptor(secretName, new Dictionary(), true) + }; + + var httpClient = new TestHttpClient + { + Handler = async entry => { - Handler = async entry => - { - await SendEmptyResponse(entry); - } - }; + await SendEmptyResponse(entry); + } + }; - var daprClient = new DaprClientBuilder() - .UseHttpClientFactory(() => httpClient) - .Build(); + var daprClient = new DaprClientBuilder() + .UseHttpClientFactory(() => httpClient) + .Build(); - var config = CreateBuilder() + var ex = Assert.Throws(() => + { + CreateBuilder() .AddDaprSecretStore(storeName, secretDescriptors, daprClient) .Build(); + }); - config[secretName].ShouldBeNull(); - } + Assert.Contains("Secret", ex.Message); + } - [Fact] - public void LoadSecrets_FromSecretStoreRequiredAndDoesNotExist_ShouldThrowException() + + [Fact] + public void AddDaprSecretStore_WithoutStore_ReportsError() + { + var daprClient = new DaprClientBuilder() + .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = new TestHttpClient() }) + .Build(); + + var ex = Assert.Throws(() => { - var storeName = "store"; - var secretName = "ConnectionStrings:DatabaseConnStr"; + CreateBuilder() + .AddDaprSecretStore(null, daprClient) + .Build(); + }); - var secretDescriptors = new[] - { - new DaprSecretDescriptor(secretName, new Dictionary(), true) - }; + Assert.Contains("store", ex.Message); + } - var httpClient = new TestHttpClient + [Fact] + public void AddDaprSecretStore_WithEmptyStore_ReportsError() + { + var daprClient = new DaprClientBuilder() + .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = new TestHttpClient() }) + .Build(); + + var ex = Assert.Throws(() => + { + CreateBuilder() + .AddDaprSecretStore(string.Empty, daprClient) + .Build(); + }); + + Assert.Contains("The value cannot be null or empty", ex.Message); + } + + [Fact] + public void AddDaprSecretStore_WithoutClient_ReportsError() + { + var ex = Assert.Throws(() => + { + CreateBuilder() + .AddDaprSecretStore("store", null) + .Build(); + }); + + Assert.Contains("client", ex.Message); + } + + [Fact] + public void AddDaprSecretStore_DuplicateSecret_ReportsError() + { + var httpClient = new TestHttpClient() + { + Handler = async (entry) => { - Handler = async entry => + var secrets = new Dictionary() { { "secretName", "secret" }, { "SecretName", "secret" } }; + await SendBulkResponseWithSecrets(secrets, entry); + } + }; + + var daprClient = new DaprClientBuilder() + .UseHttpClientFactory(() => httpClient) + .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = httpClient }) + .Build(); + + var ex = Assert.Throws(() => + { + CreateBuilder() + .AddDaprSecretStore("store", daprClient) + .Build(); + }); + + Assert.Contains("Please remove any duplicates from your secret store.", ex.Message); + } + + [Fact] + public void BulkLoadSecrets_FromSecretStoreThatReturnsOneValue() + { + // Configure Client + var httpClient = new TestHttpClient() + { + Handler = async (entry) => + { + var secrets = new Dictionary() { { "secretName", "secret" } }; + await SendBulkResponseWithSecrets(secrets, entry); + } + }; + + var daprClient = new DaprClientBuilder() + .UseHttpClientFactory(() => httpClient) + .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = httpClient }) + .Build(); + + var config = CreateBuilder() + .AddDaprSecretStore("store", daprClient) + .Build(); + + config["secretName"].ShouldBe("secret"); + } + + [Fact] + public void BulkLoadSecrets_FromSecretStoreThatCanReturnsMultipleValues() + { + // Configure Client + var httpClient = new TestHttpClient() + { + Handler = async (entry) => + { + var secrets = new Dictionary() { + { "first_secret", "secret1" }, + { "second_secret", "secret2" }}; + await SendBulkResponseWithSecrets(secrets, entry); + } + }; + + var daprClient = new DaprClientBuilder() + .UseHttpClientFactory(() => httpClient) + .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = httpClient }) + .Build(); + + var config = CreateBuilder() + .AddDaprSecretStore("store", daprClient) + .Build(); + + config["first_secret"].ShouldBe("secret1"); + config["second_secret"].ShouldBe("secret2"); + } + + [Fact] + public void LoadSecrets_FromSecretStoreThatReturnsNonNormalizedKey() + { + // Configure Client + var httpClient = new TestHttpClient() + { + Handler = async (entry) => + { + var secrets = new Dictionary() { { "secretName__value", "secret" } }; + await SendResponseWithSecrets(secrets, entry); + } + }; + + var daprClient = new DaprClientBuilder() + .UseHttpClientFactory(() => httpClient) + .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = httpClient }) + .Build(); + + var config = CreateBuilder() + .AddDaprSecretStore("store", new DaprSecretDescriptor[] { new DaprSecretDescriptor("secretName__value") }, daprClient) + .Build(); + + config["secretName:value"].ShouldBe("secret"); + } + + [Fact] + public void BulkLoadSecrets_FromSecretStoreThatReturnsNonNormalizedKey() + { + // Configure Client + var httpClient = new TestHttpClient() + { + Handler = async (entry) => + { + var secrets = new Dictionary() { + { "first_secret__value", "secret1" }}; + await SendBulkResponseWithSecrets(secrets, entry); + } + }; + + var daprClient = new DaprClientBuilder() + .UseHttpClientFactory(() => httpClient) + .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = httpClient }) + .Build(); + + var config = CreateBuilder() + .AddDaprSecretStore("store", daprClient) + .Build(); + + config["first_secret:value"].ShouldBe("secret1"); + } + + [Fact] + public void LoadSecrets_FromSecretStoreThatReturnsNonNormalizedKeyDisabledNormalizeKey() + { + // Configure Client + var httpClient = new TestHttpClient() + { + Handler = async (entry) => + { + var secrets = new Dictionary() { { "secretName__value", "secret" } }; + await SendResponseWithSecrets(secrets, entry); + } + }; + + var daprClient = new DaprClientBuilder() + .UseHttpClientFactory(() => httpClient) + .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = httpClient }) + .Build(); + + var config = CreateBuilder() + .AddDaprSecretStore((conf) => + { + conf.Store = "store"; + conf.SecretDescriptors = new DaprSecretDescriptor[] { new DaprSecretDescriptor("secretName__value") }; + conf.Client = daprClient; + conf.NormalizeKey = false; + }) + .Build(); + + config["secretName__value"].ShouldBe("secret"); + } + + [Fact] + public void BulkLoadSecrets_FromSecretStoreThatReturnsNonNormalizedKeyDisabledNormalizeKey() + { + // Configure Client + var httpClient = new TestHttpClient() + { + Handler = async (entry) => + { + var secrets = new Dictionary() { + { "first_secret__value", "secret1" }}; + await SendBulkResponseWithSecrets(secrets, entry); + } + }; + + var daprClient = new DaprClientBuilder() + .UseHttpClientFactory(() => httpClient) + .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = httpClient }) + .Build(); + + var config = CreateBuilder() + .AddDaprSecretStore((conf) => + { + conf.Store = "store"; + conf.Client = daprClient; + conf.NormalizeKey = false; + }) + .Build(); + + config["first_secret__value"].ShouldBe("secret1"); + } + + [Fact] + public void LoadSecrets_FromSecretStoreThatReturnsCustomDelimitedKey() + { + // Configure Client + var httpClient = new TestHttpClient() + { + Handler = async (entry) => + { + var secrets = new Dictionary() + { + ["secretName--value"] = "secret", + }; + await SendResponseWithSecrets(secrets, entry); + } + }; + + var daprClient = new DaprClientBuilder() + .UseHttpClientFactory(() => httpClient) + .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = httpClient }) + .Build(); + + var config = CreateBuilder() + .AddDaprSecretStore((conf) => + { + conf.Store = "store"; + conf.Client = daprClient; + conf.SecretDescriptors = new DaprSecretDescriptor[] { new DaprSecretDescriptor("secretName--value") }; + conf.KeyDelimiters = new[] { "--" }; + }) + .Build(); + + config["secretName:value"].ShouldBe("secret"); + } + + [Fact] + public void LoadSecrets_FromSecretStoreThatReturnsCustomDelimitedKey_NullDelimiters() + { + // Configure Client + var httpClient = new TestHttpClient() + { + Handler = async (entry) => + { + var secrets = new Dictionary() + { + ["secretName--value"] = "secret", + }; + await SendResponseWithSecrets(secrets, entry); + } + }; + + var daprClient = new DaprClientBuilder() + .UseHttpClientFactory(() => httpClient) + .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = httpClient }) + .Build(); + + var config = CreateBuilder() + .AddDaprSecretStore((conf) => + { + conf.Store = "store"; + conf.Client = daprClient; + conf.SecretDescriptors = new DaprSecretDescriptor[] { new DaprSecretDescriptor("secretName--value") }; + conf.KeyDelimiters = null; + }) + .Build(); + + config["secretName--value"].ShouldBe("secret"); + } + + [Fact] + public void LoadSecrets_FromSecretStoreThatReturnsCustomDelimitedKey_EmptyDelimiters() + { + // Configure Client + var httpClient = new TestHttpClient() + { + Handler = async (entry) => + { + var secrets = new Dictionary() + { + ["secretName--value"] = "secret", + }; + await SendResponseWithSecrets(secrets, entry); + } + }; + + var daprClient = new DaprClientBuilder() + .UseHttpClientFactory(() => httpClient) + .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = httpClient }) + .Build(); + + var config = CreateBuilder() + .AddDaprSecretStore((conf) => + { + conf.Store = "store"; + conf.Client = daprClient; + conf.SecretDescriptors = new DaprSecretDescriptor[] { new DaprSecretDescriptor("secretName--value") }; + conf.KeyDelimiters = new List(); + }) + .Build(); + + config["secretName--value"].ShouldBe("secret"); + } + + [Fact] + public void LoadSecrets_FromSecretStoreThatReturnsDifferentCustomDelimitedKeys() + { + // Configure Client + var httpClient = new TestHttpClient() + { + Handler = async (entry) => + { + // The following is an attempt at handling multiple secret descriptors for unit tests. + if (entry.Request.RequestUri.AbsoluteUri.Contains("healthz")) { await SendEmptyResponse(entry); } - }; - - var daprClient = new DaprClientBuilder() - .UseHttpClientFactory(() => httpClient) - .Build(); - - var ex = Assert.Throws(() => - { - CreateBuilder() - .AddDaprSecretStore(storeName, secretDescriptors, daprClient) - .Build(); - }); - - Assert.Contains("Secret", ex.Message); - } - - - [Fact] - public void AddDaprSecretStore_WithoutStore_ReportsError() - { - var daprClient = new DaprClientBuilder() - .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = new TestHttpClient() }) - .Build(); - - var ex = Assert.Throws(() => - { - CreateBuilder() - .AddDaprSecretStore(null, daprClient) - .Build(); - }); - - Assert.Contains("store", ex.Message); - } - - [Fact] - public void AddDaprSecretStore_WithEmptyStore_ReportsError() - { - var daprClient = new DaprClientBuilder() - .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = new TestHttpClient() }) - .Build(); - - var ex = Assert.Throws(() => - { - CreateBuilder() - .AddDaprSecretStore(string.Empty, daprClient) - .Build(); - }); - - Assert.Contains("The value cannot be null or empty", ex.Message); - } - - [Fact] - public void AddDaprSecretStore_WithoutClient_ReportsError() - { - var ex = Assert.Throws(() => - { - CreateBuilder() - .AddDaprSecretStore("store", null) - .Build(); - }); - - Assert.Contains("client", ex.Message); - } - - [Fact] - public void AddDaprSecretStore_DuplicateSecret_ReportsError() - { - var httpClient = new TestHttpClient() - { - Handler = async (entry) => - { - var secrets = new Dictionary() { { "secretName", "secret" }, { "SecretName", "secret" } }; - await SendBulkResponseWithSecrets(secrets, entry); - } - }; - - var daprClient = new DaprClientBuilder() - .UseHttpClientFactory(() => httpClient) - .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = httpClient }) - .Build(); - - var ex = Assert.Throws(() => - { - CreateBuilder() - .AddDaprSecretStore("store", daprClient) - .Build(); - }); - - Assert.Contains("Please remove any duplicates from your secret store.", ex.Message); - } - - [Fact] - public void BulkLoadSecrets_FromSecretStoreThatReturnsOneValue() - { - // Configure Client - var httpClient = new TestHttpClient() - { - Handler = async (entry) => - { - var secrets = new Dictionary() { { "secretName", "secret" } }; - await SendBulkResponseWithSecrets(secrets, entry); - } - }; - - var daprClient = new DaprClientBuilder() - .UseHttpClientFactory(() => httpClient) - .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = httpClient }) - .Build(); - - var config = CreateBuilder() - .AddDaprSecretStore("store", daprClient) - .Build(); - - config["secretName"].ShouldBe("secret"); - } - - [Fact] - public void BulkLoadSecrets_FromSecretStoreThatCanReturnsMultipleValues() - { - // Configure Client - var httpClient = new TestHttpClient() - { - Handler = async (entry) => - { - var secrets = new Dictionary() { - { "first_secret", "secret1" }, - { "second_secret", "secret2" }}; - await SendBulkResponseWithSecrets(secrets, entry); - } - }; - - var daprClient = new DaprClientBuilder() - .UseHttpClientFactory(() => httpClient) - .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = httpClient }) - .Build(); - - var config = CreateBuilder() - .AddDaprSecretStore("store", daprClient) - .Build(); - - config["first_secret"].ShouldBe("secret1"); - config["second_secret"].ShouldBe("secret2"); - } - - [Fact] - public void LoadSecrets_FromSecretStoreThatReturnsNonNormalizedKey() - { - // Configure Client - var httpClient = new TestHttpClient() - { - Handler = async (entry) => + else { - var secrets = new Dictionary() { { "secretName__value", "secret" } }; - await SendResponseWithSecrets(secrets, entry); - } - }; - - var daprClient = new DaprClientBuilder() - .UseHttpClientFactory(() => httpClient) - .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = httpClient }) - .Build(); - - var config = CreateBuilder() - .AddDaprSecretStore("store", new DaprSecretDescriptor[] { new DaprSecretDescriptor("secretName__value") }, daprClient) - .Build(); - - config["secretName:value"].ShouldBe("secret"); - } - - [Fact] - public void BulkLoadSecrets_FromSecretStoreThatReturnsNonNormalizedKey() - { - // Configure Client - var httpClient = new TestHttpClient() - { - Handler = async (entry) => - { - var secrets = new Dictionary() { - { "first_secret__value", "secret1" }}; - await SendBulkResponseWithSecrets(secrets, entry); - } - }; - - var daprClient = new DaprClientBuilder() - .UseHttpClientFactory(() => httpClient) - .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = httpClient }) - .Build(); - - var config = CreateBuilder() - .AddDaprSecretStore("store", daprClient) - .Build(); - - config["first_secret:value"].ShouldBe("secret1"); - } - - [Fact] - public void LoadSecrets_FromSecretStoreThatReturnsNonNormalizedKeyDisabledNormalizeKey() - { - // Configure Client - var httpClient = new TestHttpClient() - { - Handler = async (entry) => - { - var secrets = new Dictionary() { { "secretName__value", "secret" } }; - await SendResponseWithSecrets(secrets, entry); - } - }; - - var daprClient = new DaprClientBuilder() - .UseHttpClientFactory(() => httpClient) - .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = httpClient }) - .Build(); - - var config = CreateBuilder() - .AddDaprSecretStore((conf) => + var content = await entry.Request.Content.ReadAsStringAsync(); + if (content.Contains("secretName--value")) { - conf.Store = "store"; - conf.SecretDescriptors = new DaprSecretDescriptor[] { new DaprSecretDescriptor("secretName__value") }; - conf.Client = daprClient; - conf.NormalizeKey = false; - }) - .Build(); - - config["secretName__value"].ShouldBe("secret"); - } - - [Fact] - public void BulkLoadSecrets_FromSecretStoreThatReturnsNonNormalizedKeyDisabledNormalizeKey() - { - // Configure Client - var httpClient = new TestHttpClient() - { - Handler = async (entry) => - { - var secrets = new Dictionary() { - { "first_secret__value", "secret1" }}; - await SendBulkResponseWithSecrets(secrets, entry); - } - }; - - var daprClient = new DaprClientBuilder() - .UseHttpClientFactory(() => httpClient) - .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = httpClient }) - .Build(); - - var config = CreateBuilder() - .AddDaprSecretStore((conf) => - { - conf.Store = "store"; - conf.Client = daprClient; - conf.NormalizeKey = false; - }) - .Build(); - - config["first_secret__value"].ShouldBe("secret1"); - } - - [Fact] - public void LoadSecrets_FromSecretStoreThatReturnsCustomDelimitedKey() - { - // Configure Client - var httpClient = new TestHttpClient() - { - Handler = async (entry) => - { - var secrets = new Dictionary() - { - ["secretName--value"] = "secret", - }; - await SendResponseWithSecrets(secrets, entry); - } - }; - - var daprClient = new DaprClientBuilder() - .UseHttpClientFactory(() => httpClient) - .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = httpClient }) - .Build(); - - var config = CreateBuilder() - .AddDaprSecretStore((conf) => - { - conf.Store = "store"; - conf.Client = daprClient; - conf.SecretDescriptors = new DaprSecretDescriptor[] { new DaprSecretDescriptor("secretName--value") }; - conf.KeyDelimiters = new[] { "--" }; - }) - .Build(); - - config["secretName:value"].ShouldBe("secret"); - } - - [Fact] - public void LoadSecrets_FromSecretStoreThatReturnsCustomDelimitedKey_NullDelimiters() - { - // Configure Client - var httpClient = new TestHttpClient() - { - Handler = async (entry) => - { - var secrets = new Dictionary() - { - ["secretName--value"] = "secret", - }; - await SendResponseWithSecrets(secrets, entry); - } - }; - - var daprClient = new DaprClientBuilder() - .UseHttpClientFactory(() => httpClient) - .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = httpClient }) - .Build(); - - var config = CreateBuilder() - .AddDaprSecretStore((conf) => - { - conf.Store = "store"; - conf.Client = daprClient; - conf.SecretDescriptors = new DaprSecretDescriptor[] { new DaprSecretDescriptor("secretName--value") }; - conf.KeyDelimiters = null; - }) - .Build(); - - config["secretName--value"].ShouldBe("secret"); - } - - [Fact] - public void LoadSecrets_FromSecretStoreThatReturnsCustomDelimitedKey_EmptyDelimiters() - { - // Configure Client - var httpClient = new TestHttpClient() - { - Handler = async (entry) => - { - var secrets = new Dictionary() - { - ["secretName--value"] = "secret", - }; - await SendResponseWithSecrets(secrets, entry); - } - }; - - var daprClient = new DaprClientBuilder() - .UseHttpClientFactory(() => httpClient) - .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = httpClient }) - .Build(); - - var config = CreateBuilder() - .AddDaprSecretStore((conf) => - { - conf.Store = "store"; - conf.Client = daprClient; - conf.SecretDescriptors = new DaprSecretDescriptor[] { new DaprSecretDescriptor("secretName--value") }; - conf.KeyDelimiters = new List(); - }) - .Build(); - - config["secretName--value"].ShouldBe("secret"); - } - - [Fact] - public void LoadSecrets_FromSecretStoreThatReturnsDifferentCustomDelimitedKeys() - { - // Configure Client - var httpClient = new TestHttpClient() - { - Handler = async (entry) => - { - // The following is an attempt at handling multiple secret descriptors for unit tests. - if (entry.Request.RequestUri.AbsoluteUri.Contains("healthz")) - { - await SendEmptyResponse(entry); + await SendResponseWithSecrets(new Dictionary() + { + ["secretName--value"] = "secret", + }, entry); } - else + else if (content.Contains("otherSecretName≡value")) { - var content = await entry.Request.Content.ReadAsStringAsync(); - if (content.Contains("secretName--value")) + await SendResponseWithSecrets(new Dictionary() { - await SendResponseWithSecrets(new Dictionary() - { - ["secretName--value"] = "secret", - }, entry); - } - else if (content.Contains("otherSecretName≡value")) - { - await SendResponseWithSecrets(new Dictionary() - { - ["otherSecretName≡value"] = "secret", - }, entry); - } + ["otherSecretName≡value"] = "secret", + }, entry); } } - }; - - var daprClient = new DaprClientBuilder() - .UseHttpClientFactory(() => httpClient) - .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = httpClient }) - .Build(); - - var config = CreateBuilder() - .AddDaprSecretStore((conf) => - { - conf.Store = "store"; - conf.Client = daprClient; - conf.SecretDescriptors = new DaprSecretDescriptor[] - { - new DaprSecretDescriptor("secretName--value"), - new DaprSecretDescriptor("otherSecretName≡value"), - }; - conf.KeyDelimiters = new[] { "--", "≡" }; - }) - .Build(); - - config["secretName:value"].ShouldBe("secret"); - config["otherSecretName:value"].ShouldBe("secret"); - } - - [Fact] - public void BulkLoadSecrets_FromSecretStoreThatReturnsCustomDelimitedKey() - { - // Configure Client - var httpClient = new TestHttpClient() - { - Handler = async (entry) => - { - var secrets = new Dictionary() - { - ["first_secret--value"] = "secret1" - }; - await SendBulkResponseWithSecrets(secrets, entry); - } - }; - - var daprClient = new DaprClientBuilder() - .UseHttpClientFactory(() => httpClient) - .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = httpClient }) - .Build(); - - var config = CreateBuilder() - .AddDaprSecretStore("store", daprClient, new[] { "--" }) - .Build(); - - config["first_secret:value"].ShouldBe("secret1"); - } - - [Fact] - public void BulkLoadSecrets_FromSecretStoreThatReturnsDifferentCustomDelimitedKey() - { - // Configure Client - var httpClient = new TestHttpClient() - { - Handler = async (entry) => - { - var secrets = new Dictionary() - { - ["first_secret--value"] = "secret1", - ["second_secret≡value"] = "secret2", - }; - await SendBulkResponseWithSecrets(secrets, entry); - } - }; - - var daprClient = new DaprClientBuilder() - .UseHttpClientFactory(() => httpClient) - .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = httpClient }) - .Build(); - - var config = CreateBuilder() - .AddDaprSecretStore("store", daprClient, new[] { "--", "≡" }) - .Build(); - - config["first_secret:value"].ShouldBe("secret1"); - config["second_secret:value"].ShouldBe("secret2"); - } - - [Fact] - public void BulkLoadSecrets_FromSecretStoreThatReturnsPlainAndCustomDelimitedKey() - { - // Configure Client - var httpClient = new TestHttpClient() - { - Handler = async (entry) => - { - var secrets = new Dictionary() - { - ["first_secret:value"] = "secret1", - ["second_secret--value"] = "secret2", - ["third_secret≡value"] = "secret3", - }; - await SendBulkResponseWithSecrets(secrets, entry); - } - }; - - var daprClient = new DaprClientBuilder() - .UseHttpClientFactory(() => httpClient) - .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = httpClient }) - .Build(); - - var config = CreateBuilder() - .AddDaprSecretStore("store", daprClient, new[] { "--", "≡" }) - .Build(); - - config["first_secret:value"].ShouldBe("secret1"); - config["second_secret:value"].ShouldBe("secret2"); - config["third_secret:value"].ShouldBe("secret3"); - } - - [Fact] - public void LoadSecrets_FromSecretStoreThatReturnsCustomDelimitedKeyDisabledNormalizeKey() - { - // Configure Client - var httpClient = new TestHttpClient() - { - Handler = async (entry) => - { - var secrets = new Dictionary() - { - ["secretName--value"] = "secret" - }; - await SendResponseWithSecrets(secrets, entry); - } - }; - - var daprClient = new DaprClientBuilder() - .UseHttpClientFactory(() => httpClient) - .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = httpClient }) - .Build(); - - var config = CreateBuilder() - .AddDaprSecretStore((conf) => - { - conf.Store = "store"; - conf.Client = daprClient; - conf.SecretDescriptors = new DaprSecretDescriptor[] { new DaprSecretDescriptor("secretName--value") }; - conf.NormalizeKey = false; - conf.KeyDelimiters = new[] { "--" }; - }) - .Build(); - - config["secretName--value"].ShouldBe("secret"); - } - - [Fact] - public void BulkLoadSecrets_FromSecretStoreThatReturnsCustomDelimitedKeyDisabledNormalizeKey() - { - // Configure Client - var httpClient = new TestHttpClient() - { - Handler = async (entry) => - { - var secrets = new Dictionary() - { - ["first_secret--value"] = "secret1" - }; - await SendBulkResponseWithSecrets(secrets, entry); - } - }; - - var daprClient = new DaprClientBuilder() - .UseHttpClientFactory(() => httpClient) - .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = httpClient }) - .Build(); - - var config = CreateBuilder() - .AddDaprSecretStore((conf) => - { - conf.Store = "store"; - conf.Client = daprClient; - conf.NormalizeKey = false; - conf.KeyDelimiters = new[] { "--" }; - }) - .Build(); - - config["first_secret--value"].ShouldBe("secret1"); - } - - [Fact] - public void LoadSecrets_FailsIfSidecarNotAvailable() - { - var httpClient = new TestHttpClient() - { - Handler = async (entry) => - { - await SendEmptyResponse(entry, HttpStatusCode.InternalServerError); - } - }; - - var daprClient = new DaprClientBuilder() - .UseHttpClientFactory(() => httpClient) - .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = httpClient }) - .Build(); - - Assert.Throws(() => CreateBuilder() - .AddDaprSecretStore("store", daprClient, TimeSpan.FromMilliseconds(1)) - .Build()); - } - - private IConfigurationBuilder CreateBuilder() - { - return new ConfigurationBuilder(); - } - - private async Task SendResponseWithSecrets(Dictionary secrets, TestHttpClient.Entry entry) - { - var secretResponse = new Autogenerated.GetSecretResponse(); - secretResponse.Data.Add(secrets); - - var streamContent = await GrpcUtils.CreateResponseContent(secretResponse); - var response = GrpcUtils.CreateResponse(HttpStatusCode.OK, streamContent); - entry.Completion.SetResult(response); - } - - private async Task SendBulkResponseWithSecrets(Dictionary secrets, TestHttpClient.Entry entry) - { - var getBulkSecretResponse = new Autogenerated.GetBulkSecretResponse(); - foreach (var secret in secrets) - { - var secretsResponse = new Autogenerated.SecretResponse(); - secretsResponse.Secrets[secret.Key] = secret.Value; - // Bulk secret response is `MapField>`. The outer key (string) must be ignored by `DaprSecretStoreConfigurationProvider`. - getBulkSecretResponse.Data.Add("IgnoredKey" + secret.Key, secretsResponse); } + }; - var streamContent = await GrpcUtils.CreateResponseContent(getBulkSecretResponse); - var response = GrpcUtils.CreateResponse(HttpStatusCode.OK, streamContent); - entry.Completion.SetResult(response); - } + var daprClient = new DaprClientBuilder() + .UseHttpClientFactory(() => httpClient) + .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = httpClient }) + .Build(); - private async Task SendEmptyResponse(TestHttpClient.Entry entry, HttpStatusCode code = HttpStatusCode.OK) - { - var response = new Autogenerated.GetSecretResponse(); - var streamContent = await GrpcUtils.CreateResponseContent(response); - entry.Completion.SetResult(GrpcUtils.CreateResponse(code, streamContent)); - } + var config = CreateBuilder() + .AddDaprSecretStore((conf) => + { + conf.Store = "store"; + conf.Client = daprClient; + conf.SecretDescriptors = new DaprSecretDescriptor[] + { + new DaprSecretDescriptor("secretName--value"), + new DaprSecretDescriptor("otherSecretName≡value"), + }; + conf.KeyDelimiters = new[] { "--", "≡" }; + }) + .Build(); + + config["secretName:value"].ShouldBe("secret"); + config["otherSecretName:value"].ShouldBe("secret"); } -} + + [Fact] + public void BulkLoadSecrets_FromSecretStoreThatReturnsCustomDelimitedKey() + { + // Configure Client + var httpClient = new TestHttpClient() + { + Handler = async (entry) => + { + var secrets = new Dictionary() + { + ["first_secret--value"] = "secret1" + }; + await SendBulkResponseWithSecrets(secrets, entry); + } + }; + + var daprClient = new DaprClientBuilder() + .UseHttpClientFactory(() => httpClient) + .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = httpClient }) + .Build(); + + var config = CreateBuilder() + .AddDaprSecretStore("store", daprClient, new[] { "--" }) + .Build(); + + config["first_secret:value"].ShouldBe("secret1"); + } + + [Fact] + public void BulkLoadSecrets_FromSecretStoreThatReturnsDifferentCustomDelimitedKey() + { + // Configure Client + var httpClient = new TestHttpClient() + { + Handler = async (entry) => + { + var secrets = new Dictionary() + { + ["first_secret--value"] = "secret1", + ["second_secret≡value"] = "secret2", + }; + await SendBulkResponseWithSecrets(secrets, entry); + } + }; + + var daprClient = new DaprClientBuilder() + .UseHttpClientFactory(() => httpClient) + .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = httpClient }) + .Build(); + + var config = CreateBuilder() + .AddDaprSecretStore("store", daprClient, new[] { "--", "≡" }) + .Build(); + + config["first_secret:value"].ShouldBe("secret1"); + config["second_secret:value"].ShouldBe("secret2"); + } + + [Fact] + public void BulkLoadSecrets_FromSecretStoreThatReturnsPlainAndCustomDelimitedKey() + { + // Configure Client + var httpClient = new TestHttpClient() + { + Handler = async (entry) => + { + var secrets = new Dictionary() + { + ["first_secret:value"] = "secret1", + ["second_secret--value"] = "secret2", + ["third_secret≡value"] = "secret3", + }; + await SendBulkResponseWithSecrets(secrets, entry); + } + }; + + var daprClient = new DaprClientBuilder() + .UseHttpClientFactory(() => httpClient) + .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = httpClient }) + .Build(); + + var config = CreateBuilder() + .AddDaprSecretStore("store", daprClient, new[] { "--", "≡" }) + .Build(); + + config["first_secret:value"].ShouldBe("secret1"); + config["second_secret:value"].ShouldBe("secret2"); + config["third_secret:value"].ShouldBe("secret3"); + } + + [Fact] + public void LoadSecrets_FromSecretStoreThatReturnsCustomDelimitedKeyDisabledNormalizeKey() + { + // Configure Client + var httpClient = new TestHttpClient() + { + Handler = async (entry) => + { + var secrets = new Dictionary() + { + ["secretName--value"] = "secret" + }; + await SendResponseWithSecrets(secrets, entry); + } + }; + + var daprClient = new DaprClientBuilder() + .UseHttpClientFactory(() => httpClient) + .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = httpClient }) + .Build(); + + var config = CreateBuilder() + .AddDaprSecretStore((conf) => + { + conf.Store = "store"; + conf.Client = daprClient; + conf.SecretDescriptors = new DaprSecretDescriptor[] { new DaprSecretDescriptor("secretName--value") }; + conf.NormalizeKey = false; + conf.KeyDelimiters = new[] { "--" }; + }) + .Build(); + + config["secretName--value"].ShouldBe("secret"); + } + + [Fact] + public void BulkLoadSecrets_FromSecretStoreThatReturnsCustomDelimitedKeyDisabledNormalizeKey() + { + // Configure Client + var httpClient = new TestHttpClient() + { + Handler = async (entry) => + { + var secrets = new Dictionary() + { + ["first_secret--value"] = "secret1" + }; + await SendBulkResponseWithSecrets(secrets, entry); + } + }; + + var daprClient = new DaprClientBuilder() + .UseHttpClientFactory(() => httpClient) + .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = httpClient }) + .Build(); + + var config = CreateBuilder() + .AddDaprSecretStore((conf) => + { + conf.Store = "store"; + conf.Client = daprClient; + conf.NormalizeKey = false; + conf.KeyDelimiters = new[] { "--" }; + }) + .Build(); + + config["first_secret--value"].ShouldBe("secret1"); + } + + [Fact] + public void LoadSecrets_FailsIfSidecarNotAvailable() + { + var httpClient = new TestHttpClient() + { + Handler = async (entry) => + { + await SendEmptyResponse(entry, HttpStatusCode.InternalServerError); + } + }; + + var daprClient = new DaprClientBuilder() + .UseHttpClientFactory(() => httpClient) + .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = httpClient }) + .Build(); + + Assert.Throws(() => CreateBuilder() + .AddDaprSecretStore("store", daprClient, TimeSpan.FromMilliseconds(1)) + .Build()); + } + + private IConfigurationBuilder CreateBuilder() + { + return new ConfigurationBuilder(); + } + + private async Task SendResponseWithSecrets(Dictionary secrets, TestHttpClient.Entry entry) + { + var secretResponse = new Autogenerated.GetSecretResponse(); + secretResponse.Data.Add(secrets); + + var streamContent = await GrpcUtils.CreateResponseContent(secretResponse); + var response = GrpcUtils.CreateResponse(HttpStatusCode.OK, streamContent); + entry.Completion.SetResult(response); + } + + private async Task SendBulkResponseWithSecrets(Dictionary secrets, TestHttpClient.Entry entry) + { + var getBulkSecretResponse = new Autogenerated.GetBulkSecretResponse(); + foreach (var secret in secrets) + { + var secretsResponse = new Autogenerated.SecretResponse(); + secretsResponse.Secrets[secret.Key] = secret.Value; + // Bulk secret response is `MapField>`. The outer key (string) must be ignored by `DaprSecretStoreConfigurationProvider`. + getBulkSecretResponse.Data.Add("IgnoredKey" + secret.Key, secretsResponse); + } + + var streamContent = await GrpcUtils.CreateResponseContent(getBulkSecretResponse); + var response = GrpcUtils.CreateResponse(HttpStatusCode.OK, streamContent); + entry.Completion.SetResult(response); + } + + private async Task SendEmptyResponse(TestHttpClient.Entry entry, HttpStatusCode code = HttpStatusCode.OK) + { + var response = new Autogenerated.GetSecretResponse(); + var streamContent = await GrpcUtils.CreateResponseContent(response); + entry.Completion.SetResult(GrpcUtils.CreateResponse(code, streamContent)); + } +} \ No newline at end of file diff --git a/test/Dapr.Extensions.Configuration.Test/TestHttpClient.cs b/test/Dapr.Extensions.Configuration.Test/TestHttpClient.cs index 0a7e2b7a..89c81a46 100644 --- a/test/Dapr.Extensions.Configuration.Test/TestHttpClient.cs +++ b/test/Dapr.Extensions.Configuration.Test/TestHttpClient.cs @@ -11,105 +11,104 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr +namespace Dapr; + +using System; +using System.Collections.Concurrent; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +// This is an old piece of infrastructure with some limitations, don't use it in new places. +public class TestHttpClient : HttpClient { - using System; - using System.Collections.Concurrent; - using System.Net; - using System.Net.Http; - using System.Net.Http.Headers; - using System.Text.Json; - using System.Threading; - using System.Threading.Tasks; + private readonly TestHttpClientHandler handler; - // This is an old piece of infrastructure with some limitations, don't use it in new places. - public class TestHttpClient : HttpClient + public TestHttpClient() + : this(new TestHttpClientHandler()) { - private readonly TestHttpClientHandler handler; + } - public TestHttpClient() - : this(new TestHttpClientHandler()) + private TestHttpClient(TestHttpClientHandler handler) + : base(handler) + { + this.handler = handler; + } + + public ConcurrentQueue Requests => this.handler.Requests; + + public Action Handler + { + get => this.handler.Handler; + set => this.handler.Handler = value; + } + + public class Entry + { + public Entry(HttpRequestMessage request) { + this.Request = request; + + this.Completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); } - private TestHttpClient(TestHttpClientHandler handler) - : base(handler) + public TaskCompletionSource Completion { get; } + + public HttpRequestMessage Request { get; } + + public void Respond(HttpResponseMessage response) { - this.handler = handler; + this.Completion.SetResult(response); } - public ConcurrentQueue Requests => this.handler.Requests; - - public Action Handler + public void RespondWithResponse(HttpResponseMessage response) { - get => this.handler.Handler; - set => this.handler.Handler = value; + this.Completion.SetResult(response); } - public class Entry + public void RespondWithJson(TValue value, JsonSerializerOptions options = null) { - public Entry(HttpRequestMessage request) + var bytes = JsonSerializer.SerializeToUtf8Bytes(value, options); + + var response = new HttpResponseMessage(HttpStatusCode.OK) { - this.Request = request; + Content = new ByteArrayContent(bytes) + }; + response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json") { CharSet = "UTF-8", }; - this.Completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - } - - public TaskCompletionSource Completion { get; } - - public HttpRequestMessage Request { get; } - - public void Respond(HttpResponseMessage response) - { - this.Completion.SetResult(response); - } - - public void RespondWithResponse(HttpResponseMessage response) - { - this.Completion.SetResult(response); - } - - public void RespondWithJson(TValue value, JsonSerializerOptions options = null) - { - var bytes = JsonSerializer.SerializeToUtf8Bytes(value, options); - - var response = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new ByteArrayContent(bytes) - }; - response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json") { CharSet = "UTF-8", }; - - this.Completion.SetResult(response); - } - - public void Throw(Exception exception) - { - this.Completion.SetException(exception); - } + this.Completion.SetResult(response); } - private class TestHttpClientHandler : HttpMessageHandler + public void Throw(Exception exception) { - public TestHttpClientHandler() + this.Completion.SetException(exception); + } + } + + private class TestHttpClientHandler : HttpMessageHandler + { + public TestHttpClientHandler() + { + this.Requests = new ConcurrentQueue(); + } + + public ConcurrentQueue Requests { get; } + + public Action Handler { get; set; } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var entry = new Entry(request); + this.Handler?.Invoke(entry); + this.Requests.Enqueue(entry); + + using (cancellationToken.Register(() => entry.Completion.TrySetCanceled())) { - this.Requests = new ConcurrentQueue(); - } - - public ConcurrentQueue Requests { get; } - - public Action Handler { get; set; } - - protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - var entry = new Entry(request); - this.Handler?.Invoke(entry); - this.Requests.Enqueue(entry); - - using (cancellationToken.Register(() => entry.Completion.TrySetCanceled())) - { - return await entry.Completion.Task.ConfigureAwait(false); - } + return await entry.Completion.Task.ConfigureAwait(false); } } } -} +} \ No newline at end of file diff --git a/test/Dapr.Messaging.Test/PublishSubscribe/MessageHandlingPolicyTest.cs b/test/Dapr.Messaging.Test/PublishSubscribe/MessageHandlingPolicyTest.cs index 6efdd639..47dc6897 100644 --- a/test/Dapr.Messaging.Test/PublishSubscribe/MessageHandlingPolicyTest.cs +++ b/test/Dapr.Messaging.Test/PublishSubscribe/MessageHandlingPolicyTest.cs @@ -13,56 +13,55 @@ using Dapr.Messaging.PublishSubscribe; -namespace Dapr.Messaging.Test.PublishSubscribe +namespace Dapr.Messaging.Test.PublishSubscribe; + +public class MessageHandlingPolicyTest { - public class MessageHandlingPolicyTest + [Fact] + public void Test_MessageHandlingPolicy_Constructor() { - [Fact] - public void Test_MessageHandlingPolicy_Constructor() - { - var timeoutDuration = TimeSpan.FromMilliseconds(2000); - const TopicResponseAction defaultResponseAction = TopicResponseAction.Drop; + var timeoutDuration = TimeSpan.FromMilliseconds(2000); + const TopicResponseAction defaultResponseAction = TopicResponseAction.Drop; - var policy = new MessageHandlingPolicy(timeoutDuration, defaultResponseAction); + var policy = new MessageHandlingPolicy(timeoutDuration, defaultResponseAction); - Assert.Equal(timeoutDuration, policy.TimeoutDuration); - Assert.Equal(defaultResponseAction, policy.DefaultResponseAction); - } - - [Fact] - public void Test_MessageHandlingPolicy_Equality() - { - var timeSpan1 = TimeSpan.FromMilliseconds(1000); - var timeSpan2 = TimeSpan.FromMilliseconds(2000); - - var policy1 = new MessageHandlingPolicy(timeSpan1, TopicResponseAction.Success); - var policy2 = new MessageHandlingPolicy(timeSpan1, TopicResponseAction.Success); - var policy3 = new MessageHandlingPolicy(timeSpan2, TopicResponseAction.Retry); - - Assert.Equal(policy1, policy2); // Value Equality - Assert.NotEqual(policy1, policy3); // Different values - } - - [Fact] - public void Test_MessageHandlingPolicy_Immutability() - { - var timeoutDuration = TimeSpan.FromMilliseconds(2000); - const TopicResponseAction defaultResponseAction = TopicResponseAction.Drop; - - var policy1 = new MessageHandlingPolicy(timeoutDuration, defaultResponseAction); - - var newTimeoutDuration = TimeSpan.FromMilliseconds(3000); - const TopicResponseAction newDefaultResponseAction = TopicResponseAction.Retry; - - // Creating a new policy with different values. - var policy2 = policy1 with - { - TimeoutDuration = newTimeoutDuration, DefaultResponseAction = newDefaultResponseAction - }; - - // Asserting that original policy is unaffected by changes made to new policy. - Assert.Equal(timeoutDuration, policy1.TimeoutDuration); - Assert.Equal(defaultResponseAction, policy1.DefaultResponseAction); - } + Assert.Equal(timeoutDuration, policy.TimeoutDuration); + Assert.Equal(defaultResponseAction, policy.DefaultResponseAction); } -} + + [Fact] + public void Test_MessageHandlingPolicy_Equality() + { + var timeSpan1 = TimeSpan.FromMilliseconds(1000); + var timeSpan2 = TimeSpan.FromMilliseconds(2000); + + var policy1 = new MessageHandlingPolicy(timeSpan1, TopicResponseAction.Success); + var policy2 = new MessageHandlingPolicy(timeSpan1, TopicResponseAction.Success); + var policy3 = new MessageHandlingPolicy(timeSpan2, TopicResponseAction.Retry); + + Assert.Equal(policy1, policy2); // Value Equality + Assert.NotEqual(policy1, policy3); // Different values + } + + [Fact] + public void Test_MessageHandlingPolicy_Immutability() + { + var timeoutDuration = TimeSpan.FromMilliseconds(2000); + const TopicResponseAction defaultResponseAction = TopicResponseAction.Drop; + + var policy1 = new MessageHandlingPolicy(timeoutDuration, defaultResponseAction); + + var newTimeoutDuration = TimeSpan.FromMilliseconds(3000); + const TopicResponseAction newDefaultResponseAction = TopicResponseAction.Retry; + + // Creating a new policy with different values. + var policy2 = policy1 with + { + TimeoutDuration = newTimeoutDuration, DefaultResponseAction = newDefaultResponseAction + }; + + // Asserting that original policy is unaffected by changes made to new policy. + Assert.Equal(timeoutDuration, policy1.TimeoutDuration); + Assert.Equal(defaultResponseAction, policy1.DefaultResponseAction); + } +} \ No newline at end of file diff --git a/test/Dapr.Workflow.Test/WorkflowActivityTest.cs b/test/Dapr.Workflow.Test/WorkflowActivityTest.cs index f6be41fc..895a746d 100644 --- a/test/Dapr.Workflow.Test/WorkflowActivityTest.cs +++ b/test/Dapr.Workflow.Test/WorkflowActivityTest.cs @@ -11,45 +11,44 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Workflow.Test +namespace Dapr.Workflow.Test; + +using Moq; +using System.Threading.Tasks; +using Xunit; +using Xunit.Sdk; + +/// +/// Contains tests for WorkflowActivityContext. +/// +public class WorkflowActivityTest { - using Moq; - using System.Threading.Tasks; - using Xunit; - using Xunit.Sdk; + private IWorkflowActivity workflowActivity; - /// - /// Contains tests for WorkflowActivityContext. - /// - public class WorkflowActivityTest + private Mock workflowActivityContextMock; + + public WorkflowActivityTest() { - private IWorkflowActivity workflowActivity; + this.workflowActivity = new TestDaprWorkflowActivity(); + this.workflowActivityContextMock = new Mock(); + } - private Mock workflowActivityContextMock; + [Fact] + public async Task RunAsync_ShouldReturnCorrectContextInstanceId() + { + this.workflowActivityContextMock.Setup((x) => x.InstanceId).Returns("instanceId"); - public WorkflowActivityTest() + string result = (string) (await this.workflowActivity.RunAsync(this.workflowActivityContextMock.Object, "input"))!; + + Assert.Equal("instanceId", result); + } + + + public class TestDaprWorkflowActivity : WorkflowActivity + { + public override Task RunAsync(WorkflowActivityContext context, string input) { - this.workflowActivity = new TestDaprWorkflowActivity(); - this.workflowActivityContextMock = new Mock(); - } - - [Fact] - public async Task RunAsync_ShouldReturnCorrectContextInstanceId() - { - this.workflowActivityContextMock.Setup((x) => x.InstanceId).Returns("instanceId"); - - string result = (string) (await this.workflowActivity.RunAsync(this.workflowActivityContextMock.Object, "input"))!; - - Assert.Equal("instanceId", result); - } - - - public class TestDaprWorkflowActivity : WorkflowActivity - { - public override Task RunAsync(WorkflowActivityContext context, string input) - { - return Task.FromResult(context.InstanceId); - } + return Task.FromResult(context.InstanceId); } } -} +} \ No newline at end of file diff --git a/test/Shared/AppCallbackClient.cs b/test/Shared/AppCallbackClient.cs index 5454e576..71fc5705 100644 --- a/test/Shared/AppCallbackClient.cs +++ b/test/Shared/AppCallbackClient.cs @@ -11,66 +11,65 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr -{ - using System; - using System.Linq; - using System.Net; - using System.Net.Http; - using System.Threading; - using System.Threading.Tasks; - using Dapr.Client.Autogen.Grpc.v1; - using Grpc.Core; - using Grpc.Core.Testing; - using Grpc.Core.Utils; - using AppCallbackBase = AppCallback.Autogen.Grpc.v1.AppCallback.AppCallbackBase; +namespace Dapr; - // This client will forward requests to the AppCallback service implementation which then responds to the request - public class AppCallbackClient : HttpClient +using System; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Dapr.Client.Autogen.Grpc.v1; +using Grpc.Core; +using Grpc.Core.Testing; +using Grpc.Core.Utils; +using AppCallbackBase = AppCallback.Autogen.Grpc.v1.AppCallback.AppCallbackBase; + +// This client will forward requests to the AppCallback service implementation which then responds to the request +public class AppCallbackClient : HttpClient +{ + public AppCallbackClient(AppCallbackBase callbackService) + : base(new Handler(callbackService)) { - public AppCallbackClient(AppCallbackBase callbackService) - : base(new Handler(callbackService)) + } + + private class Handler : HttpMessageHandler + { + private readonly AppCallbackBase callbackService; + + public Handler(AppCallbackBase callbackService) { + this.callbackService = callbackService; } - private class Handler : HttpMessageHandler + protected override async Task SendAsync(HttpRequestMessage httpRequest, CancellationToken cancellationToken) { - private readonly AppCallbackBase callbackService; - - public Handler(AppCallbackBase callbackService) + var metadata = new Metadata(); + foreach (var (key, value) in httpRequest.Headers) { - this.callbackService = callbackService; + metadata.Add(key, string.Join(",", value.ToArray())); } - protected override async Task SendAsync(HttpRequestMessage httpRequest, CancellationToken cancellationToken) - { - var metadata = new Metadata(); - foreach (var (key, value) in httpRequest.Headers) - { - metadata.Add(key, string.Join(",", value.ToArray())); - } + var context = TestServerCallContext.Create( + method: httpRequest.Method.Method, + host: httpRequest.RequestUri.Host, + deadline: DateTime.UtcNow.AddHours(1), + requestHeaders: metadata, + cancellationToken: cancellationToken, + peer: "127.0.0.1", + authContext: null, + contextPropagationToken: null, + writeHeadersFunc: _ => TaskUtils.CompletedTask, + writeOptionsGetter: () => new WriteOptions(), + writeOptionsSetter: writeOptions => {}); - var context = TestServerCallContext.Create( - method: httpRequest.Method.Method, - host: httpRequest.RequestUri.Host, - deadline: DateTime.UtcNow.AddHours(1), - requestHeaders: metadata, - cancellationToken: cancellationToken, - peer: "127.0.0.1", - authContext: null, - contextPropagationToken: null, - writeHeadersFunc: _ => TaskUtils.CompletedTask, - writeOptionsGetter: () => new WriteOptions(), - writeOptionsSetter: writeOptions => {}); + var grpcRequest = await GrpcUtils.GetRequestFromRequestMessageAsync(httpRequest); + var grpcResponse = await this.callbackService.OnInvoke(grpcRequest.Message, context); - var grpcRequest = await GrpcUtils.GetRequestFromRequestMessageAsync(httpRequest); - var grpcResponse = await this.callbackService.OnInvoke(grpcRequest.Message, context); + var streamContent = await GrpcUtils.CreateResponseContent(grpcResponse); + var httpResponse = GrpcUtils.CreateResponse(HttpStatusCode.OK, streamContent); - var streamContent = await GrpcUtils.CreateResponseContent(grpcResponse); - var httpResponse = GrpcUtils.CreateResponse(HttpStatusCode.OK, streamContent); - - return httpResponse; - } + return httpResponse; } } -} +} \ No newline at end of file From aa70c5bb5d4e5dfd1661efb41bfc7bd710c48eca Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 1 Apr 2025 17:16:01 -0500 Subject: [PATCH 05/22] Added text specific to Dapr Actors serialization about polymorphic attributes necessary for System.Text.Json support (#1498) Signed-off-by: Whit Waldo --- .../dotnet-actors-serialization.md | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-actors/dotnet-actors-serialization.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-actors/dotnet-actors-serialization.md index 787a7e41..20271f7b 100644 --- a/daprdocs/content/en/dotnet-sdk-docs/dotnet-actors/dotnet-actors-serialization.md +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-actors/dotnet-actors-serialization.md @@ -3,7 +3,7 @@ type: docs title: "Actor serialization in the .NET SDK" linkTitle: "Actor serialization" weight: 300000 -description: Necessary steps to serialize your types using remoted Actors in .NET +description: Necessary steps to serialize your types remoted and non-remoted Actors in .NET --- # Actor Serialization @@ -254,6 +254,36 @@ a complex versioning scheme for our existing enum values in the state. {"event": "Conference", "season": "fall"} ``` +### Polymorphic Serialization +When working with polymorphic types in Dapr Actor clients, it is essential to handle serialization and deserialization correctly to ensure that the appropriate +derived types are instantiated. Polymorphic serialization allows you to serialize objects of a base type while preserving the specific derived type information. + +To enable polymorphic deserialization, you must use the `[JsonPolymorphic]` attribute on your base type. Additionally, +it is crucial to include the `[AllowOutOfOrderMetadataProperties]` attribute to ensure that metadata properties, such as `$type` +can be processed correctly by System.Text.Json even if they are not the first properties in the JSON object. + +#### Example +```cs +[JsonPolymorphic] +[AllowOutOfOrderMetadataProperties] +public abstract class SampleValueBase +{ + public string CommonProperty { get; set; } +} + +public class DerivedSampleValue : SampleValueBase +{ + public string SpecificProperty { get; set; } +} +``` +In this example, the `SampleValueBase` class is marked with both `[JsonPolymorphic]` and `[AllowOutOfOrderMetadataProperties]` +attributes. This setup ensures that the `$type` metadata property can be correctly identified and processed during +deserialization, regardless of its position in the JSON object. + +By following this approach, you can effectively manage polymorphic serialization and deserialization in your Dapr Actor +clients, ensuring that the correct derived types are instantiated and used. + + ## Strongly-typed Dapr Actor client In this section, you will learn how to configure your classes and records so they are properly serialized and deserialized at runtime when using a strongly-typed actor client. These clients are implemented using .NET interfaces and are not compatible with Dapr Actors written using other languages. From e477cf79e7a7dacb3ddb151f3d92eab659bd1142 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Wed, 9 Apr 2025 15:20:37 -0500 Subject: [PATCH 06/22] Actor timer callback method analyzer (#1513) * Removed obsolete project from solution (Messaging) * Added project containing analyzers and tests with respect to Dapr.Actors. The starting analyzer in this package validates that there's a method on the actor type matching the name provided in the timer callback argument. Signed-off-by: Whit Waldo --- Directory.Packages.props | 20 +-- all.sln | 16 +- .../AnalyzerReleases.Shipped.md | 9 ++ .../AnalyzerReleases.Unshipped.md | 1 + .../Dapr.Actors.Analyzers/AssemblyInfo.cs | 16 ++ .../Dapr.Actors.Analyzers.csproj | 37 +++++ .../Properties/launchSettings.json | 9 ++ .../Dapr.Actors.Analyzers/Readme.md | 0 .../Resources.Designer.cs | 80 ++++++++++ .../Dapr.Actors.Analyzers/Resources.resx | 27 ++++ .../TimerMethodPresentAnalyzer.cs | 105 +++++++++++++ .../Dapr.Actors.Analyzers.Tests.csproj | 35 +++++ .../TimerMethodPresentAnalyzerTests.cs | 148 ++++++++++++++++++ 13 files changed, 491 insertions(+), 12 deletions(-) create mode 100644 src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/AnalyzerReleases.Shipped.md create mode 100644 src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/AnalyzerReleases.Unshipped.md create mode 100644 src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/AssemblyInfo.cs create mode 100644 src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers.csproj create mode 100644 src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Properties/launchSettings.json create mode 100644 src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Readme.md create mode 100644 src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Resources.Designer.cs create mode 100644 src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Resources.resx create mode 100644 src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/TimerMethodPresentAnalyzer.cs create mode 100644 test/Dapr.Actors.Analyzers.Tests/Dapr.Actors.Analyzers.Tests.csproj create mode 100644 test/Dapr.Actors.Analyzers.Tests/TimerMethodPresentAnalyzerTests.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index e249a871..5ea87d5a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,7 +7,7 @@ - + @@ -17,12 +17,14 @@ - - - + + + + + - + @@ -33,7 +35,7 @@ - + @@ -45,8 +47,8 @@ - - - + + + \ No newline at end of file diff --git a/all.sln b/all.sln index eabc532e..57601210 100644 --- a/all.sln +++ b/all.sln @@ -155,7 +155,9 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JobsSample", "examples\Jobs EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Workflow.Test", "test\Dapr.Workflow.Test\Dapr.Workflow.Test.csproj", "{E90114C6-86FC-43B8-AE5C-D9273CF21FE4}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Messaging", "Messaging", "{8DB002D2-19E9-4342-A86B-025A367DF3D1}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Actors.Analyzers", "src\Dapr.Actors.Analyzers\Dapr.Actors.Analyzers\Dapr.Actors.Analyzers.csproj", "{E49C822C-E921-48DF-897B-3E603CA596D2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Actors.Analyzers.Tests", "test\Dapr.Actors.Analyzers.Tests\Dapr.Actors.Analyzers.Tests.csproj", "{A2C0F203-11FF-4B7F-A94F-B9FD873573FE}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -405,6 +407,14 @@ Global {E90114C6-86FC-43B8-AE5C-D9273CF21FE4}.Debug|Any CPU.Build.0 = Debug|Any CPU {E90114C6-86FC-43B8-AE5C-D9273CF21FE4}.Release|Any CPU.ActiveCfg = Release|Any CPU {E90114C6-86FC-43B8-AE5C-D9273CF21FE4}.Release|Any CPU.Build.0 = Release|Any CPU + {E49C822C-E921-48DF-897B-3E603CA596D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E49C822C-E921-48DF-897B-3E603CA596D2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E49C822C-E921-48DF-897B-3E603CA596D2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E49C822C-E921-48DF-897B-3E603CA596D2}.Release|Any CPU.Build.0 = Release|Any CPU + {A2C0F203-11FF-4B7F-A94F-B9FD873573FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A2C0F203-11FF-4B7F-A94F-B9FD873573FE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A2C0F203-11FF-4B7F-A94F-B9FD873573FE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A2C0F203-11FF-4B7F-A94F-B9FD873573FE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -478,8 +488,8 @@ Global {D9697361-232F-465D-A136-4561E0E88488} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78} {9CAF360E-5AD3-4C4F-89A0-327EEB70D673} = {D9697361-232F-465D-A136-4561E0E88488} {E90114C6-86FC-43B8-AE5C-D9273CF21FE4} = {DD020B34-460F-455F-8D17-CF4A949F100B} - {8DB002D2-19E9-4342-A86B-025A367DF3D1} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78} - {290D1278-F613-4DF3-9DF5-F37E38CDC363} = {8DB002D2-19E9-4342-A86B-025A367DF3D1} + {E49C822C-E921-48DF-897B-3E603CA596D2} = {27C5D71D-0721-4221-9286-B94AB07B58CF} + {A2C0F203-11FF-4B7F-A94F-B9FD873573FE} = {DD020B34-460F-455F-8D17-CF4A949F100B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40} diff --git a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/AnalyzerReleases.Shipped.md b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/AnalyzerReleases.Shipped.md new file mode 100644 index 00000000..bc73dcb0 --- /dev/null +++ b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/AnalyzerReleases.Shipped.md @@ -0,0 +1,9 @@ + + +## Release 1.16.0 + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------------------------------------------------------------------------------------ +DAPR4001 | Usage | Warning | Actor timer method invocations require the named callback method to exist on type. \ No newline at end of file diff --git a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/AnalyzerReleases.Unshipped.md b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/AnalyzerReleases.Unshipped.md new file mode 100644 index 00000000..b96f0065 --- /dev/null +++ b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/AnalyzerReleases.Unshipped.md @@ -0,0 +1 @@ +; Unshipped analyzer release \ No newline at end of file diff --git a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/AssemblyInfo.cs b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/AssemblyInfo.cs new file mode 100644 index 00000000..dcc08b3c --- /dev/null +++ b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/AssemblyInfo.cs @@ -0,0 +1,16 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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.Actors.Analyzers.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2" )] diff --git a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers.csproj b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers.csproj new file mode 100644 index 00000000..a3c751c3 --- /dev/null +++ b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers.csproj @@ -0,0 +1,37 @@ + + + + netstandard2.0 + + false + enable + enable + This package contains Roslyn analyzers for Dapr.Actors. + + true + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + + + True + True + Resources.resx + + + + diff --git a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Properties/launchSettings.json b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Properties/launchSettings.json new file mode 100644 index 00000000..a4b2b510 --- /dev/null +++ b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Properties/launchSettings.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "DebugRoslynAnalyzers": { + "commandName": "DebugRoslynComponent", + "targetProject": "../Dapr.Actors.Analyzers.Sample/Dapr.Actors.Analyzers.Sample.csproj" + } + } +} \ No newline at end of file diff --git a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Readme.md b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Readme.md new file mode 100644 index 00000000..e69de29b diff --git a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Resources.Designer.cs b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Resources.Designer.cs new file mode 100644 index 00000000..af3dd797 --- /dev/null +++ b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Resources.Designer.cs @@ -0,0 +1,80 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Dapr.Actors.Analyzers { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Dapr.Actors.Analyzers.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Actor timer method invocations require the named callback '{0}' method to exist on type '{1}'. + /// + internal static string DAPR4001MessageFormat { + get { + return ResourceManager.GetString("DAPR4001MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Actor timer method invocations require the named callback method to exist on type. + /// + internal static string DAPR4001Title { + get { + return ResourceManager.GetString("DAPR4001Title", resourceCulture); + } + } + } +} diff --git a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Resources.resx b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Resources.resx new file mode 100644 index 00000000..47f6c667 --- /dev/null +++ b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Resources.resx @@ -0,0 +1,27 @@ + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Actor timer method invocations require the named callback method to exist on type + + + Actor timer method invocations require the named callback '{0}' method to exist on type '{1}' + + \ No newline at end of file diff --git a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/TimerMethodPresentAnalyzer.cs b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/TimerMethodPresentAnalyzer.cs new file mode 100644 index 00000000..59640fd5 --- /dev/null +++ b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/TimerMethodPresentAnalyzer.cs @@ -0,0 +1,105 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Dapr.Actors.Analyzers; + +/// +/// An analyzer for Dapr Actors that validates that whenever a register is registered, the method specified to invoke +/// as the callback should actually exist on the type. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class TimerMethodPresentAnalyzer : DiagnosticAnalyzer +{ + /// + /// The rule validated by the analyzer. + /// + public static readonly DiagnosticDescriptor DaprTimerCallbackMethodRule = new( + id: "DAPR4001", + title: new LocalizableResourceString(nameof(Resources.DAPR4001Title), Resources.ResourceManager, typeof(Resources)), + messageFormat: new LocalizableResourceString(nameof(Resources.DAPR4001MessageFormat), Resources.ResourceManager, typeof(Resources)), + category: "Usage", + DiagnosticSeverity.Warning, + isEnabledByDefault: true + ); + + /// + public override ImmutableArray SupportedDiagnostics => + ImmutableArray.Create(DaprTimerCallbackMethodRule); + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterSyntaxNodeAction(AnalyzeSyntaxNode, SyntaxKind.InvocationExpression); + } + + private void AnalyzeSyntaxNode(SyntaxNodeAnalysisContext context) + { + var invocationExpr = (InvocationExpressionSyntax)context.Node; + + var methodSymbol = context.SemanticModel.GetSymbolInfo(invocationExpr.Expression).Symbol as IMethodSymbol; + + if (methodSymbol is null + || methodSymbol.Name != "RegisterTimerAsync" + || methodSymbol.ContainingType.Name != "Actor" + || methodSymbol.ContainingType.ContainingNamespace.ToDisplayString() != "Dapr.Actors.Runtime") + { + return; + } + + var ancestorType = invocationExpr.FirstAncestorOrSelf(); + if (ancestorType is null) + { + return; + } + var ancestorTypeSymbol = context.SemanticModel.GetDeclaredSymbol(ancestorType); + if (ancestorTypeSymbol is null) + { + return; + } + var ancestorTypeName = ancestorTypeSymbol?.Name; + + var arguments = invocationExpr.ArgumentList.Arguments; + if (arguments.Count < 2) + { + return; + } + + var secondArgument = arguments[1].Expression; + var methodNameValue = context.SemanticModel.GetConstantValue(secondArgument); + if (!methodNameValue.HasValue || methodNameValue.Value is not string methodName) + { + return; + } + + var members = ancestorTypeSymbol?.GetMembers().OfType().ToList(); + var methodExists = members?.Any(m => m.Name == methodName) == true; + if (!methodExists) + { + //Get the type that contains our RegisterTimerAsync method + + var diagnostic = Diagnostic.Create(SupportedDiagnostics[0], secondArgument.GetLocation(), methodName, + ancestorTypeName); + context.ReportDiagnostic(diagnostic); + } + } +} diff --git a/test/Dapr.Actors.Analyzers.Tests/Dapr.Actors.Analyzers.Tests.csproj b/test/Dapr.Actors.Analyzers.Tests/Dapr.Actors.Analyzers.Tests.csproj new file mode 100644 index 00000000..d801c2df --- /dev/null +++ b/test/Dapr.Actors.Analyzers.Tests/Dapr.Actors.Analyzers.Tests.csproj @@ -0,0 +1,35 @@ + + + + enable + enable + false + true + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + diff --git a/test/Dapr.Actors.Analyzers.Tests/TimerMethodPresentAnalyzerTests.cs b/test/Dapr.Actors.Analyzers.Tests/TimerMethodPresentAnalyzerTests.cs new file mode 100644 index 00000000..bdcf0e46 --- /dev/null +++ b/test/Dapr.Actors.Analyzers.Tests/TimerMethodPresentAnalyzerTests.cs @@ -0,0 +1,148 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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.Tasks; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Testing; +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + +namespace Dapr.Actors.Analyzers.Tests; + +public class TimerMethodPresentAnalyzerTests +{ + #if NET8_0 + private static readonly ReferenceAssemblies assemblies = ReferenceAssemblies.Net.Net80; + #elif NET9_0 + private static readonly ReferenceAssemblies assemblies = ReferenceAssemblies.Net.Net90; + #endif + + [Fact] + public async Task TestActor_TimerRegistration_NotPresent() + { + var context = new CSharpAnalyzerTest(); + context.ReferenceAssemblies = assemblies.AddPackages([ + new ("Dapr.Actors", "1.15.3") + ]); + + context.TestCode = """ + using System; + using System.Threading.Tasks; + using Dapr.Actors.Runtime; + internal sealed class TestActorTimerRegistrationNotPresent(ActorHost host) : Actor(host) + { + public async Task DoSomethingAsync() + { + await Task.Delay(TimeSpan.FromMilliseconds(250)); + } + } + """; + + context.ExpectedDiagnostics.Clear(); + await context.RunAsync(); + } + + [Fact] + public async Task TestActor_TimerRegistration_NameOfCallbackPresent() + { + var context = new CSharpAnalyzerTest(); + context.ReferenceAssemblies = assemblies.AddPackages([ + new ("Dapr.Actors", "1.15.3") + ]); + + + context.TestCode = """ + using System; + using System.Threading.Tasks; + using Dapr.Actors.Runtime; + internal sealed class TestActorTimerRegistrationTimerCallbackPresent(ActorHost host) : Actor(host) + { + public async Task DoSomethingAsync() + { + await RegisterTimerAsync("MyTimer", nameof(TimerCallback), null, TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(4), TimeSpan.FromSeconds(10)); + } + + private static async Task TimerCallback(byte[] data) + { + await Task.Delay(TimeSpan.FromMilliseconds(250)); + } + } + """; + + context.ExpectedDiagnostics.Clear(); + await context.RunAsync(); + } + + [Fact] + public async Task TestActor_TimerRegistration_LiteralCallbackPresent() + { + var context = new CSharpAnalyzerTest(); + context.ReferenceAssemblies = assemblies.AddPackages([ + new ("Dapr.Actors", "1.15.3") + ]); + + + context.TestCode = """ + using System; + using System.Threading.Tasks; + using Dapr.Actors.Runtime; + internal sealed class TestActorTimerRegistrationTimerCallbackPresent(ActorHost host) : Actor(host) + { + public async Task DoSomethingAsync() + { + await RegisterTimerAsync("MyTimer", "TimerCallback", null, TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(4), TimeSpan.FromSeconds(10)); + } + + private static async Task TimerCallback(byte[] data) + { + await Task.Delay(TimeSpan.FromMilliseconds(250)); + } + } + """; + + context.ExpectedDiagnostics.Clear(); + await context.RunAsync(); + } + + [Fact] + public async Task TestActor_TimerRegistration_CallbackNotPresent() + { + var context = new CSharpAnalyzerTest(); + context.ReferenceAssemblies = assemblies.AddPackages([ + new ("Dapr.Actors", "1.15.3") + ]); + + context.TestCode = """ + using System; + using System.Threading.Tasks; + using Dapr.Actors.Runtime; + internal sealed class TestActorTimerRegistrationTimerCallbackNotPresent(ActorHost host) : Actor(host) + { + public async Task DoSomethingAsync() + { + await RegisterTimerAsync("MyTimer", "TimerCallback", null, TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(4), TimeSpan.FromSeconds(10)); + } + } + """; + + context.ExpectedDiagnostics.Add(new DiagnosticResult(TimerMethodPresentAnalyzer.DaprTimerCallbackMethodRule) + .WithSpan(8, 45, 8, 60) + .WithArguments("TimerCallback", "TestActorTimerRegistrationTimerCallbackNotPresent")); + await context.RunAsync(); + } +} + + + + + + From 76bfed2ce8dd1c29fe37899783090cf882a6876a Mon Sep 17 00:00:00 2001 From: Nils Gruson Date: Thu, 10 Apr 2025 00:45:07 +0200 Subject: [PATCH 07/22] Add Roslyn analyzer for actor registration (#1441) Added Roslyn analyzers and code fixes for: - Encouraging JSON serialization for actors - Validating actors are properly registered in DI - Ensuring that the actor handler mapping is performed during startup Signed-off-by: Nils Gruson Signed-off-by: Whit Waldo Co-authored-by: Whit Waldo --- Directory.Packages.props | 1 + src/Dapr.Actors.Analyzers/ActorAnalyzer.cs | 206 ++++++++++++ .../ActorJsonSerializationCodeFixProvider.cs | 120 +++++++ .../ActorRegistrationCodeFixProvider.cs | 220 ++++++++++++ .../AnalyzerReleases.Shipped.md | 9 + .../AnalyzerReleases.Unshipped.md | 3 + .../Dapr.Actors.Analyzers.csproj | 43 +++ .../ActorRegistrationAnalyzer.cs | 122 +++++++ .../ActorRegistrationCodeFixProvider.cs | 230 +++++++++++++ .../AnalyzerReleases.Shipped.md | 5 +- .../Dapr.Actors.Analyzers/AssemblyInfo.cs | 2 +- .../Dapr.Actors.Analyzers.csproj | 3 +- .../MappedActorHandlersAnalyzer.cs | 90 +++++ .../MappedActorHandlersCodeFixProvider.cs | 134 ++++++++ .../PreferActorJsonSerializationAnalyzer.cs | 93 ++++++ ...erActorJsonSerializationCodeFixProvider.cs | 133 ++++++++ .../Resources.Designer.cs | 54 +++ .../Dapr.Actors.Analyzers/Resources.resx | 18 + .../Dapr.Actors.Analyzers/SharedUtilities.cs | 38 +++ ... => TimerCallbackMethodPresentAnalyzer.cs} | 8 +- .../MapActorsHandlersCodeFixProvider.cs | 125 +++++++ .../ActorAnalyzerTests.cs | 313 ++++++++++++++++++ ...orJsonSerializationCodeFixProviderTests.cs | 55 +++ .../ActorRegistrationCodeFixProviderTests.cs | 175 ++++++++++ .../Dapr.Actors.Analyzers.Test.csproj | 35 ++ .../MapActorsHandlersCodeFixProviderTests.cs | 124 +++++++ test/Dapr.Actors.Analyzers.Test/Utilities.cs | 77 +++++ .../VerifyAnalyzer.cs | 68 ++++ .../VerifyCodeFix.cs | 56 ++++ .../ActorRegistrationAnalyzerTests.cs | 180 ++++++++++ .../ActorRegistrationCodeFixProviderTests.cs | 186 +++++++++++ .../Dapr.Actors.Analyzers.Tests.csproj | 5 +- .../MappedActorHandlersAnalyzerTests.cs | 71 ++++ ...MappedActorHandlersCodeFixProviderTests.cs | 131 ++++++++ ...eferActorJsonSerializationAnalyzerTests.cs | 73 ++++ ...orJsonSerializationCodeFixProviderTests.cs | 62 ++++ .../TestUtilities.cs | 88 +++++ ...imerCallbackMethodPresentAnalyzerTests.cs} | 13 +- .../VerifyAnalyzer.cs | 83 +++++ .../VerifyCodeFix.cs | 70 ++++ 40 files changed, 3505 insertions(+), 17 deletions(-) create mode 100644 src/Dapr.Actors.Analyzers/ActorAnalyzer.cs create mode 100644 src/Dapr.Actors.Analyzers/ActorJsonSerializationCodeFixProvider.cs create mode 100644 src/Dapr.Actors.Analyzers/ActorRegistrationCodeFixProvider.cs create mode 100644 src/Dapr.Actors.Analyzers/AnalyzerReleases.Shipped.md create mode 100644 src/Dapr.Actors.Analyzers/AnalyzerReleases.Unshipped.md create mode 100644 src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers.csproj create mode 100644 src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/ActorRegistrationAnalyzer.cs create mode 100644 src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/ActorRegistrationCodeFixProvider.cs create mode 100644 src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/MappedActorHandlersAnalyzer.cs create mode 100644 src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/MappedActorHandlersCodeFixProvider.cs create mode 100644 src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/PreferActorJsonSerializationAnalyzer.cs create mode 100644 src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/PreferActorJsonSerializationCodeFixProvider.cs create mode 100644 src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/SharedUtilities.cs rename src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/{TimerMethodPresentAnalyzer.cs => TimerCallbackMethodPresentAnalyzer.cs} (94%) create mode 100644 src/Dapr.Actors.Analyzers/MapActorsHandlersCodeFixProvider.cs create mode 100644 test/Dapr.Actors.Analyzers.Test/ActorAnalyzerTests.cs create mode 100644 test/Dapr.Actors.Analyzers.Test/ActorJsonSerializationCodeFixProviderTests.cs create mode 100644 test/Dapr.Actors.Analyzers.Test/ActorRegistrationCodeFixProviderTests.cs create mode 100644 test/Dapr.Actors.Analyzers.Test/Dapr.Actors.Analyzers.Test.csproj create mode 100644 test/Dapr.Actors.Analyzers.Test/MapActorsHandlersCodeFixProviderTests.cs create mode 100644 test/Dapr.Actors.Analyzers.Test/Utilities.cs create mode 100644 test/Dapr.Actors.Analyzers.Test/VerifyAnalyzer.cs create mode 100644 test/Dapr.Actors.Analyzers.Test/VerifyCodeFix.cs create mode 100644 test/Dapr.Actors.Analyzers.Tests/ActorRegistrationAnalyzerTests.cs create mode 100644 test/Dapr.Actors.Analyzers.Tests/ActorRegistrationCodeFixProviderTests.cs create mode 100644 test/Dapr.Actors.Analyzers.Tests/MappedActorHandlersAnalyzerTests.cs create mode 100644 test/Dapr.Actors.Analyzers.Tests/MappedActorHandlersCodeFixProviderTests.cs create mode 100644 test/Dapr.Actors.Analyzers.Tests/PreferActorJsonSerializationAnalyzerTests.cs create mode 100644 test/Dapr.Actors.Analyzers.Tests/PreferActorJsonSerializationCodeFixProviderTests.cs create mode 100644 test/Dapr.Actors.Analyzers.Tests/TestUtilities.cs rename test/Dapr.Actors.Analyzers.Tests/{TimerMethodPresentAnalyzerTests.cs => TimerCallbackMethodPresentAnalyzerTests.cs} (91%) create mode 100644 test/Dapr.Actors.Analyzers.Tests/VerifyAnalyzer.cs create mode 100644 test/Dapr.Actors.Analyzers.Tests/VerifyCodeFix.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 5ea87d5a..21fccd0a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -25,6 +25,7 @@ + diff --git a/src/Dapr.Actors.Analyzers/ActorAnalyzer.cs b/src/Dapr.Actors.Analyzers/ActorAnalyzer.cs new file mode 100644 index 00000000..02edc7b3 --- /dev/null +++ b/src/Dapr.Actors.Analyzers/ActorAnalyzer.cs @@ -0,0 +1,206 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Dapr.Actors.Analyzers; + +/// +/// Analyzes actor registration in Dapr applications. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class ActorAnalyzer : DiagnosticAnalyzer +{ + private static readonly DiagnosticDescriptor DiagnosticDescriptorActorRegistration = new( + "DAPR0001", + "Actor class not registered", + "The actor class '{0}' is not registered", + "Usage", + DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + private static readonly DiagnosticDescriptor DiagnosticDescriptorJsonSerialization = new( + "DAPR0002", + "Use JsonSerialization", + "Add options.UseJsonSerialization to support interoperability with non-.NET actors", + "Usage", + DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + private static readonly DiagnosticDescriptor DiagnosticDescriptorMapActorsHandlers = new( + "DAPR0003", + "Call MapActorsHandlers", + "Call app.MapActorsHandlers to map endpoints for Dapr actors", + "Usage", + DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + /// + /// Gets the supported diagnostics for this analyzer. + /// + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create( + DiagnosticDescriptorActorRegistration, + DiagnosticDescriptorJsonSerialization, + DiagnosticDescriptorMapActorsHandlers); + + /// + /// Initializes the analyzer. + /// + /// The analysis context. + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeActorRegistration, SyntaxKind.ClassDeclaration); + context.RegisterSyntaxNodeAction(AnalyzeSerialization, SyntaxKind.CompilationUnit); + context.RegisterSyntaxNodeAction(AnalyzeMapActorsHandlers, SyntaxKind.CompilationUnit); + } + + private void AnalyzeActorRegistration(SyntaxNodeAnalysisContext context) + { + var classDeclaration = (ClassDeclarationSyntax)context.Node; + + if (classDeclaration.BaseList != null) + { + var baseTypeSyntax = classDeclaration.BaseList.Types[0].Type; + + if (context.SemanticModel.GetSymbolInfo(baseTypeSyntax).Symbol is INamedTypeSymbol baseTypeSymbol) + { + var baseTypeName = baseTypeSymbol.ToDisplayString(); + + { + var actorTypeName = classDeclaration.Identifier.Text; + bool isRegistered = CheckIfActorIsRegistered(actorTypeName, context.SemanticModel); + if (!isRegistered) + { + var diagnostic = Diagnostic.Create(DiagnosticDescriptorActorRegistration, classDeclaration.Identifier.GetLocation(), actorTypeName); + context.ReportDiagnostic(diagnostic); + } + } + } + } + } + + private static bool CheckIfActorIsRegistered(string actorTypeName, SemanticModel semanticModel) + { + var methodInvocations = new List(); + foreach (var syntaxTree in semanticModel.Compilation.SyntaxTrees) + { + var root = syntaxTree.GetRoot(); + methodInvocations.AddRange(root.DescendantNodes().OfType()); + } + + foreach (var invocation in methodInvocations) + { + if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess) + { + continue; + } + + var methodName = memberAccess.Name.Identifier.Text; + if (methodName == "RegisterActor") + { + if (memberAccess.Name is GenericNameSyntax typeArgumentList && typeArgumentList.TypeArgumentList.Arguments.Count > 0) + { + if (typeArgumentList.TypeArgumentList.Arguments[0] is IdentifierNameSyntax typeArgument) + { + if (typeArgument.Identifier.Text == actorTypeName) + { + return true; + } + } + else if (typeArgumentList.TypeArgumentList.Arguments[0] is QualifiedNameSyntax qualifiedName) + { + if (qualifiedName.Right.Identifier.Text == actorTypeName) + { + return true; + } + } + } + } + } + + return false; + } + + private void AnalyzeSerialization(SyntaxNodeAnalysisContext context) + { + var addActorsInvocation = FindInvocation(context, "AddActors"); + + if (addActorsInvocation != null) + { + var optionsLambda = addActorsInvocation.ArgumentList.Arguments + .Select(arg => arg.Expression) + .OfType() + .FirstOrDefault(); + + if (optionsLambda != null) + { + var lambdaBody = optionsLambda.Body; + var assignments = lambdaBody.DescendantNodes().OfType(); + + var useJsonSerialization = assignments.Any(assignment => + assignment.Left is MemberAccessExpressionSyntax memberAccess && + memberAccess.Name is IdentifierNameSyntax identifier && + identifier.Identifier.Text == "UseJsonSerialization" && + assignment.Right is LiteralExpressionSyntax literal && + literal.Token.ValueText == "true"); + + if (!useJsonSerialization) + { + var diagnostic = Diagnostic.Create(DiagnosticDescriptorJsonSerialization, addActorsInvocation.GetLocation()); + context.ReportDiagnostic(diagnostic); + } + } + } + } + + private InvocationExpressionSyntax? FindInvocation(SyntaxNodeAnalysisContext context, string methodName) + { + foreach (var syntaxTree in context.SemanticModel.Compilation.SyntaxTrees) + { + var root = syntaxTree.GetRoot(); + var invocation = root.DescendantNodes().OfType() + .FirstOrDefault(invocation => invocation.Expression is MemberAccessExpressionSyntax memberAccess && + memberAccess.Name.Identifier.Text == methodName); + + if (invocation != null) + { + return invocation; + } + } + + return null; + } + + private void AnalyzeMapActorsHandlers(SyntaxNodeAnalysisContext context) + { + var addActorsInvocation = FindInvocation(context, "AddActors"); + + if (addActorsInvocation != null) + { + bool invokedByWebApplication = false; + var mapActorsHandlersInvocation = FindInvocation(context, "MapActorsHandlers"); + + if (mapActorsHandlersInvocation?.Expression is MemberAccessExpressionSyntax memberAccess) + { + var symbolInfo = context.SemanticModel.GetSymbolInfo(memberAccess.Expression); + if (symbolInfo.Symbol is ILocalSymbol localSymbol) + { + var type = localSymbol.Type; + if (type.ToDisplayString() == "Microsoft.AspNetCore.Builder.WebApplication") + { + invokedByWebApplication = true; + } + } + } + + if (mapActorsHandlersInvocation == null || !invokedByWebApplication) + { + var diagnostic = Diagnostic.Create(DiagnosticDescriptorMapActorsHandlers, addActorsInvocation.GetLocation()); + context.ReportDiagnostic(diagnostic); + } + } + } +} diff --git a/src/Dapr.Actors.Analyzers/ActorJsonSerializationCodeFixProvider.cs b/src/Dapr.Actors.Analyzers/ActorJsonSerializationCodeFixProvider.cs new file mode 100644 index 00000000..df3e832c --- /dev/null +++ b/src/Dapr.Actors.Analyzers/ActorJsonSerializationCodeFixProvider.cs @@ -0,0 +1,120 @@ +using System.Composition; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis; +using System.Collections.Immutable; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.CSharp; + +namespace Dapr.Actors.Analyzers; + +/// +/// Provides code fix to enable JSON serialization for actors. +/// +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(ActorRegistrationCodeFixProvider))] +[Shared] +public class ActorJsonSerializationCodeFixProvider : CodeFixProvider +{ + /// + /// Gets the diagnostic IDs that this provider can fix. + /// + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create("DAPR0002"); + + /// + /// Gets the FixAllProvider for this code fix provider. + /// + /// The FixAllProvider. + public override FixAllProvider? GetFixAllProvider() + { + return WellKnownFixAllProviders.BatchFixer; + } + + /// + /// Registers code fixes for the specified diagnostics. + /// + /// The context to register the code fixes. + /// A task representing the asynchronous operation. + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + var title = "Use JSON serialization"; + context.RegisterCodeFix( + CodeAction.Create( + title, + createChangedDocument: c => UseJsonSerializationAsync(context.Document, context.Diagnostics.First(), c), + equivalenceKey: title), + context.Diagnostics); + return Task.CompletedTask; + } + + private async Task UseJsonSerializationAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken) + { + (_, var addActorsInvocation) = await FindAddActorsInvocationAsync(document.Project, cancellationToken); + + if (addActorsInvocation == null) + { + return document; + } + + var optionsLambda = addActorsInvocation?.ArgumentList.Arguments + .Select(arg => arg.Expression) + .OfType() + .FirstOrDefault(); + + if (optionsLambda == null || optionsLambda.Body is not BlockSyntax optionsBlock) + return document; + + // Extract the parameter name from the lambda expression + var parameterName = optionsLambda.Parameter.Identifier.Text; + + // Check if the lambda body already contains the assignment + var assignmentExists = optionsBlock.Statements + .OfType() + .Any(statement => statement.Expression is AssignmentExpressionSyntax assignment && + assignment.Left is MemberAccessExpressionSyntax memberAccess && + memberAccess.Name is IdentifierNameSyntax identifier && + identifier.Identifier.Text == parameterName && + memberAccess.Name.Identifier.Text == "UseJsonSerialization"); + + if (!assignmentExists) + { + var assignmentStatement = SyntaxFactory.ExpressionStatement( + SyntaxFactory.AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.IdentifierName(parameterName), + SyntaxFactory.IdentifierName("UseJsonSerialization")), + SyntaxFactory.LiteralExpression(SyntaxKind.TrueLiteralExpression))); + + var newOptionsBlock = optionsBlock.AddStatements(assignmentStatement); + var root = await document.GetSyntaxRootAsync(cancellationToken); + var newRoot = root?.ReplaceNode(optionsBlock, newOptionsBlock); + return document.WithSyntaxRoot(newRoot!); + } + + return document; + } + + private async Task<(Document?, InvocationExpressionSyntax?)> FindAddActorsInvocationAsync(Project project, CancellationToken cancellationToken) + { + var compilation = await project.GetCompilationAsync(cancellationToken); + + foreach (var syntaxTree in compilation!.SyntaxTrees) + { + var syntaxRoot = await syntaxTree.GetRootAsync(cancellationToken); + + var addActorsInvocation = syntaxRoot.DescendantNodes() + .OfType() + .FirstOrDefault(invocation => invocation.Expression is MemberAccessExpressionSyntax memberAccess && + memberAccess.Name.Identifier.Text == "AddActors"); + + if (addActorsInvocation != null) + { + var document = project.GetDocument(addActorsInvocation.SyntaxTree); + return (document, addActorsInvocation); + } + } + + return (null, null); + } +} diff --git a/src/Dapr.Actors.Analyzers/ActorRegistrationCodeFixProvider.cs b/src/Dapr.Actors.Analyzers/ActorRegistrationCodeFixProvider.cs new file mode 100644 index 00000000..efb7d299 --- /dev/null +++ b/src/Dapr.Actors.Analyzers/ActorRegistrationCodeFixProvider.cs @@ -0,0 +1,220 @@ +using System.Collections.Immutable; +using System.Composition; +using System.Diagnostics; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Formatting; + +namespace Dapr.Actors.Analyzers; + +/// +/// Provides code fixes for actor registration issues. +/// +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(ActorRegistrationCodeFixProvider))] +[Shared] +public class ActorRegistrationCodeFixProvider : CodeFixProvider +{ + /// + /// Gets the diagnostic IDs that this provider can fix. + /// + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create("DAPR0001"); + + /// + /// Registers code fixes for the specified diagnostics. + /// + /// The context to register the code fixes. + /// A task representing the asynchronous operation. + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + var title = "Register actor"; + context.RegisterCodeFix( + CodeAction.Create( + title, + createChangedDocument: c => RegisterActorAsync(context.Document, context.Diagnostics.First(), c), + equivalenceKey: title), + context.Diagnostics); + return Task.CompletedTask; + } + + private async Task RegisterActorAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken) + { + var root = await document.GetSyntaxRootAsync(cancellationToken); + var diagnosticSpan = diagnostic.Location.SourceSpan; + + var classDeclaration = root?.FindToken(diagnosticSpan.Start).Parent?.AncestorsAndSelf().OfType().First(); + + if (root == null || classDeclaration == null) + return document; + + // Get the semantic model + var semanticModel = await document.GetSemanticModelAsync(cancellationToken); + + if (semanticModel == null) + return document; + + // Get the symbol for the class declaration + + if (semanticModel.GetDeclaredSymbol(classDeclaration, cancellationToken) is not INamedTypeSymbol classSymbol) + return document; + + // Get the fully qualified name + var actorType = classSymbol.ToDisplayString(new SymbolDisplayFormat( + typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces)); + + if (string.IsNullOrEmpty(actorType)) + return document; + + // Get the compilation + var compilation = await document.Project.GetCompilationAsync(cancellationToken).ConfigureAwait(false); + + if (compilation == null) + return document; + + (var targetDocument, var addActorsInvocation) = await FindAddActorsInvocationAsync(document.Project, cancellationToken); + + if (addActorsInvocation == null) + { + (targetDocument, addActorsInvocation) = await CreateAddActorsInvocation(document.Project, cancellationToken); + } + + if (addActorsInvocation == null) + return document; + + var targetRoot = await addActorsInvocation.SyntaxTree.GetRootAsync(cancellationToken); + + if (targetRoot == null || targetDocument == null) + return document; + + // Find the options lambda block + var optionsLambda = addActorsInvocation?.ArgumentList.Arguments + .Select(arg => arg.Expression) + .OfType() + .FirstOrDefault(); + + if (optionsLambda == null || optionsLambda.Body is not BlockSyntax optionsBlock) + return document; + + // Extract the parameter name from the lambda expression + var parameterName = optionsLambda.Parameter.Identifier.Text; + + // Create the new workflow registration statement + var registerWorkflowStatement = SyntaxFactory.ParseStatement($"{parameterName}.Actors.RegisterActor<{actorType}>();"); + + // Add the new registration statement to the options block + var newOptionsBlock = optionsBlock.AddStatements(registerWorkflowStatement); + + // Replace the old options block with the new one + var newRoot = targetRoot?.ReplaceNode(optionsBlock, newOptionsBlock); + + // Format the new root. + newRoot = Formatter.Format(newRoot!, document.Project.Solution.Workspace); + + return targetDocument.WithSyntaxRoot(newRoot); + } + + /// + /// Gets the FixAllProvider for this code fix provider. + /// + /// The FixAllProvider. + public override FixAllProvider? GetFixAllProvider() + { + return WellKnownFixAllProviders.BatchFixer; + } + + private async Task<(Document?, InvocationExpressionSyntax?)> FindAddActorsInvocationAsync(Project project, CancellationToken cancellationToken) + { + var compilation = await project.GetCompilationAsync(cancellationToken); + + foreach (var syntaxTree in compilation!.SyntaxTrees) + { + var syntaxRoot = await syntaxTree.GetRootAsync(cancellationToken); + + var addActorsInvocation = syntaxRoot.DescendantNodes() + .OfType() + .FirstOrDefault(invocation => invocation.Expression is MemberAccessExpressionSyntax memberAccess && + memberAccess.Name.Identifier.Text == "AddActors"); + + if (addActorsInvocation != null) + { + var document = project.GetDocument(addActorsInvocation.SyntaxTree); + return (document, addActorsInvocation); + } + } + + return (null, null); + } + + private async Task FindCreateBuilderInvocationAsync(Project project, CancellationToken cancellationToken) + { + var compilation = await project.GetCompilationAsync(cancellationToken); + + foreach (var syntaxTree in compilation!.SyntaxTrees) + { + var syntaxRoot = await syntaxTree.GetRootAsync(cancellationToken); + + // Find the invocation expression for WebApplication.CreateBuilder() + var createBuilderInvocation = syntaxRoot.DescendantNodes() + .OfType() + .FirstOrDefault(invocation => invocation.Expression is MemberAccessExpressionSyntax memberAccess && + memberAccess.Expression is IdentifierNameSyntax identifier && + identifier.Identifier.Text == "WebApplication" && + memberAccess.Name.Identifier.Text == "CreateBuilder"); + + if (createBuilderInvocation != null) + { + return createBuilderInvocation; + } + } + + return null; + } + + private async Task<(Document?, InvocationExpressionSyntax?)> CreateAddActorsInvocation(Project project, CancellationToken cancellationToken) + { + var createBuilderInvocation = await FindCreateBuilderInvocationAsync(project, cancellationToken); + + var variableDeclarator = createBuilderInvocation?.Ancestors() + .OfType() + .FirstOrDefault(); + + var builderVariable = variableDeclarator?.Identifier.Text; + + if (createBuilderInvocation != null) + { + var targetRoot = await createBuilderInvocation.SyntaxTree.GetRootAsync(cancellationToken); + var document = project.GetDocument(createBuilderInvocation.SyntaxTree); + + if (createBuilderInvocation.Expression is MemberAccessExpressionSyntax { Expression: IdentifierNameSyntax builderIdentifier }) + { + var addActorsStatement = SyntaxFactory.ParseStatement($"{builderVariable}.Services.AddActors(options => {{ }});"); + + if (createBuilderInvocation.Ancestors().OfType().FirstOrDefault() is SyntaxNode parentBlock) + { + var firstChild = parentBlock.ChildNodes().FirstOrDefault(node => node is not UsingDirectiveSyntax); + var newParentBlock = parentBlock.InsertNodesAfter(firstChild, new[] { addActorsStatement }); + targetRoot = targetRoot.ReplaceNode(parentBlock, newParentBlock); + } + else + { + var compilationUnitSyntax = createBuilderInvocation.Ancestors().OfType().FirstOrDefault(); + var firstChild = compilationUnitSyntax.ChildNodes().FirstOrDefault(node => node is not UsingDirectiveSyntax); + var globalStatement = SyntaxFactory.GlobalStatement(addActorsStatement); + var newCompilationUnitSyntax = compilationUnitSyntax.InsertNodesAfter(firstChild, new[] { globalStatement }); + targetRoot = targetRoot.ReplaceNode(compilationUnitSyntax, newCompilationUnitSyntax); + } + + var addActorsInvocation = targetRoot?.DescendantNodes() + .OfType() + .FirstOrDefault(invocation => invocation.Expression is MemberAccessExpressionSyntax memberAccess && + memberAccess.Name.Identifier.Text == "AddActors"); + + return (document, addActorsInvocation); + } + } + + return (null, null); + } +} diff --git a/src/Dapr.Actors.Analyzers/AnalyzerReleases.Shipped.md b/src/Dapr.Actors.Analyzers/AnalyzerReleases.Shipped.md new file mode 100644 index 00000000..d8abdf2e --- /dev/null +++ b/src/Dapr.Actors.Analyzers/AnalyzerReleases.Shipped.md @@ -0,0 +1,9 @@ +## Release 1.16 + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|-------------------- +DAPR0001| Usage | Warning | The actor class '{0}' is not registered +DAPR0002| Usage | Warning | Set options.UseJsonSerialization to true to support interoperability with non-.NET actors +DAPR0003| Usage | Warning | Call app.MapActorsHandlers to map endpoints for Dapr actors \ No newline at end of file diff --git a/src/Dapr.Actors.Analyzers/AnalyzerReleases.Unshipped.md b/src/Dapr.Actors.Analyzers/AnalyzerReleases.Unshipped.md new file mode 100644 index 00000000..b1b99aaf --- /dev/null +++ b/src/Dapr.Actors.Analyzers/AnalyzerReleases.Unshipped.md @@ -0,0 +1,3 @@ +; Unshipped analyzer release +; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + diff --git a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers.csproj b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers.csproj new file mode 100644 index 00000000..910ecb29 --- /dev/null +++ b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers.csproj @@ -0,0 +1,43 @@ + + + + netstandard2.0 + + enable + enable + true + + + + + + + + + true + + + false + + + false + + + This package contains Roslyn analyzers for actors. + $(PackageTags) + + + + + + + + + + + + + + + + diff --git a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/ActorRegistrationAnalyzer.cs b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/ActorRegistrationAnalyzer.cs new file mode 100644 index 00000000..164da75b --- /dev/null +++ b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/ActorRegistrationAnalyzer.cs @@ -0,0 +1,122 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Dapr.Actors.Analyzers; + +/// +/// An analyzer for Dapr actors that validates that each discovered Actor implementation is properly registered with +/// dependency injection during startup. +/// +[DiagnosticAnalyzer((LanguageNames.CSharp))] +public sealed class ActorRegistrationAnalyzer : DiagnosticAnalyzer +{ + internal static readonly DiagnosticDescriptor DiagnosticDescriptorActorRegistration = new( + id: "DAPR4002", + title: new LocalizableResourceString(nameof(Resources.DAPR4002Title), Resources.ResourceManager, + typeof(Resources)), + messageFormat: new LocalizableResourceString(nameof(Resources.DAPR4002MessageFormat), Resources.ResourceManager, + typeof(Resources)), + category: "Usage", + DiagnosticSeverity.Warning, + isEnabledByDefault: true + ); + + /// + /// Returns a set of descriptors for the diagnostics that this analyzer is capable of producing. + /// + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create( + DiagnosticDescriptorActorRegistration); + + /// + /// Called once at session start to register actions in the analysis context. + /// + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeActorRegistration, SyntaxKind.ClassDeclaration); + } + + private static void AnalyzeActorRegistration(SyntaxNodeAnalysisContext context) + { + var classDeclaration = (ClassDeclarationSyntax)context.Node; + + if (classDeclaration.BaseList == null) + { + return; + } + + var baseTypeSyntax = classDeclaration.BaseList.Types[0].Type; + + if (context.SemanticModel.GetSymbolInfo(baseTypeSyntax).Symbol is not INamedTypeSymbol baseTypeSymbol) + { + return; + } + + var actorTypeName = classDeclaration.Identifier.Text; + var isRegistered = CheckIfActorIsRegistered(actorTypeName, context.SemanticModel); + if (isRegistered) + { + return; + } + + var diagnostic = Diagnostic.Create(DiagnosticDescriptorActorRegistration, classDeclaration.Identifier.GetLocation(), actorTypeName); + context.ReportDiagnostic(diagnostic); + } + + private static bool CheckIfActorIsRegistered(string actorTypeName, SemanticModel semanticModel) + { + var methodInvocations = new List(); + foreach (var syntaxTree in semanticModel.Compilation.SyntaxTrees) + { + var root = syntaxTree.GetRoot(); + methodInvocations.AddRange(root.DescendantNodes().OfType()); + } + + foreach (var invocation in methodInvocations) + { + if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess) + { + continue; + } + + var methodName = memberAccess.Name.Identifier.Text; + if (methodName != "RegisterActor") + { + continue; + } + + if (memberAccess.Name is not GenericNameSyntax typeArgumentList || + typeArgumentList.TypeArgumentList.Arguments.Count <= 0) + { + continue; + } + + switch (typeArgumentList.TypeArgumentList.Arguments[0]) + { + case IdentifierNameSyntax typeArgument when typeArgument.Identifier.Text == actorTypeName: + case QualifiedNameSyntax qualifiedName when qualifiedName.Right.Identifier.Text == actorTypeName: + return true; + } + } + + return false; + } +} diff --git a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/ActorRegistrationCodeFixProvider.cs b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/ActorRegistrationCodeFixProvider.cs new file mode 100644 index 00000000..828f9917 --- /dev/null +++ b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/ActorRegistrationCodeFixProvider.cs @@ -0,0 +1,230 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Formatter = Microsoft.CodeAnalysis.Formatting.Formatter; + +namespace Dapr.Actors.Analyzers; + +/// +/// Provides code fixes for missing actor registrations during dependency injection configuration. +/// +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(ActorRegistrationCodeFixProvider))] +public sealed class ActorRegistrationCodeFixProvider : CodeFixProvider +{ + /// + /// Gets the diagnostic IDs that this provider can fix. + /// + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create("DAPR4002"); + + /// + /// Registers code fixes for the specified diagnostics. + /// + /// The context to register the code fixes. + /// A task representing the asynchronous operation. + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + const string title = "Register Dapr actor"; + context.RegisterCodeFix( + CodeAction.Create( + title, + createChangedDocument: c => RegisterActorAsync(context.Document, context.Diagnostics.First(), c), + equivalenceKey: title), + context.Diagnostics); + return Task.CompletedTask; + } + + private async Task RegisterActorAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken) + { + var root = await document.GetSyntaxRootAsync(cancellationToken); + var diagnosticSpan = diagnostic.Location.SourceSpan; + + var classDeclaration = root?.FindToken(diagnosticSpan.Start).Parent?.AncestorsAndSelf().OfType().First(); + + if (root == null || classDeclaration == null) + return document; + + // Get the semantic model + var semanticModel = await document.GetSemanticModelAsync(cancellationToken); + + if (semanticModel == null) + return document; + + // Get the symbol for the class declaration + + if (ModelExtensions.GetDeclaredSymbol(semanticModel, classDeclaration, cancellationToken) is not INamedTypeSymbol classSymbol) + return document; + + // Get the fully qualified name + var actorType = classSymbol.ToDisplayString(new SymbolDisplayFormat( + typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces)); + + if (string.IsNullOrEmpty(actorType)) + return document; + + // Get the compilation + var compilation = await document.Project.GetCompilationAsync(cancellationToken).ConfigureAwait(false); + + if (compilation == null) + return document; + + var (targetDocument, addActorsInvocation) = await FindAddActorsInvocationAsync(document.Project, cancellationToken); + + if (addActorsInvocation == null) + { + (targetDocument, addActorsInvocation) = await CreateAddActorsInvocation(document.Project, cancellationToken); + } + + if (addActorsInvocation == null) + return document; + + var targetRoot = await addActorsInvocation.SyntaxTree.GetRootAsync(cancellationToken); + + if (targetRoot == null || targetDocument == null) + return document; + + // Find the options lambda block + var optionsLambda = addActorsInvocation?.ArgumentList.Arguments + .Select(arg => arg.Expression) + .OfType() + .FirstOrDefault(); + + if (optionsLambda == null || optionsLambda.Body is not BlockSyntax optionsBlock) + return document; + + // Extract the parameter name from the lambda expression + var parameterName = optionsLambda.Parameter.Identifier.Text; + + // Create the new workflow registration statement + var registerWorkflowStatement = SyntaxFactory.ParseStatement($"{parameterName}.Actors.RegisterActor<{actorType}>();"); + + // Add the new registration statement to the options block + var newOptionsBlock = optionsBlock.AddStatements(registerWorkflowStatement); + + // Replace the old options block with the new one + var newRoot = targetRoot?.ReplaceNode(optionsBlock, newOptionsBlock); + + // Format the new root. + newRoot = Formatter.Format(newRoot!, document.Project.Solution.Workspace); + + return targetDocument.WithSyntaxRoot(newRoot); + } + + /// + /// Gets the FixAllProvider for this code fix provider. + /// + /// The FixAllProvider. + public override FixAllProvider? GetFixAllProvider() + { + return WellKnownFixAllProviders.BatchFixer; + } + + private async Task<(Document?, InvocationExpressionSyntax?)> FindAddActorsInvocationAsync(Project project, CancellationToken cancellationToken) + { + var compilation = await project.GetCompilationAsync(cancellationToken); + + foreach (var syntaxTree in compilation!.SyntaxTrees) + { + var syntaxRoot = await syntaxTree.GetRootAsync(cancellationToken); + + var addActorsInvocation = syntaxRoot.DescendantNodes() + .OfType() + .FirstOrDefault(invocation => invocation.Expression is MemberAccessExpressionSyntax memberAccess && + memberAccess.Name.Identifier.Text == "AddActors"); + + if (addActorsInvocation != null) + { + var document = project.GetDocument(addActorsInvocation.SyntaxTree); + return (document, addActorsInvocation); + } + } + + return (null, null); + } + + private async Task FindCreateBuilderInvocationAsync(Project project, CancellationToken cancellationToken) + { + var compilation = await project.GetCompilationAsync(cancellationToken); + + foreach (var syntaxTree in compilation!.SyntaxTrees) + { + var syntaxRoot = await syntaxTree.GetRootAsync(cancellationToken); + + // Find the invocation expression for WebApplication.CreateBuilder() + var createBuilderInvocation = syntaxRoot.DescendantNodes() + .OfType() + .FirstOrDefault(invocation => invocation.Expression is MemberAccessExpressionSyntax memberAccess && + memberAccess.Expression is IdentifierNameSyntax identifier && + identifier.Identifier.Text == "WebApplication" && + memberAccess.Name.Identifier.Text == "CreateBuilder"); + + if (createBuilderInvocation != null) + { + return createBuilderInvocation; + } + } + + return null; + } + + private async Task<(Document?, InvocationExpressionSyntax?)> CreateAddActorsInvocation(Project project, CancellationToken cancellationToken) + { + var createBuilderInvocation = await FindCreateBuilderInvocationAsync(project, cancellationToken); + + var variableDeclarator = createBuilderInvocation?.Ancestors() + .OfType() + .FirstOrDefault(); + + var builderVariable = variableDeclarator?.Identifier.Text; + + if (createBuilderInvocation != null) + { + var targetRoot = await createBuilderInvocation.SyntaxTree.GetRootAsync(cancellationToken); + var document = project.GetDocument(createBuilderInvocation.SyntaxTree); + + if (createBuilderInvocation.Expression is MemberAccessExpressionSyntax { Expression: IdentifierNameSyntax builderIdentifier }) + { + var addActorsStatement = SyntaxFactory.ParseStatement($"{builderVariable}.Services.AddActors(options => {{ }});"); + + if (createBuilderInvocation.Ancestors().OfType().FirstOrDefault() is SyntaxNode parentBlock) + { + var firstChild = parentBlock.ChildNodes().FirstOrDefault(node => node is not UsingDirectiveSyntax); + var newParentBlock = parentBlock.InsertNodesAfter(firstChild, new[] { addActorsStatement }); + targetRoot = targetRoot.ReplaceNode(parentBlock, newParentBlock); + } + else + { + var compilationUnitSyntax = createBuilderInvocation.Ancestors().OfType().FirstOrDefault(); + var firstChild = compilationUnitSyntax.ChildNodes().FirstOrDefault(node => node is not UsingDirectiveSyntax); + var globalStatement = SyntaxFactory.GlobalStatement(addActorsStatement); + var newCompilationUnitSyntax = compilationUnitSyntax.InsertNodesAfter(firstChild, new[] { globalStatement }); + targetRoot = targetRoot.ReplaceNode(compilationUnitSyntax, newCompilationUnitSyntax); + } + + var addActorsInvocation = targetRoot?.DescendantNodes() + .OfType() + .FirstOrDefault(invocation => invocation.Expression is MemberAccessExpressionSyntax memberAccess && + memberAccess.Name.Identifier.Text == "AddActors"); + + return (document, addActorsInvocation); + } + } + + return (null, null); + } +} diff --git a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/AnalyzerReleases.Shipped.md b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/AnalyzerReleases.Shipped.md index bc73dcb0..f82775b1 100644 --- a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/AnalyzerReleases.Shipped.md +++ b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/AnalyzerReleases.Shipped.md @@ -6,4 +6,7 @@ Rule ID | Category | Severity | Notes --------|----------|----------|------------------------------------------------------------------------------------ -DAPR4001 | Usage | Warning | Actor timer method invocations require the named callback method to exist on type. \ No newline at end of file +DAPR4001 | Usage | Warning | Actor timer method invocations require the named callback method to exist on type. +DAPR4002 | Usage | Warning | The actor type is not registered with dependency injection +DAPR4003 | Usage | Info | Set options.UseJsonSerialization to true to support interoperability with non-.NET actors +DAPR4004 | Usage | Warning | Call app.MapActorsHandlers to map endpoints for Dapr actors. diff --git a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/AssemblyInfo.cs b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/AssemblyInfo.cs index dcc08b3c..e07f509e 100644 --- a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/AssemblyInfo.cs +++ b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/AssemblyInfo.cs @@ -13,4 +13,4 @@ using System.Runtime.CompilerServices; -[assembly: InternalsVisibleTo("Dapr.Actors.Analyzers.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2" )] +[assembly: InternalsVisibleTo("Dapr.Actors.Analyzers.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2" )] diff --git a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers.csproj b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers.csproj index a3c751c3..a75e37a3 100644 --- a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers.csproj +++ b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers.csproj @@ -3,15 +3,16 @@ netstandard2.0 - false enable enable This package contains Roslyn analyzers for Dapr.Actors. true + $(NoWarn);RS1038 + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/MappedActorHandlersAnalyzer.cs b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/MappedActorHandlersAnalyzer.cs new file mode 100644 index 00000000..c85a8066 --- /dev/null +++ b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/MappedActorHandlersAnalyzer.cs @@ -0,0 +1,90 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Dapr.Actors.Analyzers; + +/// +/// An analyzer for Dapr actors that validates that the handler is set up during initial setup to map +/// the actor endpoints. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class MappedActorHandlersAnalyzer : DiagnosticAnalyzer +{ + internal static readonly DiagnosticDescriptor DiagnosticDescriptorMapActorsHandlers = new( + id: "DAPR4004", + title: new LocalizableResourceString(nameof(Resources.DAPR4004Title), Resources.ResourceManager, + typeof(Resources)), + messageFormat: new LocalizableResourceString(nameof(Resources.DAPR4004MessageFormat), Resources.ResourceManager, + typeof(Resources)), + category: "Usage", + DiagnosticSeverity.Warning, + isEnabledByDefault: true + ); + + /// + /// Returns a set of descriptors for the diagnostics that this analyzer is capable of producing. + /// + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create( + DiagnosticDescriptorMapActorsHandlers); + + /// + /// Called once at session start to register actions in the analysis context. + /// + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeMapActorsHandlers, SyntaxKind.CompilationUnit); + } + + private static void AnalyzeMapActorsHandlers(SyntaxNodeAnalysisContext context) + { + var addActorsInvocation = SharedUtilities.FindInvocation(context, "AddActors"); + + if (addActorsInvocation == null) + { + return; + } + + var invokedByWebApplication = false; + var mapActorsHandlersInvocation = SharedUtilities.FindInvocation(context, "MapActorsHandlers"); + + if (mapActorsHandlersInvocation?.Expression is MemberAccessExpressionSyntax memberAccess) + { + var symbolInfo = ModelExtensions.GetSymbolInfo(context.SemanticModel, memberAccess.Expression); + if (symbolInfo.Symbol is ILocalSymbol localSymbol) + { + var type = localSymbol.Type; + if (type.ToDisplayString() == "Microsoft.AspNetCore.Builder.WebApplication") + { + invokedByWebApplication = true; + } + } + } + + if (mapActorsHandlersInvocation != null && invokedByWebApplication) + { + return; + } + + var diagnostic = Diagnostic.Create(DiagnosticDescriptorMapActorsHandlers, addActorsInvocation.GetLocation()); + context.ReportDiagnostic(diagnostic); + } +} diff --git a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/MappedActorHandlersCodeFixProvider.cs b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/MappedActorHandlersCodeFixProvider.cs new file mode 100644 index 00000000..f5982ad8 --- /dev/null +++ b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/MappedActorHandlersCodeFixProvider.cs @@ -0,0 +1,134 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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.Collections.Immutable; +using System.Composition; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Dapr.Actors.Analyzers; + +/// +/// +/// +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(MappedActorHandlersCodeFixProvider))] +public sealed class MappedActorHandlersCodeFixProvider : CodeFixProvider +{ + /// + /// A list of diagnostic IDs that this provider can provide fixes for. + /// + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create("DAPR4004"); + + /// + /// Gets the FixAllProvider for this code fix provider. + /// + /// The FixAllProvider. + public override FixAllProvider? GetFixAllProvider() + { + return WellKnownFixAllProviders.BatchFixer; + } + + /// + /// Registers code fixes for the specified diagnostic. + /// + /// A context for code fix registration. + /// A task that represents the asynchronous operation. + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + const string title = "Register Dapr actor mappings"; + context.RegisterCodeFix( + CodeAction.Create( + title, + createChangedDocument: c => AddMapActorsHandlersAsync(context.Document, context.Diagnostics.First(), c), + equivalenceKey: title), + context.Diagnostics); + return Task.CompletedTask; + } + + /// + /// Adds a call to MapActorsHandlers to the specified document. + /// + /// The document to modify. + /// The diagnostic to fix. + /// A cancellation token. + /// A task that represents the asynchronous operation. The task result contains the modified document. + private static async Task AddMapActorsHandlersAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken) + { + var root = await document.GetSyntaxRootAsync(cancellationToken); + var invocationExpressions = root!.DescendantNodes().OfType(); + + var createBuilderInvocation = invocationExpressions + .FirstOrDefault(invocation => invocation.Expression is MemberAccessExpressionSyntax { Name.Identifier.Text: "CreateBuilder", Expression: IdentifierNameSyntax + { + Identifier.Text: "WebApplication" + } + }); + + var variableDeclarator = createBuilderInvocation + .AncestorsAndSelf() + .OfType() + .FirstOrDefault(); + + var variableName = variableDeclarator.Identifier.Text; + + var buildInvocation = invocationExpressions + .FirstOrDefault(invocation => invocation.Expression is MemberAccessExpressionSyntax + { + Name.Identifier.Text: "Build", + Expression: IdentifierNameSyntax identifier + } && + identifier.Identifier.Text == variableName); + + var buildVariableDeclarator = buildInvocation + .AncestorsAndSelf() + .OfType() + .FirstOrDefault(); + + var buildVariableName = buildVariableDeclarator.Identifier.Text; + + var mapActorsHandlersInvocation = SyntaxFactory.ExpressionStatement( + SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.IdentifierName(buildVariableName), + SyntaxFactory.IdentifierName("MapActorsHandlers")))); + + if (buildInvocation.Ancestors().OfType().FirstOrDefault() is SyntaxNode parentBlock) + { + var localDeclaration = buildInvocation + .AncestorsAndSelf() + .OfType() + .FirstOrDefault(); + + var newParentBlock = parentBlock.InsertNodesAfter(localDeclaration, [mapActorsHandlersInvocation]); + root = root.ReplaceNode(parentBlock, newParentBlock); + } + else + { + var buildInvocationGlobalStatement = buildInvocation + .AncestorsAndSelf() + .OfType() + .FirstOrDefault(); + + var compilationUnitSyntax = createBuilderInvocation.Ancestors().OfType().FirstOrDefault(); + var newCompilationUnitSyntax = compilationUnitSyntax.InsertNodesAfter(buildInvocationGlobalStatement, + [SyntaxFactory.GlobalStatement(mapActorsHandlersInvocation)]); + root = root.ReplaceNode(compilationUnitSyntax, newCompilationUnitSyntax); + } + + return document.WithSyntaxRoot(root); + } +} diff --git a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/PreferActorJsonSerializationAnalyzer.cs b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/PreferActorJsonSerializationAnalyzer.cs new file mode 100644 index 00000000..9d36454d --- /dev/null +++ b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/PreferActorJsonSerializationAnalyzer.cs @@ -0,0 +1,93 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Dapr.Actors.Analyzers; + +/// +/// An analyzer for Dapr Actors that suggests configuring JSON serialization during Actor DI registration for +/// better interoperability with non-.NET actors throughout a Dapr project. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class PreferActorJsonSerializationAnalyzer : DiagnosticAnalyzer +{ + internal static readonly DiagnosticDescriptor DiagnosticDescriptorJsonSerialization = new( + id: "DAPR4003", + title: new LocalizableResourceString(nameof(Resources.DAPR4003Title), Resources.ResourceManager, + typeof(Resources)), + messageFormat: new LocalizableResourceString(nameof(Resources.DAPR4003MessageFormat), Resources.ResourceManager, + typeof(Resources)), + category: "Usage", + DiagnosticSeverity.Info, + isEnabledByDefault: true + ); + + /// + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create( + DiagnosticDescriptorJsonSerialization + ); + + /// + /// Called once at session start to register actions in the analysis context. + /// + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeSerialization, SyntaxKind.CompilationUnit); + } + + private static void AnalyzeSerialization(SyntaxNodeAnalysisContext context) + { + var addActorsInvocation = SharedUtilities.FindInvocation(context, "AddActors"); + + if (addActorsInvocation is null) + { + return; + } + + var optionsLambda = addActorsInvocation.ArgumentList.Arguments + .Select(arg => arg.Expression) + .OfType() + .FirstOrDefault(); + + if (optionsLambda == null) + { + return; + } + + var lambdaBody = optionsLambda.Body; + var assignments = lambdaBody.DescendantNodes().OfType(); + + var useJsonSerialization = assignments.Any(assignment => + assignment.Left is MemberAccessExpressionSyntax memberAccess && + memberAccess.Name is IdentifierNameSyntax identifier && + identifier.Identifier.Text == "UseJsonSerialization" && + assignment.Right is LiteralExpressionSyntax literal && + literal.Token.ValueText == "true"); + + if (useJsonSerialization) + { + return; + } + + var diagnostic = Diagnostic.Create(DiagnosticDescriptorJsonSerialization, addActorsInvocation.GetLocation()); + context.ReportDiagnostic(diagnostic); + } +} diff --git a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/PreferActorJsonSerializationCodeFixProvider.cs b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/PreferActorJsonSerializationCodeFixProvider.cs new file mode 100644 index 00000000..fc3da963 --- /dev/null +++ b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/PreferActorJsonSerializationCodeFixProvider.cs @@ -0,0 +1,133 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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.Collections.Immutable; +using System.Composition; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Dapr.Actors.Analyzers; + +/// +/// Provides the code fix to enable JSON serialization for Dapr actors. +/// +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(PreferActorJsonSerializationCodeFixProvider))] +public sealed class PreferActorJsonSerializationCodeFixProvider : CodeFixProvider +{ + /// + /// A list of diagnostic IDs that this provider can provide fixes for. + /// + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create("DAPR4003"); + + /// + /// Gets the FixAllProvider for this code fix provider. + /// + /// The FixAllProvider. + public override FixAllProvider? GetFixAllProvider() + { + return WellKnownFixAllProviders.BatchFixer; + } + + /// + /// Registers code fixes for the specified diagnostics. + /// + /// The context to register the code fixes. + /// A task representing the asynchronous operation. + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + const string title = "Use JSON serialization"; + context.RegisterCodeFix( + CodeAction.Create( + title, + createChangedDocument: c => UseJsonSerializationAsync(context.Document, context.Diagnostics.First(), c), + equivalenceKey: title), + context.Diagnostics); + return Task.CompletedTask; + } + + private async Task UseJsonSerializationAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken) + { + var (_, addActorsInvocation) = await FindAddActorsInvocationAsync(document.Project, cancellationToken); + + if (addActorsInvocation == null) + { + return document; + } + + var optionsLambda = addActorsInvocation?.ArgumentList.Arguments + .Select(arg => arg.Expression) + .OfType() + .FirstOrDefault(); + + if (optionsLambda == null || optionsLambda.Body is not BlockSyntax optionsBlock) + return document; + + // Extract the parameter name from the lambda expression + var parameterName = optionsLambda.Parameter.Identifier.Text; + + // Check if the lambda body already contains the assignment + var assignmentExists = optionsBlock.Statements + .OfType() + .Any(statement => statement.Expression is AssignmentExpressionSyntax assignment && + assignment.Left is MemberAccessExpressionSyntax memberAccess && + memberAccess.Name is IdentifierNameSyntax identifier && + identifier.Identifier.Text == parameterName && + memberAccess.Name.Identifier.Text == "UseJsonSerialization"); + + if (assignmentExists) + { + return document; + } + + var assignmentStatement = SyntaxFactory.ExpressionStatement( + SyntaxFactory.AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.IdentifierName(parameterName), + SyntaxFactory.IdentifierName("UseJsonSerialization")), + SyntaxFactory.LiteralExpression(SyntaxKind.TrueLiteralExpression))); + + var newOptionsBlock = optionsBlock.AddStatements(assignmentStatement); + var root = await document.GetSyntaxRootAsync(cancellationToken); + var newRoot = root?.ReplaceNode(optionsBlock, newOptionsBlock); + return document.WithSyntaxRoot(newRoot!); + + } + + private async Task<(Document?, InvocationExpressionSyntax?)> FindAddActorsInvocationAsync(Project project, CancellationToken cancellationToken) + { + var compilation = await project.GetCompilationAsync(cancellationToken); + + foreach (var syntaxTree in compilation!.SyntaxTrees) + { + var syntaxRoot = await syntaxTree.GetRootAsync(cancellationToken); + + var addActorsInvocation = syntaxRoot.DescendantNodes() + .OfType() + .FirstOrDefault(invocation => invocation.Expression is MemberAccessExpressionSyntax memberAccess && + memberAccess.Name.Identifier.Text == "AddActors"); + + if (addActorsInvocation != null) + { + var document = project.GetDocument(addActorsInvocation.SyntaxTree); + return (document, addActorsInvocation); + } + } + + return (null, null); + } +} diff --git a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Resources.Designer.cs b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Resources.Designer.cs index af3dd797..ac9c06d8 100644 --- a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Resources.Designer.cs +++ b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Resources.Designer.cs @@ -76,5 +76,59 @@ namespace Dapr.Actors.Analyzers { return ResourceManager.GetString("DAPR4001Title", resourceCulture); } } + + /// + /// Looks up a localized string similar to The actor type '{0}' is not registered with dependency injection. + /// + internal static string DAPR4002MessageFormat { + get { + return ResourceManager.GetString("DAPR4002MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The actor type is not registered with dependency injection. + /// + internal static string DAPR4002Title { + get { + return ResourceManager.GetString("DAPR4002Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Set options.UseJsonSerialization to true to support interoperability with non-.NET actors. + /// + internal static string DAPR4003MessageFormat { + get { + return ResourceManager.GetString("DAPR4003MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Set options.UseJsonSerialization to true to support interoperability with non-.NET actors. + /// + internal static string DAPR4003Title { + get { + return ResourceManager.GetString("DAPR4003Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Call app.MapActorsHandlers to map endpoints for Dapr actors. + /// + internal static string DAPR4004MessageFormat { + get { + return ResourceManager.GetString("DAPR4004MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Call app.MapActorsHandlers to map endpoints for Dapr actors. + /// + internal static string DAPR4004Title { + get { + return ResourceManager.GetString("DAPR4004Title", resourceCulture); + } + } } } diff --git a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Resources.resx b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Resources.resx index 47f6c667..93c31362 100644 --- a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Resources.resx +++ b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Resources.resx @@ -24,4 +24,22 @@ Actor timer method invocations require the named callback '{0}' method to exist on type '{1}' + + The actor type is not registered with dependency injection + + + The actor type '{0}' is not registered with dependency injection + + + Set options.UseJsonSerialization to true to support interoperability with non-.NET actors + + + Set options.UseJsonSerialization to true to support interoperability with non-.NET actors + + + Call app.MapActorsHandlers to map endpoints for Dapr actors + + + Call app.MapActorsHandlers to map endpoints for Dapr actors + \ No newline at end of file diff --git a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/SharedUtilities.cs b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/SharedUtilities.cs new file mode 100644 index 00000000..d411ec16 --- /dev/null +++ b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/SharedUtilities.cs @@ -0,0 +1,38 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Dapr.Actors.Analyzers; + +internal class SharedUtilities +{ + internal static InvocationExpressionSyntax? FindInvocation(SyntaxNodeAnalysisContext context, string methodName) + { + foreach (var syntaxTree in context.SemanticModel.Compilation.SyntaxTrees) + { + var root = syntaxTree.GetRoot(); + var invocation = root.DescendantNodes().OfType() + .FirstOrDefault(invocation => invocation.Expression is MemberAccessExpressionSyntax memberAccess && + memberAccess.Name.Identifier.Text == methodName); + + if (invocation != null) + { + return invocation; + } + } + + return null; + } +} diff --git a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/TimerMethodPresentAnalyzer.cs b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/TimerCallbackMethodPresentAnalyzer.cs similarity index 94% rename from src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/TimerMethodPresentAnalyzer.cs rename to src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/TimerCallbackMethodPresentAnalyzer.cs index 59640fd5..49f03ece 100644 --- a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/TimerMethodPresentAnalyzer.cs +++ b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/TimerCallbackMethodPresentAnalyzer.cs @@ -12,7 +12,6 @@ // ------------------------------------------------------------------------ using System.Collections.Immutable; -using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -25,12 +24,9 @@ namespace Dapr.Actors.Analyzers; /// as the callback should actually exist on the type. /// [DiagnosticAnalyzer(LanguageNames.CSharp)] -public sealed class TimerMethodPresentAnalyzer : DiagnosticAnalyzer +public sealed class TimerCallbackMethodPresentAnalyzer : DiagnosticAnalyzer { - /// - /// The rule validated by the analyzer. - /// - public static readonly DiagnosticDescriptor DaprTimerCallbackMethodRule = new( + internal static readonly DiagnosticDescriptor DaprTimerCallbackMethodRule = new( id: "DAPR4001", title: new LocalizableResourceString(nameof(Resources.DAPR4001Title), Resources.ResourceManager, typeof(Resources)), messageFormat: new LocalizableResourceString(nameof(Resources.DAPR4001MessageFormat), Resources.ResourceManager, typeof(Resources)), diff --git a/src/Dapr.Actors.Analyzers/MapActorsHandlersCodeFixProvider.cs b/src/Dapr.Actors.Analyzers/MapActorsHandlersCodeFixProvider.cs new file mode 100644 index 00000000..60dc7a4e --- /dev/null +++ b/src/Dapr.Actors.Analyzers/MapActorsHandlersCodeFixProvider.cs @@ -0,0 +1,125 @@ +using System.Collections.Immutable; +using System.Composition; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Dapr.Actors.Analyzers; + +/// +/// Provides a code fix for the diagnostic "DAPR0003" by adding a call to MapActorsHandlers. +/// +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(ActorRegistrationCodeFixProvider))] +[Shared] +public class MapActorsHandlersCodeFixProvider : CodeFixProvider +{ + /// + /// Gets the diagnostic IDs that this code fix provider can fix. + /// + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create("DAPR0003"); + + /// + /// Gets the FixAllProvider for this code fix provider. + /// + /// The FixAllProvider. + public override FixAllProvider? GetFixAllProvider() + { + return WellKnownFixAllProviders.BatchFixer; + } + + /// + /// Registers code fixes for the specified diagnostic. + /// + /// A context for code fix registration. + /// A task that represents the asynchronous operation. + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + var title = "Call MapActorsHandlers"; + context.RegisterCodeFix( + CodeAction.Create( + title, + createChangedDocument: c => AddMapActorsHandlersAsync(context.Document, context.Diagnostics.First(), c), + equivalenceKey: title), + context.Diagnostics); + return Task.CompletedTask; + } + + /// + /// Adds a call to MapActorsHandlers to the specified document. + /// + /// The document to modify. + /// The diagnostic to fix. + /// A cancellation token. + /// A task that represents the asynchronous operation. The task result contains the modified document. + private async Task AddMapActorsHandlersAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken) + { + var root = await document.GetSyntaxRootAsync(cancellationToken); + var invocationExpressions = root!.DescendantNodes().OfType(); + + var createBuilderInvocation = invocationExpressions + .FirstOrDefault(invocation => + { + return invocation.Expression is MemberAccessExpressionSyntax memberAccess && + memberAccess.Name.Identifier.Text == "CreateBuilder" && + memberAccess.Expression is IdentifierNameSyntax identifier && + identifier.Identifier.Text == "WebApplication"; + }); + + var variableDeclarator = createBuilderInvocation + .AncestorsAndSelf() + .OfType() + .FirstOrDefault(); + + var variableName = variableDeclarator.Identifier.Text; + + var buildInvocation = invocationExpressions + .FirstOrDefault(invocation => + { + return invocation.Expression is MemberAccessExpressionSyntax memberAccess && + memberAccess.Name.Identifier.Text == "Build" && + memberAccess.Expression is IdentifierNameSyntax identifier && + identifier.Identifier.Text == variableName; + }); + + var buildVariableDeclarator = buildInvocation + .AncestorsAndSelf() + .OfType() + .FirstOrDefault(); + + var buildVariableName = buildVariableDeclarator.Identifier.Text; + + var mapActorsHandlersInvocation = SyntaxFactory.ExpressionStatement( + SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.IdentifierName(buildVariableName), + SyntaxFactory.IdentifierName("MapActorsHandlers")))); + + if (buildInvocation.Ancestors().OfType().FirstOrDefault() is SyntaxNode parentBlock) + { + var localDeclaration = buildInvocation + .AncestorsAndSelf() + .OfType() + .FirstOrDefault(); + + var newParentBlock = parentBlock.InsertNodesAfter(localDeclaration, new[] { mapActorsHandlersInvocation }); + root = root.ReplaceNode(parentBlock, newParentBlock); + } + else + { + var buildInvocationGlobalStatement = buildInvocation + .AncestorsAndSelf() + .OfType() + .FirstOrDefault(); + + var compilationUnitSyntax = createBuilderInvocation.Ancestors().OfType().FirstOrDefault(); + var newCompilationUnitSyntax = compilationUnitSyntax.InsertNodesAfter(buildInvocationGlobalStatement, + new[] { SyntaxFactory.GlobalStatement(mapActorsHandlersInvocation) }); + root = root.ReplaceNode(compilationUnitSyntax, newCompilationUnitSyntax); + } + + return document.WithSyntaxRoot(root); + } +} diff --git a/test/Dapr.Actors.Analyzers.Test/ActorAnalyzerTests.cs b/test/Dapr.Actors.Analyzers.Test/ActorAnalyzerTests.cs new file mode 100644 index 00000000..8d6b9abe --- /dev/null +++ b/test/Dapr.Actors.Analyzers.Test/ActorAnalyzerTests.cs @@ -0,0 +1,313 @@ +using Microsoft.CodeAnalysis; + +namespace Dapr.Actors.Analyzers.Test; + +public class ActorAnalyzerTests +{ + public class ActorNotRegistered + { + [Fact] + public async Task ReportDiagnostic_DAPR0001() + { + var testCode = @" + using Dapr.Actors.Runtime; + + class TestActor : Actor + { + public TestActor(ActorHost host) : base(host) + { + } + } + "; + + var expected = VerifyAnalyzer.Diagnostic("DAPR0001", DiagnosticSeverity.Warning) + .WithSpan(4, 23, 4, 32).WithMessage("The actor class 'TestActor' is not registered"); + + await VerifyAnalyzer.VerifyAnalyzerAsync(testCode, expected); + } + + [Fact] + public async Task ReportDiagnostic_DAPR0001_FullyQualified() + { + var testCode = @" + class TestActor : Dapr.Actors.Runtime.Actor + { + public TestActor(Dapr.Actors.Runtime.ActorHost host) : base(host) + { + } + } + "; + + var expected = VerifyAnalyzer.Diagnostic("DAPR0001", DiagnosticSeverity.Warning) + .WithSpan(2, 23, 2, 32).WithMessage("The actor class 'TestActor' is not registered"); + + await VerifyAnalyzer.VerifyAnalyzerAsync(testCode, expected); + } + + [Fact] + public async Task ReportDiagnostic_DAPR0001_NamespaceAlias() + { + var testCode = @" + using alias = Dapr.Actors.Runtime; + + class TestActor : alias.Actor + { + public TestActor(alias.ActorHost host) : base(host) + { + } + } + "; + + var expected = VerifyAnalyzer.Diagnostic("DAPR0001", DiagnosticSeverity.Warning) + .WithSpan(4, 23, 4, 32).WithMessage("The actor class 'TestActor' is not registered"); + + await VerifyAnalyzer.VerifyAnalyzerAsync(testCode, expected); + } + } + + public class ActorRegistered + { + [Fact] + public async Task ReportNoDiagnostic() + { + var testCode = @" + using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; + using Microsoft.Extensions.DependencyInjection; + + public static class Program + { + public static void Main() + { + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddActors(options => + { + options.Actors.RegisterActor(); + options.UseJsonSerialization = true; + }); + + var app = builder.Build(); + + app.MapActorsHandlers(); + } + } + + class TestActor : Actor + { + public TestActor(ActorHost host) : base(host) + { + } + } + "; + + await VerifyAnalyzer.VerifyAnalyzerAsync(testCode); + } + + [Fact] + public async Task ReportNoDiagnostic_WithNamespace() + { + var testCode = @" + using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; + using Microsoft.Extensions.DependencyInjection; + + public static class Program + { + public static void Main() + { + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddActors(options => + { + options.Actors.RegisterActor(); + options.UseJsonSerialization = true; + }); + + var app = builder.Build(); + + app.MapActorsHandlers(); + } + } + + namespace TestNamespace + { + class TestActor : Actor + { + public TestActor(ActorHost host) : base(host) + { + } + } + } + "; + + await VerifyAnalyzer.VerifyAnalyzerAsync(testCode); + } + + [Fact] + public async Task ReportNoDiagnostic_WithNamespaceAlias () + { + var testCode = @" + using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; + using Microsoft.Extensions.DependencyInjection; + using alias = TestNamespace; + + public static class Program + { + public static void Main() + { + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddActors(options => + { + options.Actors.RegisterActor(); + options.UseJsonSerialization = true; + }); + + var app = builder.Build(); + + app.MapActorsHandlers(); + } + } + + namespace TestNamespace + { + class TestActor : Actor + { + public TestActor(ActorHost host) : base(host) + { + } + } + } + "; + + await VerifyAnalyzer.VerifyAnalyzerAsync(testCode); + } + } + + public class JsonSerialization + { + [Fact] + public async Task ReportDiagnostic() + { + var testCode = @" + using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; + using Microsoft.Extensions.DependencyInjection; + + public static class Program + { + public static void Main() + { + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddActors(options => + { + }); + + var app = builder.Build(); + + app.MapActorsHandlers(); + } + } + "; + + var expected = VerifyAnalyzer.Diagnostic("DAPR0002", DiagnosticSeverity.Warning) + .WithSpan(12, 25, 14, 27).WithMessage("Add options.UseJsonSerialization to support interoperability with non-.NET actors"); + + await VerifyAnalyzer.VerifyAnalyzerAsync(testCode, expected); + } + + [Fact] + public async Task ReportNoDiagnostic() + { + var testCode = @" + using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; + using Microsoft.Extensions.DependencyInjection; + + public static class Program + { + public static void Main() + { + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddActors(options => + { + options.UseJsonSerialization = true; + }); + + var app = builder.Build(); + + app.MapActorsHandlers(); + } + } + "; + + await VerifyAnalyzer.VerifyAnalyzerAsync(testCode); + } + } + + public class MapActorsHandlers + { + [Fact] + public async Task ReportDiagnostic() + { + var testCode = @" + using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; + using Microsoft.Extensions.DependencyInjection; + + public static class Program + { + public static void Main() + { + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddActors(options => + { + options.UseJsonSerialization = true; + }); + + var app = builder.Build(); + } + } + "; + + var expected = VerifyAnalyzer.Diagnostic("DAPR0003", DiagnosticSeverity.Warning) + .WithSpan(12, 25, 15, 27).WithMessage("Call app.MapActorsHandlers to map endpoints for Dapr actors"); + + await VerifyAnalyzer.VerifyAnalyzerAsync(testCode, expected); + } + + [Fact] + public async Task ReportNoDiagnostic() + { + var testCode = @" + using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; + using Microsoft.Extensions.DependencyInjection; + + public static class Program + { + public static void Main() + { + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddActors(options => + { + options.UseJsonSerialization = true; + }); + + var app = builder.Build(); + + app.MapActorsHandlers(); + } + } + "; + + await VerifyAnalyzer.VerifyAnalyzerAsync(testCode); + } + } +} diff --git a/test/Dapr.Actors.Analyzers.Test/ActorJsonSerializationCodeFixProviderTests.cs b/test/Dapr.Actors.Analyzers.Test/ActorJsonSerializationCodeFixProviderTests.cs new file mode 100644 index 00000000..7d3fe6ca --- /dev/null +++ b/test/Dapr.Actors.Analyzers.Test/ActorJsonSerializationCodeFixProviderTests.cs @@ -0,0 +1,55 @@ +namespace Dapr.Actors.Analyzers.Test; + +public class ActorJsonSerializationCodeFixProviderTests +{ + [Fact] + public async Task UseJsonSerialization() + { + var code = @" + //using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; + using Microsoft.Extensions.DependencyInjection; + + public static class Program + { + public static void Main() + { + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddActors(options => + { + }); + + var app = builder.Build(); + + app.MapActorsHandlers(); + } + } + "; + + var expectedChangedCode = @" + //using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; + using Microsoft.Extensions.DependencyInjection; + + public static class Program + { + public static void Main() + { + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddActors(options => + { + options.UseJsonSerialization = true; + }); + + var app = builder.Build(); + + app.MapActorsHandlers(); + } + } + "; + + await VerifyCodeFix.RunTest(code, expectedChangedCode); + } +} diff --git a/test/Dapr.Actors.Analyzers.Test/ActorRegistrationCodeFixProviderTests.cs b/test/Dapr.Actors.Analyzers.Test/ActorRegistrationCodeFixProviderTests.cs new file mode 100644 index 00000000..d148ed90 --- /dev/null +++ b/test/Dapr.Actors.Analyzers.Test/ActorRegistrationCodeFixProviderTests.cs @@ -0,0 +1,175 @@ +namespace Dapr.Actors.Analyzers.Test; + +public class ActorRegistrationCodeFixProviderTests +{ + [Fact] + public async Task RegisterActor() + { + var code = @" + using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; + using Microsoft.Extensions.DependencyInjection; + + public static class Program + { + public static void Main() + { + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddActors(options => + { + options.UseJsonSerialization = true; + }); + + var app = builder.Build(); + + app.MapActorsHandlers(); + } + } + + class TestActor : Actor + { + public TestActor(ActorHost host) : base(host) + { + } + } + "; + + var expectedChangedCode = @" + using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; + using Microsoft.Extensions.DependencyInjection; + + public static class Program + { + public static void Main() + { + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddActors(options => + { + options.UseJsonSerialization = true; + options.Actors.RegisterActor(); + }); + + var app = builder.Build(); + + app.MapActorsHandlers(); + } + } + + class TestActor : Actor + { + public TestActor(ActorHost host) : base(host) + { + } + } + "; + + await VerifyCodeFix.RunTest(code, expectedChangedCode); + } + + [Fact] + public async Task RegisterActor_WhenAddActorsIsNotFound() + { + var code = @" + using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; + + public static class Program + { + public static void Main() + { + var builder = WebApplication.CreateBuilder(); + + var app = builder.Build(); + } + } + + class TestActor : Actor + { + public TestActor(ActorHost host) : base(host) + { + } + } + "; + + var expectedChangedCode = @" + using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; + + public static class Program + { + public static void Main() + { + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddActors(options => + { + options.Actors.RegisterActor(); + }); + + var app = builder.Build(); + } + } + + class TestActor : Actor + { + public TestActor(ActorHost host) : base(host) + { + } + } + "; + + await VerifyCodeFix.RunTest(code, expectedChangedCode); + } + + [Fact] + public async Task RegisterActor_WhenAddActorsIsNotFound_TopLevelStatements() + { + var code = @" + using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; + + var builder = WebApplication.CreateBuilder(); + + var app = builder.Build(); + + namespace TestNamespace + { + class TestActor : Actor + { + public TestActor(ActorHost host) : base(host) + { + } + } + } + "; + + var expectedChangedCode = @" + using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; + + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddActors(options => + { + options.Actors.RegisterActor(); + }); + + var app = builder.Build(); + + namespace TestNamespace + { + class TestActor : Actor + { + public TestActor(ActorHost host) : base(host) + { + } + } + } + "; + + await VerifyCodeFix.RunTest(code, expectedChangedCode); + } +} diff --git a/test/Dapr.Actors.Analyzers.Test/Dapr.Actors.Analyzers.Test.csproj b/test/Dapr.Actors.Analyzers.Test/Dapr.Actors.Analyzers.Test.csproj new file mode 100644 index 00000000..934fcbfd --- /dev/null +++ b/test/Dapr.Actors.Analyzers.Test/Dapr.Actors.Analyzers.Test.csproj @@ -0,0 +1,35 @@ + + + + enable + enable + false + true + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + diff --git a/test/Dapr.Actors.Analyzers.Test/MapActorsHandlersCodeFixProviderTests.cs b/test/Dapr.Actors.Analyzers.Test/MapActorsHandlersCodeFixProviderTests.cs new file mode 100644 index 00000000..8d0f5835 --- /dev/null +++ b/test/Dapr.Actors.Analyzers.Test/MapActorsHandlersCodeFixProviderTests.cs @@ -0,0 +1,124 @@ +namespace Dapr.Actors.Analyzers.Test; + +public class MapActorsHandlersCodeFixProviderTests +{ + [Fact] + public async Task RegisterActor() + { + var code = @" + using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; + using Microsoft.Extensions.DependencyInjection; + + public static class Program + { + public static void Main() + { + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddActors(options => + { + options.Actors.RegisterActor(); + options.UseJsonSerialization = true; + }); + + var app = builder.Build(); + } + } + + class TestActor : Actor + { + public TestActor(ActorHost host) : base(host) + { + } + } + "; + + var expectedChangedCode = @" + using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; + using Microsoft.Extensions.DependencyInjection; + + public static class Program + { + public static void Main() + { + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddActors(options => + { + options.Actors.RegisterActor(); + options.UseJsonSerialization = true; + }); + + var app = builder.Build(); + + app.MapActorsHandlers(); + } + } + + class TestActor : Actor + { + public TestActor(ActorHost host) : base(host) + { + } + } + "; + + await VerifyCodeFix.RunTest(code, expectedChangedCode); + } + + [Fact] + public async Task RegisterActor_TopLevelStatements() + { + var code = @" + using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; + using Microsoft.Extensions.DependencyInjection; + + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddActors(options => + { + options.Actors.RegisterActor(); + options.UseJsonSerialization = true; + }); + + var app = builder.Build(); + + class TestActor : Actor + { + public TestActor(ActorHost host) : base(host) + { + } + } + "; + + var expectedChangedCode = @" + using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; + using Microsoft.Extensions.DependencyInjection; + + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddActors(options => + { + options.Actors.RegisterActor(); + options.UseJsonSerialization = true; + }); + + var app = builder.Build(); + + app.MapActorsHandlers(); + + class TestActor : Actor + { + public TestActor(ActorHost host) : base(host) + { + } + } + "; + + await VerifyCodeFix.RunTest(code, expectedChangedCode); + } +} diff --git a/test/Dapr.Actors.Analyzers.Test/Utilities.cs b/test/Dapr.Actors.Analyzers.Test/Utilities.cs new file mode 100644 index 00000000..4b6beed0 --- /dev/null +++ b/test/Dapr.Actors.Analyzers.Test/Utilities.cs @@ -0,0 +1,77 @@ +using System.Collections.Immutable; +using System.Reflection; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.AspNetCore.Builder; +using Microsoft.CodeAnalysis.Testing; + +namespace Dapr.Actors.Analyzers.Test; + +internal static class Utilities +{ + public static async Task<(ImmutableArray diagnostics, Document document, Workspace workspace)> GetDiagnosticsAdvanced(string code) + { + var workspace = new AdhocWorkspace(); + +#if NET6_0 + var referenceAssemblies = ReferenceAssemblies.Net.Net60; +#elif NET7_0 + var referenceAssemblies = ReferenceAssemblies.Net.Net70; +#elif NET8_0 + var referenceAssemblies = ReferenceAssemblies.Net.Net80; +#elif NET9_0 + var referenceAssemblies = ReferenceAssemblies.Net.Net90; +#endif + + // Create a new project with necessary references + var project = workspace.AddProject("TestProject", LanguageNames.CSharp) + .WithCompilationOptions(new CSharpCompilationOptions(OutputKind.ConsoleApplication) + .WithSpecificDiagnosticOptions(new Dictionary + { + { "CS1701", ReportDiagnostic.Suppress } + })) + .AddMetadataReferences(await referenceAssemblies.ResolveAsync(LanguageNames.CSharp, default)) + .AddMetadataReference(MetadataReference.CreateFromFile(typeof(object).Assembly.Location)) + .AddMetadataReferences(GetAllReferencesNeededForType(typeof(WebApplication))) + .AddMetadataReferences(GetAllReferencesNeededForType(typeof(IHost))) + .AddMetadataReferences(GetAllReferencesNeededForType(typeof(ActorsEndpointRouteBuilderExtensions))) + .AddMetadataReferences(GetAllReferencesNeededForType(typeof(ActorsServiceCollectionExtensions))); + + // Add the document to the project + var document = project.AddDocument("TestDocument.cs", code); + + // Get the syntax tree and create a compilation + var syntaxTree = await document.GetSyntaxTreeAsync() ?? throw new InvalidOperationException("Syntax tree is null"); + var compilation = CSharpCompilation.Create("TestCompilation") + .AddSyntaxTrees(syntaxTree) + .AddReferences(project.MetadataReferences) + .WithOptions(project.CompilationOptions!); + + var compilationWithAnalyzer = compilation.WithAnalyzers( + ImmutableArray.Create( + new ActorAnalyzer())); + + // Get diagnostics from the compilation + var diagnostics = await compilationWithAnalyzer.GetAllDiagnosticsAsync(); + return (diagnostics, document, workspace); + } + + public static MetadataReference[] GetAllReferencesNeededForType(Type type) + { + var files = GetAllAssemblyFilesNeededForType(type); + + return files.Select(x => MetadataReference.CreateFromFile(x)).Cast().ToArray(); + } + + private static ImmutableArray GetAllAssemblyFilesNeededForType(Type type) + { + return type.Assembly.GetReferencedAssemblies() + .Select(x => Assembly.Load(x.FullName)) + .Append(type.Assembly) + .Select(x => x.Location) + .ToImmutableArray(); + } +} diff --git a/test/Dapr.Actors.Analyzers.Test/VerifyAnalyzer.cs b/test/Dapr.Actors.Analyzers.Test/VerifyAnalyzer.cs new file mode 100644 index 00000000..d114caed --- /dev/null +++ b/test/Dapr.Actors.Analyzers.Test/VerifyAnalyzer.cs @@ -0,0 +1,68 @@ +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.CodeAnalysis; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Hosting; + +namespace Dapr.Actors.Analyzers.Test; + +internal static class VerifyAnalyzer +{ + public static DiagnosticResult Diagnostic(string diagnosticId, DiagnosticSeverity diagnosticSeverity) + { + return new DiagnosticResult(diagnosticId, diagnosticSeverity); + } + + public static async Task VerifyAnalyzerAsync(string source, params DiagnosticResult[] expected) + { + await VerifyAnalyzerAsync(source, null, expected); + } + + public static async Task VerifyAnalyzerAsync(string source, string? program, params DiagnosticResult[] expected) + { + var test = new Test { TestCode = source }; + +#if NET6_0 + test.ReferenceAssemblies = ReferenceAssemblies.Net.Net60; +#elif NET7_0 + test.ReferenceAssemblies = ReferenceAssemblies.Net.Net70; +#elif NET8_0 + test.ReferenceAssemblies = ReferenceAssemblies.Net.Net80; +#elif NET9_0 + test.ReferenceAssemblies = ReferenceAssemblies.Net.Net90; +#endif + + if (program != null) + { + test.TestState.Sources.Add(("Program.cs", program)); + } + + var metadataReferences = Utilities.GetAllReferencesNeededForType(typeof(ActorAnalyzer)).ToList(); + metadataReferences.AddRange(Utilities.GetAllReferencesNeededForType(typeof(ActorsServiceCollectionExtensions))); + metadataReferences.Add(MetadataReference.CreateFromFile(typeof(object).Assembly.Location)); + metadataReferences.Add(MetadataReference.CreateFromFile(typeof(WebApplication).Assembly.Location)); + metadataReferences.Add(MetadataReference.CreateFromFile(typeof(IHost).Assembly.Location)); + + foreach (var reference in metadataReferences) + { + test.TestState.AdditionalReferences.Add(reference); + } + + test.ExpectedDiagnostics.AddRange(expected); + await test.RunAsync(CancellationToken.None); + } + + private class Test : CSharpAnalyzerTest + { + public Test() + { + SolutionTransforms.Add((solution, projectId) => + { + var compilationOptions = solution.GetProject(projectId)!.CompilationOptions!; + solution = solution.WithProjectCompilationOptions(projectId, compilationOptions); + return solution; + }); + } + } +} diff --git a/test/Dapr.Actors.Analyzers.Test/VerifyCodeFix.cs b/test/Dapr.Actors.Analyzers.Test/VerifyCodeFix.cs new file mode 100644 index 00000000..f7d0d7eb --- /dev/null +++ b/test/Dapr.Actors.Analyzers.Test/VerifyCodeFix.cs @@ -0,0 +1,56 @@ +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; + +namespace Dapr.Actors.Analyzers.Test; + +internal class VerifyCodeFix +{ + public static async Task RunTest(string code, string expectedChangedCode) where T : CodeFixProvider, new() + { + var (diagnostics, document, workspace) = await Utilities.GetDiagnosticsAdvanced(code); + + Assert.Single(diagnostics); + + var diagnostic = diagnostics[0]; + + var codeFixProvider = new T(); + + CodeAction? registeredCodeAction = null; + + var context = new CodeFixContext(document, diagnostic, (codeAction, _) => + { + if (registeredCodeAction != null) + throw new Exception("Code action was registered more than once"); + + registeredCodeAction = codeAction; + + }, CancellationToken.None); + + await codeFixProvider.RegisterCodeFixesAsync(context); + + if (registeredCodeAction == null) + throw new Exception("Code action was not registered"); + + var operations = await registeredCodeAction.GetOperationsAsync(CancellationToken.None); + + foreach (var operation in operations) + { + operation.Apply(workspace, CancellationToken.None); + } + + var updatedDocument = workspace.CurrentSolution.GetDocument(document.Id) ?? throw new Exception("Updated document is null"); + var newCode = (await updatedDocument.GetTextAsync()).ToString(); + + // Normalize whitespace + string NormalizeWhitespace(string input) + { + var separator = new[] { ' ', '\r', '\n' }; + return string.Join(" ", input.Split(separator, StringSplitOptions.RemoveEmptyEntries)); + } + + var normalizedExpectedCode = NormalizeWhitespace(expectedChangedCode); + var normalizedNewCode = NormalizeWhitespace(newCode); + + Assert.Equal(normalizedExpectedCode, normalizedNewCode); + } +} diff --git a/test/Dapr.Actors.Analyzers.Tests/ActorRegistrationAnalyzerTests.cs b/test/Dapr.Actors.Analyzers.Tests/ActorRegistrationAnalyzerTests.cs new file mode 100644 index 00000000..63a775b1 --- /dev/null +++ b/test/Dapr.Actors.Analyzers.Tests/ActorRegistrationAnalyzerTests.cs @@ -0,0 +1,180 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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.Actors.Analyzers.Tests; + +public class ActorRegistrationAnalyzerTests +{ + [Fact] + public async Task NotRegistered_ReportDiagnostic_DAPR4002() + { + const string testCode = """ + using Dapr.Actors.Runtime; + class TestActor : Actor + { + public TestActor(ActorHost host) : base(host) + { + } + } + """; + + var expected = VerifyAnalyzer.Diagnostic(ActorRegistrationAnalyzer.DiagnosticDescriptorActorRegistration) + .WithSpan(2, 7, 2, 16).WithMessage("The actor type 'TestActor' is not registered with dependency injection"); + + await VerifyAnalyzer.VerifyAnalyzerAsync(testCode, expected); + } + + [Fact] + public async Task NotRegistered_ReportDiagnostic_DAPR4002_FullyQualified() + { + const string testCode = """ + class TestActor : Dapr.Actors.Runtime.Actor + { + public TestActor(Dapr.Actors.Runtime.ActorHost host) : base(host) + { + } + } + """; + + var expected = VerifyAnalyzer.Diagnostic(ActorRegistrationAnalyzer.DiagnosticDescriptorActorRegistration) + .WithSpan(1, 7, 1, 16).WithMessage("The actor type 'TestActor' is not registered with dependency injection"); + + await VerifyAnalyzer.VerifyAnalyzerAsync(testCode, expected); + } + + + [Fact] + public async Task NotRegistered_ReportDiagnostic_DAPR4002_NamespaceAlias() + { + const string testCode = """ + using alias = Dapr.Actors.Runtime; + class TestActor : alias.Actor + { + public TestActor(alias.ActorHost host) : base(host) + { + } + } + """; + + var expected = VerifyAnalyzer.Diagnostic(ActorRegistrationAnalyzer.DiagnosticDescriptorActorRegistration) + .WithSpan(2, 15, 2, 24).WithMessage("The actor type 'TestActor' is not registered with dependency injection"); + + await VerifyAnalyzer.VerifyAnalyzerAsync(testCode, expected); + } + + [Fact] + public async Task Registered_ReportNoDiagnostic() + { + const string testCode = """ + using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; + using Microsoft.Extensions.DependencyInjection; + public static class Program + { + public static void Main() + { + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddActors(options => + { + options.Actors.RegisterActor(); + options.UseJsonSerialization = true; + }); + var app = builder.Build(); + app.MapActorsHandlers(); + } + } + class TestActor : Actor + { + public TestActor(ActorHost host) : base(host) + { + } + } + """; + + await VerifyAnalyzer.VerifyAnalyzerAsync(testCode); + } + + [Fact] + public async Task Registered_ReportNoDiagnostic_WithNamespace() + { + const string testCode = """ + using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; + using Microsoft.Extensions.DependencyInjection; + public static class Program + { + public static void Main() + { + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddActors(options => + { + options.Actors.RegisterActor(); + options.UseJsonSerialization = true; + }); + var app = builder.Build(); + app.MapActorsHandlers(); + } + } + namespace TestNamespace + { + class TestActor : Actor + { + public TestActor(ActorHost host) : base(host) + { + } + } + } + """; + + await VerifyAnalyzer.VerifyAnalyzerAsync(testCode); + } + + [Fact] + public async Task Registered_ReportNoDiagnostic_WithNamespaceAlias() + { + const string testCode = """ + using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; + using Microsoft.Extensions.DependencyInjection; + using alias = TestNamespace; + public static class Program + { + public static void Main() + { + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddActors(options => + { + options.Actors.RegisterActor(); + options.UseJsonSerialization = true; + }); + var app = builder.Build(); + app.MapActorsHandlers(); + } + } + namespace TestNamespace + { + class TestActor : Actor + { + public TestActor(ActorHost host) : base(host) + { + } + } + } + """; + + await VerifyAnalyzer.VerifyAnalyzerAsync(testCode); + } +} diff --git a/test/Dapr.Actors.Analyzers.Tests/ActorRegistrationCodeFixProviderTests.cs b/test/Dapr.Actors.Analyzers.Tests/ActorRegistrationCodeFixProviderTests.cs new file mode 100644 index 00000000..9ae4afc6 --- /dev/null +++ b/test/Dapr.Actors.Analyzers.Tests/ActorRegistrationCodeFixProviderTests.cs @@ -0,0 +1,186 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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.Actors.Analyzers.Tests; + +public class ActorRegistrationCodeFixProviderTests +{ + [Fact] + public async Task RegisterActor() + { + const string code = """ + + using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; + using Microsoft.Extensions.DependencyInjection; + + public static class Program + { + public static void Main() + { + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddActors(options => + { + options.UseJsonSerialization = true; + }); + var app = builder.Build(); + app.MapActorsHandlers(); + } + } + class TestActor : Actor + { + public TestActor(ActorHost host) : base(host) + { + } + } + + """; + + const string expectedChangedCode = """ + + using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; + using Microsoft.Extensions.DependencyInjection; + + public static class Program + { + public static void Main() + { + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddActors(options => + { + options.UseJsonSerialization = true; + options.Actors.RegisterActor(); + }); + var app = builder.Build(); + app.MapActorsHandlers(); + } + } + class TestActor : Actor + { + public TestActor(ActorHost host) : base(host) + { + } + } + + """; + + await VerifyCodeFix.RunTest(code, expectedChangedCode); + } + + [Fact] + public async Task RegisterActor_WhenAddActorsIsNotFound() + { + const string code = """ + + using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; + + public static class Program + { + public static void Main() + { + var builder = WebApplication.CreateBuilder(); + var app = builder.Build(); + } + } + class TestActor : Actor + { + public TestActor(ActorHost host) : base(host) + { + } + } + + """; + + const string expectedChangedCode = """ + + using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; + + public static class Program + { + public static void Main() + { + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddActors(options => + { + options.Actors.RegisterActor(); + }); + var app = builder.Build(); + } + } + class TestActor : Actor + { + public TestActor(ActorHost host) : base(host) + { + } + } + + """; + + await VerifyCodeFix.RunTest(code, expectedChangedCode); + } + + [Fact] + public async Task RegisterActor_WhenAddActorsIsNotFound_TopLevelStatements() + { + const string code = """ + + using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; + var builder = WebApplication.CreateBuilder(); + + var app = builder.Build(); + namespace TestNamespace + { + class TestActor : Actor + { + public TestActor(ActorHost host) : base(host) + { + } + } + } + + """; + + const string expectedChangedCode = """ + + using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; + + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddActors(options => + { + options.Actors.RegisterActor(); + }); + var app = builder.Build(); + namespace TestNamespace + { + class TestActor : Actor + { + public TestActor(ActorHost host) : base(host) + { + } + } + } + + """; + + await VerifyCodeFix.RunTest(code, expectedChangedCode); + } +} diff --git a/test/Dapr.Actors.Analyzers.Tests/Dapr.Actors.Analyzers.Tests.csproj b/test/Dapr.Actors.Analyzers.Tests/Dapr.Actors.Analyzers.Tests.csproj index d801c2df..482379a8 100644 --- a/test/Dapr.Actors.Analyzers.Tests/Dapr.Actors.Analyzers.Tests.csproj +++ b/test/Dapr.Actors.Analyzers.Tests/Dapr.Actors.Analyzers.Tests.csproj @@ -9,8 +9,11 @@ + + + @@ -29,7 +32,7 @@ - + diff --git a/test/Dapr.Actors.Analyzers.Tests/MappedActorHandlersAnalyzerTests.cs b/test/Dapr.Actors.Analyzers.Tests/MappedActorHandlersAnalyzerTests.cs new file mode 100644 index 00000000..ed649f98 --- /dev/null +++ b/test/Dapr.Actors.Analyzers.Tests/MappedActorHandlersAnalyzerTests.cs @@ -0,0 +1,71 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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.Actors.Analyzers.Tests; + +public class MappedActorHandlersAnalyzerTests +{ + [Fact] + public async Task ReportDiagnostic() + { + const string testCode = """ + using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; + using Microsoft.Extensions.DependencyInjection; + public static class Program + { + public static void Main() + { + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddActors(options => + { + options.UseJsonSerialization = true; + }); + var app = builder.Build(); + } + } + """; + + var expected = VerifyAnalyzer.Diagnostic(MappedActorHandlersAnalyzer.DiagnosticDescriptorMapActorsHandlers) + .WithSpan(10, 25, 13, 27).WithMessage("Call app.MapActorsHandlers to map endpoints for Dapr actors"); + + await VerifyAnalyzer.VerifyAnalyzerAsync(testCode, expected); + } + + [Fact] + public async Task ReportNoDiagnostic() + { + const string testCode = """ + using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; + using Microsoft.Extensions.DependencyInjection; + public static class Program + { + public static void Main() + { + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddActors(options => + { + options.UseJsonSerialization = true; + }); + var app = builder.Build(); + app.MapActorsHandlers(); + } + } + """; + + await VerifyAnalyzer.VerifyAnalyzerAsync(testCode); + } +} diff --git a/test/Dapr.Actors.Analyzers.Tests/MappedActorHandlersCodeFixProviderTests.cs b/test/Dapr.Actors.Analyzers.Tests/MappedActorHandlersCodeFixProviderTests.cs new file mode 100644 index 00000000..7c66a265 --- /dev/null +++ b/test/Dapr.Actors.Analyzers.Tests/MappedActorHandlersCodeFixProviderTests.cs @@ -0,0 +1,131 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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.Actors.Analyzers.Tests; + +public class MappedActorHandlersCodeFixProviderTests +{ + [Fact] + public async Task RegisterActor() + { + const string code = """ + using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; + using Microsoft.Extensions.DependencyInjection; + + public static class Program + { + public static void Main() + { + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddActors(options => + { + options.Actors.RegisterActor(); + options.UseJsonSerialization = true; + }); + var app = builder.Build(); + } + } + class TestActor : Actor + { + public TestActor(ActorHost host) : base(host) + { + } + } + """; + + const string expectedChangedCode = """ + using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; + using Microsoft.Extensions.DependencyInjection; + + public static class Program + { + public static void Main() + { + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddActors(options => + { + options.Actors.RegisterActor(); + options.UseJsonSerialization = true; + }); + var app = builder.Build(); + app.MapActorsHandlers(); + } + } + class TestActor : Actor + { + public TestActor(ActorHost host) : base(host) + { + } + } + """; + + await VerifyCodeFix.RunTest(code, expectedChangedCode); + } + + [Fact] + public async Task RegisterActor_TopLevelStatements() + { + const string code = """ + + using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; + using Microsoft.Extensions.DependencyInjection; + + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddActors(options => + { + options.Actors.RegisterActor(); + options.UseJsonSerialization = true; + }); + var app = builder.Build(); + class TestActor : Actor + { + public TestActor(ActorHost host) : base(host) + { + } + } + + """; + + const string expectedChangedCode = """ + + using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; + using Microsoft.Extensions.DependencyInjection; + + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddActors(options => + { + options.Actors.RegisterActor(); + options.UseJsonSerialization = true; + }); + var app = builder.Build(); + app.MapActorsHandlers(); + class TestActor : Actor + { + public TestActor(ActorHost host) : base(host) + { + } + } + + """; + + await VerifyCodeFix.RunTest(code, expectedChangedCode); + } +} diff --git a/test/Dapr.Actors.Analyzers.Tests/PreferActorJsonSerializationAnalyzerTests.cs b/test/Dapr.Actors.Analyzers.Tests/PreferActorJsonSerializationAnalyzerTests.cs new file mode 100644 index 00000000..2fd8f5e7 --- /dev/null +++ b/test/Dapr.Actors.Analyzers.Tests/PreferActorJsonSerializationAnalyzerTests.cs @@ -0,0 +1,73 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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.Actors.Analyzers.Tests; + +public class PreferActorJsonSerializationAnalyzerTests +{ + [Fact] + public async Task ReportDiagnostic() + { + const string testCode = """ + using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; + using Microsoft.Extensions.DependencyInjection; + public static class Program + { + public static void Main() + { + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddActors(options => + { + }); + var app = builder.Build(); + app.MapActorsHandlers(); + } + } + """; + + var expected = VerifyAnalyzer + .Diagnostic(PreferActorJsonSerializationAnalyzer.DiagnosticDescriptorJsonSerialization) + .WithSpan(10, 25, 12, 27) + .WithMessage("Set options.UseJsonSerialization to true to support interoperability with non-.NET actors"); + + await VerifyAnalyzer.VerifyAnalyzerAsync(testCode, expected); + } + + [Fact] + public async Task ReportNoDiagnostic() + { + const string testCode = """ + using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; + using Microsoft.Extensions.DependencyInjection; + public static class Program + { + public static void Main() + { + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddActors(options => + { + options.UseJsonSerialization = true; + }); + var app = builder.Build(); + app.MapActorsHandlers(); + } + } + """; + + await VerifyAnalyzer.VerifyAnalyzerAsync(testCode); + } +} diff --git a/test/Dapr.Actors.Analyzers.Tests/PreferActorJsonSerializationCodeFixProviderTests.cs b/test/Dapr.Actors.Analyzers.Tests/PreferActorJsonSerializationCodeFixProviderTests.cs new file mode 100644 index 00000000..0a7288c2 --- /dev/null +++ b/test/Dapr.Actors.Analyzers.Tests/PreferActorJsonSerializationCodeFixProviderTests.cs @@ -0,0 +1,62 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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.Actors.Analyzers.Tests; + +public class PreferActorJsonSerializationCodeFixProviderTests +{ + [Fact] + public async Task UseJsonSerialization() + { + const string code = """ + //using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; + using Microsoft.Extensions.DependencyInjection; + + public static class Program + { + public static void Main() + { + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddActors(options => + { + }); + var app = builder.Build(); + app.MapActorsHandlers(); + } + } + """; + + const string expectedChangedCode = """ + //using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; + using Microsoft.Extensions.DependencyInjection; + public static class Program + { + public static void Main() + { + var builder = WebApplication.CreateBuilder(); + builder.Services.AddActors(options => + { + options.UseJsonSerialization = true; + }); + var app = builder.Build(); + app.MapActorsHandlers(); + } + } + """; + + await VerifyCodeFix.RunTest(code, expectedChangedCode); + } +} diff --git a/test/Dapr.Actors.Analyzers.Tests/TestUtilities.cs b/test/Dapr.Actors.Analyzers.Tests/TestUtilities.cs new file mode 100644 index 00000000..acfec6ba --- /dev/null +++ b/test/Dapr.Actors.Analyzers.Tests/TestUtilities.cs @@ -0,0 +1,88 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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.Collections.Immutable; +using System.Reflection; +using Microsoft.AspNetCore.Builder; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Dapr.Actors.Analyzers.Tests; + +internal static class TestUtilities +{ + public static async Task<(ImmutableArray diagnostics, Document document, Workspace workspace)> GetDiagnosticsAdvanced(string code) + { + var workspace = new AdhocWorkspace(); + +#if NET8_0 + var referenceAssemblies = ReferenceAssemblies.Net.Net80; +#elif NET9_0 + var referenceAssemblies = ReferenceAssemblies.Net.Net90; +#endif + + // Create a new project with necessary references + var project = workspace.AddProject("TestProject", LanguageNames.CSharp) + .WithCompilationOptions(new CSharpCompilationOptions(OutputKind.ConsoleApplication) + .WithSpecificDiagnosticOptions(new Dictionary + { + { "CS1701", ReportDiagnostic.Suppress } + })) + .AddMetadataReferences(await referenceAssemblies.ResolveAsync(LanguageNames.CSharp, default)) + .AddMetadataReference(MetadataReference.CreateFromFile(typeof(object).Assembly.Location)) + .AddMetadataReferences(GetAllReferencesNeededForType(typeof(WebApplication))) + .AddMetadataReferences(GetAllReferencesNeededForType(typeof(IHost))) + .AddMetadataReferences(GetAllReferencesNeededForType(typeof(ActorsEndpointRouteBuilderExtensions))) + .AddMetadataReferences(GetAllReferencesNeededForType(typeof(ActorsServiceCollectionExtensions))); + + // Add the document to the project + var document = project.AddDocument("TestDocument.cs", code); + + // Get the syntax tree and create a compilation + var syntaxTree = await document.GetSyntaxTreeAsync() ?? throw new InvalidOperationException("Syntax tree is null"); + var compilation = CSharpCompilation.Create("TestCompilation") + .AddSyntaxTrees(syntaxTree) + .AddReferences(project.MetadataReferences) + .WithOptions(project.CompilationOptions!); + + var compilationWithAnalyzer = compilation.WithAnalyzers( + [ + new ActorRegistrationAnalyzer(), + new MappedActorHandlersAnalyzer(), + new PreferActorJsonSerializationAnalyzer(), + new TimerCallbackMethodPresentAnalyzer() + ]); + + // Get diagnostics from the compilation + var diagnostics = await compilationWithAnalyzer.GetAllDiagnosticsAsync(); + return (diagnostics, document, workspace); + } + + public static MetadataReference[] GetAllReferencesNeededForType(Type type) + { + var files = GetAllAssemblyFilesNeededForType(type); + + return files.Select(x => MetadataReference.CreateFromFile(x)).Cast().ToArray(); + } + + private static ImmutableArray GetAllAssemblyFilesNeededForType(Type type) => type.Assembly + .GetReferencedAssemblies() + .Select(x => Assembly.Load(x.FullName)) + .Append(type.Assembly) + .Select(x => x.Location) + .ToImmutableArray(); +} diff --git a/test/Dapr.Actors.Analyzers.Tests/TimerMethodPresentAnalyzerTests.cs b/test/Dapr.Actors.Analyzers.Tests/TimerCallbackMethodPresentAnalyzerTests.cs similarity index 91% rename from test/Dapr.Actors.Analyzers.Tests/TimerMethodPresentAnalyzerTests.cs rename to test/Dapr.Actors.Analyzers.Tests/TimerCallbackMethodPresentAnalyzerTests.cs index bdcf0e46..e3fd47f3 100644 --- a/test/Dapr.Actors.Analyzers.Tests/TimerMethodPresentAnalyzerTests.cs +++ b/test/Dapr.Actors.Analyzers.Tests/TimerCallbackMethodPresentAnalyzerTests.cs @@ -11,14 +11,13 @@ // limitations under the License. // ------------------------------------------------------------------------ -using System.Threading.Tasks; using Microsoft.CodeAnalysis.CSharp.Testing; using Microsoft.CodeAnalysis.Testing; #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member namespace Dapr.Actors.Analyzers.Tests; -public class TimerMethodPresentAnalyzerTests +public class TimerCallbackMethodPresentAnalyzerTests { #if NET8_0 private static readonly ReferenceAssemblies assemblies = ReferenceAssemblies.Net.Net80; @@ -29,7 +28,7 @@ public class TimerMethodPresentAnalyzerTests [Fact] public async Task TestActor_TimerRegistration_NotPresent() { - var context = new CSharpAnalyzerTest(); + var context = new CSharpAnalyzerTest(); context.ReferenceAssemblies = assemblies.AddPackages([ new ("Dapr.Actors", "1.15.3") ]); @@ -54,7 +53,7 @@ public class TimerMethodPresentAnalyzerTests [Fact] public async Task TestActor_TimerRegistration_NameOfCallbackPresent() { - var context = new CSharpAnalyzerTest(); + var context = new CSharpAnalyzerTest(); context.ReferenceAssemblies = assemblies.AddPackages([ new ("Dapr.Actors", "1.15.3") ]); @@ -85,7 +84,7 @@ public class TimerMethodPresentAnalyzerTests [Fact] public async Task TestActor_TimerRegistration_LiteralCallbackPresent() { - var context = new CSharpAnalyzerTest(); + var context = new CSharpAnalyzerTest(); context.ReferenceAssemblies = assemblies.AddPackages([ new ("Dapr.Actors", "1.15.3") ]); @@ -116,7 +115,7 @@ public class TimerMethodPresentAnalyzerTests [Fact] public async Task TestActor_TimerRegistration_CallbackNotPresent() { - var context = new CSharpAnalyzerTest(); + var context = new CSharpAnalyzerTest(); context.ReferenceAssemblies = assemblies.AddPackages([ new ("Dapr.Actors", "1.15.3") ]); @@ -134,7 +133,7 @@ public class TimerMethodPresentAnalyzerTests } """; - context.ExpectedDiagnostics.Add(new DiagnosticResult(TimerMethodPresentAnalyzer.DaprTimerCallbackMethodRule) + context.ExpectedDiagnostics.Add(new DiagnosticResult(TimerCallbackMethodPresentAnalyzer.DaprTimerCallbackMethodRule) .WithSpan(8, 45, 8, 60) .WithArguments("TimerCallback", "TestActorTimerRegistrationTimerCallbackNotPresent")); await context.RunAsync(); diff --git a/test/Dapr.Actors.Analyzers.Tests/VerifyAnalyzer.cs b/test/Dapr.Actors.Analyzers.Tests/VerifyAnalyzer.cs new file mode 100644 index 00000000..87136e41 --- /dev/null +++ b/test/Dapr.Actors.Analyzers.Tests/VerifyAnalyzer.cs @@ -0,0 +1,83 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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.AspNetCore.Builder; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Dapr.Actors.Analyzers.Tests; + +internal static class VerifyAnalyzer +{ + public static DiagnosticResult Diagnostic(DiagnosticDescriptor descriptor) => new(descriptor); + + public static async Task VerifyAnalyzerAsync(string source, params DiagnosticResult[] expected) + where TAnalyzer : DiagnosticAnalyzer, new() + { + await VerifyAnalyzerAsync(source, null, expected); + } + + public static async Task VerifyAnalyzerAsync(string source, string? program, params DiagnosticResult[] expected) + where TAnalyzer : DiagnosticAnalyzer, new() + { + var test = new Test { TestCode = source }; + +#if NET8_0 + test.ReferenceAssemblies = ReferenceAssemblies.Net.Net80; +#elif NET9_0 + test.ReferenceAssemblies = ReferenceAssemblies.Net.Net90; +#endif + + if (program != null) + { + test.TestState.Sources.Add(("Program.cs", program)); + } + + var metadataReferences = TestUtilities.GetAllReferencesNeededForType(typeof(ActorRegistrationAnalyzer)).ToList(); + metadataReferences.AddRange(TestUtilities.GetAllReferencesNeededForType(typeof(MappedActorHandlersAnalyzer))); + metadataReferences.AddRange(TestUtilities.GetAllReferencesNeededForType(typeof(PreferActorJsonSerializationAnalyzer))); + metadataReferences.AddRange(TestUtilities.GetAllReferencesNeededForType(typeof(TimerCallbackMethodPresentAnalyzer))); + metadataReferences.AddRange(TestUtilities.GetAllReferencesNeededForType(typeof(ActorsServiceCollectionExtensions))); + metadataReferences.Add(MetadataReference.CreateFromFile(typeof(object).Assembly.Location)); + metadataReferences.Add(MetadataReference.CreateFromFile(typeof(WebApplication).Assembly.Location)); + metadataReferences.Add(MetadataReference.CreateFromFile(typeof(IHost).Assembly.Location)); + + foreach (var reference in metadataReferences) + { + test.TestState.AdditionalReferences.Add(reference); + } + + test.ExpectedDiagnostics.AddRange(expected); + await test.RunAsync(CancellationToken.None); + } + + + + private sealed class Test : CSharpAnalyzerTest + where TAnalyzer : DiagnosticAnalyzer, new() + { + public Test() + { + SolutionTransforms.Add((solution, projectId) => + { + var compilationOptions = solution.GetProject(projectId)!.CompilationOptions!; + solution = solution.WithProjectCompilationOptions(projectId, compilationOptions); + return solution; + }); + } + } +} diff --git a/test/Dapr.Actors.Analyzers.Tests/VerifyCodeFix.cs b/test/Dapr.Actors.Analyzers.Tests/VerifyCodeFix.cs new file mode 100644 index 00000000..d09e427a --- /dev/null +++ b/test/Dapr.Actors.Analyzers.Tests/VerifyCodeFix.cs @@ -0,0 +1,70 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; + +namespace Dapr.Actors.Analyzers.Tests; + +internal static class VerifyCodeFix +{ + public static async Task RunTest(string code, string expectedChangedCode) where T : CodeFixProvider, new() + { + var (diagnostics, document, workspace) = await TestUtilities.GetDiagnosticsAdvanced(code); + + Assert.Single(diagnostics); + + var diagnostic = diagnostics[0]; + + var codeFixProvider = new T(); + + CodeAction? registeredCodeAction = null; + + var context = new CodeFixContext(document, diagnostic, (codeAction, _) => + { + if (registeredCodeAction != null) + throw new Exception("Code action was registered more than once"); + + registeredCodeAction = codeAction; + + }, CancellationToken.None); + + await codeFixProvider.RegisterCodeFixesAsync(context); + + if (registeredCodeAction == null) + throw new Exception("Code action was not registered"); + + var operations = await registeredCodeAction.GetOperationsAsync(CancellationToken.None); + + foreach (var operation in operations) + { + operation.Apply(workspace, CancellationToken.None); + } + + var updatedDocument = workspace.CurrentSolution.GetDocument(document.Id) ?? throw new Exception("Updated document is null"); + var newCode = (await updatedDocument.GetTextAsync()).ToString(); + + var normalizedExpectedCode = NormalizeWhitespace(expectedChangedCode); + var normalizedNewCode = NormalizeWhitespace(newCode); + + Assert.Equal(normalizedExpectedCode, normalizedNewCode); + return; + + // Normalize whitespace + string NormalizeWhitespace(string input) + { + var separator = new[] { ' ', '\r', '\n' }; + return string.Join(" ", input.Split(separator, StringSplitOptions.RemoveEmptyEntries)); + } + } +} From 9e9b39d1a2eb987c77ddd456674d213a6131f177 Mon Sep 17 00:00:00 2001 From: Siri Varma Vegiraju Date: Thu, 10 Apr 2025 05:04:22 +0530 Subject: [PATCH 08/22] Adds an analyzer to validate present configured mapping endpoint when jobs are scheduled (#1477) * Add Analyzer * Refactored for consistent naming. Updated to use Resources instead of constant strings and to share values via internal values where possible. Added location to job names that trigger diagnostic and updated verbiage of formatted value. Co-authored-by: Siri Varma Vegiraju Co-authored-by: Whit Waldo --- all.sln | 14 + examples/Jobs/JobsSample/Program.cs | 2 +- .../AnalyzerReleases.Shipped.md | 7 + .../AnalyzerReleases.Unshipped.md | 3 + src/Dapr.Jobs.Analyzer/AssemblyInfo.cs | 16 ++ .../Dapr.Jobs.Analyzers.csproj | 49 ++++ .../MapDaprScheduledJobHandlerAnalyzer.cs | 109 +++++++ src/Dapr.Jobs.Analyzer/Resources.Designer.cs | 80 ++++++ src/Dapr.Jobs.Analyzer/Resources.resx | 27 ++ .../Dapr.Jobs.Analyzers.Test.csproj | 35 +++ .../MapDaprScheduledJobHandlerTest.cs | 272 ++++++++++++++++++ 11 files changed, 613 insertions(+), 1 deletion(-) create mode 100644 src/Dapr.Jobs.Analyzer/AnalyzerReleases.Shipped.md create mode 100644 src/Dapr.Jobs.Analyzer/AnalyzerReleases.Unshipped.md create mode 100644 src/Dapr.Jobs.Analyzer/AssemblyInfo.cs create mode 100644 src/Dapr.Jobs.Analyzer/Dapr.Jobs.Analyzers.csproj create mode 100644 src/Dapr.Jobs.Analyzer/MapDaprScheduledJobHandlerAnalyzer.cs create mode 100644 src/Dapr.Jobs.Analyzer/Resources.Designer.cs create mode 100644 src/Dapr.Jobs.Analyzer/Resources.resx create mode 100644 test/Dapr.Jobs.Analyzer.Test/Dapr.Jobs.Analyzers.Test.csproj create mode 100644 test/Dapr.Jobs.Analyzer.Test/MapDaprScheduledJobHandlerTest.cs diff --git a/all.sln b/all.sln index 57601210..0788eb80 100644 --- a/all.sln +++ b/all.sln @@ -155,6 +155,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JobsSample", "examples\Jobs EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Workflow.Test", "test\Dapr.Workflow.Test\Dapr.Workflow.Test.csproj", "{E90114C6-86FC-43B8-AE5C-D9273CF21FE4}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Jobs.Analyzers", "src\Dapr.Jobs.Analyzer\Dapr.Jobs.Analyzers.csproj", "{28B87C37-4B52-400F-B84D-64F134931BDC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Jobs.Analyzers.Test", "test\Dapr.Jobs.Analyzer.Test\Dapr.Jobs.Analyzers.Test.csproj", "{CADEAE45-8981-4723-B641-9C28251C7D3B}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Actors.Analyzers", "src\Dapr.Actors.Analyzers\Dapr.Actors.Analyzers\Dapr.Actors.Analyzers.csproj", "{E49C822C-E921-48DF-897B-3E603CA596D2}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Actors.Analyzers.Tests", "test\Dapr.Actors.Analyzers.Tests\Dapr.Actors.Analyzers.Tests.csproj", "{A2C0F203-11FF-4B7F-A94F-B9FD873573FE}" @@ -407,6 +411,14 @@ Global {E90114C6-86FC-43B8-AE5C-D9273CF21FE4}.Debug|Any CPU.Build.0 = Debug|Any CPU {E90114C6-86FC-43B8-AE5C-D9273CF21FE4}.Release|Any CPU.ActiveCfg = Release|Any CPU {E90114C6-86FC-43B8-AE5C-D9273CF21FE4}.Release|Any CPU.Build.0 = Release|Any CPU + {28B87C37-4B52-400F-B84D-64F134931BDC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {28B87C37-4B52-400F-B84D-64F134931BDC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {28B87C37-4B52-400F-B84D-64F134931BDC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {28B87C37-4B52-400F-B84D-64F134931BDC}.Release|Any CPU.Build.0 = Release|Any CPU + {CADEAE45-8981-4723-B641-9C28251C7D3B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CADEAE45-8981-4723-B641-9C28251C7D3B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CADEAE45-8981-4723-B641-9C28251C7D3B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CADEAE45-8981-4723-B641-9C28251C7D3B}.Release|Any CPU.Build.0 = Release|Any CPU {E49C822C-E921-48DF-897B-3E603CA596D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E49C822C-E921-48DF-897B-3E603CA596D2}.Debug|Any CPU.Build.0 = Debug|Any CPU {E49C822C-E921-48DF-897B-3E603CA596D2}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -488,6 +500,8 @@ Global {D9697361-232F-465D-A136-4561E0E88488} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78} {9CAF360E-5AD3-4C4F-89A0-327EEB70D673} = {D9697361-232F-465D-A136-4561E0E88488} {E90114C6-86FC-43B8-AE5C-D9273CF21FE4} = {DD020B34-460F-455F-8D17-CF4A949F100B} + {28B87C37-4B52-400F-B84D-64F134931BDC} = {27C5D71D-0721-4221-9286-B94AB07B58CF} + {CADEAE45-8981-4723-B641-9C28251C7D3B} = {DD020B34-460F-455F-8D17-CF4A949F100B} {E49C822C-E921-48DF-897B-3E603CA596D2} = {27C5D71D-0721-4221-9286-B94AB07B58CF} {A2C0F203-11FF-4B7F-A94F-B9FD873573FE} = {DD020B34-460F-455F-8D17-CF4A949F100B} EndGlobalSection diff --git a/examples/Jobs/JobsSample/Program.cs b/examples/Jobs/JobsSample/Program.cs index f19e375b..ed1e7152 100644 --- a/examples/Jobs/JobsSample/Program.cs +++ b/examples/Jobs/JobsSample/Program.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // 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. diff --git a/src/Dapr.Jobs.Analyzer/AnalyzerReleases.Shipped.md b/src/Dapr.Jobs.Analyzer/AnalyzerReleases.Shipped.md new file mode 100644 index 00000000..5fab0bb6 --- /dev/null +++ b/src/Dapr.Jobs.Analyzer/AnalyzerReleases.Shipped.md @@ -0,0 +1,7 @@ +## Release 1.16 + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|-------------------- +DAPR3001| Usage | Warning | Job invocations require the MapDaprScheduledJobHandler be set and configured for each anticipated job on IEndpointRouteBuilder \ No newline at end of file diff --git a/src/Dapr.Jobs.Analyzer/AnalyzerReleases.Unshipped.md b/src/Dapr.Jobs.Analyzer/AnalyzerReleases.Unshipped.md new file mode 100644 index 00000000..b1b99aaf --- /dev/null +++ b/src/Dapr.Jobs.Analyzer/AnalyzerReleases.Unshipped.md @@ -0,0 +1,3 @@ +; Unshipped analyzer release +; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + diff --git a/src/Dapr.Jobs.Analyzer/AssemblyInfo.cs b/src/Dapr.Jobs.Analyzer/AssemblyInfo.cs new file mode 100644 index 00000000..d4c8883f --- /dev/null +++ b/src/Dapr.Jobs.Analyzer/AssemblyInfo.cs @@ -0,0 +1,16 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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.Jobs.Analyzers.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2" )] diff --git a/src/Dapr.Jobs.Analyzer/Dapr.Jobs.Analyzers.csproj b/src/Dapr.Jobs.Analyzer/Dapr.Jobs.Analyzers.csproj new file mode 100644 index 00000000..6c199cc2 --- /dev/null +++ b/src/Dapr.Jobs.Analyzer/Dapr.Jobs.Analyzers.csproj @@ -0,0 +1,49 @@ + + + + netstandard2.0 + + false + enable + enable + true + $(NoWarn);RS1038 + This package contains Roslyn analyers for Dapr.Jobs + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + + + True + True + Resources.resx + + + + + + true + + + false + + + This package contains Roslyn analyzers for jobs. + $(PackageTags) + + diff --git a/src/Dapr.Jobs.Analyzer/MapDaprScheduledJobHandlerAnalyzer.cs b/src/Dapr.Jobs.Analyzer/MapDaprScheduledJobHandlerAnalyzer.cs new file mode 100644 index 00000000..bcfd9bae --- /dev/null +++ b/src/Dapr.Jobs.Analyzer/MapDaprScheduledJobHandlerAnalyzer.cs @@ -0,0 +1,109 @@ +using System.Collections.Immutable; +using System.Resources; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Dapr.Jobs.Analyzers +{ + /// + /// DaprJobsAnalyzer. + /// + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public sealed class MapDaprScheduledJobHandlerAnalyzer : DiagnosticAnalyzer + { + internal static readonly DiagnosticDescriptor DaprJobHandlerRule = new ( + id: "DAPR3001", + title: new LocalizableResourceString(nameof(Resources.DAPR3001Title), Resources.ResourceManager, typeof(Resources)), + messageFormat: new LocalizableResourceString(nameof(Resources.DAPR3001MessageFormat), Resources.ResourceManager, typeof(Resources)), + category: "Usage", + DiagnosticSeverity.Warning, + isEnabledByDefault: true + ); + + private const string DaprJobsNameSpace = "Dapr.Jobs"; + private const string DaprJobScheduleJobAsyncMethod = "ScheduleJobAsync"; + private const string MethodNameSpace = "Dapr.Jobs.Extensions"; + private const string MapDaprScheduledJobHandlerMethod = "MapDaprScheduledJobHandler"; + + /// + public override ImmutableArray SupportedDiagnostics => [DaprJobHandlerRule]; + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterSyntaxNodeAction(AnalyzeJobSchedulerHandler, SyntaxKind.InvocationExpression); + } + + private static void AnalyzeJobSchedulerHandler(SyntaxNodeAnalysisContext context) + { + var invocationExpression = (InvocationExpressionSyntax)context.Node; + + if (!IsNamespaceAndMethodNameEqual(context, invocationExpression, DaprJobsNameSpace, DaprJobScheduleJobAsyncMethod)) + { + return; + } + + var arguments = invocationExpression.ArgumentList.Arguments; + if (arguments.Count > 0 && arguments[0].Expression is LiteralExpressionSyntax literal) + { + string jobName = literal.Token.ValueText; + + // Now, we will check for a corresponding endpoint route. + var jobNameLocation = invocationExpression.GetLocation(); + CheckForEndpointRoute(context, jobName, jobNameLocation); + } + } + + private static void CheckForEndpointRoute(SyntaxNodeAnalysisContext context, string jobName, Location jobNameLocation) + { + var root = context.SemanticModel.SyntaxTree.GetRoot(); + + // Search for MapPost with the corresponding route + var mapDaprScheduledJobHandlersCount = root + .DescendantNodes() + .OfType() + .Count(invocation => IsNamespaceAndMethodNameEqual(context, invocation, MethodNameSpace, MapDaprScheduledJobHandlerMethod)); + + if (mapDaprScheduledJobHandlersCount > 0) + { + return; + } + + // If no matching route was found, report a diagnostic + var diagnostic = Diagnostic.Create(DaprJobHandlerRule, jobNameLocation, jobName); + context.ReportDiagnostic(diagnostic); + } + + /// + /// Determines whether a given method invocation matches a specified method name + /// within a given namespace. + /// + /// For eg: MapDaprScheduledJobHandler (methodname) from the "Dapr.Jobs.Extension" (symbolNamespace) is being called. + /// + /// The syntax analysis context providing semantic information. + /// The invocation expression to analyze. + /// The expected namespace of the method. + /// The expected method name. + /// + /// true if the method belongs to the specified namespace and has the expected name; + /// otherwise, false. + /// + private static bool IsNamespaceAndMethodNameEqual(SyntaxNodeAnalysisContext context, InvocationExpressionSyntax invocation, string symbolNamespace, string methodName) + { + var symbolInfo = context.SemanticModel.GetSymbolInfo(invocation.Expression); + if (symbolInfo.Symbol is not IMethodSymbol methodSymbol) + { + return false; + } + + // Check if the receiver is of type DaprJobsClient + return methodSymbol.Name == methodName && + methodSymbol.ContainingNamespace.ToDisplayString() == symbolNamespace; + } + } +} diff --git a/src/Dapr.Jobs.Analyzer/Resources.Designer.cs b/src/Dapr.Jobs.Analyzer/Resources.Designer.cs new file mode 100644 index 00000000..409e2ec6 --- /dev/null +++ b/src/Dapr.Jobs.Analyzer/Resources.Designer.cs @@ -0,0 +1,80 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Dapr.Jobs.Analyzers { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Dapr.Jobs.Analyzers.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Job invocations require the MapDaprScheduledJobHandler be set and configured for job name '{0}' on IEndpointRouteBuilder. + /// + internal static string DAPR3001MessageFormat { + get { + return ResourceManager.GetString("DAPR3001MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Ensure handler endpoint is present for all scheduled Jobs. + /// + internal static string DAPR3001Title { + get { + return ResourceManager.GetString("DAPR3001Title", resourceCulture); + } + } + } +} diff --git a/src/Dapr.Jobs.Analyzer/Resources.resx b/src/Dapr.Jobs.Analyzer/Resources.resx new file mode 100644 index 00000000..aaef9fdc --- /dev/null +++ b/src/Dapr.Jobs.Analyzer/Resources.resx @@ -0,0 +1,27 @@ + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Ensure handler endpoint is present for all scheduled Jobs + + + Job invocations require the MapDaprScheduledJobHandler be set and configured for job name '{0}' on IEndpointRouteBuilder + + \ No newline at end of file diff --git a/test/Dapr.Jobs.Analyzer.Test/Dapr.Jobs.Analyzers.Test.csproj b/test/Dapr.Jobs.Analyzer.Test/Dapr.Jobs.Analyzers.Test.csproj new file mode 100644 index 00000000..57ca6b87 --- /dev/null +++ b/test/Dapr.Jobs.Analyzer.Test/Dapr.Jobs.Analyzers.Test.csproj @@ -0,0 +1,35 @@ + + + + enable + enable + false + true + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + diff --git a/test/Dapr.Jobs.Analyzer.Test/MapDaprScheduledJobHandlerTest.cs b/test/Dapr.Jobs.Analyzer.Test/MapDaprScheduledJobHandlerTest.cs new file mode 100644 index 00000000..19a34cbc --- /dev/null +++ b/test/Dapr.Jobs.Analyzer.Test/MapDaprScheduledJobHandlerTest.cs @@ -0,0 +1,272 @@ +using Dapr.Jobs.Models; +using Microsoft.AspNetCore.Builder; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.Extensions.Hosting; + +namespace Dapr.Jobs.Analyzers.Test; + +public class DaprJobsAnalyzerAnalyzerTests +{ + +#if NET8_0 + private static readonly ReferenceAssemblies referenceAssemblies = ReferenceAssemblies.Net.Net80; +#elif NET9_0 + private static readonly ReferenceAssemblies referenceAssemblies = ReferenceAssemblies.Net.Net90; +#endif + + [Fact] + public async Task AnalyzeJobSchedulerHandler_ShouldRaiseDiagnostic_WhenJobHasNoEndpointMapping() + { + const string testCode = """ + + using System; + using System.Text; + using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.AspNetCore.Builder; + using Dapr.Jobs; + using Dapr.Jobs.Extensions; + using Dapr.Jobs.Models; + + public static class Program + { + public static void Main() + { + var builder = WebApplication.CreateBuilder(); + builder.Services.AddDaprJobsClient(); + var app = builder.Build(); + using var scope = app.Services.CreateScope(); + + var daprJobsClient = scope.ServiceProvider.GetRequiredService(); + + daprJobsClient.ScheduleJobAsync("myJob", DaprJobSchedule.FromDuration(TimeSpan.FromSeconds(2)), + Encoding.UTF8.GetBytes("This is a test"), repeats: 10).GetAwaiter().GetResult(); + } + } + """; + + await VerifyAnalyzerAsync(testCode, + new DiagnosticResult(MapDaprScheduledJobHandlerAnalyzer.DaprJobHandlerRule) + .WithSpan(22, 25, 23, 83) + .WithMessage( + "Job invocations require the MapDaprScheduledJobHandler be set and configured for job name 'myJob' on IEndpointRouteBuilder")); + } + + [Fact] + public async Task AnalyzeJobSchedulerHandler_ShouldNotRaiseDiagnostic_WhenScheduleJobIsNotCalled() + { + const string testCode = """ + + using System; + using System.Text; + using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.AspNetCore.Builder; + using Dapr.Jobs; + using Dapr.Jobs.Extensions; + using Dapr.Jobs.Models; + + public static class Program + { + public static void Main() + { + var builder = WebApplication.CreateBuilder(); + builder.Services.AddDaprJobsClient(); + var app = builder.Build(); + using var scope = app.Services.CreateScope(); + + var daprJobsClient = scope.ServiceProvider.GetRequiredService(); + } + } + """; + + await VerifyAnalyzerAsync(testCode); + } + + [Fact] + public async Task AnalyzeJobSchedulerHandler_ShouldRaiseDiagnostic_ForEachInstanceOfScheduledJobsDontHaveMappings() + { + const string testCode = """ + + using System; + using System.Text; + using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.AspNetCore.Builder; + using Dapr.Jobs; + using Dapr.Jobs.Extensions; + using Dapr.Jobs.Models; + + public static class Program + { + public static void Main() + { + var builder = WebApplication.CreateBuilder(); + builder.Services.AddDaprJobsClient(); + var app = builder.Build(); + using var scope = app.Services.CreateScope(); + + var daprJobsClient = scope.ServiceProvider.GetRequiredService(); + + daprJobsClient.ScheduleJobAsync("myJob", DaprJobSchedule.FromDuration(TimeSpan.FromSeconds(2)), + Encoding.UTF8.GetBytes("This is a test"), repeats: 10).GetAwaiter().GetResult(); + daprJobsClient.ScheduleJobAsync("myJob2", DaprJobSchedule.FromDuration(TimeSpan.FromSeconds(2)), + Encoding.UTF8.GetBytes("This is a test"), repeats: 10).GetAwaiter().GetResult(); + } + } + """; + + await VerifyAnalyzerAsync(testCode, + new DiagnosticResult(MapDaprScheduledJobHandlerAnalyzer.DaprJobHandlerRule) + .WithSpan(22, 25, 23, 83) + .WithMessage("Job invocations require the MapDaprScheduledJobHandler be set and configured for job name 'myJob' on IEndpointRouteBuilder"), + new DiagnosticResult(MapDaprScheduledJobHandlerAnalyzer.DaprJobHandlerRule) + .WithSpan(24, 25, 25, 83) + .WithMessage("Job invocations require the MapDaprScheduledJobHandler be set and configured for job name 'myJob2' on IEndpointRouteBuilder")); + } + + [Fact] + public async Task AnalyzeJobSchedulerHandler_ShouldNotRaiseDiagnostic_WhenJobHasEndpointMapping() + { + const string testCode = """ + + using System; + using System.Text; + using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.AspNetCore.Builder; + using Dapr.Jobs; + using Dapr.Jobs.Extensions; + using Dapr.Jobs.Models; + + public static class Program + { + public static void Main() + { + var builder = WebApplication.CreateBuilder(); + builder.Services.AddDaprJobsClient(); + var app = builder.Build(); + using var scope = app.Services.CreateScope(); + + var daprJobsClient = scope.ServiceProvider.GetRequiredService(); + + daprJobsClient.ScheduleJobAsync("myJob", DaprJobSchedule.FromDuration(TimeSpan.FromSeconds(2)), + Encoding.UTF8.GetBytes("This is a test"), repeats: 10).GetAwaiter().GetResult(); + daprJobsClient.ScheduleJobAsync("myJob2", DaprJobSchedule.FromDuration(TimeSpan.FromSeconds(2)), + Encoding.UTF8.GetBytes("This is a test"), repeats: 10).GetAwaiter().GetResult(); + + app.MapDaprScheduledJobHandler(async (string jobName, ReadOnlyMemory jobPayload) => + { + return Task.CompletedTask; + }, TimeSpan.FromSeconds(5)); + } + } + """; + + await VerifyAnalyzerAsync(testCode); + } + + [Fact] + public async Task AnalyzeJobSchedulerHandler_ShouldNotRaiseDiagnostic_WhenJobHasEndpointMappingIrrespectiveOfNumberOfMethodCallsOnScheduleJob() + { + const string testCode = """ + + using System; + using System.Text; + using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.AspNetCore.Builder; + using Dapr.Jobs; + using Dapr.Jobs.Extensions; + using Dapr.Jobs.Models; + + public static class Program + { + public static async Task Main() + { + var builder = WebApplication.CreateBuilder(); + builder.Services.AddDaprJobsClient(); + var app = builder.Build(); + using var scope = app.Services.CreateScope(); + + var daprJobsClient = scope.ServiceProvider.GetRequiredService(); + + daprJobsClient.ScheduleJobAsync("myJob", DaprJobSchedule.FromDuration(TimeSpan.FromSeconds(2)), + Encoding.UTF8.GetBytes("This is a test"), repeats: 10).GetAwaiter().GetResult(); + await daprJobsClient.ScheduleJobAsync("myJob2", DaprJobSchedule.FromDuration(TimeSpan.FromSeconds(2)), + Encoding.UTF8.GetBytes("This is a test"), repeats: 10); + + app.MapDaprScheduledJobHandler(async (string jobName, ReadOnlyMemory jobPayload) => + { + return Task.CompletedTask; + }, TimeSpan.FromSeconds(5)); + } + } + """; + + await VerifyAnalyzerAsync(testCode); + } + + [Fact] + public async Task AnalyzeJobSchedulerHandler_ShouldNotRaiseDiagnostic_WhenScheduleJobDoesNotBelongToDaprJobClient() + { + const string testCode = """ + + using System; + using System.Text; + using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.AspNetCore.Builder; + using Dapr.Jobs; + using Dapr.Jobs.Extensions; + using Dapr.Jobs.Models; + + public static class Program + { + public static async Task Main() + { + var builder = WebApplication.CreateBuilder(); + builder.Services.AddDaprJobsClient(); + var app = builder.Build(); + using var scope = app.Services.CreateScope(); + + var daprJobsClient = scope.ServiceProvider.GetRequiredService(); + + await ScheduleJobAsync("myJob"); + } + + public static Task ScheduleJobAsync(string jobNAme) + { + return Task.CompletedTask; + } + } + + """; + + await VerifyAnalyzerAsync(testCode); + } + + private static async Task VerifyAnalyzerAsync(string testCode, params DiagnosticResult[] expectedDiagnostics) + { + var test = new CSharpAnalyzerTest + { + TestCode = testCode + }; + + test.TestState.ReferenceAssemblies = referenceAssemblies; + + test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(WebApplication).Assembly.Location)); + test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(DaprJobsClient).Assembly.Location)); + test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(DaprJobSchedule).Assembly.Location)); + test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(EndpointRouteBuilderExtensions).Assembly.Location)); + test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(IApplicationBuilder).Assembly.Location)); + test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile( + typeof(Microsoft.Extensions.DependencyInjection.ServiceCollection).Assembly.Location)); + test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(IHost).Assembly.Location)); + + test.ExpectedDiagnostics.AddRange(expectedDiagnostics); + await test.RunAsync(); + } +} From d00da14d289589ad61d81f54d9c97d9407586c4a Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Wed, 9 Apr 2025 18:52:26 -0500 Subject: [PATCH 09/22] Project directory cleanup (#1514) * Removed erroneous duplicate project directory * Corrected project name for consistency across test projects --- all.sln | 2 +- .../Dapr.Actors.Analyzers/AssemblyInfo.cs | 2 +- .../ActorAnalyzerTests.cs | 313 ------------------ ...orJsonSerializationCodeFixProviderTests.cs | 55 --- .../ActorRegistrationAnalyzerTests.cs | 0 .../ActorRegistrationCodeFixProviderTests.cs | 307 ++++++++--------- .../Dapr.Actors.Analyzers.Test.csproj | 62 ++-- .../MapActorsHandlersCodeFixProviderTests.cs | 124 ------- .../MappedActorHandlersAnalyzerTests.cs | 0 ...MappedActorHandlersCodeFixProviderTests.cs | 0 ...eferActorJsonSerializationAnalyzerTests.cs | 0 ...orJsonSerializationCodeFixProviderTests.cs | 0 .../TestUtilities.cs | 0 ...TimerCallbackMethodPresentAnalyzerTests.cs | 0 test/Dapr.Actors.Analyzers.Test/Utilities.cs | 77 ----- .../VerifyAnalyzer.cs | 59 ++-- .../VerifyCodeFix.cs | 32 +- .../ActorRegistrationCodeFixProviderTests.cs | 186 ----------- .../Dapr.Actors.Analyzers.Tests.csproj | 38 --- .../VerifyAnalyzer.cs | 83 ----- .../VerifyCodeFix.cs | 70 ---- 21 files changed, 254 insertions(+), 1156 deletions(-) delete mode 100644 test/Dapr.Actors.Analyzers.Test/ActorAnalyzerTests.cs delete mode 100644 test/Dapr.Actors.Analyzers.Test/ActorJsonSerializationCodeFixProviderTests.cs rename test/{Dapr.Actors.Analyzers.Tests => Dapr.Actors.Analyzers.Test}/ActorRegistrationAnalyzerTests.cs (100%) delete mode 100644 test/Dapr.Actors.Analyzers.Test/MapActorsHandlersCodeFixProviderTests.cs rename test/{Dapr.Actors.Analyzers.Tests => Dapr.Actors.Analyzers.Test}/MappedActorHandlersAnalyzerTests.cs (100%) rename test/{Dapr.Actors.Analyzers.Tests => Dapr.Actors.Analyzers.Test}/MappedActorHandlersCodeFixProviderTests.cs (100%) rename test/{Dapr.Actors.Analyzers.Tests => Dapr.Actors.Analyzers.Test}/PreferActorJsonSerializationAnalyzerTests.cs (100%) rename test/{Dapr.Actors.Analyzers.Tests => Dapr.Actors.Analyzers.Test}/PreferActorJsonSerializationCodeFixProviderTests.cs (100%) rename test/{Dapr.Actors.Analyzers.Tests => Dapr.Actors.Analyzers.Test}/TestUtilities.cs (100%) rename test/{Dapr.Actors.Analyzers.Tests => Dapr.Actors.Analyzers.Test}/TimerCallbackMethodPresentAnalyzerTests.cs (100%) delete mode 100644 test/Dapr.Actors.Analyzers.Test/Utilities.cs delete mode 100644 test/Dapr.Actors.Analyzers.Tests/ActorRegistrationCodeFixProviderTests.cs delete mode 100644 test/Dapr.Actors.Analyzers.Tests/Dapr.Actors.Analyzers.Tests.csproj delete mode 100644 test/Dapr.Actors.Analyzers.Tests/VerifyAnalyzer.cs delete mode 100644 test/Dapr.Actors.Analyzers.Tests/VerifyCodeFix.cs diff --git a/all.sln b/all.sln index 0788eb80..b41526cf 100644 --- a/all.sln +++ b/all.sln @@ -161,7 +161,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Jobs.Analyzers.Test", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Actors.Analyzers", "src\Dapr.Actors.Analyzers\Dapr.Actors.Analyzers\Dapr.Actors.Analyzers.csproj", "{E49C822C-E921-48DF-897B-3E603CA596D2}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Actors.Analyzers.Tests", "test\Dapr.Actors.Analyzers.Tests\Dapr.Actors.Analyzers.Tests.csproj", "{A2C0F203-11FF-4B7F-A94F-B9FD873573FE}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Actors.Analyzers.Test", "test\Dapr.Actors.Analyzers.Test\Dapr.Actors.Analyzers.Test.csproj", "{A2C0F203-11FF-4B7F-A94F-B9FD873573FE}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/AssemblyInfo.cs b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/AssemblyInfo.cs index e07f509e..dcc08b3c 100644 --- a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/AssemblyInfo.cs +++ b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/AssemblyInfo.cs @@ -13,4 +13,4 @@ using System.Runtime.CompilerServices; -[assembly: InternalsVisibleTo("Dapr.Actors.Analyzers.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2" )] +[assembly: InternalsVisibleTo("Dapr.Actors.Analyzers.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2" )] diff --git a/test/Dapr.Actors.Analyzers.Test/ActorAnalyzerTests.cs b/test/Dapr.Actors.Analyzers.Test/ActorAnalyzerTests.cs deleted file mode 100644 index 8d6b9abe..00000000 --- a/test/Dapr.Actors.Analyzers.Test/ActorAnalyzerTests.cs +++ /dev/null @@ -1,313 +0,0 @@ -using Microsoft.CodeAnalysis; - -namespace Dapr.Actors.Analyzers.Test; - -public class ActorAnalyzerTests -{ - public class ActorNotRegistered - { - [Fact] - public async Task ReportDiagnostic_DAPR0001() - { - var testCode = @" - using Dapr.Actors.Runtime; - - class TestActor : Actor - { - public TestActor(ActorHost host) : base(host) - { - } - } - "; - - var expected = VerifyAnalyzer.Diagnostic("DAPR0001", DiagnosticSeverity.Warning) - .WithSpan(4, 23, 4, 32).WithMessage("The actor class 'TestActor' is not registered"); - - await VerifyAnalyzer.VerifyAnalyzerAsync(testCode, expected); - } - - [Fact] - public async Task ReportDiagnostic_DAPR0001_FullyQualified() - { - var testCode = @" - class TestActor : Dapr.Actors.Runtime.Actor - { - public TestActor(Dapr.Actors.Runtime.ActorHost host) : base(host) - { - } - } - "; - - var expected = VerifyAnalyzer.Diagnostic("DAPR0001", DiagnosticSeverity.Warning) - .WithSpan(2, 23, 2, 32).WithMessage("The actor class 'TestActor' is not registered"); - - await VerifyAnalyzer.VerifyAnalyzerAsync(testCode, expected); - } - - [Fact] - public async Task ReportDiagnostic_DAPR0001_NamespaceAlias() - { - var testCode = @" - using alias = Dapr.Actors.Runtime; - - class TestActor : alias.Actor - { - public TestActor(alias.ActorHost host) : base(host) - { - } - } - "; - - var expected = VerifyAnalyzer.Diagnostic("DAPR0001", DiagnosticSeverity.Warning) - .WithSpan(4, 23, 4, 32).WithMessage("The actor class 'TestActor' is not registered"); - - await VerifyAnalyzer.VerifyAnalyzerAsync(testCode, expected); - } - } - - public class ActorRegistered - { - [Fact] - public async Task ReportNoDiagnostic() - { - var testCode = @" - using Dapr.Actors.Runtime; - using Microsoft.AspNetCore.Builder; - using Microsoft.Extensions.DependencyInjection; - - public static class Program - { - public static void Main() - { - var builder = WebApplication.CreateBuilder(); - - builder.Services.AddActors(options => - { - options.Actors.RegisterActor(); - options.UseJsonSerialization = true; - }); - - var app = builder.Build(); - - app.MapActorsHandlers(); - } - } - - class TestActor : Actor - { - public TestActor(ActorHost host) : base(host) - { - } - } - "; - - await VerifyAnalyzer.VerifyAnalyzerAsync(testCode); - } - - [Fact] - public async Task ReportNoDiagnostic_WithNamespace() - { - var testCode = @" - using Dapr.Actors.Runtime; - using Microsoft.AspNetCore.Builder; - using Microsoft.Extensions.DependencyInjection; - - public static class Program - { - public static void Main() - { - var builder = WebApplication.CreateBuilder(); - - builder.Services.AddActors(options => - { - options.Actors.RegisterActor(); - options.UseJsonSerialization = true; - }); - - var app = builder.Build(); - - app.MapActorsHandlers(); - } - } - - namespace TestNamespace - { - class TestActor : Actor - { - public TestActor(ActorHost host) : base(host) - { - } - } - } - "; - - await VerifyAnalyzer.VerifyAnalyzerAsync(testCode); - } - - [Fact] - public async Task ReportNoDiagnostic_WithNamespaceAlias () - { - var testCode = @" - using Dapr.Actors.Runtime; - using Microsoft.AspNetCore.Builder; - using Microsoft.Extensions.DependencyInjection; - using alias = TestNamespace; - - public static class Program - { - public static void Main() - { - var builder = WebApplication.CreateBuilder(); - - builder.Services.AddActors(options => - { - options.Actors.RegisterActor(); - options.UseJsonSerialization = true; - }); - - var app = builder.Build(); - - app.MapActorsHandlers(); - } - } - - namespace TestNamespace - { - class TestActor : Actor - { - public TestActor(ActorHost host) : base(host) - { - } - } - } - "; - - await VerifyAnalyzer.VerifyAnalyzerAsync(testCode); - } - } - - public class JsonSerialization - { - [Fact] - public async Task ReportDiagnostic() - { - var testCode = @" - using Dapr.Actors.Runtime; - using Microsoft.AspNetCore.Builder; - using Microsoft.Extensions.DependencyInjection; - - public static class Program - { - public static void Main() - { - var builder = WebApplication.CreateBuilder(); - - builder.Services.AddActors(options => - { - }); - - var app = builder.Build(); - - app.MapActorsHandlers(); - } - } - "; - - var expected = VerifyAnalyzer.Diagnostic("DAPR0002", DiagnosticSeverity.Warning) - .WithSpan(12, 25, 14, 27).WithMessage("Add options.UseJsonSerialization to support interoperability with non-.NET actors"); - - await VerifyAnalyzer.VerifyAnalyzerAsync(testCode, expected); - } - - [Fact] - public async Task ReportNoDiagnostic() - { - var testCode = @" - using Dapr.Actors.Runtime; - using Microsoft.AspNetCore.Builder; - using Microsoft.Extensions.DependencyInjection; - - public static class Program - { - public static void Main() - { - var builder = WebApplication.CreateBuilder(); - - builder.Services.AddActors(options => - { - options.UseJsonSerialization = true; - }); - - var app = builder.Build(); - - app.MapActorsHandlers(); - } - } - "; - - await VerifyAnalyzer.VerifyAnalyzerAsync(testCode); - } - } - - public class MapActorsHandlers - { - [Fact] - public async Task ReportDiagnostic() - { - var testCode = @" - using Dapr.Actors.Runtime; - using Microsoft.AspNetCore.Builder; - using Microsoft.Extensions.DependencyInjection; - - public static class Program - { - public static void Main() - { - var builder = WebApplication.CreateBuilder(); - - builder.Services.AddActors(options => - { - options.UseJsonSerialization = true; - }); - - var app = builder.Build(); - } - } - "; - - var expected = VerifyAnalyzer.Diagnostic("DAPR0003", DiagnosticSeverity.Warning) - .WithSpan(12, 25, 15, 27).WithMessage("Call app.MapActorsHandlers to map endpoints for Dapr actors"); - - await VerifyAnalyzer.VerifyAnalyzerAsync(testCode, expected); - } - - [Fact] - public async Task ReportNoDiagnostic() - { - var testCode = @" - using Dapr.Actors.Runtime; - using Microsoft.AspNetCore.Builder; - using Microsoft.Extensions.DependencyInjection; - - public static class Program - { - public static void Main() - { - var builder = WebApplication.CreateBuilder(); - - builder.Services.AddActors(options => - { - options.UseJsonSerialization = true; - }); - - var app = builder.Build(); - - app.MapActorsHandlers(); - } - } - "; - - await VerifyAnalyzer.VerifyAnalyzerAsync(testCode); - } - } -} diff --git a/test/Dapr.Actors.Analyzers.Test/ActorJsonSerializationCodeFixProviderTests.cs b/test/Dapr.Actors.Analyzers.Test/ActorJsonSerializationCodeFixProviderTests.cs deleted file mode 100644 index 7d3fe6ca..00000000 --- a/test/Dapr.Actors.Analyzers.Test/ActorJsonSerializationCodeFixProviderTests.cs +++ /dev/null @@ -1,55 +0,0 @@ -namespace Dapr.Actors.Analyzers.Test; - -public class ActorJsonSerializationCodeFixProviderTests -{ - [Fact] - public async Task UseJsonSerialization() - { - var code = @" - //using Dapr.Actors.Runtime; - using Microsoft.AspNetCore.Builder; - using Microsoft.Extensions.DependencyInjection; - - public static class Program - { - public static void Main() - { - var builder = WebApplication.CreateBuilder(); - - builder.Services.AddActors(options => - { - }); - - var app = builder.Build(); - - app.MapActorsHandlers(); - } - } - "; - - var expectedChangedCode = @" - //using Dapr.Actors.Runtime; - using Microsoft.AspNetCore.Builder; - using Microsoft.Extensions.DependencyInjection; - - public static class Program - { - public static void Main() - { - var builder = WebApplication.CreateBuilder(); - - builder.Services.AddActors(options => - { - options.UseJsonSerialization = true; - }); - - var app = builder.Build(); - - app.MapActorsHandlers(); - } - } - "; - - await VerifyCodeFix.RunTest(code, expectedChangedCode); - } -} diff --git a/test/Dapr.Actors.Analyzers.Tests/ActorRegistrationAnalyzerTests.cs b/test/Dapr.Actors.Analyzers.Test/ActorRegistrationAnalyzerTests.cs similarity index 100% rename from test/Dapr.Actors.Analyzers.Tests/ActorRegistrationAnalyzerTests.cs rename to test/Dapr.Actors.Analyzers.Test/ActorRegistrationAnalyzerTests.cs diff --git a/test/Dapr.Actors.Analyzers.Test/ActorRegistrationCodeFixProviderTests.cs b/test/Dapr.Actors.Analyzers.Test/ActorRegistrationCodeFixProviderTests.cs index d148ed90..9ae4afc6 100644 --- a/test/Dapr.Actors.Analyzers.Test/ActorRegistrationCodeFixProviderTests.cs +++ b/test/Dapr.Actors.Analyzers.Test/ActorRegistrationCodeFixProviderTests.cs @@ -1,70 +1,81 @@ -namespace Dapr.Actors.Analyzers.Test; +// ------------------------------------------------------------------------ +// Copyright 2025 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.Actors.Analyzers.Tests; public class ActorRegistrationCodeFixProviderTests { [Fact] public async Task RegisterActor() { - var code = @" - using Dapr.Actors.Runtime; - using Microsoft.AspNetCore.Builder; - using Microsoft.Extensions.DependencyInjection; - - public static class Program - { - public static void Main() - { - var builder = WebApplication.CreateBuilder(); - - builder.Services.AddActors(options => - { - options.UseJsonSerialization = true; - }); + const string code = """ + + using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; + using Microsoft.Extensions.DependencyInjection; + + public static class Program + { + public static void Main() + { + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddActors(options => + { + options.UseJsonSerialization = true; + }); + var app = builder.Build(); + app.MapActorsHandlers(); + } + } + class TestActor : Actor + { + public TestActor(ActorHost host) : base(host) + { + } + } + + """; - var app = builder.Build(); - - app.MapActorsHandlers(); - } - } - - class TestActor : Actor - { - public TestActor(ActorHost host) : base(host) - { - } - } - "; - - var expectedChangedCode = @" - using Dapr.Actors.Runtime; - using Microsoft.AspNetCore.Builder; - using Microsoft.Extensions.DependencyInjection; - - public static class Program - { - public static void Main() - { - var builder = WebApplication.CreateBuilder(); - - builder.Services.AddActors(options => - { - options.UseJsonSerialization = true; - options.Actors.RegisterActor(); - }); - - var app = builder.Build(); - - app.MapActorsHandlers(); - } - } - - class TestActor : Actor - { - public TestActor(ActorHost host) : base(host) - { - } - } - "; + const string expectedChangedCode = """ + + using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; + using Microsoft.Extensions.DependencyInjection; + + public static class Program + { + public static void Main() + { + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddActors(options => + { + options.UseJsonSerialization = true; + options.Actors.RegisterActor(); + }); + var app = builder.Build(); + app.MapActorsHandlers(); + } + } + class TestActor : Actor + { + public TestActor(ActorHost host) : base(host) + { + } + } + + """; await VerifyCodeFix.RunTest(code, expectedChangedCode); } @@ -72,54 +83,54 @@ public class ActorRegistrationCodeFixProviderTests [Fact] public async Task RegisterActor_WhenAddActorsIsNotFound() { - var code = @" - using Dapr.Actors.Runtime; - using Microsoft.AspNetCore.Builder; - - public static class Program - { - public static void Main() - { - var builder = WebApplication.CreateBuilder(); + const string code = """ + + using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; + + public static class Program + { + public static void Main() + { + var builder = WebApplication.CreateBuilder(); + var app = builder.Build(); + } + } + class TestActor : Actor + { + public TestActor(ActorHost host) : base(host) + { + } + } + + """; - var app = builder.Build(); - } - } - - class TestActor : Actor - { - public TestActor(ActorHost host) : base(host) - { - } - } - "; - - var expectedChangedCode = @" - using Dapr.Actors.Runtime; - using Microsoft.AspNetCore.Builder; - - public static class Program - { - public static void Main() - { - var builder = WebApplication.CreateBuilder(); - - builder.Services.AddActors(options => - { - options.Actors.RegisterActor(); - }); - - var app = builder.Build(); - } - } - - class TestActor : Actor - { - public TestActor(ActorHost host) : base(host) - { - } - } - "; + const string expectedChangedCode = """ + + using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; + + public static class Program + { + public static void Main() + { + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddActors(options => + { + options.Actors.RegisterActor(); + }); + var app = builder.Build(); + } + } + class TestActor : Actor + { + public TestActor(ActorHost host) : base(host) + { + } + } + + """; await VerifyCodeFix.RunTest(code, expectedChangedCode); } @@ -127,48 +138,48 @@ public class ActorRegistrationCodeFixProviderTests [Fact] public async Task RegisterActor_WhenAddActorsIsNotFound_TopLevelStatements() { - var code = @" - using Dapr.Actors.Runtime; - using Microsoft.AspNetCore.Builder; + const string code = """ + + using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; + var builder = WebApplication.CreateBuilder(); + + var app = builder.Build(); + namespace TestNamespace + { + class TestActor : Actor + { + public TestActor(ActorHost host) : base(host) + { + } + } + } + + """; - var builder = WebApplication.CreateBuilder(); - - var app = builder.Build(); - - namespace TestNamespace - { - class TestActor : Actor - { - public TestActor(ActorHost host) : base(host) - { - } - } - } - "; - - var expectedChangedCode = @" - using Dapr.Actors.Runtime; - using Microsoft.AspNetCore.Builder; - - var builder = WebApplication.CreateBuilder(); - - builder.Services.AddActors(options => - { - options.Actors.RegisterActor(); - }); - - var app = builder.Build(); - - namespace TestNamespace - { - class TestActor : Actor - { - public TestActor(ActorHost host) : base(host) - { - } - } - } - "; + const string expectedChangedCode = """ + + using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; + + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddActors(options => + { + options.Actors.RegisterActor(); + }); + var app = builder.Build(); + namespace TestNamespace + { + class TestActor : Actor + { + public TestActor(ActorHost host) : base(host) + { + } + } + } + + """; await VerifyCodeFix.RunTest(code, expectedChangedCode); } diff --git a/test/Dapr.Actors.Analyzers.Test/Dapr.Actors.Analyzers.Test.csproj b/test/Dapr.Actors.Analyzers.Test/Dapr.Actors.Analyzers.Test.csproj index 934fcbfd..8d9458e2 100644 --- a/test/Dapr.Actors.Analyzers.Test/Dapr.Actors.Analyzers.Test.csproj +++ b/test/Dapr.Actors.Analyzers.Test/Dapr.Actors.Analyzers.Test.csproj @@ -1,35 +1,39 @@ - + - - enable - enable - false - true - + + enable + enable + false + true + Dapr.Actors.Analyzers.Tests + - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + - - - - + + + - - - + + + + diff --git a/test/Dapr.Actors.Analyzers.Test/MapActorsHandlersCodeFixProviderTests.cs b/test/Dapr.Actors.Analyzers.Test/MapActorsHandlersCodeFixProviderTests.cs deleted file mode 100644 index 8d0f5835..00000000 --- a/test/Dapr.Actors.Analyzers.Test/MapActorsHandlersCodeFixProviderTests.cs +++ /dev/null @@ -1,124 +0,0 @@ -namespace Dapr.Actors.Analyzers.Test; - -public class MapActorsHandlersCodeFixProviderTests -{ - [Fact] - public async Task RegisterActor() - { - var code = @" - using Dapr.Actors.Runtime; - using Microsoft.AspNetCore.Builder; - using Microsoft.Extensions.DependencyInjection; - - public static class Program - { - public static void Main() - { - var builder = WebApplication.CreateBuilder(); - - builder.Services.AddActors(options => - { - options.Actors.RegisterActor(); - options.UseJsonSerialization = true; - }); - - var app = builder.Build(); - } - } - - class TestActor : Actor - { - public TestActor(ActorHost host) : base(host) - { - } - } - "; - - var expectedChangedCode = @" - using Dapr.Actors.Runtime; - using Microsoft.AspNetCore.Builder; - using Microsoft.Extensions.DependencyInjection; - - public static class Program - { - public static void Main() - { - var builder = WebApplication.CreateBuilder(); - - builder.Services.AddActors(options => - { - options.Actors.RegisterActor(); - options.UseJsonSerialization = true; - }); - - var app = builder.Build(); - - app.MapActorsHandlers(); - } - } - - class TestActor : Actor - { - public TestActor(ActorHost host) : base(host) - { - } - } - "; - - await VerifyCodeFix.RunTest(code, expectedChangedCode); - } - - [Fact] - public async Task RegisterActor_TopLevelStatements() - { - var code = @" - using Dapr.Actors.Runtime; - using Microsoft.AspNetCore.Builder; - using Microsoft.Extensions.DependencyInjection; - - var builder = WebApplication.CreateBuilder(); - - builder.Services.AddActors(options => - { - options.Actors.RegisterActor(); - options.UseJsonSerialization = true; - }); - - var app = builder.Build(); - - class TestActor : Actor - { - public TestActor(ActorHost host) : base(host) - { - } - } - "; - - var expectedChangedCode = @" - using Dapr.Actors.Runtime; - using Microsoft.AspNetCore.Builder; - using Microsoft.Extensions.DependencyInjection; - - var builder = WebApplication.CreateBuilder(); - - builder.Services.AddActors(options => - { - options.Actors.RegisterActor(); - options.UseJsonSerialization = true; - }); - - var app = builder.Build(); - - app.MapActorsHandlers(); - - class TestActor : Actor - { - public TestActor(ActorHost host) : base(host) - { - } - } - "; - - await VerifyCodeFix.RunTest(code, expectedChangedCode); - } -} diff --git a/test/Dapr.Actors.Analyzers.Tests/MappedActorHandlersAnalyzerTests.cs b/test/Dapr.Actors.Analyzers.Test/MappedActorHandlersAnalyzerTests.cs similarity index 100% rename from test/Dapr.Actors.Analyzers.Tests/MappedActorHandlersAnalyzerTests.cs rename to test/Dapr.Actors.Analyzers.Test/MappedActorHandlersAnalyzerTests.cs diff --git a/test/Dapr.Actors.Analyzers.Tests/MappedActorHandlersCodeFixProviderTests.cs b/test/Dapr.Actors.Analyzers.Test/MappedActorHandlersCodeFixProviderTests.cs similarity index 100% rename from test/Dapr.Actors.Analyzers.Tests/MappedActorHandlersCodeFixProviderTests.cs rename to test/Dapr.Actors.Analyzers.Test/MappedActorHandlersCodeFixProviderTests.cs diff --git a/test/Dapr.Actors.Analyzers.Tests/PreferActorJsonSerializationAnalyzerTests.cs b/test/Dapr.Actors.Analyzers.Test/PreferActorJsonSerializationAnalyzerTests.cs similarity index 100% rename from test/Dapr.Actors.Analyzers.Tests/PreferActorJsonSerializationAnalyzerTests.cs rename to test/Dapr.Actors.Analyzers.Test/PreferActorJsonSerializationAnalyzerTests.cs diff --git a/test/Dapr.Actors.Analyzers.Tests/PreferActorJsonSerializationCodeFixProviderTests.cs b/test/Dapr.Actors.Analyzers.Test/PreferActorJsonSerializationCodeFixProviderTests.cs similarity index 100% rename from test/Dapr.Actors.Analyzers.Tests/PreferActorJsonSerializationCodeFixProviderTests.cs rename to test/Dapr.Actors.Analyzers.Test/PreferActorJsonSerializationCodeFixProviderTests.cs diff --git a/test/Dapr.Actors.Analyzers.Tests/TestUtilities.cs b/test/Dapr.Actors.Analyzers.Test/TestUtilities.cs similarity index 100% rename from test/Dapr.Actors.Analyzers.Tests/TestUtilities.cs rename to test/Dapr.Actors.Analyzers.Test/TestUtilities.cs diff --git a/test/Dapr.Actors.Analyzers.Tests/TimerCallbackMethodPresentAnalyzerTests.cs b/test/Dapr.Actors.Analyzers.Test/TimerCallbackMethodPresentAnalyzerTests.cs similarity index 100% rename from test/Dapr.Actors.Analyzers.Tests/TimerCallbackMethodPresentAnalyzerTests.cs rename to test/Dapr.Actors.Analyzers.Test/TimerCallbackMethodPresentAnalyzerTests.cs diff --git a/test/Dapr.Actors.Analyzers.Test/Utilities.cs b/test/Dapr.Actors.Analyzers.Test/Utilities.cs deleted file mode 100644 index 4b6beed0..00000000 --- a/test/Dapr.Actors.Analyzers.Test/Utilities.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System.Collections.Immutable; -using System.Reflection; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.AspNetCore.Builder; -using Microsoft.CodeAnalysis.Testing; - -namespace Dapr.Actors.Analyzers.Test; - -internal static class Utilities -{ - public static async Task<(ImmutableArray diagnostics, Document document, Workspace workspace)> GetDiagnosticsAdvanced(string code) - { - var workspace = new AdhocWorkspace(); - -#if NET6_0 - var referenceAssemblies = ReferenceAssemblies.Net.Net60; -#elif NET7_0 - var referenceAssemblies = ReferenceAssemblies.Net.Net70; -#elif NET8_0 - var referenceAssemblies = ReferenceAssemblies.Net.Net80; -#elif NET9_0 - var referenceAssemblies = ReferenceAssemblies.Net.Net90; -#endif - - // Create a new project with necessary references - var project = workspace.AddProject("TestProject", LanguageNames.CSharp) - .WithCompilationOptions(new CSharpCompilationOptions(OutputKind.ConsoleApplication) - .WithSpecificDiagnosticOptions(new Dictionary - { - { "CS1701", ReportDiagnostic.Suppress } - })) - .AddMetadataReferences(await referenceAssemblies.ResolveAsync(LanguageNames.CSharp, default)) - .AddMetadataReference(MetadataReference.CreateFromFile(typeof(object).Assembly.Location)) - .AddMetadataReferences(GetAllReferencesNeededForType(typeof(WebApplication))) - .AddMetadataReferences(GetAllReferencesNeededForType(typeof(IHost))) - .AddMetadataReferences(GetAllReferencesNeededForType(typeof(ActorsEndpointRouteBuilderExtensions))) - .AddMetadataReferences(GetAllReferencesNeededForType(typeof(ActorsServiceCollectionExtensions))); - - // Add the document to the project - var document = project.AddDocument("TestDocument.cs", code); - - // Get the syntax tree and create a compilation - var syntaxTree = await document.GetSyntaxTreeAsync() ?? throw new InvalidOperationException("Syntax tree is null"); - var compilation = CSharpCompilation.Create("TestCompilation") - .AddSyntaxTrees(syntaxTree) - .AddReferences(project.MetadataReferences) - .WithOptions(project.CompilationOptions!); - - var compilationWithAnalyzer = compilation.WithAnalyzers( - ImmutableArray.Create( - new ActorAnalyzer())); - - // Get diagnostics from the compilation - var diagnostics = await compilationWithAnalyzer.GetAllDiagnosticsAsync(); - return (diagnostics, document, workspace); - } - - public static MetadataReference[] GetAllReferencesNeededForType(Type type) - { - var files = GetAllAssemblyFilesNeededForType(type); - - return files.Select(x => MetadataReference.CreateFromFile(x)).Cast().ToArray(); - } - - private static ImmutableArray GetAllAssemblyFilesNeededForType(Type type) - { - return type.Assembly.GetReferencedAssemblies() - .Select(x => Assembly.Load(x.FullName)) - .Append(type.Assembly) - .Select(x => x.Location) - .ToImmutableArray(); - } -} diff --git a/test/Dapr.Actors.Analyzers.Test/VerifyAnalyzer.cs b/test/Dapr.Actors.Analyzers.Test/VerifyAnalyzer.cs index d114caed..87136e41 100644 --- a/test/Dapr.Actors.Analyzers.Test/VerifyAnalyzer.cs +++ b/test/Dapr.Actors.Analyzers.Test/VerifyAnalyzer.cs @@ -1,33 +1,42 @@ -using Microsoft.CodeAnalysis.CSharp.Testing; -using Microsoft.CodeAnalysis.Testing; -using Microsoft.CodeAnalysis; -using Microsoft.Extensions.DependencyInjection; +// ------------------------------------------------------------------------ +// Copyright 2025 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.AspNetCore.Builder; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -namespace Dapr.Actors.Analyzers.Test; +namespace Dapr.Actors.Analyzers.Tests; internal static class VerifyAnalyzer { - public static DiagnosticResult Diagnostic(string diagnosticId, DiagnosticSeverity diagnosticSeverity) + public static DiagnosticResult Diagnostic(DiagnosticDescriptor descriptor) => new(descriptor); + + public static async Task VerifyAnalyzerAsync(string source, params DiagnosticResult[] expected) + where TAnalyzer : DiagnosticAnalyzer, new() { - return new DiagnosticResult(diagnosticId, diagnosticSeverity); + await VerifyAnalyzerAsync(source, null, expected); } - public static async Task VerifyAnalyzerAsync(string source, params DiagnosticResult[] expected) + public static async Task VerifyAnalyzerAsync(string source, string? program, params DiagnosticResult[] expected) + where TAnalyzer : DiagnosticAnalyzer, new() { - await VerifyAnalyzerAsync(source, null, expected); - } + var test = new Test { TestCode = source }; - public static async Task VerifyAnalyzerAsync(string source, string? program, params DiagnosticResult[] expected) - { - var test = new Test { TestCode = source }; - -#if NET6_0 - test.ReferenceAssemblies = ReferenceAssemblies.Net.Net60; -#elif NET7_0 - test.ReferenceAssemblies = ReferenceAssemblies.Net.Net70; -#elif NET8_0 +#if NET8_0 test.ReferenceAssemblies = ReferenceAssemblies.Net.Net80; #elif NET9_0 test.ReferenceAssemblies = ReferenceAssemblies.Net.Net90; @@ -38,8 +47,11 @@ internal static class VerifyAnalyzer test.TestState.Sources.Add(("Program.cs", program)); } - var metadataReferences = Utilities.GetAllReferencesNeededForType(typeof(ActorAnalyzer)).ToList(); - metadataReferences.AddRange(Utilities.GetAllReferencesNeededForType(typeof(ActorsServiceCollectionExtensions))); + var metadataReferences = TestUtilities.GetAllReferencesNeededForType(typeof(ActorRegistrationAnalyzer)).ToList(); + metadataReferences.AddRange(TestUtilities.GetAllReferencesNeededForType(typeof(MappedActorHandlersAnalyzer))); + metadataReferences.AddRange(TestUtilities.GetAllReferencesNeededForType(typeof(PreferActorJsonSerializationAnalyzer))); + metadataReferences.AddRange(TestUtilities.GetAllReferencesNeededForType(typeof(TimerCallbackMethodPresentAnalyzer))); + metadataReferences.AddRange(TestUtilities.GetAllReferencesNeededForType(typeof(ActorsServiceCollectionExtensions))); metadataReferences.Add(MetadataReference.CreateFromFile(typeof(object).Assembly.Location)); metadataReferences.Add(MetadataReference.CreateFromFile(typeof(WebApplication).Assembly.Location)); metadataReferences.Add(MetadataReference.CreateFromFile(typeof(IHost).Assembly.Location)); @@ -53,7 +65,10 @@ internal static class VerifyAnalyzer await test.RunAsync(CancellationToken.None); } - private class Test : CSharpAnalyzerTest + + + private sealed class Test : CSharpAnalyzerTest + where TAnalyzer : DiagnosticAnalyzer, new() { public Test() { diff --git a/test/Dapr.Actors.Analyzers.Test/VerifyCodeFix.cs b/test/Dapr.Actors.Analyzers.Test/VerifyCodeFix.cs index f7d0d7eb..d09e427a 100644 --- a/test/Dapr.Actors.Analyzers.Test/VerifyCodeFix.cs +++ b/test/Dapr.Actors.Analyzers.Test/VerifyCodeFix.cs @@ -1,13 +1,26 @@ -using Microsoft.CodeAnalysis.CodeActions; +// ------------------------------------------------------------------------ +// Copyright 2025 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.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CodeFixes; -namespace Dapr.Actors.Analyzers.Test; +namespace Dapr.Actors.Analyzers.Tests; -internal class VerifyCodeFix +internal static class VerifyCodeFix { public static async Task RunTest(string code, string expectedChangedCode) where T : CodeFixProvider, new() { - var (diagnostics, document, workspace) = await Utilities.GetDiagnosticsAdvanced(code); + var (diagnostics, document, workspace) = await TestUtilities.GetDiagnosticsAdvanced(code); Assert.Single(diagnostics); @@ -41,16 +54,17 @@ internal class VerifyCodeFix var updatedDocument = workspace.CurrentSolution.GetDocument(document.Id) ?? throw new Exception("Updated document is null"); var newCode = (await updatedDocument.GetTextAsync()).ToString(); + var normalizedExpectedCode = NormalizeWhitespace(expectedChangedCode); + var normalizedNewCode = NormalizeWhitespace(newCode); + + Assert.Equal(normalizedExpectedCode, normalizedNewCode); + return; + // Normalize whitespace string NormalizeWhitespace(string input) { var separator = new[] { ' ', '\r', '\n' }; return string.Join(" ", input.Split(separator, StringSplitOptions.RemoveEmptyEntries)); } - - var normalizedExpectedCode = NormalizeWhitespace(expectedChangedCode); - var normalizedNewCode = NormalizeWhitespace(newCode); - - Assert.Equal(normalizedExpectedCode, normalizedNewCode); } } diff --git a/test/Dapr.Actors.Analyzers.Tests/ActorRegistrationCodeFixProviderTests.cs b/test/Dapr.Actors.Analyzers.Tests/ActorRegistrationCodeFixProviderTests.cs deleted file mode 100644 index 9ae4afc6..00000000 --- a/test/Dapr.Actors.Analyzers.Tests/ActorRegistrationCodeFixProviderTests.cs +++ /dev/null @@ -1,186 +0,0 @@ -// ------------------------------------------------------------------------ -// Copyright 2025 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.Actors.Analyzers.Tests; - -public class ActorRegistrationCodeFixProviderTests -{ - [Fact] - public async Task RegisterActor() - { - const string code = """ - - using Dapr.Actors.Runtime; - using Microsoft.AspNetCore.Builder; - using Microsoft.Extensions.DependencyInjection; - - public static class Program - { - public static void Main() - { - var builder = WebApplication.CreateBuilder(); - - builder.Services.AddActors(options => - { - options.UseJsonSerialization = true; - }); - var app = builder.Build(); - app.MapActorsHandlers(); - } - } - class TestActor : Actor - { - public TestActor(ActorHost host) : base(host) - { - } - } - - """; - - const string expectedChangedCode = """ - - using Dapr.Actors.Runtime; - using Microsoft.AspNetCore.Builder; - using Microsoft.Extensions.DependencyInjection; - - public static class Program - { - public static void Main() - { - var builder = WebApplication.CreateBuilder(); - - builder.Services.AddActors(options => - { - options.UseJsonSerialization = true; - options.Actors.RegisterActor(); - }); - var app = builder.Build(); - app.MapActorsHandlers(); - } - } - class TestActor : Actor - { - public TestActor(ActorHost host) : base(host) - { - } - } - - """; - - await VerifyCodeFix.RunTest(code, expectedChangedCode); - } - - [Fact] - public async Task RegisterActor_WhenAddActorsIsNotFound() - { - const string code = """ - - using Dapr.Actors.Runtime; - using Microsoft.AspNetCore.Builder; - - public static class Program - { - public static void Main() - { - var builder = WebApplication.CreateBuilder(); - var app = builder.Build(); - } - } - class TestActor : Actor - { - public TestActor(ActorHost host) : base(host) - { - } - } - - """; - - const string expectedChangedCode = """ - - using Dapr.Actors.Runtime; - using Microsoft.AspNetCore.Builder; - - public static class Program - { - public static void Main() - { - var builder = WebApplication.CreateBuilder(); - - builder.Services.AddActors(options => - { - options.Actors.RegisterActor(); - }); - var app = builder.Build(); - } - } - class TestActor : Actor - { - public TestActor(ActorHost host) : base(host) - { - } - } - - """; - - await VerifyCodeFix.RunTest(code, expectedChangedCode); - } - - [Fact] - public async Task RegisterActor_WhenAddActorsIsNotFound_TopLevelStatements() - { - const string code = """ - - using Dapr.Actors.Runtime; - using Microsoft.AspNetCore.Builder; - var builder = WebApplication.CreateBuilder(); - - var app = builder.Build(); - namespace TestNamespace - { - class TestActor : Actor - { - public TestActor(ActorHost host) : base(host) - { - } - } - } - - """; - - const string expectedChangedCode = """ - - using Dapr.Actors.Runtime; - using Microsoft.AspNetCore.Builder; - - var builder = WebApplication.CreateBuilder(); - - builder.Services.AddActors(options => - { - options.Actors.RegisterActor(); - }); - var app = builder.Build(); - namespace TestNamespace - { - class TestActor : Actor - { - public TestActor(ActorHost host) : base(host) - { - } - } - } - - """; - - await VerifyCodeFix.RunTest(code, expectedChangedCode); - } -} diff --git a/test/Dapr.Actors.Analyzers.Tests/Dapr.Actors.Analyzers.Tests.csproj b/test/Dapr.Actors.Analyzers.Tests/Dapr.Actors.Analyzers.Tests.csproj deleted file mode 100644 index 482379a8..00000000 --- a/test/Dapr.Actors.Analyzers.Tests/Dapr.Actors.Analyzers.Tests.csproj +++ /dev/null @@ -1,38 +0,0 @@ - - - - enable - enable - false - true - - - - - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - - - - - - diff --git a/test/Dapr.Actors.Analyzers.Tests/VerifyAnalyzer.cs b/test/Dapr.Actors.Analyzers.Tests/VerifyAnalyzer.cs deleted file mode 100644 index 87136e41..00000000 --- a/test/Dapr.Actors.Analyzers.Tests/VerifyAnalyzer.cs +++ /dev/null @@ -1,83 +0,0 @@ -// ------------------------------------------------------------------------ -// Copyright 2025 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.AspNetCore.Builder; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Testing; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Testing; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - -namespace Dapr.Actors.Analyzers.Tests; - -internal static class VerifyAnalyzer -{ - public static DiagnosticResult Diagnostic(DiagnosticDescriptor descriptor) => new(descriptor); - - public static async Task VerifyAnalyzerAsync(string source, params DiagnosticResult[] expected) - where TAnalyzer : DiagnosticAnalyzer, new() - { - await VerifyAnalyzerAsync(source, null, expected); - } - - public static async Task VerifyAnalyzerAsync(string source, string? program, params DiagnosticResult[] expected) - where TAnalyzer : DiagnosticAnalyzer, new() - { - var test = new Test { TestCode = source }; - -#if NET8_0 - test.ReferenceAssemblies = ReferenceAssemblies.Net.Net80; -#elif NET9_0 - test.ReferenceAssemblies = ReferenceAssemblies.Net.Net90; -#endif - - if (program != null) - { - test.TestState.Sources.Add(("Program.cs", program)); - } - - var metadataReferences = TestUtilities.GetAllReferencesNeededForType(typeof(ActorRegistrationAnalyzer)).ToList(); - metadataReferences.AddRange(TestUtilities.GetAllReferencesNeededForType(typeof(MappedActorHandlersAnalyzer))); - metadataReferences.AddRange(TestUtilities.GetAllReferencesNeededForType(typeof(PreferActorJsonSerializationAnalyzer))); - metadataReferences.AddRange(TestUtilities.GetAllReferencesNeededForType(typeof(TimerCallbackMethodPresentAnalyzer))); - metadataReferences.AddRange(TestUtilities.GetAllReferencesNeededForType(typeof(ActorsServiceCollectionExtensions))); - metadataReferences.Add(MetadataReference.CreateFromFile(typeof(object).Assembly.Location)); - metadataReferences.Add(MetadataReference.CreateFromFile(typeof(WebApplication).Assembly.Location)); - metadataReferences.Add(MetadataReference.CreateFromFile(typeof(IHost).Assembly.Location)); - - foreach (var reference in metadataReferences) - { - test.TestState.AdditionalReferences.Add(reference); - } - - test.ExpectedDiagnostics.AddRange(expected); - await test.RunAsync(CancellationToken.None); - } - - - - private sealed class Test : CSharpAnalyzerTest - where TAnalyzer : DiagnosticAnalyzer, new() - { - public Test() - { - SolutionTransforms.Add((solution, projectId) => - { - var compilationOptions = solution.GetProject(projectId)!.CompilationOptions!; - solution = solution.WithProjectCompilationOptions(projectId, compilationOptions); - return solution; - }); - } - } -} diff --git a/test/Dapr.Actors.Analyzers.Tests/VerifyCodeFix.cs b/test/Dapr.Actors.Analyzers.Tests/VerifyCodeFix.cs deleted file mode 100644 index d09e427a..00000000 --- a/test/Dapr.Actors.Analyzers.Tests/VerifyCodeFix.cs +++ /dev/null @@ -1,70 +0,0 @@ -// ------------------------------------------------------------------------ -// Copyright 2025 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.CodeAnalysis.CodeActions; -using Microsoft.CodeAnalysis.CodeFixes; - -namespace Dapr.Actors.Analyzers.Tests; - -internal static class VerifyCodeFix -{ - public static async Task RunTest(string code, string expectedChangedCode) where T : CodeFixProvider, new() - { - var (diagnostics, document, workspace) = await TestUtilities.GetDiagnosticsAdvanced(code); - - Assert.Single(diagnostics); - - var diagnostic = diagnostics[0]; - - var codeFixProvider = new T(); - - CodeAction? registeredCodeAction = null; - - var context = new CodeFixContext(document, diagnostic, (codeAction, _) => - { - if (registeredCodeAction != null) - throw new Exception("Code action was registered more than once"); - - registeredCodeAction = codeAction; - - }, CancellationToken.None); - - await codeFixProvider.RegisterCodeFixesAsync(context); - - if (registeredCodeAction == null) - throw new Exception("Code action was not registered"); - - var operations = await registeredCodeAction.GetOperationsAsync(CancellationToken.None); - - foreach (var operation in operations) - { - operation.Apply(workspace, CancellationToken.None); - } - - var updatedDocument = workspace.CurrentSolution.GetDocument(document.Id) ?? throw new Exception("Updated document is null"); - var newCode = (await updatedDocument.GetTextAsync()).ToString(); - - var normalizedExpectedCode = NormalizeWhitespace(expectedChangedCode); - var normalizedNewCode = NormalizeWhitespace(newCode); - - Assert.Equal(normalizedExpectedCode, normalizedNewCode); - return; - - // Normalize whitespace - string NormalizeWhitespace(string input) - { - var separator = new[] { ' ', '\r', '\n' }; - return string.Join(" ", input.Split(separator, StringSplitOptions.RemoveEmptyEntries)); - } - } -} From 34f36d9295093c722dedb563e38b449cc8af8e6d Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Wed, 9 Apr 2025 19:35:50 -0500 Subject: [PATCH 10/22] Adding Analyzer documentation, consolidating diagnostic IDs (#1515) * Added initial pass at documentation for the various analyzers. Placing the documentation link above the "Error handling" section in leftnav. * Updated diagnostic IDs throughout solution to match values in documentation Signed-off-by: Whit Waldo --- .../dotnet-code-analysis/_index.md | 79 +++++++++++++++++++ .../ActorRegistrationAnalyzer.cs | 6 +- .../AnalyzerReleases.Shipped.md | 8 +- .../MappedActorHandlersAnalyzer.cs | 6 +- .../PreferActorJsonSerializationAnalyzer.cs | 6 +- .../Resources.Designer.cs | 32 ++++---- .../Dapr.Actors.Analyzers/Resources.resx | 16 ++-- .../TimerCallbackMethodPresentAnalyzer.cs | 6 +- .../AnalyzerReleases.Shipped.md | 2 +- .../MapDaprScheduledJobHandlerAnalyzer.cs | 6 +- src/Dapr.Jobs.Analyzer/Resources.Designer.cs | 8 +- src/Dapr.Jobs.Analyzer/Resources.resx | 4 +- 12 files changed, 129 insertions(+), 50 deletions(-) create mode 100644 daprdocs/content/en/dotnet-sdk-docs/dotnet-code-analysis/_index.md diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-code-analysis/_index.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-code-analysis/_index.md new file mode 100644 index 00000000..73d37df6 --- /dev/null +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-code-analysis/_index.md @@ -0,0 +1,79 @@ +--- +type: docs +title: "Overview of Dapr source code analysis" +linkTitle: "Code Analysis" +weight: 70000 +description: Code analyzers and fixes for common Dapr issues +no_list: true +--- + +Dapr supports a growing collection of optional Roslyn analyzers and code fix providers that inspect your code for +code quality issues. Starting with the release of v1.16, developers have the opportunity to install additional projects +from NuGet alongside each of the standard capability packages to enable these analyzers in their solutions. + +{{% alert title="Note" color="primary" %}} + +A future release of the Dapr .NET SDK will include these analyzers by default without necessitating a separate package +install. + +{{% /alert %}} + +Rule violations will typically be marked as `Info` or `Warning` so that if the analyzer identifies an issue, it won't +necessarily break builds. All code analysis violations appear with the prefix "DAPR" and are uniquely distinguished +by a number following this prefix. + +{{% alert title="Note" color="primary" %}} + +At this time, the first two digits of the diagnostic identifier map one-to-one to distinct Dapr packages, but this +is subject to change in the future as more analyzers are developed. + +{{% /alert %}} + +## Install and configure analyzers +The following packages will be available via NuGet following the v1.16 Dapr release: +- Dapr.Actors.Analyzers +- Dapr.Jobs.Analyzers + +Install each NuGet package on every project where you want the analyzers to run. The package will be installed as a +project dependency and analyzers will run as you write your code or as part of a CI/CD build. The analyzers will flag +issues in your existing code and warn you about new issues as you build your project. + +Many of our analyzers have associated code fixes that can be applied to automatically correct the problem. If your IDE +supports this capability, any available code fixes will show up as an inline menu option in your code. + +Further, most of our analyzers should also report a specific line and column number in your code of the syntax that's +been identified as a key aspect of the rule. If your IDE supports it, double clicking any of the analyzer warnings +should jump directly to the part of your code responsible for the violating the analyzer's rule. + +### Suppress specific analyzers +If you wish to keep an analyzer from firing against some particular piece of your project, their outputs can be +individually targeted for suppression through a number of ways. Read more about suppressing analyzers in projects +or files in the associated [.NET documentation](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/suppress-warnings#use-the-suppressmessageattribute). + +### Disable all analyzers +If you wish to disable all analyzers in your project without removing any packages providing them, set +the `EnableNETAnalyzers` property to `false` in your csproj file. + +## Available Analyzers + +| Diagnostic ID | Dapr Package | Category | Severity | Version Added | Description | Code Fix Available | +| -- | -- |------------------| -- | -- | -- | -- | +| DAPR1401 | Dapr.Actors | Usage | Warning | 1.16 | Actor timer method invocations require the named callback method to exist on type | No | +| DAPR1402 | Dapr.Actors | Usage | Warning | The actor type is not registered with dependency injection | Yes | +| DAPR1403 | Dapr.Actors | Interoperability | Info | Set options.UseJsonSerialization to true to support interoperability with non-.NET actors | Yes | +| DAPR1404 | Dapr.Actors | Usage | Warning | Call app.MapActorsHandlers to map endpoints for Dapr actors | Yes | +| DAPR1501 | Dapr.Jobs | Usage | Warning | Job invocations require the MapDaprScheduledJobHandler to be set and configured for each anticipated job on IEndpointRouteBuilder | No | + +## Analyzer Categories +The following are each of the eligible categories that an analyzer can be assigned to and are modeled after the +standard categories used by the.NET analyzers: +- Design +- Documentation +- Globalization +- Interoperability +- Maintainability +- Naming +- Performance +- Reliability +- Security +- Usage \ No newline at end of file diff --git a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/ActorRegistrationAnalyzer.cs b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/ActorRegistrationAnalyzer.cs index 164da75b..b2ab8af7 100644 --- a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/ActorRegistrationAnalyzer.cs +++ b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/ActorRegistrationAnalyzer.cs @@ -27,10 +27,10 @@ namespace Dapr.Actors.Analyzers; public sealed class ActorRegistrationAnalyzer : DiagnosticAnalyzer { internal static readonly DiagnosticDescriptor DiagnosticDescriptorActorRegistration = new( - id: "DAPR4002", - title: new LocalizableResourceString(nameof(Resources.DAPR4002Title), Resources.ResourceManager, + id: "DAPR1402", + title: new LocalizableResourceString(nameof(Resources.DAPR1402Title), Resources.ResourceManager, typeof(Resources)), - messageFormat: new LocalizableResourceString(nameof(Resources.DAPR4002MessageFormat), Resources.ResourceManager, + messageFormat: new LocalizableResourceString(nameof(Resources.DAPR1402MessageFormat), Resources.ResourceManager, typeof(Resources)), category: "Usage", DiagnosticSeverity.Warning, diff --git a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/AnalyzerReleases.Shipped.md b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/AnalyzerReleases.Shipped.md index f82775b1..b56e3ab4 100644 --- a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/AnalyzerReleases.Shipped.md +++ b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/AnalyzerReleases.Shipped.md @@ -6,7 +6,7 @@ Rule ID | Category | Severity | Notes --------|----------|----------|------------------------------------------------------------------------------------ -DAPR4001 | Usage | Warning | Actor timer method invocations require the named callback method to exist on type. -DAPR4002 | Usage | Warning | The actor type is not registered with dependency injection -DAPR4003 | Usage | Info | Set options.UseJsonSerialization to true to support interoperability with non-.NET actors -DAPR4004 | Usage | Warning | Call app.MapActorsHandlers to map endpoints for Dapr actors. +DAPR1401 | Usage | Warning | Actor timer method invocations require the named callback method to exist on type. +DAPR1402 | Usage | Warning | The actor type is not registered with dependency injection +DAPR1403 | Usage | Info | Set options.UseJsonSerialization to true to support interoperability with non-.NET actors +DAPR1404 | Usage | Warning | Call app.MapActorsHandlers to map endpoints for Dapr actors. diff --git a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/MappedActorHandlersAnalyzer.cs b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/MappedActorHandlersAnalyzer.cs index c85a8066..6bb276f4 100644 --- a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/MappedActorHandlersAnalyzer.cs +++ b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/MappedActorHandlersAnalyzer.cs @@ -27,10 +27,10 @@ namespace Dapr.Actors.Analyzers; public sealed class MappedActorHandlersAnalyzer : DiagnosticAnalyzer { internal static readonly DiagnosticDescriptor DiagnosticDescriptorMapActorsHandlers = new( - id: "DAPR4004", - title: new LocalizableResourceString(nameof(Resources.DAPR4004Title), Resources.ResourceManager, + id: "DAPR1404", + title: new LocalizableResourceString(nameof(Resources.DAPR1404Title), Resources.ResourceManager, typeof(Resources)), - messageFormat: new LocalizableResourceString(nameof(Resources.DAPR4004MessageFormat), Resources.ResourceManager, + messageFormat: new LocalizableResourceString(nameof(Resources.DAPR1404MessageFormat), Resources.ResourceManager, typeof(Resources)), category: "Usage", DiagnosticSeverity.Warning, diff --git a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/PreferActorJsonSerializationAnalyzer.cs b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/PreferActorJsonSerializationAnalyzer.cs index 9d36454d..17f22d0e 100644 --- a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/PreferActorJsonSerializationAnalyzer.cs +++ b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/PreferActorJsonSerializationAnalyzer.cs @@ -27,10 +27,10 @@ namespace Dapr.Actors.Analyzers; public sealed class PreferActorJsonSerializationAnalyzer : DiagnosticAnalyzer { internal static readonly DiagnosticDescriptor DiagnosticDescriptorJsonSerialization = new( - id: "DAPR4003", - title: new LocalizableResourceString(nameof(Resources.DAPR4003Title), Resources.ResourceManager, + id: "DAPR1403", + title: new LocalizableResourceString(nameof(Resources.DAPR1403Title), Resources.ResourceManager, typeof(Resources)), - messageFormat: new LocalizableResourceString(nameof(Resources.DAPR4003MessageFormat), Resources.ResourceManager, + messageFormat: new LocalizableResourceString(nameof(Resources.DAPR1403MessageFormat), Resources.ResourceManager, typeof(Resources)), category: "Usage", DiagnosticSeverity.Info, diff --git a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Resources.Designer.cs b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Resources.Designer.cs index ac9c06d8..35b7bb0f 100644 --- a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Resources.Designer.cs +++ b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Resources.Designer.cs @@ -62,72 +62,72 @@ namespace Dapr.Actors.Analyzers { /// /// Looks up a localized string similar to Actor timer method invocations require the named callback '{0}' method to exist on type '{1}'. /// - internal static string DAPR4001MessageFormat { + internal static string DAPR1401MessageFormat { get { - return ResourceManager.GetString("DAPR4001MessageFormat", resourceCulture); + return ResourceManager.GetString("DAPR1401MessageFormat", resourceCulture); } } /// /// Looks up a localized string similar to Actor timer method invocations require the named callback method to exist on type. /// - internal static string DAPR4001Title { + internal static string DAPR1401Title { get { - return ResourceManager.GetString("DAPR4001Title", resourceCulture); + return ResourceManager.GetString("DAPR1401Title", resourceCulture); } } /// /// Looks up a localized string similar to The actor type '{0}' is not registered with dependency injection. /// - internal static string DAPR4002MessageFormat { + internal static string DAPR1402MessageFormat { get { - return ResourceManager.GetString("DAPR4002MessageFormat", resourceCulture); + return ResourceManager.GetString("DAPR1402MessageFormat", resourceCulture); } } /// /// Looks up a localized string similar to The actor type is not registered with dependency injection. /// - internal static string DAPR4002Title { + internal static string DAPR1402Title { get { - return ResourceManager.GetString("DAPR4002Title", resourceCulture); + return ResourceManager.GetString("DAPR1402Title", resourceCulture); } } /// /// Looks up a localized string similar to Set options.UseJsonSerialization to true to support interoperability with non-.NET actors. /// - internal static string DAPR4003MessageFormat { + internal static string DAPR1403MessageFormat { get { - return ResourceManager.GetString("DAPR4003MessageFormat", resourceCulture); + return ResourceManager.GetString("DAPR1403MessageFormat", resourceCulture); } } /// /// Looks up a localized string similar to Set options.UseJsonSerialization to true to support interoperability with non-.NET actors. /// - internal static string DAPR4003Title { + internal static string DAPR1403Title { get { - return ResourceManager.GetString("DAPR4003Title", resourceCulture); + return ResourceManager.GetString("DAPR1403Title", resourceCulture); } } /// /// Looks up a localized string similar to Call app.MapActorsHandlers to map endpoints for Dapr actors. /// - internal static string DAPR4004MessageFormat { + internal static string DAPR1404MessageFormat { get { - return ResourceManager.GetString("DAPR4004MessageFormat", resourceCulture); + return ResourceManager.GetString("DAPR1404MessageFormat", resourceCulture); } } /// /// Looks up a localized string similar to Call app.MapActorsHandlers to map endpoints for Dapr actors. /// - internal static string DAPR4004Title { + internal static string DAPR1404Title { get { - return ResourceManager.GetString("DAPR4004Title", resourceCulture); + return ResourceManager.GetString("DAPR1404Title", resourceCulture); } } } diff --git a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Resources.resx b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Resources.resx index 93c31362..b84c1243 100644 --- a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Resources.resx +++ b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Resources.resx @@ -18,28 +18,28 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - + Actor timer method invocations require the named callback method to exist on type - + Actor timer method invocations require the named callback '{0}' method to exist on type '{1}' - + The actor type is not registered with dependency injection - + The actor type '{0}' is not registered with dependency injection - + Set options.UseJsonSerialization to true to support interoperability with non-.NET actors - + Set options.UseJsonSerialization to true to support interoperability with non-.NET actors - + Call app.MapActorsHandlers to map endpoints for Dapr actors - + Call app.MapActorsHandlers to map endpoints for Dapr actors \ No newline at end of file diff --git a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/TimerCallbackMethodPresentAnalyzer.cs b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/TimerCallbackMethodPresentAnalyzer.cs index 49f03ece..7927e2f8 100644 --- a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/TimerCallbackMethodPresentAnalyzer.cs +++ b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/TimerCallbackMethodPresentAnalyzer.cs @@ -27,9 +27,9 @@ namespace Dapr.Actors.Analyzers; public sealed class TimerCallbackMethodPresentAnalyzer : DiagnosticAnalyzer { internal static readonly DiagnosticDescriptor DaprTimerCallbackMethodRule = new( - id: "DAPR4001", - title: new LocalizableResourceString(nameof(Resources.DAPR4001Title), Resources.ResourceManager, typeof(Resources)), - messageFormat: new LocalizableResourceString(nameof(Resources.DAPR4001MessageFormat), Resources.ResourceManager, typeof(Resources)), + id: "DAPR1401", + title: new LocalizableResourceString(nameof(Resources.DAPR1401Title), Resources.ResourceManager, typeof(Resources)), + messageFormat: new LocalizableResourceString(nameof(Resources.DAPR1401MessageFormat), Resources.ResourceManager, typeof(Resources)), category: "Usage", DiagnosticSeverity.Warning, isEnabledByDefault: true diff --git a/src/Dapr.Jobs.Analyzer/AnalyzerReleases.Shipped.md b/src/Dapr.Jobs.Analyzer/AnalyzerReleases.Shipped.md index 5fab0bb6..e7144fde 100644 --- a/src/Dapr.Jobs.Analyzer/AnalyzerReleases.Shipped.md +++ b/src/Dapr.Jobs.Analyzer/AnalyzerReleases.Shipped.md @@ -4,4 +4,4 @@ Rule ID | Category | Severity | Notes --------|----------|----------|-------------------- -DAPR3001| Usage | Warning | Job invocations require the MapDaprScheduledJobHandler be set and configured for each anticipated job on IEndpointRouteBuilder \ No newline at end of file +DAPR1501 | Usage | Warning | Job invocations require the MapDaprScheduledJobHandler be set and configured for each anticipated job on IEndpointRouteBuilder \ No newline at end of file diff --git a/src/Dapr.Jobs.Analyzer/MapDaprScheduledJobHandlerAnalyzer.cs b/src/Dapr.Jobs.Analyzer/MapDaprScheduledJobHandlerAnalyzer.cs index bcfd9bae..3d7eede3 100644 --- a/src/Dapr.Jobs.Analyzer/MapDaprScheduledJobHandlerAnalyzer.cs +++ b/src/Dapr.Jobs.Analyzer/MapDaprScheduledJobHandlerAnalyzer.cs @@ -14,9 +14,9 @@ namespace Dapr.Jobs.Analyzers public sealed class MapDaprScheduledJobHandlerAnalyzer : DiagnosticAnalyzer { internal static readonly DiagnosticDescriptor DaprJobHandlerRule = new ( - id: "DAPR3001", - title: new LocalizableResourceString(nameof(Resources.DAPR3001Title), Resources.ResourceManager, typeof(Resources)), - messageFormat: new LocalizableResourceString(nameof(Resources.DAPR3001MessageFormat), Resources.ResourceManager, typeof(Resources)), + id: "DAPR1501", + title: new LocalizableResourceString(nameof(Resources.DAPR1501Title), Resources.ResourceManager, typeof(Resources)), + messageFormat: new LocalizableResourceString(nameof(Resources.DAPR1501MessageFormat), Resources.ResourceManager, typeof(Resources)), category: "Usage", DiagnosticSeverity.Warning, isEnabledByDefault: true diff --git a/src/Dapr.Jobs.Analyzer/Resources.Designer.cs b/src/Dapr.Jobs.Analyzer/Resources.Designer.cs index 409e2ec6..0b301048 100644 --- a/src/Dapr.Jobs.Analyzer/Resources.Designer.cs +++ b/src/Dapr.Jobs.Analyzer/Resources.Designer.cs @@ -62,18 +62,18 @@ namespace Dapr.Jobs.Analyzers { /// /// Looks up a localized string similar to Job invocations require the MapDaprScheduledJobHandler be set and configured for job name '{0}' on IEndpointRouteBuilder. /// - internal static string DAPR3001MessageFormat { + internal static string DAPR1501MessageFormat { get { - return ResourceManager.GetString("DAPR3001MessageFormat", resourceCulture); + return ResourceManager.GetString("DAPR1501MessageFormat", resourceCulture); } } /// /// Looks up a localized string similar to Ensure handler endpoint is present for all scheduled Jobs. /// - internal static string DAPR3001Title { + internal static string DAPR1501Title { get { - return ResourceManager.GetString("DAPR3001Title", resourceCulture); + return ResourceManager.GetString("DAPR1501Title", resourceCulture); } } } diff --git a/src/Dapr.Jobs.Analyzer/Resources.resx b/src/Dapr.Jobs.Analyzer/Resources.resx index aaef9fdc..b548c4cd 100644 --- a/src/Dapr.Jobs.Analyzer/Resources.resx +++ b/src/Dapr.Jobs.Analyzer/Resources.resx @@ -18,10 +18,10 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - + Ensure handler endpoint is present for all scheduled Jobs - + Job invocations require the MapDaprScheduledJobHandler be set and configured for job name '{0}' on IEndpointRouteBuilder \ No newline at end of file From d1c820fde8e78ebc16544f0db73957e112db91fb Mon Sep 17 00:00:00 2001 From: Nils Gruson Date: Thu, 10 Apr 2025 07:49:39 +0200 Subject: [PATCH 11/22] Add Roslyn analyzer for workflows (#1440) Introduced two diagnostic analyzers: `WorkflowRegistrationAnalyzer` and `WorkflowActivityRegistrationAnalyzer` that validate that both any types that implement `Workflow<,>` or `WorkflowActivity<,>` have a corresponding registration entry in the DI provider. This PR also includes a code fix provider for either analyzer that provides a remedial registration for the developer. Signed-off-by: Nils Gruson Co-authored-by: Whit Waldo --- all.sln | 21 ++ .../dotnet-code-analysis/_index.md | 17 +- .../Resources.Designer.cs | 4 +- .../Dapr.Actors.Analyzers/Resources.resx | 4 +- .../AnalyzerReleases.Shipped.md | 8 + .../AnalyzerReleases.Unshipped.md | 3 + .../Dapr.Workflow.Analyzers.csproj | 53 ++++ .../Resources.Designer.cs | 98 +++++++ src/Dapr.Workflow.Analyzers/Resources.resx | 33 +++ .../WorkflowActivityRegistrationAnalyzer.cs | 143 +++++++++++ ...flowActivityRegistrationCodeFixProvider.cs | 128 +++++++++ .../WorkflowRegistrationAnalyzer.cs | 122 +++++++++ .../WorkflowRegistrationCodeFixProvider.cs | 242 ++++++++++++++++++ src/Dapr.Workflow/Dapr.Workflow.csproj | 1 - .../ActorRegistrationAnalyzerTests.cs | 27 +- .../ActorRegistrationCodeFixProviderTests.cs | 12 +- .../Dapr.Actors.Analyzers.Test.csproj | 4 +- .../MappedActorHandlersAnalyzerTests.cs | 9 +- ...MappedActorHandlersCodeFixProviderTests.cs | 105 ++++---- ...eferActorJsonSerializationAnalyzerTests.cs | 9 +- ...orJsonSerializationCodeFixProviderTests.cs | 6 +- test/Dapr.Actors.Analyzers.Test/Utilities.cs | 48 ++++ .../Dapr.Analyzers.Common.csproj | 24 ++ .../TestUtilities.cs | 47 ++-- .../VerifyAnalyzer.cs | 30 +-- .../VerifyCodeFix.cs | 21 +- .../Dapr.Jobs.Analyzers.Test.csproj | 1 + .../MapDaprScheduledJobHandlerTest.cs | 70 ++--- test/Dapr.Jobs.Analyzer.Test/Utilities.cs | 43 ++++ .../Dapr.Workflow.Analyzers.Test.csproj | 39 +++ .../GlobalUsings.cs | 14 + .../TestModels.cs | 4 + .../Dapr.Workflow.Analyzers.Test/Utilities.cs | 35 +++ ...rkflowActivityRegistrationAnalyzerTests.cs | 99 +++++++ ...ctivityRegistrationCodeFixProviderTests.cs | 93 +++++++ .../WorkflowRegistrationAnalyzerTests.cs | 79 ++++++ ...orkflowRegistrationCodeFixProviderTests.cs | 226 ++++++++++++++++ 37 files changed, 1749 insertions(+), 173 deletions(-) create mode 100644 src/Dapr.Workflow.Analyzers/AnalyzerReleases.Shipped.md create mode 100644 src/Dapr.Workflow.Analyzers/AnalyzerReleases.Unshipped.md create mode 100644 src/Dapr.Workflow.Analyzers/Dapr.Workflow.Analyzers.csproj create mode 100644 src/Dapr.Workflow.Analyzers/Resources.Designer.cs create mode 100644 src/Dapr.Workflow.Analyzers/Resources.resx create mode 100644 src/Dapr.Workflow.Analyzers/WorkflowActivityRegistrationAnalyzer.cs create mode 100644 src/Dapr.Workflow.Analyzers/WorkflowActivityRegistrationCodeFixProvider.cs create mode 100644 src/Dapr.Workflow.Analyzers/WorkflowRegistrationAnalyzer.cs create mode 100644 src/Dapr.Workflow.Analyzers/WorkflowRegistrationCodeFixProvider.cs create mode 100644 test/Dapr.Actors.Analyzers.Test/Utilities.cs create mode 100644 test/Dapr.Analyzers.Common/Dapr.Analyzers.Common.csproj rename test/{Dapr.Actors.Analyzers.Test => Dapr.Analyzers.Common}/TestUtilities.cs (65%) rename test/{Dapr.Actors.Analyzers.Test => Dapr.Analyzers.Common}/VerifyAnalyzer.cs (62%) rename test/{Dapr.Actors.Analyzers.Test => Dapr.Analyzers.Common}/VerifyCodeFix.cs (75%) create mode 100644 test/Dapr.Jobs.Analyzer.Test/Utilities.cs create mode 100644 test/Dapr.Workflow.Analyzers.Test/Dapr.Workflow.Analyzers.Test.csproj create mode 100644 test/Dapr.Workflow.Analyzers.Test/GlobalUsings.cs create mode 100644 test/Dapr.Workflow.Analyzers.Test/TestModels.cs create mode 100644 test/Dapr.Workflow.Analyzers.Test/Utilities.cs create mode 100644 test/Dapr.Workflow.Analyzers.Test/WorkflowActivityRegistrationAnalyzerTests.cs create mode 100644 test/Dapr.Workflow.Analyzers.Test/WorkflowActivityRegistrationCodeFixProviderTests.cs create mode 100644 test/Dapr.Workflow.Analyzers.Test/WorkflowRegistrationAnalyzerTests.cs create mode 100644 test/Dapr.Workflow.Analyzers.Test/WorkflowRegistrationCodeFixProviderTests.cs diff --git a/all.sln b/all.sln index b41526cf..ae51012e 100644 --- a/all.sln +++ b/all.sln @@ -155,6 +155,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JobsSample", "examples\Jobs EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Workflow.Test", "test\Dapr.Workflow.Test\Dapr.Workflow.Test.csproj", "{E90114C6-86FC-43B8-AE5C-D9273CF21FE4}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Workflow.Analyzers", "src\Dapr.Workflow.Analyzers\Dapr.Workflow.Analyzers.csproj", "{55A7D436-CC8C-47E6-B43A-DFE32E0FE38C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Workflow.Analyzers.Test", "test\Dapr.Workflow.Analyzers.Test\Dapr.Workflow.Analyzers.Test.csproj", "{CE0D5FEB-F6DB-4EB8-B8A9-6A4A32944539}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Jobs.Analyzers", "src\Dapr.Jobs.Analyzer\Dapr.Jobs.Analyzers.csproj", "{28B87C37-4B52-400F-B84D-64F134931BDC}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Jobs.Analyzers.Test", "test\Dapr.Jobs.Analyzer.Test\Dapr.Jobs.Analyzers.Test.csproj", "{CADEAE45-8981-4723-B641-9C28251C7D3B}" @@ -163,6 +167,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Actors.Analyzers", "sr EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Actors.Analyzers.Test", "test\Dapr.Actors.Analyzers.Test\Dapr.Actors.Analyzers.Test.csproj", "{A2C0F203-11FF-4B7F-A94F-B9FD873573FE}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Analyzers.Common", "test\Dapr.Analyzers.Common\Dapr.Analyzers.Common.csproj", "{7E23E229-6823-4D84-AF3A-AE14CEAEF52A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -411,6 +417,14 @@ Global {E90114C6-86FC-43B8-AE5C-D9273CF21FE4}.Debug|Any CPU.Build.0 = Debug|Any CPU {E90114C6-86FC-43B8-AE5C-D9273CF21FE4}.Release|Any CPU.ActiveCfg = Release|Any CPU {E90114C6-86FC-43B8-AE5C-D9273CF21FE4}.Release|Any CPU.Build.0 = Release|Any CPU + {55A7D436-CC8C-47E6-B43A-DFE32E0FE38C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {55A7D436-CC8C-47E6-B43A-DFE32E0FE38C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {55A7D436-CC8C-47E6-B43A-DFE32E0FE38C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {55A7D436-CC8C-47E6-B43A-DFE32E0FE38C}.Release|Any CPU.Build.0 = Release|Any CPU + {CE0D5FEB-F6DB-4EB8-B8A9-6A4A32944539}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CE0D5FEB-F6DB-4EB8-B8A9-6A4A32944539}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CE0D5FEB-F6DB-4EB8-B8A9-6A4A32944539}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CE0D5FEB-F6DB-4EB8-B8A9-6A4A32944539}.Release|Any CPU.Build.0 = Release|Any CPU {28B87C37-4B52-400F-B84D-64F134931BDC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {28B87C37-4B52-400F-B84D-64F134931BDC}.Debug|Any CPU.Build.0 = Debug|Any CPU {28B87C37-4B52-400F-B84D-64F134931BDC}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -427,6 +441,10 @@ Global {A2C0F203-11FF-4B7F-A94F-B9FD873573FE}.Debug|Any CPU.Build.0 = Debug|Any CPU {A2C0F203-11FF-4B7F-A94F-B9FD873573FE}.Release|Any CPU.ActiveCfg = Release|Any CPU {A2C0F203-11FF-4B7F-A94F-B9FD873573FE}.Release|Any CPU.Build.0 = Release|Any CPU + {7E23E229-6823-4D84-AF3A-AE14CEAEF52A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7E23E229-6823-4D84-AF3A-AE14CEAEF52A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7E23E229-6823-4D84-AF3A-AE14CEAEF52A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7E23E229-6823-4D84-AF3A-AE14CEAEF52A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -500,10 +518,13 @@ Global {D9697361-232F-465D-A136-4561E0E88488} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78} {9CAF360E-5AD3-4C4F-89A0-327EEB70D673} = {D9697361-232F-465D-A136-4561E0E88488} {E90114C6-86FC-43B8-AE5C-D9273CF21FE4} = {DD020B34-460F-455F-8D17-CF4A949F100B} + {55A7D436-CC8C-47E6-B43A-DFE32E0FE38C} = {27C5D71D-0721-4221-9286-B94AB07B58CF} + {CE0D5FEB-F6DB-4EB8-B8A9-6A4A32944539} = {DD020B34-460F-455F-8D17-CF4A949F100B} {28B87C37-4B52-400F-B84D-64F134931BDC} = {27C5D71D-0721-4221-9286-B94AB07B58CF} {CADEAE45-8981-4723-B641-9C28251C7D3B} = {DD020B34-460F-455F-8D17-CF4A949F100B} {E49C822C-E921-48DF-897B-3E603CA596D2} = {27C5D71D-0721-4221-9286-B94AB07B58CF} {A2C0F203-11FF-4B7F-A94F-B9FD873573FE} = {DD020B34-460F-455F-8D17-CF4A949F100B} + {7E23E229-6823-4D84-AF3A-AE14CEAEF52A} = {DD020B34-460F-455F-8D17-CF4A949F100B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40} diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-code-analysis/_index.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-code-analysis/_index.md index 73d37df6..d04ace66 100644 --- a/daprdocs/content/en/dotnet-sdk-docs/dotnet-code-analysis/_index.md +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-code-analysis/_index.md @@ -33,6 +33,7 @@ is subject to change in the future as more analyzers are developed. The following packages will be available via NuGet following the v1.16 Dapr release: - Dapr.Actors.Analyzers - Dapr.Jobs.Analyzers +- Dapr.Workflow.Analyzers Install each NuGet package on every project where you want the analyzers to run. The package will be installed as a project dependency and analyzers will run as you write your code or as part of a CI/CD build. The analyzers will flag @@ -56,13 +57,15 @@ the `EnableNETAnalyzers` property to `false` in your csproj file. ## Available Analyzers -| Diagnostic ID | Dapr Package | Category | Severity | Version Added | Description | Code Fix Available | -| -- | -- |------------------| -- | -- | -- | -- | -| DAPR1401 | Dapr.Actors | Usage | Warning | 1.16 | Actor timer method invocations require the named callback method to exist on type | No | -| DAPR1402 | Dapr.Actors | Usage | Warning | The actor type is not registered with dependency injection | Yes | -| DAPR1403 | Dapr.Actors | Interoperability | Info | Set options.UseJsonSerialization to true to support interoperability with non-.NET actors | Yes | -| DAPR1404 | Dapr.Actors | Usage | Warning | Call app.MapActorsHandlers to map endpoints for Dapr actors | Yes | -| DAPR1501 | Dapr.Jobs | Usage | Warning | Job invocations require the MapDaprScheduledJobHandler to be set and configured for each anticipated job on IEndpointRouteBuilder | No | +| Diagnostic ID | Dapr Package | Category | Severity | Version Added | Description | Code Fix Available | +| -- | -- |------------------|--------------|-----------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------| -- | +| DAPR1301 | Dapr.Workflow | Usage | Warning | 1.16 | The workflow type is not registered with the dependency injection provider | Yes | +| DAPR1302 | Dapr.Workflow | Usage | Warning | 1.16 | The workflow activity type is not registered with the dependency injection provider | Yes | +| DAPR1401 | Dapr.Actors | Usage | Warning | 1.16 | Actor timer method invocations require the named callback method to exist on type | No | +| DAPR1402 | Dapr.Actors | Usage | Warning | The actor type is not registered with dependency injection | Yes | +| DAPR1403 | Dapr.Actors | Interoperability | Info | Set options.UseJsonSerialization to true to support interoperability with non-.NET actors | Yes | +| DAPR1404 | Dapr.Actors | Usage | Warning | Call app.MapActorsHandlers to map endpoints for Dapr actors | Yes | +| DAPR1501 | Dapr.Jobs | Usage | Warning | Job invocations require the MapDaprScheduledJobHandler to be set and configured for each anticipated job on IEndpointRouteBuilder | No | ## Analyzer Categories The following are each of the eligible categories that an analyzer can be assigned to and are modeled after the diff --git a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Resources.Designer.cs b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Resources.Designer.cs index 35b7bb0f..5795ba3f 100644 --- a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Resources.Designer.cs +++ b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Resources.Designer.cs @@ -78,7 +78,7 @@ namespace Dapr.Actors.Analyzers { } /// - /// Looks up a localized string similar to The actor type '{0}' is not registered with dependency injection. + /// Looks up a localized string similar to The actor type '{0}' is not registered with the dependency injection provider. /// internal static string DAPR1402MessageFormat { get { @@ -87,7 +87,7 @@ namespace Dapr.Actors.Analyzers { } /// - /// Looks up a localized string similar to The actor type is not registered with dependency injection. + /// Looks up a localized string similar to The actor type is not registered with the dependency injection provider. /// internal static string DAPR1402Title { get { diff --git a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Resources.resx b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Resources.resx index b84c1243..eb9ddcfc 100644 --- a/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Resources.resx +++ b/src/Dapr.Actors.Analyzers/Dapr.Actors.Analyzers/Resources.resx @@ -25,10 +25,10 @@ Actor timer method invocations require the named callback '{0}' method to exist on type '{1}' - The actor type is not registered with dependency injection + The actor type is not registered with the dependency injection provider - The actor type '{0}' is not registered with dependency injection + The actor type '{0}' is not registered with the dependency injection provider Set options.UseJsonSerialization to true to support interoperability with non-.NET actors diff --git a/src/Dapr.Workflow.Analyzers/AnalyzerReleases.Shipped.md b/src/Dapr.Workflow.Analyzers/AnalyzerReleases.Shipped.md new file mode 100644 index 00000000..ad7437da --- /dev/null +++ b/src/Dapr.Workflow.Analyzers/AnalyzerReleases.Shipped.md @@ -0,0 +1,8 @@ +## Release 1.16 + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|-------------------- +DAPR1301 | Usage | Warning | The workflow class '{0}' is not registered +DAPR1302 | Usage | Warning | The workflow activity class '{0}' is not registered \ No newline at end of file diff --git a/src/Dapr.Workflow.Analyzers/AnalyzerReleases.Unshipped.md b/src/Dapr.Workflow.Analyzers/AnalyzerReleases.Unshipped.md new file mode 100644 index 00000000..b1b99aaf --- /dev/null +++ b/src/Dapr.Workflow.Analyzers/AnalyzerReleases.Unshipped.md @@ -0,0 +1,3 @@ +; Unshipped analyzer release +; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + diff --git a/src/Dapr.Workflow.Analyzers/Dapr.Workflow.Analyzers.csproj b/src/Dapr.Workflow.Analyzers/Dapr.Workflow.Analyzers.csproj new file mode 100644 index 00000000..75aa5b4d --- /dev/null +++ b/src/Dapr.Workflow.Analyzers/Dapr.Workflow.Analyzers.csproj @@ -0,0 +1,53 @@ + + + + netstandard2.0 + + enable + enable + true + + + + + + + + + true + + + false + + + false + + + This package contains analyzers for interacting with Dapr workflows. + $(NoWarn);RS1038 + + + + + + + + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + + + True + True + Resources.resx + + + + diff --git a/src/Dapr.Workflow.Analyzers/Resources.Designer.cs b/src/Dapr.Workflow.Analyzers/Resources.Designer.cs new file mode 100644 index 00000000..42e524df --- /dev/null +++ b/src/Dapr.Workflow.Analyzers/Resources.Designer.cs @@ -0,0 +1,98 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Dapr.Workflow.Analyzers { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Dapr.Workflow.Analyzers.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to The workflow type '{0}' is not registered with the dependency injection provider. + /// + internal static string DAPR1301MessageFormat { + get { + return ResourceManager.GetString("DAPR1301MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The workflow type is not registered with the dependency injection provider. + /// + internal static string DAPR1301Title { + get { + return ResourceManager.GetString("DAPR1301Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The workflow activity type '{0}' is not registered with the dependency injection provider. + /// + internal static string DAPR1302MessageFormat { + get { + return ResourceManager.GetString("DAPR1302MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The workflow activity type is not registered with the dependency injection provider. + /// + internal static string DAPR1302Title { + get { + return ResourceManager.GetString("DAPR1302Title", resourceCulture); + } + } + } +} diff --git a/src/Dapr.Workflow.Analyzers/Resources.resx b/src/Dapr.Workflow.Analyzers/Resources.resx new file mode 100644 index 00000000..12af4551 --- /dev/null +++ b/src/Dapr.Workflow.Analyzers/Resources.resx @@ -0,0 +1,33 @@ + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The workflow type is not registered with the dependency injection provider + + + The workflow type '{0}' is not registered with the dependency injection provider + + + The workflow activity type is not registered with the dependency injection provider + + + The workflow activity type '{0}' is not registered with the dependency injection provider + + \ No newline at end of file diff --git a/src/Dapr.Workflow.Analyzers/WorkflowActivityRegistrationAnalyzer.cs b/src/Dapr.Workflow.Analyzers/WorkflowActivityRegistrationAnalyzer.cs new file mode 100644 index 00000000..9141327c --- /dev/null +++ b/src/Dapr.Workflow.Analyzers/WorkflowActivityRegistrationAnalyzer.cs @@ -0,0 +1,143 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Dapr.Workflow.Analyzers; + +/// +/// An analyzer for Dapr workflows that validates that each workflow activity is registered with the +/// dependency injection provider. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class WorkflowActivityRegistrationAnalyzer : DiagnosticAnalyzer +{ + internal static readonly DiagnosticDescriptor WorkflowActivityRegistrationDescriptor = new( + id: "DAPR1302", + title: new LocalizableResourceString(nameof(Resources.DAPR1302Title), Resources.ResourceManager, + typeof(Resources)), + messageFormat: new LocalizableResourceString(nameof(Resources.DAPR1302MessageFormat), Resources.ResourceManager, + typeof(Resources)), + category: "Usage", + DiagnosticSeverity.Warning, + isEnabledByDefault: true + ); + + /// + /// Returns a set of descriptors for the diagnostics that this analyzer is capable of producing. + /// + public override ImmutableArray SupportedDiagnostics => + [ + WorkflowActivityRegistrationDescriptor + ]; + + + /// + /// Called once at session start to register actions in the analysis context. + /// + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeWorkflowActivityRegistration, SyntaxKind.InvocationExpression); + } + + private static void AnalyzeWorkflowActivityRegistration(SyntaxNodeAnalysisContext context) + { + var invocationExpr = (InvocationExpressionSyntax)context.Node; + + if (invocationExpr.Expression is not MemberAccessExpressionSyntax memberAccessExpr) + { + return; + } + + if (memberAccessExpr.Name.Identifier.Text != "CallActivityAsync") + { + return; + } + + var argumentList = invocationExpr.ArgumentList.Arguments; + if (argumentList.Count == 0) + { + return; + } + + var firstArgument = argumentList[0].Expression; + if (firstArgument is not InvocationExpressionSyntax nameofInvocation) + { + return; + } + + var activityName = nameofInvocation.ArgumentList.Arguments.FirstOrDefault()?.Expression.ToString().Trim('"'); + if (activityName == null) + { + return; + } + + bool isRegistered = CheckIfActivityIsRegistered(activityName, context.SemanticModel); + if (isRegistered) + { + return; + } + + var diagnostic = Diagnostic.Create(WorkflowActivityRegistrationDescriptor, firstArgument.GetLocation(), activityName); + context.ReportDiagnostic(diagnostic); + } + + private static bool CheckIfActivityIsRegistered(string activityName, SemanticModel semanticModel) + { + var methodInvocations = new List(); + foreach (var syntaxTree in semanticModel.Compilation.SyntaxTrees) + { + var root = syntaxTree.GetRoot(); + methodInvocations.AddRange(root.DescendantNodes().OfType()); + } + + foreach (var invocation in methodInvocations) + { + if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess) + { + continue; + } + + var methodName = memberAccess.Name.Identifier.Text; + if (methodName != "RegisterActivity") + { + continue; + } + + if (memberAccess.Name is not GenericNameSyntax typeArgumentList || + typeArgumentList.TypeArgumentList.Arguments.Count <= 0) + { + continue; + } + + if (typeArgumentList.TypeArgumentList.Arguments[0] is not IdentifierNameSyntax typeArgument) + { + continue; + } + + if (typeArgument.Identifier.Text == activityName) + { + return true; + } + } + + return false; + } +} diff --git a/src/Dapr.Workflow.Analyzers/WorkflowActivityRegistrationCodeFixProvider.cs b/src/Dapr.Workflow.Analyzers/WorkflowActivityRegistrationCodeFixProvider.cs new file mode 100644 index 00000000..77ed6c9c --- /dev/null +++ b/src/Dapr.Workflow.Analyzers/WorkflowActivityRegistrationCodeFixProvider.cs @@ -0,0 +1,128 @@ +using System.Collections.Immutable; +using System.Composition; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Formatting; + +namespace Dapr.Workflow.Analyzers; + +/// +/// Provides code fixes for DAPR1002 diagnostic. +/// +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(WorkflowActivityRegistrationCodeFixProvider))] +public sealed class WorkflowActivityRegistrationCodeFixProvider : CodeFixProvider +{ + /// + /// Gets the diagnostic IDs that this provider can fix. + /// + public override ImmutableArray FixableDiagnosticIds => ["DAPR1002"]; + + /// + /// Registers the code fix for the diagnostic. + /// + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + const string title = "Register Dapr workflow activity"; + context.RegisterCodeFix( + CodeAction.Create( + title, + createChangedDocument: c => RegisterWorkflowActivityAsync(context.Document, context.Diagnostics.First(), c), + equivalenceKey: title), + context.Diagnostics); + return Task.CompletedTask; + } + + private static async Task RegisterWorkflowActivityAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken) + { + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var diagnosticSpan = diagnostic.Location.SourceSpan; + + var oldInvocation = root?.FindToken(diagnosticSpan.Start).Parent?.AncestorsAndSelf().OfType().First(); + + if (oldInvocation == null) + { + return document; + } + + // Extract the workflow activity type name + var workflowActivityType = oldInvocation.ArgumentList.Arguments.FirstOrDefault()?.Expression.ToString(); + + if (string.IsNullOrEmpty(workflowActivityType)) + { + return document; + } + + // Get the compilation + var compilation = await document.Project.GetCompilationAsync(cancellationToken).ConfigureAwait(false); + + if (compilation == null) + { + return document; + } + + InvocationExpressionSyntax? addDaprWorkflowInvocation = null; + SyntaxNode? targetRoot = null; + Document? targetDocument = null; + + // Iterate through all syntax trees in the compilation + foreach (var syntaxTree in compilation.SyntaxTrees) + { + var syntaxRoot = await syntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false); + + addDaprWorkflowInvocation = syntaxRoot.DescendantNodes() + .OfType() + .FirstOrDefault(invocation => invocation.Expression is MemberAccessExpressionSyntax memberAccess && + memberAccess.Name.Identifier.Text == "AddDaprWorkflow"); + + if (addDaprWorkflowInvocation != null) + { + targetRoot = syntaxRoot; + targetDocument = document.Project.GetDocument(syntaxTree); + break; + } + } + + if (addDaprWorkflowInvocation == null || targetRoot == null || targetDocument == null) + return document; + + // Find the options lambda block + var optionsLambda = addDaprWorkflowInvocation.ArgumentList.Arguments + .Select(arg => arg.Expression) + .OfType() + .FirstOrDefault(); + + // Extract the parameter name from the lambda expression + var parameterName = optionsLambda?.Parameter.Identifier.Text; + + // Create the new workflow registration statement + var registerWorkflowStatement = SyntaxFactory.ParseStatement($"{parameterName}.RegisterActivity<{workflowActivityType}>();"); + + if (optionsLambda is not { Body: BlockSyntax optionsBlock }) + { + return document; + } + + // Add the new registration statement to the options block + var newOptionsBlock = optionsBlock.AddStatements(registerWorkflowStatement); + + // Replace the old options block with the new one + var newRoot = targetRoot.ReplaceNode(optionsBlock, newOptionsBlock); + + // Format the new root. + newRoot = Formatter.Format(newRoot, document.Project.Solution.Workspace); + + return targetDocument.WithSyntaxRoot(newRoot); + } + + /// + /// Gets the FixAllProvider for this code fix provider. + /// + /// The FixAllProvider instance. + public override FixAllProvider? GetFixAllProvider() + { + return WellKnownFixAllProviders.BatchFixer; + } +} diff --git a/src/Dapr.Workflow.Analyzers/WorkflowRegistrationAnalyzer.cs b/src/Dapr.Workflow.Analyzers/WorkflowRegistrationAnalyzer.cs new file mode 100644 index 00000000..91593c8f --- /dev/null +++ b/src/Dapr.Workflow.Analyzers/WorkflowRegistrationAnalyzer.cs @@ -0,0 +1,122 @@ +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis; +using System.Collections.Immutable; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Dapr.Workflow.Analyzers; + +/// +/// Analyzes whether or not workflow activities are registered. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class WorkflowRegistrationAnalyzer : DiagnosticAnalyzer +{ + internal static readonly DiagnosticDescriptor WorkflowDiagnosticDescriptor = new( + id: "DAPR1301", + title: new LocalizableResourceString(nameof(Resources.DAPR1301Title), Resources.ResourceManager, typeof(Resources)), + messageFormat: new LocalizableResourceString(nameof(Resources.DAPR1301MessageFormat), Resources.ResourceManager, typeof(Resources)), + category: "Usage", + DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + /// + /// Gets the supported diagnostics for this analyzer. + /// + public override ImmutableArray SupportedDiagnostics => [WorkflowDiagnosticDescriptor]; + + /// + /// Initializes the analyzer. + /// + /// The analysis context. + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeWorkflowRegistration, SyntaxKind.InvocationExpression); + } + + private static void AnalyzeWorkflowRegistration(SyntaxNodeAnalysisContext context) + { + var invocationExpr = (InvocationExpressionSyntax)context.Node; + + if (invocationExpr.Expression is not MemberAccessExpressionSyntax memberAccessExpr) + { + return; + } + + if (memberAccessExpr.Name.Identifier.Text != "ScheduleNewWorkflowAsync") + { + return; + } + + var argumentList = invocationExpr.ArgumentList.Arguments; + if (argumentList.Count == 0) + { + return; + } + + var firstArgument = argumentList[0].Expression; + if (firstArgument is not InvocationExpressionSyntax nameofInvocation) + { + return; + } + + var workflowName = nameofInvocation.ArgumentList.Arguments.FirstOrDefault()?.Expression.ToString().Trim('"'); + if (workflowName == null) + { + return; + } + + bool isRegistered = CheckIfWorkflowIsRegistered(workflowName, context.SemanticModel); + if (isRegistered) + { + return; + } + + var diagnostic = Diagnostic.Create(WorkflowDiagnosticDescriptor, firstArgument.GetLocation(), workflowName); + context.ReportDiagnostic(diagnostic); + } + + private static bool CheckIfWorkflowIsRegistered(string workflowName, SemanticModel semanticModel) + { + var methodInvocations = new List(); + foreach (var syntaxTree in semanticModel.Compilation.SyntaxTrees) + { + var root = syntaxTree.GetRoot(); + methodInvocations.AddRange(root.DescendantNodes().OfType()); + } + + foreach (var invocation in methodInvocations) + { + if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess) + { + continue; + } + + var methodName = memberAccess.Name.Identifier.Text; + if (methodName != "RegisterWorkflow") + { + continue; + } + + if (memberAccess.Name is not GenericNameSyntax typeArgumentList || + typeArgumentList.TypeArgumentList.Arguments.Count <= 0) + { + continue; + } + + if (typeArgumentList.TypeArgumentList.Arguments[0] is not IdentifierNameSyntax typeArgument) + { + continue; + } + + if (typeArgument.Identifier.Text == workflowName) + { + return true; + } + } + + return false; + } +} diff --git a/src/Dapr.Workflow.Analyzers/WorkflowRegistrationCodeFixProvider.cs b/src/Dapr.Workflow.Analyzers/WorkflowRegistrationCodeFixProvider.cs new file mode 100644 index 00000000..34aebca3 --- /dev/null +++ b/src/Dapr.Workflow.Analyzers/WorkflowRegistrationCodeFixProvider.cs @@ -0,0 +1,242 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Formatting; + +namespace Dapr.Workflow.Analyzers; + +/// +/// Provides code fixes for DAPR1001 diagnostic. +/// +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(WorkflowRegistrationCodeFixProvider))] +public sealed class WorkflowRegistrationCodeFixProvider : CodeFixProvider +{ + /// + /// Gets the diagnostic IDs that this provider can fix. + /// + public override ImmutableArray FixableDiagnosticIds => ["DAPR1001"]; + + /// + /// Registers the code fix for the diagnostic. + /// + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + const string title = "Register Dapr workflow"; + context.RegisterCodeFix( + CodeAction.Create( + title, + createChangedDocument: c => RegisterWorkflowAsync(context.Document, context.Diagnostics.First(), c), + equivalenceKey: title), + context.Diagnostics); + return Task.CompletedTask; + } + + private async Task RegisterWorkflowAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken) + { + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var diagnosticSpan = diagnostic.Location.SourceSpan; + + var oldInvocation = root?.FindToken(diagnosticSpan.Start).Parent?.AncestorsAndSelf().OfType().First(); + + if (oldInvocation is null || root is null) + { + return document; + } + + // Get the semantic model + var semanticModel = await document.GetSemanticModelAsync(cancellationToken); + + // Extract the workflow type name + var workflowTypeSyntax = oldInvocation.ArgumentList.Arguments.FirstOrDefault()?.Expression; + + if (workflowTypeSyntax == null) + { + return document; + } + + // Get the symbol for the workflow type + if (semanticModel.GetSymbolInfo(workflowTypeSyntax, cancellationToken).Symbol is not INamedTypeSymbol + workflowTypeSymbol) + { + return document; + } + + // Get the fully qualified name + var workflowType = workflowTypeSymbol.ToDisplayString(new SymbolDisplayFormat( + typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces)); + + if (string.IsNullOrEmpty(workflowType)) + { + return document; + } + + // Get the compilation + var compilation = await document.Project.GetCompilationAsync(cancellationToken); + + if (compilation == null) + { + return document; + } + + var (targetDocument, addDaprWorkflowInvocation) = await FindAddDaprWorkflowInvocationAsync(document.Project, cancellationToken); + + if (addDaprWorkflowInvocation == null) + { + (targetDocument, addDaprWorkflowInvocation) = await CreateAddDaprWorkflowInvocation(document.Project, cancellationToken); + } + + if (addDaprWorkflowInvocation == null) + { + return document; + } + + var targetRoot = await addDaprWorkflowInvocation.SyntaxTree.GetRootAsync(cancellationToken); + + if (targetRoot == null || targetDocument == null) + return document; + + // Find the options lambda block + var optionsLambda = addDaprWorkflowInvocation.ArgumentList.Arguments + .Select(arg => arg.Expression) + .OfType() + .FirstOrDefault(); + + if (optionsLambda is not { Body: BlockSyntax optionsBlock }) + return document; + + // Extract the parameter name from the lambda expression + var parameterName = optionsLambda.Parameter.Identifier.Text; + + // Create the new workflow registration statement + var registerWorkflowStatement = SyntaxFactory.ParseStatement($"{parameterName}.RegisterWorkflow<{workflowType}>();"); + + // Add the new registration statement to the options block + var newOptionsBlock = optionsBlock.AddStatements(registerWorkflowStatement); + + // Replace the old options block with the new one + var newRoot = targetRoot.ReplaceNode(optionsBlock, newOptionsBlock); + + // Format the new root. + newRoot = Formatter.Format(newRoot, document.Project.Solution.Workspace); + + return targetDocument.WithSyntaxRoot(newRoot); + } + + /// + /// Gets the FixAllProvider for this code fix provider. + /// + /// The FixAllProvider instance. + public override FixAllProvider? GetFixAllProvider() + { + return WellKnownFixAllProviders.BatchFixer; + } + + private static async Task<(Document?, InvocationExpressionSyntax?)> FindAddDaprWorkflowInvocationAsync(Project project, CancellationToken cancellationToken) + { + var compilation = await project.GetCompilationAsync(cancellationToken); + + foreach (var syntaxTree in compilation!.SyntaxTrees) + { + var syntaxRoot = await syntaxTree.GetRootAsync(cancellationToken); + + var addDaprWorkflowInvocation = syntaxRoot.DescendantNodes() + .OfType() + .FirstOrDefault(invocation => invocation.Expression is MemberAccessExpressionSyntax memberAccess && + memberAccess.Name.Identifier.Text == "AddDaprWorkflow"); + + if (addDaprWorkflowInvocation == null) + { + continue; + } + + var document = project.GetDocument(addDaprWorkflowInvocation.SyntaxTree); + return (document, addDaprWorkflowInvocation); + } + + return (null, null); + } + + private async Task<(Document?, InvocationExpressionSyntax?)> CreateAddDaprWorkflowInvocation(Project project, CancellationToken cancellationToken) + { + var createBuilderInvocation = await FindCreateBuilderInvocationAsync(project, cancellationToken); + + var variableDeclarator = createBuilderInvocation?.Ancestors() + .OfType() + .FirstOrDefault(); + + var builderVariable = variableDeclarator?.Identifier.Text; + + if (createBuilderInvocation == null) + { + return (null, null); + } + + var targetRoot = await createBuilderInvocation.SyntaxTree.GetRootAsync(cancellationToken); + var document = project.GetDocument(createBuilderInvocation.SyntaxTree); + + if (createBuilderInvocation.Expression is not MemberAccessExpressionSyntax { Expression: IdentifierNameSyntax }) + { + return (null, null); + } + + var addDaprWorkflowStatement = SyntaxFactory.ParseStatement($"{builderVariable}.Services.AddDaprWorkflow(options => {{ }});"); + + if (createBuilderInvocation.Ancestors().OfType().FirstOrDefault() is SyntaxNode parentBlock) + { + var firstChild = parentBlock.ChildNodes().FirstOrDefault(node => node is not UsingDirectiveSyntax); + var newParentBlock = parentBlock.InsertNodesAfter(firstChild, new[] { addDaprWorkflowStatement }); + targetRoot = targetRoot.ReplaceNode(parentBlock, newParentBlock); + } + else + { + var compilationUnitSyntax = createBuilderInvocation.Ancestors().OfType().FirstOrDefault(); + if (compilationUnitSyntax != null) + { + var firstChild = compilationUnitSyntax.ChildNodes().FirstOrDefault(node => node is not UsingDirectiveSyntax); + var globalStatement = SyntaxFactory.GlobalStatement(addDaprWorkflowStatement); + var newCompilationUnitSyntax = compilationUnitSyntax.InsertNodesAfter(firstChild, [globalStatement]); + targetRoot = targetRoot.ReplaceNode(compilationUnitSyntax, newCompilationUnitSyntax); + } + } + + var addDaprWorkflowInvocation = targetRoot?.DescendantNodes() + .OfType() + .FirstOrDefault(invocation => invocation.Expression is MemberAccessExpressionSyntax memberAccess && + memberAccess.Name.Identifier.Text == "AddDaprWorkflow"); + + return (document, addDaprWorkflowInvocation); + + } + + private static async Task FindCreateBuilderInvocationAsync(Project project, CancellationToken cancellationToken) + { + var compilation = await project.GetCompilationAsync(cancellationToken); + + foreach (var syntaxTree in compilation!.SyntaxTrees) + { + var syntaxRoot = await syntaxTree.GetRootAsync(cancellationToken); + + // Find the invocation expression for WebApplication.CreateBuilder() + var createBuilderInvocation = syntaxRoot.DescendantNodes() + .OfType() + .FirstOrDefault(invocation => invocation.Expression is MemberAccessExpressionSyntax + { + Expression: IdentifierNameSyntax + { + Identifier.Text: "WebApplication" + }, + Name.Identifier.Text: "CreateBuilder" + }); + + if (createBuilderInvocation != null) + { + return createBuilderInvocation; + } + } + + return null; + } +} diff --git a/src/Dapr.Workflow/Dapr.Workflow.csproj b/src/Dapr.Workflow/Dapr.Workflow.csproj index 08859546..0598e91f 100644 --- a/src/Dapr.Workflow/Dapr.Workflow.csproj +++ b/src/Dapr.Workflow/Dapr.Workflow.csproj @@ -17,7 +17,6 @@ - \ No newline at end of file diff --git a/test/Dapr.Actors.Analyzers.Test/ActorRegistrationAnalyzerTests.cs b/test/Dapr.Actors.Analyzers.Test/ActorRegistrationAnalyzerTests.cs index 63a775b1..e0cf4ae3 100644 --- a/test/Dapr.Actors.Analyzers.Test/ActorRegistrationAnalyzerTests.cs +++ b/test/Dapr.Actors.Analyzers.Test/ActorRegistrationAnalyzerTests.cs @@ -11,6 +11,9 @@ // limitations under the License. // ------------------------------------------------------------------------ +using Dapr.Actors.Analyzers.Test; +using Dapr.Analyzers.Common; + namespace Dapr.Actors.Analyzers.Tests; public class ActorRegistrationAnalyzerTests @@ -29,9 +32,10 @@ public class ActorRegistrationAnalyzerTests """; var expected = VerifyAnalyzer.Diagnostic(ActorRegistrationAnalyzer.DiagnosticDescriptorActorRegistration) - .WithSpan(2, 7, 2, 16).WithMessage("The actor type 'TestActor' is not registered with dependency injection"); + .WithSpan(2, 7, 2, 16).WithMessage("The actor type 'TestActor' is not registered with the dependency injection provider"); - await VerifyAnalyzer.VerifyAnalyzerAsync(testCode, expected); + var analyzer = new VerifyAnalyzer(Utilities.GetReferences()); + await analyzer.VerifyAnalyzerAsync(testCode, expected); } [Fact] @@ -47,9 +51,10 @@ public class ActorRegistrationAnalyzerTests """; var expected = VerifyAnalyzer.Diagnostic(ActorRegistrationAnalyzer.DiagnosticDescriptorActorRegistration) - .WithSpan(1, 7, 1, 16).WithMessage("The actor type 'TestActor' is not registered with dependency injection"); + .WithSpan(1, 7, 1, 16).WithMessage("The actor type 'TestActor' is not registered with the dependency injection provider"); - await VerifyAnalyzer.VerifyAnalyzerAsync(testCode, expected); + var analyzer = new VerifyAnalyzer(Utilities.GetReferences()); + await analyzer.VerifyAnalyzerAsync(testCode, expected); } @@ -67,9 +72,10 @@ public class ActorRegistrationAnalyzerTests """; var expected = VerifyAnalyzer.Diagnostic(ActorRegistrationAnalyzer.DiagnosticDescriptorActorRegistration) - .WithSpan(2, 15, 2, 24).WithMessage("The actor type 'TestActor' is not registered with dependency injection"); + .WithSpan(2, 15, 2, 24).WithMessage("The actor type 'TestActor' is not registered with the dependency injection provider"); - await VerifyAnalyzer.VerifyAnalyzerAsync(testCode, expected); + var analyzer = new VerifyAnalyzer(Utilities.GetReferences()); + await analyzer.VerifyAnalyzerAsync(testCode, expected); } [Fact] @@ -102,7 +108,8 @@ public class ActorRegistrationAnalyzerTests } """; - await VerifyAnalyzer.VerifyAnalyzerAsync(testCode); + var analyzer = new VerifyAnalyzer(Utilities.GetReferences()); + await analyzer.VerifyAnalyzerAsync(testCode); } [Fact] @@ -138,7 +145,8 @@ public class ActorRegistrationAnalyzerTests } """; - await VerifyAnalyzer.VerifyAnalyzerAsync(testCode); + var analyzer = new VerifyAnalyzer(Utilities.GetReferences()); + await analyzer.VerifyAnalyzerAsync(testCode); } [Fact] @@ -175,6 +183,7 @@ public class ActorRegistrationAnalyzerTests } """; - await VerifyAnalyzer.VerifyAnalyzerAsync(testCode); + var analyzer = new VerifyAnalyzer(Utilities.GetReferences()); + await analyzer.VerifyAnalyzerAsync(testCode); } } diff --git a/test/Dapr.Actors.Analyzers.Test/ActorRegistrationCodeFixProviderTests.cs b/test/Dapr.Actors.Analyzers.Test/ActorRegistrationCodeFixProviderTests.cs index 9ae4afc6..bf2ff0a8 100644 --- a/test/Dapr.Actors.Analyzers.Test/ActorRegistrationCodeFixProviderTests.cs +++ b/test/Dapr.Actors.Analyzers.Test/ActorRegistrationCodeFixProviderTests.cs @@ -11,6 +11,9 @@ // limitations under the License. // ------------------------------------------------------------------------ +using Dapr.Actors.Analyzers.Test; +using Dapr.Analyzers.Common; + namespace Dapr.Actors.Analyzers.Tests; public class ActorRegistrationCodeFixProviderTests @@ -77,7 +80,8 @@ public class ActorRegistrationCodeFixProviderTests """; - await VerifyCodeFix.RunTest(code, expectedChangedCode); + await VerifyCodeFix.RunTest(code, expectedChangedCode, + typeof(object).Assembly.Location, Utilities.GetReferences(), Utilities.GetAnalyzers()); } [Fact] @@ -132,7 +136,8 @@ public class ActorRegistrationCodeFixProviderTests """; - await VerifyCodeFix.RunTest(code, expectedChangedCode); + await VerifyCodeFix.RunTest(code, expectedChangedCode, + typeof(object).Assembly.Location, Utilities.GetReferences(), Utilities.GetAnalyzers()); } [Fact] @@ -181,6 +186,7 @@ public class ActorRegistrationCodeFixProviderTests """; - await VerifyCodeFix.RunTest(code, expectedChangedCode); + await VerifyCodeFix.RunTest(code, expectedChangedCode, + typeof(object).Assembly.Location, Utilities.GetReferences(), Utilities.GetAnalyzers()); } } diff --git a/test/Dapr.Actors.Analyzers.Test/Dapr.Actors.Analyzers.Test.csproj b/test/Dapr.Actors.Analyzers.Test/Dapr.Actors.Analyzers.Test.csproj index 8d9458e2..a7351874 100644 --- a/test/Dapr.Actors.Analyzers.Test/Dapr.Actors.Analyzers.Test.csproj +++ b/test/Dapr.Actors.Analyzers.Test/Dapr.Actors.Analyzers.Test.csproj @@ -5,17 +5,16 @@ enable false true - Dapr.Actors.Analyzers.Tests - + runtime; build; native; contentfiles; analyzers; buildtransitive @@ -34,6 +33,7 @@ + diff --git a/test/Dapr.Actors.Analyzers.Test/MappedActorHandlersAnalyzerTests.cs b/test/Dapr.Actors.Analyzers.Test/MappedActorHandlersAnalyzerTests.cs index ed649f98..592f3208 100644 --- a/test/Dapr.Actors.Analyzers.Test/MappedActorHandlersAnalyzerTests.cs +++ b/test/Dapr.Actors.Analyzers.Test/MappedActorHandlersAnalyzerTests.cs @@ -11,6 +11,9 @@ // limitations under the License. // ------------------------------------------------------------------------ +using Dapr.Actors.Analyzers.Test; +using Dapr.Analyzers.Common; + namespace Dapr.Actors.Analyzers.Tests; public class MappedActorHandlersAnalyzerTests @@ -40,7 +43,8 @@ public class MappedActorHandlersAnalyzerTests var expected = VerifyAnalyzer.Diagnostic(MappedActorHandlersAnalyzer.DiagnosticDescriptorMapActorsHandlers) .WithSpan(10, 25, 13, 27).WithMessage("Call app.MapActorsHandlers to map endpoints for Dapr actors"); - await VerifyAnalyzer.VerifyAnalyzerAsync(testCode, expected); + var analyzer = new VerifyAnalyzer(Utilities.GetReferences()); + await analyzer.VerifyAnalyzerAsync(testCode, expected); } [Fact] @@ -66,6 +70,7 @@ public class MappedActorHandlersAnalyzerTests } """; - await VerifyAnalyzer.VerifyAnalyzerAsync(testCode); + var analyzer = new VerifyAnalyzer(Utilities.GetReferences()); + await analyzer.VerifyAnalyzerAsync(testCode); } } diff --git a/test/Dapr.Actors.Analyzers.Test/MappedActorHandlersCodeFixProviderTests.cs b/test/Dapr.Actors.Analyzers.Test/MappedActorHandlersCodeFixProviderTests.cs index 7c66a265..b7bf0209 100644 --- a/test/Dapr.Actors.Analyzers.Test/MappedActorHandlersCodeFixProviderTests.cs +++ b/test/Dapr.Actors.Analyzers.Test/MappedActorHandlersCodeFixProviderTests.cs @@ -11,6 +11,9 @@ // limitations under the License. // ------------------------------------------------------------------------ +using Dapr.Actors.Analyzers.Test; +using Dapr.Analyzers.Common; + namespace Dapr.Actors.Analyzers.Tests; public class MappedActorHandlersCodeFixProviderTests @@ -46,34 +49,35 @@ public class MappedActorHandlersCodeFixProviderTests """; const string expectedChangedCode = """ - using Dapr.Actors.Runtime; - using Microsoft.AspNetCore.Builder; - using Microsoft.Extensions.DependencyInjection; - - public static class Program - { - public static void Main() - { - var builder = WebApplication.CreateBuilder(); - - builder.Services.AddActors(options => - { - options.Actors.RegisterActor(); - options.UseJsonSerialization = true; - }); - var app = builder.Build(); - app.MapActorsHandlers(); - } - } - class TestActor : Actor - { - public TestActor(ActorHost host) : base(host) - { - } - } - """; + using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; + using Microsoft.Extensions.DependencyInjection; + + public static class Program + { + public static void Main() + { + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddActors(options => + { + options.Actors.RegisterActor(); + options.UseJsonSerialization = true; + }); + var app = builder.Build(); + app.MapActorsHandlers(); + } + } + class TestActor : Actor + { + public TestActor(ActorHost host) : base(host) + { + } + } + """; - await VerifyCodeFix.RunTest(code, expectedChangedCode); + await VerifyCodeFix.RunTest(code, expectedChangedCode, + typeof(object).Assembly.Location, Utilities.GetReferences(), Utilities.GetAnalyzers()); } [Fact] @@ -103,29 +107,30 @@ public class MappedActorHandlersCodeFixProviderTests """; const string expectedChangedCode = """ - - using Dapr.Actors.Runtime; - using Microsoft.AspNetCore.Builder; - using Microsoft.Extensions.DependencyInjection; - - var builder = WebApplication.CreateBuilder(); - - builder.Services.AddActors(options => - { - options.Actors.RegisterActor(); - options.UseJsonSerialization = true; - }); - var app = builder.Build(); - app.MapActorsHandlers(); - class TestActor : Actor - { - public TestActor(ActorHost host) : base(host) - { - } - } - - """; + + using Dapr.Actors.Runtime; + using Microsoft.AspNetCore.Builder; + using Microsoft.Extensions.DependencyInjection; + + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddActors(options => + { + options.Actors.RegisterActor(); + options.UseJsonSerialization = true; + }); + var app = builder.Build(); + app.MapActorsHandlers(); + class TestActor : Actor + { + public TestActor(ActorHost host) : base(host) + { + } + } + + """; - await VerifyCodeFix.RunTest(code, expectedChangedCode); + await VerifyCodeFix.RunTest(code, expectedChangedCode, + typeof(object).Assembly.Location, Utilities.GetReferences(), Utilities.GetAnalyzers()); } } diff --git a/test/Dapr.Actors.Analyzers.Test/PreferActorJsonSerializationAnalyzerTests.cs b/test/Dapr.Actors.Analyzers.Test/PreferActorJsonSerializationAnalyzerTests.cs index 2fd8f5e7..f186daac 100644 --- a/test/Dapr.Actors.Analyzers.Test/PreferActorJsonSerializationAnalyzerTests.cs +++ b/test/Dapr.Actors.Analyzers.Test/PreferActorJsonSerializationAnalyzerTests.cs @@ -11,6 +11,9 @@ // limitations under the License. // ------------------------------------------------------------------------ +using Dapr.Actors.Analyzers.Test; +using Dapr.Analyzers.Common; + namespace Dapr.Actors.Analyzers.Tests; public class PreferActorJsonSerializationAnalyzerTests @@ -42,7 +45,8 @@ public class PreferActorJsonSerializationAnalyzerTests .WithSpan(10, 25, 12, 27) .WithMessage("Set options.UseJsonSerialization to true to support interoperability with non-.NET actors"); - await VerifyAnalyzer.VerifyAnalyzerAsync(testCode, expected); + var analyzer = new VerifyAnalyzer(Utilities.GetReferences()); + await analyzer.VerifyAnalyzerAsync(testCode, expected); } [Fact] @@ -68,6 +72,7 @@ public class PreferActorJsonSerializationAnalyzerTests } """; - await VerifyAnalyzer.VerifyAnalyzerAsync(testCode); + var analyzer = new VerifyAnalyzer(Utilities.GetReferences()); + await analyzer.VerifyAnalyzerAsync(testCode); } } diff --git a/test/Dapr.Actors.Analyzers.Test/PreferActorJsonSerializationCodeFixProviderTests.cs b/test/Dapr.Actors.Analyzers.Test/PreferActorJsonSerializationCodeFixProviderTests.cs index 0a7288c2..f8d89ef3 100644 --- a/test/Dapr.Actors.Analyzers.Test/PreferActorJsonSerializationCodeFixProviderTests.cs +++ b/test/Dapr.Actors.Analyzers.Test/PreferActorJsonSerializationCodeFixProviderTests.cs @@ -11,6 +11,9 @@ // limitations under the License. // ------------------------------------------------------------------------ +using Dapr.Actors.Analyzers.Test; +using Dapr.Analyzers.Common; + namespace Dapr.Actors.Analyzers.Tests; public class PreferActorJsonSerializationCodeFixProviderTests @@ -57,6 +60,7 @@ public class PreferActorJsonSerializationCodeFixProviderTests } """; - await VerifyCodeFix.RunTest(code, expectedChangedCode); + await VerifyCodeFix.RunTest(code, expectedChangedCode, + typeof(object).Assembly.Location, Utilities.GetReferences(), Utilities.GetAnalyzers()); } } diff --git a/test/Dapr.Actors.Analyzers.Test/Utilities.cs b/test/Dapr.Actors.Analyzers.Test/Utilities.cs new file mode 100644 index 00000000..cad509db --- /dev/null +++ b/test/Dapr.Actors.Analyzers.Test/Utilities.cs @@ -0,0 +1,48 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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.Collections.Immutable; +using Dapr.Analyzers.Common; +using Microsoft.AspNetCore.Builder; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Dapr.Actors.Analyzers.Test; + +internal static class Utilities +{ + internal static ImmutableArray GetAnalyzers() => + [ + new ActorRegistrationAnalyzer(), + new MappedActorHandlersAnalyzer(), + new PreferActorJsonSerializationAnalyzer(), + new TimerCallbackMethodPresentAnalyzer() + ]; + + internal static IReadOnlyList GetReferences() + { + var metadataReferences = TestUtilities.GetAllReferencesNeededForType(typeof(ActorRegistrationAnalyzer)).ToList(); + metadataReferences.AddRange(TestUtilities.GetAllReferencesNeededForType(typeof(MappedActorHandlersAnalyzer))); + metadataReferences.AddRange(TestUtilities.GetAllReferencesNeededForType(typeof(PreferActorJsonSerializationAnalyzer))); + metadataReferences.AddRange(TestUtilities.GetAllReferencesNeededForType(typeof(TimerCallbackMethodPresentAnalyzer))); + metadataReferences.AddRange(TestUtilities.GetAllReferencesNeededForType(typeof(ActorsServiceCollectionExtensions))); + metadataReferences.Add(MetadataReference.CreateFromFile(typeof(object).Assembly.Location)); + metadataReferences.Add(MetadataReference.CreateFromFile(typeof(WebApplication).Assembly.Location)); + metadataReferences.Add(MetadataReference.CreateFromFile(typeof(IHost).Assembly.Location)); + metadataReferences.Add(MetadataReference.CreateFromFile(typeof(ActorsEndpointRouteBuilderExtensions).Assembly.Location)); + metadataReferences.Add(MetadataReference.CreateFromFile(typeof(ActorsServiceCollectionExtensions).Assembly.Location)); + return metadataReferences; + } +} diff --git a/test/Dapr.Analyzers.Common/Dapr.Analyzers.Common.csproj b/test/Dapr.Analyzers.Common/Dapr.Analyzers.Common.csproj new file mode 100644 index 00000000..8a225e61 --- /dev/null +++ b/test/Dapr.Analyzers.Common/Dapr.Analyzers.Common.csproj @@ -0,0 +1,24 @@ + + + + enable + enable + + + + + + + + + + + + + + + + + + + diff --git a/test/Dapr.Actors.Analyzers.Test/TestUtilities.cs b/test/Dapr.Analyzers.Common/TestUtilities.cs similarity index 65% rename from test/Dapr.Actors.Analyzers.Test/TestUtilities.cs rename to test/Dapr.Analyzers.Common/TestUtilities.cs index acfec6ba..b6db5147 100644 --- a/test/Dapr.Actors.Analyzers.Test/TestUtilities.cs +++ b/test/Dapr.Analyzers.Common/TestUtilities.cs @@ -13,19 +13,21 @@ using System.Collections.Immutable; using System.Reflection; -using Microsoft.AspNetCore.Builder; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Testing; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -namespace Dapr.Actors.Analyzers.Tests; +namespace Dapr.Analyzers.Common; internal static class TestUtilities { - public static async Task<(ImmutableArray diagnostics, Document document, Workspace workspace)> GetDiagnosticsAdvanced(string code) + internal static async Task<(ImmutableArray diagnostics, Document document, Workspace workspace)> + GetDiagnosticsAdvanced( + string code, + string assemblyLocation, + IReadOnlyList additionalMetadataReferences, + ImmutableArray analyzers) { var workspace = new AdhocWorkspace(); @@ -42,47 +44,40 @@ internal static class TestUtilities { { "CS1701", ReportDiagnostic.Suppress } })) - .AddMetadataReferences(await referenceAssemblies.ResolveAsync(LanguageNames.CSharp, default)) - .AddMetadataReference(MetadataReference.CreateFromFile(typeof(object).Assembly.Location)) - .AddMetadataReferences(GetAllReferencesNeededForType(typeof(WebApplication))) - .AddMetadataReferences(GetAllReferencesNeededForType(typeof(IHost))) - .AddMetadataReferences(GetAllReferencesNeededForType(typeof(ActorsEndpointRouteBuilderExtensions))) - .AddMetadataReferences(GetAllReferencesNeededForType(typeof(ActorsServiceCollectionExtensions))); + .AddMetadataReferences(await referenceAssemblies.ResolveAsync(LanguageNames.CSharp, CancellationToken.None)) + .AddMetadataReference(MetadataReference.CreateFromFile(assemblyLocation)) + .AddMetadataReferences(additionalMetadataReferences); // Add the document to the project var document = project.AddDocument("TestDocument.cs", code); // Get the syntax tree and create a compilation - var syntaxTree = await document.GetSyntaxTreeAsync() ?? throw new InvalidOperationException("Syntax tree is null"); + var syntaxTree = await document.GetSyntaxTreeAsync() ?? + throw new InvalidOperationException("Syntax tree is null"); var compilation = CSharpCompilation.Create("TestCompilation") .AddSyntaxTrees(syntaxTree) .AddReferences(project.MetadataReferences) .WithOptions(project.CompilationOptions!); - var compilationWithAnalyzer = compilation.WithAnalyzers( - [ - new ActorRegistrationAnalyzer(), - new MappedActorHandlersAnalyzer(), - new PreferActorJsonSerializationAnalyzer(), - new TimerCallbackMethodPresentAnalyzer() - ]); + var compilationWithAnalyzer = compilation.WithAnalyzers(analyzers); // Get diagnostics from the compilation var diagnostics = await compilationWithAnalyzer.GetAllDiagnosticsAsync(); return (diagnostics, document, workspace); } - public static MetadataReference[] GetAllReferencesNeededForType(Type type) + internal static MetadataReference[] GetAllReferencesNeededForType(Type type) { var files = GetAllAssemblyFilesNeededForType(type); return files.Select(x => MetadataReference.CreateFromFile(x)).Cast().ToArray(); } - private static ImmutableArray GetAllAssemblyFilesNeededForType(Type type) => type.Assembly - .GetReferencedAssemblies() - .Select(x => Assembly.Load(x.FullName)) - .Append(type.Assembly) - .Select(x => x.Location) - .ToImmutableArray(); + private static ImmutableArray GetAllAssemblyFilesNeededForType(Type type) => [ + ..type.Assembly + .GetReferencedAssemblies() + .Select(x => Assembly.Load(x.FullName)) + .Append(type.Assembly) + .Select(x => x.Location) + ]; } diff --git a/test/Dapr.Actors.Analyzers.Test/VerifyAnalyzer.cs b/test/Dapr.Analyzers.Common/VerifyAnalyzer.cs similarity index 62% rename from test/Dapr.Actors.Analyzers.Test/VerifyAnalyzer.cs rename to test/Dapr.Analyzers.Common/VerifyAnalyzer.cs index 87136e41..f2d72506 100644 --- a/test/Dapr.Actors.Analyzers.Test/VerifyAnalyzer.cs +++ b/test/Dapr.Analyzers.Common/VerifyAnalyzer.cs @@ -11,27 +11,24 @@ // limitations under the License. // ------------------------------------------------------------------------ -using Microsoft.AspNetCore.Builder; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Testing; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Testing; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -namespace Dapr.Actors.Analyzers.Tests; +namespace Dapr.Analyzers.Common; -internal static class VerifyAnalyzer +internal class VerifyAnalyzer(IReadOnlyList metadataReferences) { public static DiagnosticResult Diagnostic(DiagnosticDescriptor descriptor) => new(descriptor); - public static async Task VerifyAnalyzerAsync(string source, params DiagnosticResult[] expected) + public async Task VerifyAnalyzerAsync(string source, params DiagnosticResult[] expected) where TAnalyzer : DiagnosticAnalyzer, new() { await VerifyAnalyzerAsync(source, null, expected); } - public static async Task VerifyAnalyzerAsync(string source, string? program, params DiagnosticResult[] expected) + public async Task VerifyAnalyzerAsync(string source, string? program, params DiagnosticResult[] expected) where TAnalyzer : DiagnosticAnalyzer, new() { var test = new Test { TestCode = source }; @@ -47,14 +44,14 @@ internal static class VerifyAnalyzer test.TestState.Sources.Add(("Program.cs", program)); } - var metadataReferences = TestUtilities.GetAllReferencesNeededForType(typeof(ActorRegistrationAnalyzer)).ToList(); - metadataReferences.AddRange(TestUtilities.GetAllReferencesNeededForType(typeof(MappedActorHandlersAnalyzer))); - metadataReferences.AddRange(TestUtilities.GetAllReferencesNeededForType(typeof(PreferActorJsonSerializationAnalyzer))); - metadataReferences.AddRange(TestUtilities.GetAllReferencesNeededForType(typeof(TimerCallbackMethodPresentAnalyzer))); - metadataReferences.AddRange(TestUtilities.GetAllReferencesNeededForType(typeof(ActorsServiceCollectionExtensions))); - metadataReferences.Add(MetadataReference.CreateFromFile(typeof(object).Assembly.Location)); - metadataReferences.Add(MetadataReference.CreateFromFile(typeof(WebApplication).Assembly.Location)); - metadataReferences.Add(MetadataReference.CreateFromFile(typeof(IHost).Assembly.Location)); + // var metadataReferences = TestUtilities.GetAllReferencesNeededForType(typeof(ActorRegistrationAnalyzer)).ToList(); + // metadataReferences.AddRange(TestUtilities.GetAllReferencesNeededForType(typeof(MappedActorHandlersAnalyzer))); + // metadataReferences.AddRange(TestUtilities.GetAllReferencesNeededForType(typeof(PreferActorJsonSerializationAnalyzer))); + // metadataReferences.AddRange(TestUtilities.GetAllReferencesNeededForType(typeof(TimerCallbackMethodPresentAnalyzer))); + // metadataReferences.AddRange(TestUtilities.GetAllReferencesNeededForType(typeof(ActorsServiceCollectionExtensions))); + // metadataReferences.Add(MetadataReference.CreateFromFile(typeof(object).Assembly.Location)); + // metadataReferences.Add(MetadataReference.CreateFromFile(typeof(WebApplication).Assembly.Location)); + // metadataReferences.Add(MetadataReference.CreateFromFile(typeof(IHost).Assembly.Location)); foreach (var reference in metadataReferences) { @@ -65,8 +62,6 @@ internal static class VerifyAnalyzer await test.RunAsync(CancellationToken.None); } - - private sealed class Test : CSharpAnalyzerTest where TAnalyzer : DiagnosticAnalyzer, new() { @@ -81,3 +76,4 @@ internal static class VerifyAnalyzer } } } + diff --git a/test/Dapr.Actors.Analyzers.Test/VerifyCodeFix.cs b/test/Dapr.Analyzers.Common/VerifyCodeFix.cs similarity index 75% rename from test/Dapr.Actors.Analyzers.Test/VerifyCodeFix.cs rename to test/Dapr.Analyzers.Common/VerifyCodeFix.cs index d09e427a..aac71aa9 100644 --- a/test/Dapr.Actors.Analyzers.Test/VerifyCodeFix.cs +++ b/test/Dapr.Analyzers.Common/VerifyCodeFix.cs @@ -11,16 +11,26 @@ // limitations under the License. // ------------------------------------------------------------------------ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.Diagnostics; +using Xunit; -namespace Dapr.Actors.Analyzers.Tests; +namespace Dapr.Analyzers.Common; internal static class VerifyCodeFix { - public static async Task RunTest(string code, string expectedChangedCode) where T : CodeFixProvider, new() + public static async Task RunTest( + string code, + string expectedChangedCode, + string assemblyLocation, + IReadOnlyList metadataReferences, + ImmutableArray analyzers) where T : CodeFixProvider, new() { - var (diagnostics, document, workspace) = await TestUtilities.GetDiagnosticsAdvanced(code); + var (diagnostics, document, workspace) = + await TestUtilities.GetDiagnosticsAdvanced(code, assemblyLocation, metadataReferences, analyzers); Assert.Single(diagnostics); @@ -51,7 +61,8 @@ internal static class VerifyCodeFix operation.Apply(workspace, CancellationToken.None); } - var updatedDocument = workspace.CurrentSolution.GetDocument(document.Id) ?? throw new Exception("Updated document is null"); + var updatedDocument = workspace.CurrentSolution.GetDocument(document.Id) ?? + throw new Exception("Updated document is null"); var newCode = (await updatedDocument.GetTextAsync()).ToString(); var normalizedExpectedCode = NormalizeWhitespace(expectedChangedCode); @@ -63,7 +74,7 @@ internal static class VerifyCodeFix // Normalize whitespace string NormalizeWhitespace(string input) { - var separator = new[] { ' ', '\r', '\n' }; + char[] separator = [' ', '\r', '\n']; return string.Join(" ", input.Split(separator, StringSplitOptions.RemoveEmptyEntries)); } } diff --git a/test/Dapr.Jobs.Analyzer.Test/Dapr.Jobs.Analyzers.Test.csproj b/test/Dapr.Jobs.Analyzer.Test/Dapr.Jobs.Analyzers.Test.csproj index 57ca6b87..339b39d6 100644 --- a/test/Dapr.Jobs.Analyzer.Test/Dapr.Jobs.Analyzers.Test.csproj +++ b/test/Dapr.Jobs.Analyzer.Test/Dapr.Jobs.Analyzers.Test.csproj @@ -26,6 +26,7 @@ + diff --git a/test/Dapr.Jobs.Analyzer.Test/MapDaprScheduledJobHandlerTest.cs b/test/Dapr.Jobs.Analyzer.Test/MapDaprScheduledJobHandlerTest.cs index 19a34cbc..e8d4cb3e 100644 --- a/test/Dapr.Jobs.Analyzer.Test/MapDaprScheduledJobHandlerTest.cs +++ b/test/Dapr.Jobs.Analyzer.Test/MapDaprScheduledJobHandlerTest.cs @@ -1,9 +1,5 @@ -using Dapr.Jobs.Models; -using Microsoft.AspNetCore.Builder; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Testing; +using Dapr.Analyzers.Common; using Microsoft.CodeAnalysis.Testing; -using Microsoft.Extensions.Hosting; namespace Dapr.Jobs.Analyzers.Test; @@ -13,7 +9,7 @@ public class DaprJobsAnalyzerAnalyzerTests #if NET8_0 private static readonly ReferenceAssemblies referenceAssemblies = ReferenceAssemblies.Net.Net80; #elif NET9_0 - private static readonly ReferenceAssemblies referenceAssemblies = ReferenceAssemblies.Net.Net90; + private static readonly ReferenceAssemblies referenceAssemblies = ReferenceAssemblies.Net.Net90; #endif [Fact] @@ -47,11 +43,13 @@ public class DaprJobsAnalyzerAnalyzerTests } """; - await VerifyAnalyzerAsync(testCode, - new DiagnosticResult(MapDaprScheduledJobHandlerAnalyzer.DaprJobHandlerRule) - .WithSpan(22, 25, 23, 83) - .WithMessage( - "Job invocations require the MapDaprScheduledJobHandler be set and configured for job name 'myJob' on IEndpointRouteBuilder")); + var expected = VerifyAnalyzer.Diagnostic(MapDaprScheduledJobHandlerAnalyzer.DaprJobHandlerRule) + .WithSpan(22, 25, 23, 83) + .WithMessage( + "Job invocations require the MapDaprScheduledJobHandler be set and configured for job name 'myJob' on IEndpointRouteBuilder"); + + var analyzer = new VerifyAnalyzer(Utilities.GetReferences()); + await analyzer.VerifyAnalyzerAsync(testCode, expected); } [Fact] @@ -82,7 +80,8 @@ public class DaprJobsAnalyzerAnalyzerTests } """; - await VerifyAnalyzerAsync(testCode); + var analyzer = new VerifyAnalyzer(Utilities.GetReferences()); + await analyzer.VerifyAnalyzerAsync(testCode); } [Fact] @@ -118,13 +117,15 @@ public class DaprJobsAnalyzerAnalyzerTests } """; - await VerifyAnalyzerAsync(testCode, - new DiagnosticResult(MapDaprScheduledJobHandlerAnalyzer.DaprJobHandlerRule) - .WithSpan(22, 25, 23, 83) - .WithMessage("Job invocations require the MapDaprScheduledJobHandler be set and configured for job name 'myJob' on IEndpointRouteBuilder"), - new DiagnosticResult(MapDaprScheduledJobHandlerAnalyzer.DaprJobHandlerRule) - .WithSpan(24, 25, 25, 83) - .WithMessage("Job invocations require the MapDaprScheduledJobHandler be set and configured for job name 'myJob2' on IEndpointRouteBuilder")); + var expected1 = VerifyAnalyzer.Diagnostic(MapDaprScheduledJobHandlerAnalyzer.DaprJobHandlerRule) + .WithSpan(22, 25, 23, 83) + .WithMessage( + "Job invocations require the MapDaprScheduledJobHandler be set and configured for job name 'myJob' on IEndpointRouteBuilder"); + var expected2 = VerifyAnalyzer.Diagnostic(MapDaprScheduledJobHandlerAnalyzer.DaprJobHandlerRule) + .WithSpan(24, 25, 25, 83) + .WithMessage("Job invocations require the MapDaprScheduledJobHandler be set and configured for job name 'myJob2' on IEndpointRouteBuilder"); + var analyzer = new VerifyAnalyzer(Utilities.GetReferences()); + await analyzer.VerifyAnalyzerAsync(testCode, expected1, expected2); } [Fact] @@ -165,7 +166,8 @@ public class DaprJobsAnalyzerAnalyzerTests } """; - await VerifyAnalyzerAsync(testCode); + var analyzer = new VerifyAnalyzer(Utilities.GetReferences()); + await analyzer.VerifyAnalyzerAsync(testCode); } [Fact] @@ -206,7 +208,8 @@ public class DaprJobsAnalyzerAnalyzerTests } """; - await VerifyAnalyzerAsync(testCode); + var analyzer = new VerifyAnalyzer(Utilities.GetReferences()); + await analyzer.VerifyAnalyzerAsync(testCode); } [Fact] @@ -245,28 +248,7 @@ public class DaprJobsAnalyzerAnalyzerTests """; - await VerifyAnalyzerAsync(testCode); - } - - private static async Task VerifyAnalyzerAsync(string testCode, params DiagnosticResult[] expectedDiagnostics) - { - var test = new CSharpAnalyzerTest - { - TestCode = testCode - }; - - test.TestState.ReferenceAssemblies = referenceAssemblies; - - test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(WebApplication).Assembly.Location)); - test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(DaprJobsClient).Assembly.Location)); - test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(DaprJobSchedule).Assembly.Location)); - test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(EndpointRouteBuilderExtensions).Assembly.Location)); - test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(IApplicationBuilder).Assembly.Location)); - test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile( - typeof(Microsoft.Extensions.DependencyInjection.ServiceCollection).Assembly.Location)); - test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(IHost).Assembly.Location)); - - test.ExpectedDiagnostics.AddRange(expectedDiagnostics); - await test.RunAsync(); + var analyzer = new VerifyAnalyzer(Utilities.GetReferences()); + await analyzer.VerifyAnalyzerAsync(testCode); } } diff --git a/test/Dapr.Jobs.Analyzer.Test/Utilities.cs b/test/Dapr.Jobs.Analyzer.Test/Utilities.cs new file mode 100644 index 00000000..f3fcf507 --- /dev/null +++ b/test/Dapr.Jobs.Analyzer.Test/Utilities.cs @@ -0,0 +1,43 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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.Collections.Immutable; +using Dapr.Analyzers.Common; +using Dapr.Jobs.Models; +using Microsoft.AspNetCore.Builder; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.Extensions.Hosting; + +namespace Dapr.Jobs.Analyzers.Test; + +internal static class Utilities +{ + internal static ImmutableArray GetAnalyzers() => + [ + new MapDaprScheduledJobHandlerAnalyzer() + ]; + + internal static IReadOnlyList GetReferences() + { + var metadataReferences = TestUtilities.GetAllReferencesNeededForType(typeof(MapDaprScheduledJobHandlerAnalyzer)).ToList(); + metadataReferences.Add(MetadataReference.CreateFromFile(typeof(WebApplication).Assembly.Location)); + metadataReferences.Add(MetadataReference.CreateFromFile(typeof(DaprJobsClient).Assembly.Location)); + metadataReferences.Add(MetadataReference.CreateFromFile(typeof(DaprJobSchedule).Assembly.Location)); + metadataReferences.Add(MetadataReference.CreateFromFile(typeof(EndpointRouteBuilderExtensions).Assembly.Location)); + metadataReferences.Add(MetadataReference.CreateFromFile(typeof(IApplicationBuilder).Assembly.Location)); + metadataReferences.Add(MetadataReference.CreateFromFile(typeof(Microsoft.Extensions.DependencyInjection.ServiceCollection).Assembly.Location)); + metadataReferences.Add(MetadataReference.CreateFromFile(typeof(IHost).Assembly.Location)); + return metadataReferences; + } +} diff --git a/test/Dapr.Workflow.Analyzers.Test/Dapr.Workflow.Analyzers.Test.csproj b/test/Dapr.Workflow.Analyzers.Test/Dapr.Workflow.Analyzers.Test.csproj new file mode 100644 index 00000000..83139ac4 --- /dev/null +++ b/test/Dapr.Workflow.Analyzers.Test/Dapr.Workflow.Analyzers.Test.csproj @@ -0,0 +1,39 @@ + + + + enable + enable + false + true + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + diff --git a/test/Dapr.Workflow.Analyzers.Test/GlobalUsings.cs b/test/Dapr.Workflow.Analyzers.Test/GlobalUsings.cs new file mode 100644 index 00000000..48f0c59b --- /dev/null +++ b/test/Dapr.Workflow.Analyzers.Test/GlobalUsings.cs @@ -0,0 +1,14 @@ +// ------------------------------------------------------------------------ +// Copyright 2023 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +global using Xunit; \ No newline at end of file diff --git a/test/Dapr.Workflow.Analyzers.Test/TestModels.cs b/test/Dapr.Workflow.Analyzers.Test/TestModels.cs new file mode 100644 index 00000000..696a289b --- /dev/null +++ b/test/Dapr.Workflow.Analyzers.Test/TestModels.cs @@ -0,0 +1,4 @@ +namespace Dapr.Workflow.Analyzers.Test; + +internal record OrderPayload(string OrderId, string CustomerId); +internal record class OrderResult(string Result); diff --git a/test/Dapr.Workflow.Analyzers.Test/Utilities.cs b/test/Dapr.Workflow.Analyzers.Test/Utilities.cs new file mode 100644 index 00000000..64be64db --- /dev/null +++ b/test/Dapr.Workflow.Analyzers.Test/Utilities.cs @@ -0,0 +1,35 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using System.Collections.Immutable; +using System.Reflection; +using Dapr.Analyzers.Common; +using Microsoft.Extensions.Hosting; + +namespace Dapr.Workflow.Analyzers.Test; + +internal static class Utilities +{ + internal static ImmutableArray GetAnalyzers() => + [ + new WorkflowRegistrationAnalyzer(), + new WorkflowActivityRegistrationAnalyzer() + ]; + + internal static IReadOnlyList GetReferences() + { + var metadataReferences = TestUtilities.GetAllReferencesNeededForType(typeof(WorkflowActivityRegistrationAnalyzer)).ToList(); + metadataReferences.AddRange(TestUtilities.GetAllReferencesNeededForType(typeof(WorkflowRegistrationAnalyzer))); + metadataReferences.AddRange(TestUtilities.GetAllReferencesNeededForType(typeof(TimeSpan))); + metadataReferences.AddRange(TestUtilities.GetAllReferencesNeededForType(typeof(Workflow<,>))); + metadataReferences.AddRange(TestUtilities.GetAllReferencesNeededForType(typeof(WorkflowActivity<,>))); + metadataReferences.Add(MetadataReference.CreateFromFile(typeof(Task).Assembly.Location)); + metadataReferences.Add(MetadataReference.CreateFromFile(typeof(WebApplication).Assembly.Location)); + metadataReferences.Add(MetadataReference.CreateFromFile(typeof(DaprWorkflowClient).Assembly.Location)); + metadataReferences.Add(MetadataReference.CreateFromFile(typeof(Microsoft.Extensions.DependencyInjection.ServiceCollection).Assembly.Location)); + metadataReferences.Add(MetadataReference.CreateFromFile(typeof(IApplicationBuilder).Assembly.Location)); + metadataReferences.Add(MetadataReference.CreateFromFile(typeof(IHost).Assembly.Location)); + return metadataReferences; + } +} diff --git a/test/Dapr.Workflow.Analyzers.Test/WorkflowActivityRegistrationAnalyzerTests.cs b/test/Dapr.Workflow.Analyzers.Test/WorkflowActivityRegistrationAnalyzerTests.cs new file mode 100644 index 00000000..9e96ccb5 --- /dev/null +++ b/test/Dapr.Workflow.Analyzers.Test/WorkflowActivityRegistrationAnalyzerTests.cs @@ -0,0 +1,99 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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.Analyzers.Common; + +namespace Dapr.Workflow.Analyzers.Test; + +public sealed class WorkflowActivityRegistrationAnalyzerTests +{ + [Fact] + public async Task VerifyActivityNotRegistered() + { + const string testCode = """ + using Dapr.Workflow; + using System.Threading.Tasks; + + class OrderProcessingWorkflow : Workflow + { + public override async Task RunAsync(WorkflowContext context, OrderPayload order) + { + await context.CallActivityAsync(nameof(NotifyActivity), new Notification("Order received")); + return new OrderResult("Order processed"); + } + } + + record OrderPayload { } + record OrderResult(string message) { } + record Notification { public Notification(string message) { } } + class NotifyActivity { } + """; + + var expected = VerifyAnalyzer.Diagnostic(WorkflowActivityRegistrationAnalyzer.WorkflowActivityRegistrationDescriptor) + .WithSpan(8, 57, 8, 79).WithMessage("The workflow activity type 'NotifyActivity' is not registered with the dependency injection provider"); + + var analyzer = new VerifyAnalyzer(Utilities.GetReferences()); + await analyzer.VerifyAnalyzerAsync(testCode, expected); + } + + [Fact] + public async Task VerifyActivityRegistered() + { + const string testCode = """ + using Dapr.Workflow; + using Microsoft.Extensions.DependencyInjection; + using System.Threading.Tasks; + + class OrderProcessingWorkflow : Workflow + { + public override async Task RunAsync(WorkflowContext context, OrderPayload order) + { + await context.CallActivityAsync(nameof(NotifyActivity), new Notification("Order received")); + return new OrderResult("Order processed"); + } + } + + record OrderPayload { } + record OrderResult(string message) { } + record Notification(string Message); + + class NotifyActivity : WorkflowActivity + { + + public override Task RunAsync(WorkflowActivityContext context, Notification notification) + { + return Task.FromResult(null); + } + } + """; + + const string startupCode = """ + using Dapr.Workflow; + using Microsoft.Extensions.DependencyInjection; + + internal static class Extensions + { + public static void AddApplicationServices(this IServiceCollection services) + { + services.AddDaprWorkflow(options => + { + options.RegisterActivity(); + }); + } + } + """; + + var analyzer = new VerifyAnalyzer(Utilities.GetReferences()); + await analyzer.VerifyAnalyzerAsync(testCode, startupCode); + } +} diff --git a/test/Dapr.Workflow.Analyzers.Test/WorkflowActivityRegistrationCodeFixProviderTests.cs b/test/Dapr.Workflow.Analyzers.Test/WorkflowActivityRegistrationCodeFixProviderTests.cs new file mode 100644 index 00000000..9944a578 --- /dev/null +++ b/test/Dapr.Workflow.Analyzers.Test/WorkflowActivityRegistrationCodeFixProviderTests.cs @@ -0,0 +1,93 @@ +using Dapr.Analyzers.Common; + +namespace Dapr.Workflow.Analyzers.Test; + +public sealed class WorkflowActivityRegistrationCodeFixProviderTests +{ + [Fact] + public async Task VerifyWorkflowActivityRegistrationCodeFix() + { + const string code = """ + using Dapr.Workflow; + using System; + using Microsoft.Extensions.DependencyInjection; + using System.Threading.Tasks; + + public static class Program + { + public static void Main() + { + var services = new ServiceCollection(); + + services.AddDaprWorkflow(options => + { + }); + } + } + + class OrderProcessingWorkflow : Workflow + { + public override async Task RunAsync(WorkflowContext context, OrderPayload order) + { + await context.CallActivityAsync(nameof(NotifyActivity), new Notification("Order received")); + return new OrderResult("Order processed"); + } + } + + record OrderPayload; + record OrderResult(string Message) { }; + record Notification { public Notification(string message) { } }; + internal sealed class NotifyActivity : WorkflowActivity + { + public override async Task RunAsync(WorkflowActivityContext context, string input) + { + await Task.Delay(TimeSpan.FromSeconds(15)); + return true; + } + } + """; + + const string expectedChangedCode = """ + using Dapr.Workflow; + using System; + using Microsoft.Extensions.DependencyInjection; + using System.Threading.Tasks; + + public static class Program + { + public static void Main() + { + var services = new ServiceCollection(); + + services.AddDaprWorkflow(options => + { + options.RegisterActivity(); + }); + } + } + + class OrderProcessingWorkflow : Workflow + { + public override async Task RunAsync(WorkflowContext context, OrderPayload order) + { + await context.CallActivityAsync(nameof(NotifyActivity), new Notification("Order received")); + return new OrderResult("Order processed"); + } + } + + record OrderPayload; + record OrderResult(string Message) { }; + record Notification { public Notification(string message) { } }; + internal sealed class NotifyActivity : WorkflowActivity + { + public override async Task RunAsync(WorkflowActivityContext context, string input) + { + await Task.Delay(TimeSpan.FromSeconds(15)); + return true; + } + } + """; + + await VerifyCodeFix.RunTest(code, expectedChangedCode, typeof(object).Assembly.Location, Utilities.GetReferences(), Utilities.GetAnalyzers()); + } +} diff --git a/test/Dapr.Workflow.Analyzers.Test/WorkflowRegistrationAnalyzerTests.cs b/test/Dapr.Workflow.Analyzers.Test/WorkflowRegistrationAnalyzerTests.cs new file mode 100644 index 00000000..ee684db2 --- /dev/null +++ b/test/Dapr.Workflow.Analyzers.Test/WorkflowRegistrationAnalyzerTests.cs @@ -0,0 +1,79 @@ +using Dapr.Analyzers.Common; + +namespace Dapr.Workflow.Analyzers.Test; + +public sealed class WorkflowRegistrationAnalyzerTests +{ + [Fact] + public async Task VerifyWorkflowNotRegistered() + { + const string testCode = """ + using Dapr.Workflow; + using System.Threading.Tasks; + + class OrderProcessingWorkflow : Workflow + { + public override async Task RunAsync(WorkflowContext context, OrderPayload order) + { + return new OrderResult("Order processed"); + } + } + + class UseWorkflow() + { + public async Task RunWorkflow(DaprWorkflowClient client, OrderPayload order) + { + await client.ScheduleNewWorkflowAsync(nameof(OrderProcessingWorkflow), null, order); + } + } + + record OrderPayload { } + record OrderResult(string message) { } + """; + + var expected = VerifyAnalyzer.Diagnostic( WorkflowRegistrationAnalyzer.WorkflowDiagnosticDescriptor) + .WithSpan(16, 63, 16, 94).WithMessage("The workflow type 'OrderProcessingWorkflow' is not registered with the dependency injection provider"); + + var analyzer = new VerifyAnalyzer(Utilities.GetReferences()); + await analyzer.VerifyAnalyzerAsync(testCode, expected); + } + + [Fact] + public async Task VerifyWorkflowRegistered() + { + const string testCode = """ + using Dapr.Workflow; + using System.Threading.Tasks; + + class OrderProcessingWorkflow : Workflow + { + public override async Task RunAsync(WorkflowContext context, OrderPayload order) + { + return new OrderResult("Order processed"); + } + } + + record OrderPayload { } + record OrderResult(string message) { } + """; + + const string startupCode = """ + using Dapr.Workflow; + using Microsoft.Extensions.DependencyInjection; + + internal static class Extensions + { + public static void AddApplicationServices(this IServiceCollection services) + { + services.AddDaprWorkflow(options => + { + options.RegisterWorkflow(); + }); + } + } + """; + + var analyzer = new VerifyAnalyzer(Utilities.GetReferences()); + await analyzer.VerifyAnalyzerAsync(testCode, startupCode); + } +} diff --git a/test/Dapr.Workflow.Analyzers.Test/WorkflowRegistrationCodeFixProviderTests.cs b/test/Dapr.Workflow.Analyzers.Test/WorkflowRegistrationCodeFixProviderTests.cs new file mode 100644 index 00000000..91339aea --- /dev/null +++ b/test/Dapr.Workflow.Analyzers.Test/WorkflowRegistrationCodeFixProviderTests.cs @@ -0,0 +1,226 @@ +using Dapr.Analyzers.Common; + +namespace Dapr.Workflow.Analyzers.Test; + +public sealed class WorkflowRegistrationCodeFixProviderTests +{ + [Fact] + public async Task RegisterWorkflow() + { + const string code = """ + using Dapr.Workflow; + using Microsoft.Extensions.DependencyInjection; + using System.Threading.Tasks; + + public static class Program + { + public static void Main() + { + var services = new ServiceCollection(); + + services.AddDaprWorkflow(options => + { + }); + } + + private static async Task ScheduleWorkflow(DaprWorkflowClient client) + { + await client.ScheduleNewWorkflowAsync(nameof(OrderProcessingWorkflow), null, new OrderPayload()); + } + } + + class OrderProcessingWorkflow : Workflow + { + public override Task RunAsync(WorkflowContext context, OrderPayload order) + { + return Task.FromResult(new OrderResult("Order processed")); + } + } + + record OrderPayload { } + record OrderResult(string message) { } + + """; + + const string expectedChangedCode = """ + + using Dapr.Workflow; + using Microsoft.Extensions.DependencyInjection; + using System.Threading.Tasks; + + public static class Program + { + public static void Main() + { + var services = new ServiceCollection(); + + services.AddDaprWorkflow(options => + { + options.RegisterWorkflow(); + }); + } + + private static async Task ScheduleWorkflow(DaprWorkflowClient client) + { + await client.ScheduleNewWorkflowAsync(nameof(OrderProcessingWorkflow), null, new OrderPayload()); + } + } + + class OrderProcessingWorkflow : Workflow + { + public override Task RunAsync(WorkflowContext context, OrderPayload order) + { + return Task.FromResult(new OrderResult("Order processed")); + } + } + + record OrderPayload { } + record OrderResult(string message) { } + + """; + + await VerifyCodeFix.RunTest(code, expectedChangedCode, typeof(object).Assembly.Location, Utilities.GetReferences(), Utilities.GetAnalyzers()); + } + + [Fact] + public async Task RegisterWorkflow_WhenAddDaprWorkflowIsNotFound() + { + const string code = """ + using Dapr.Workflow; + using Microsoft.AspNetCore.Builder; + using System.Threading.Tasks; + + public static class Program + { + public static void Main() + { + var builder = WebApplication.CreateBuilder(); + + var app = builder.Build(); + } + + private static async Task ScheduleWorkflow(DaprWorkflowClient client) + { + await client.ScheduleNewWorkflowAsync(nameof(OrderProcessingWorkflow), null, new OrderPayload()); + } + } + + class OrderProcessingWorkflow : Workflow + { + public override Task RunAsync(WorkflowContext context, OrderPayload order) + { + return Task.FromResult(new OrderResult("Order processed")); + } + } + + record OrderPayload { } + record OrderResult(string message) { } + """; + + const string expectedChangedCode = """ + using Dapr.Workflow; + using Microsoft.AspNetCore.Builder; + using System.Threading.Tasks; + + public static class Program + { + public static void Main() + { + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddDaprWorkflow(options => + { + options.RegisterWorkflow(); + }); + + var app = builder.Build(); + } + + private static async Task ScheduleWorkflow(DaprWorkflowClient client) + { + await client.ScheduleNewWorkflowAsync(nameof(OrderProcessingWorkflow), null, new OrderPayload()); + } + } + + class OrderProcessingWorkflow : Workflow + { + public override Task RunAsync(WorkflowContext context, OrderPayload order) + { + return Task.FromResult(new OrderResult("Order processed")); + } + } + + record OrderPayload { } + record OrderResult(string message) { } + """; + + await VerifyCodeFix.RunTest(code, expectedChangedCode, typeof(object).Assembly.Location, Utilities.GetReferences(), Utilities.GetAnalyzers()); + } + + [Fact] + public async Task RegisterWorkflow_WhenAddDaprWorkflowIsNotFound_TopLevelStatements() + { + const string code = """ + using Dapr.Workflow; + using Microsoft.AspNetCore.Builder; + using Microsoft.Extensions.DependencyInjection; + using System.Threading.Tasks; + + var builder = WebApplication.CreateBuilder(); + + var app = builder.Build(); + + var workflowClient = app.Services.GetRequiredService(); + await workflowClient.ScheduleNewWorkflowAsync(nameof(TestNamespace.OrderProcessingWorkflow), null, new OrderPayload()); + + namespace TestNamespace + { + class OrderProcessingWorkflow : Workflow + { + public override Task RunAsync(WorkflowContext context, OrderPayload order) + { + return Task.FromResult(new OrderResult("Order processed")); + } + } + } + + record OrderPayload { } + record OrderResult(string message) { } + """; + + const string expectedChangedCode = """ + using Dapr.Workflow; + using Microsoft.AspNetCore.Builder; + using Microsoft.Extensions.DependencyInjection; + using System.Threading.Tasks; + + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddDaprWorkflow(options => + { + options.RegisterWorkflow(); + }); + + var app = builder.Build(); + + var workflowClient = app.Services.GetRequiredService(); + await workflowClient.ScheduleNewWorkflowAsync(nameof(TestNamespace.OrderProcessingWorkflow), null, new OrderPayload()); + + namespace TestNamespace + { + class OrderProcessingWorkflow : Workflow + { + public override Task RunAsync(WorkflowContext context, OrderPayload order) + { + return Task.FromResult(new OrderResult("Order processed")); + } + } + } + + record OrderPayload { } + record OrderResult(string message) { } + """; + + await VerifyCodeFix.RunTest(code, expectedChangedCode, typeof(object).Assembly.Location, Utilities.GetReferences(), Utilities.GetAnalyzers()); + } +} From f588665f229c74a7ca6af40539502e520fe3e004 Mon Sep 17 00:00:00 2001 From: Chris Wlash <140447546+cwalsh2189@users.noreply.github.com> Date: Sun, 13 Apr 2025 08:33:30 -0400 Subject: [PATCH 12/22] Attach metadata to each individual message in bulkpublish, if it exists (#1437) * Instead of just attaching metadata to the full envelope, the metadata is attached to each individual message as well, if it exists at all. Signed-off-by: Chris Walsh Co-authored-by: Whit Waldo --- src/Dapr.Client/DaprClientGrpc.cs | 6 ++++ .../BulkPublishEventApiTest.cs | 32 +++++++++++++++---- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/Dapr.Client/DaprClientGrpc.cs b/src/Dapr.Client/DaprClientGrpc.cs index 9ee860cf..70fe750a 100644 --- a/src/Dapr.Client/DaprClientGrpc.cs +++ b/src/Dapr.Client/DaprClientGrpc.cs @@ -215,6 +215,12 @@ internal class DaprClientGrpc : DaprClient ? Constants.ContentTypeCloudEvent : Constants.ContentTypeApplicationJson, }; + + if (metadata != null) + { + entry.Metadata.Add(metadata); + } + envelope.Entries.Add(entry); entryMap.Add(counter.ToString(), new BulkPublishEntry( entry.EntryId, events[counter], entry.ContentType, entry.Metadata)); diff --git a/test/Dapr.Client.Test/BulkPublishEventApiTest.cs b/test/Dapr.Client.Test/BulkPublishEventApiTest.cs index 95d10bcd..62afeba6 100644 --- a/test/Dapr.Client.Test/BulkPublishEventApiTest.cs +++ b/test/Dapr.Client.Test/BulkPublishEventApiTest.cs @@ -87,30 +87,40 @@ public class BulkPublishEventApiTest request.Dismiss(); var envelope = await request.GetRequestEnvelopeAsync(); - + envelope.Entries.Count.ShouldBe(2); envelope.PubsubName.ShouldBe(TestPubsubName); envelope.Topic.ShouldBe(TestTopicName); + //Validate that the metadata is on the envelope envelope.Metadata.Count.ShouldBe(2); envelope.Metadata.Keys.Contains("key1").ShouldBeTrue(); envelope.Metadata.Keys.Contains("key2").ShouldBeTrue(); envelope.Metadata["key1"].ShouldBe("value1"); envelope.Metadata["key2"].ShouldBe("value2"); + //Validate that the metadata is also on each entry var firstEntry = envelope.Entries[0]; firstEntry.EntryId.ShouldBe("0"); firstEntry.ContentType.ShouldBe(TestContentType); firstEntry.Event.ToStringUtf8().ShouldBe(JsonSerializer.Serialize(bulkPublishData[0], client.InnerClient.JsonSerializerOptions)); - firstEntry.Metadata.ShouldBeEmpty(); + firstEntry.Metadata.Keys.Count.ShouldBe(2); + firstEntry.Metadata.ContainsKey("key1").ShouldBeTrue(); + firstEntry.Metadata["key1"].ShouldBe("value1"); + firstEntry.Metadata.ContainsKey("key2").ShouldBeTrue(); + firstEntry.Metadata["key2"].ShouldBe("value2"); var secondEntry = envelope.Entries[1]; secondEntry.EntryId.ShouldBe("1"); secondEntry.ContentType.ShouldBe(TestContentType); secondEntry.Event.ToStringUtf8().ShouldBe(JsonSerializer.Serialize(bulkPublishData[1], client.InnerClient.JsonSerializerOptions)); - secondEntry.Metadata.ShouldBeEmpty(); + secondEntry.Metadata.Keys.Count.ShouldBe(2); + secondEntry.Metadata.ContainsKey("key1").ShouldBeTrue(); + secondEntry.Metadata["key1"].ShouldBe("value1"); + secondEntry.Metadata.ContainsKey("key2").ShouldBeTrue(); + secondEntry.Metadata["key2"].ShouldBe("value2"); // Create Response & Respond var response = new Autogenerated.BulkPublishResponse @@ -183,25 +193,35 @@ public class BulkPublishEventApiTest envelope.PubsubName.ShouldBe(TestPubsubName); envelope.Topic.ShouldBe(TestTopicName); + //Validate the metadata on the envelope envelope.Metadata.Count.ShouldBe(2); envelope.Metadata.Keys.Contains("key1").ShouldBeTrue(); envelope.Metadata.Keys.Contains("key2").ShouldBeTrue(); envelope.Metadata["key1"].ShouldBe("value1"); envelope.Metadata["key2"].ShouldBe("value2"); + //Validate the metadata on each entry var firstEntry = envelope.Entries[0]; firstEntry.EntryId.ShouldBe("0"); firstEntry.ContentType.ShouldBe(TestContentType); firstEntry.Event.ToStringUtf8().ShouldBe(JsonSerializer.Serialize(bulkPublishData[0], client.InnerClient.JsonSerializerOptions)); - firstEntry.Metadata.ShouldBeEmpty(); + firstEntry.Metadata.Keys.Count.ShouldBe(2); + firstEntry.Metadata.ContainsKey("key1").ShouldBeTrue(); + firstEntry.Metadata["key1"].ShouldBe("value1"); + firstEntry.Metadata.ContainsKey("key2").ShouldBeTrue(); + firstEntry.Metadata["key2"].ShouldBe("value2"); var secondEntry = envelope.Entries[1]; secondEntry.EntryId.ShouldBe("1"); secondEntry.ContentType.ShouldBe(TestContentType); secondEntry.Event.ToStringUtf8().ShouldBe(JsonSerializer.Serialize(bulkPublishData[1], client.InnerClient.JsonSerializerOptions)); - secondEntry.Metadata.ShouldBeEmpty(); + secondEntry.Metadata.Keys.Count.ShouldBe(2); + secondEntry.Metadata.ContainsKey("key1").ShouldBeTrue(); + secondEntry.Metadata["key1"].ShouldBe("value1"); + secondEntry.Metadata.ContainsKey("key2").ShouldBeTrue(); + secondEntry.Metadata["key2"].ShouldBe("value2"); // Create Response & Respond var response = new Autogenerated.BulkPublishResponse @@ -336,4 +356,4 @@ public class BulkPublishEventApiTest public string Size { get; set; } public string Color { get; set; } } -} \ No newline at end of file +} From 6d9211378867573be463eb2957945ea395337052 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sat, 26 Apr 2025 00:32:04 -0500 Subject: [PATCH 13/22] Updated solution to remove unnecessary dependencies on Newtonsoft.Json (#1524) * Updated solution to remove unnecessary dependencies on Newtonsoft.Json in favor of just using System.Text.Json throughout for JSON serialization. --- Directory.Packages.props | 1 - .../DaprFormatTimeSpanTests.cs | 24 ++++++------------- .../Widget.cs | 6 ++++- .../AuthenticationTest.cs | 15 ++++++++---- .../ControllerIntegrationTest.cs | 17 ++++++------- 5 files changed, 31 insertions(+), 32 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 21fccd0a..eee08679 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -40,7 +40,6 @@ - diff --git a/test/Dapr.Actors.Test/DaprFormatTimeSpanTests.cs b/test/Dapr.Actors.Test/DaprFormatTimeSpanTests.cs index ed493e8a..338e17b0 100644 --- a/test/Dapr.Actors.Test/DaprFormatTimeSpanTests.cs +++ b/test/Dapr.Actors.Test/DaprFormatTimeSpanTests.cs @@ -11,15 +11,14 @@ // limitations under the License. // ------------------------------------------------------------------------ +using System.Text.Json; + #pragma warning disable 0618 namespace Dapr.Actors.Test; using System; -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; +using System.Collections.Generic;using System.Threading.Tasks; using Dapr.Actors.Runtime; -using Newtonsoft.Json; using Xunit; public class DaprFormatTimeSpanTests @@ -28,12 +27,12 @@ public class DaprFormatTimeSpanTests { new object[] { - "{\"dueTime\":\"4h15m50s60ms\"", + "4h15m50s60ms", new TimeSpan(0, 4, 15, 50, 60), }, new object[] { - "{\"dueTime\":\"0h35m10s12ms\"", + "0h35m10s12ms", new TimeSpan(0, 0, 35, 10, 12), }, }; @@ -90,17 +89,8 @@ public class DaprFormatTimeSpanTests [Theory] [MemberData(nameof(DaprFormatTimeSpanJsonStringsAndExpectedDeserializedValues))] - public void DaprFormat_TimeSpan_Parsing(string daprFormatTimeSpanJsonString, TimeSpan expectedDeserializedValue) + public void DaprFormat_TimeSpan_Parsing(string timespanString, TimeSpan expectedDeserializedValue) { - using var textReader = new StringReader(daprFormatTimeSpanJsonString); - using var jsonTextReader = new JsonTextReader(textReader); - - while (jsonTextReader.TokenType != JsonToken.String) - { - jsonTextReader.Read(); - } - - var timespanString = (string)jsonTextReader.Value; var deserializedTimeSpan = ConverterUtils.ConvertTimeSpanFromDaprFormat(timespanString); Assert.Equal(expectedDeserializedValue, deserializedTimeSpan); @@ -116,7 +106,7 @@ public class DaprFormatTimeSpanTests state: null, dueTime: dueTime, period: period); - return Task.FromResult(System.Text.Json.JsonSerializer.Serialize(timerInfo)); + return Task.FromResult(JsonSerializer.Serialize(timerInfo)); } var inTheFuture = TimeSpan.FromMilliseconds(20); diff --git a/test/Dapr.AspNetCore.IntegrationTest.App/Widget.cs b/test/Dapr.AspNetCore.IntegrationTest.App/Widget.cs index 3177ef08..467204d4 100644 --- a/test/Dapr.AspNetCore.IntegrationTest.App/Widget.cs +++ b/test/Dapr.AspNetCore.IntegrationTest.App/Widget.cs @@ -11,11 +11,15 @@ // limitations under the License. // ------------------------------------------------------------------------ +using System.Text.Json.Serialization; + namespace Dapr.AspNetCore.IntegrationTest.App; public class Widget { + [JsonPropertyName("size")] public string Size { get; set; } + [JsonPropertyName("count")] public int Count { get; set; } -} \ No newline at end of file +} diff --git a/test/Dapr.AspNetCore.IntegrationTest/AuthenticationTest.cs b/test/Dapr.AspNetCore.IntegrationTest/AuthenticationTest.cs index ba91828f..fca31134 100644 --- a/test/Dapr.AspNetCore.IntegrationTest/AuthenticationTest.cs +++ b/test/Dapr.AspNetCore.IntegrationTest/AuthenticationTest.cs @@ -14,16 +14,21 @@ using System.Net; using System.Net.Http; using System.Text; +using System.Text.Json; using System.Threading.Tasks; using Dapr.AspNetCore.IntegrationTest.App; using Shouldly; -using Newtonsoft.Json; using Xunit; namespace Dapr.AspNetCore.IntegrationTest; public class AuthenticationTest { + private JsonSerializerOptions serializerOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + [Fact] public async Task ValidToken_ShouldBeAuthenticatedAndAuthorized() { @@ -36,13 +41,13 @@ public class AuthenticationTest var httpClient = factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions { HandleCookies = false }); var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/requires-api-token") { - Content = new StringContent(JsonConvert.SerializeObject(userInfo), Encoding.UTF8, "application/json") + Content = new StringContent(JsonSerializer.Serialize(userInfo), Encoding.UTF8, "application/json") }; request.Headers.Add("Dapr-Api-Token", "abcdefg"); var response = await httpClient.SendAsync(request); response.EnsureSuccessStatusCode(); var responseContent = await response.Content.ReadAsStringAsync(); - var responseUserInfo = JsonConvert.DeserializeObject(responseContent); + var responseUserInfo = JsonSerializer.Deserialize(responseContent, serializerOptions); responseUserInfo.Name.ShouldBe(userInfo.Name); } } @@ -59,7 +64,7 @@ public class AuthenticationTest var httpClient = factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions { HandleCookies = false }); var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/requires-api-token") { - Content = new StringContent(JsonConvert.SerializeObject(userInfo), Encoding.UTF8, "application/json") + Content = new StringContent(JsonSerializer.Serialize(userInfo, serializerOptions), Encoding.UTF8, "application/json") }; request.Headers.Add("Dapr-Api-Token", "asdfgh"); var response = await httpClient.SendAsync(request); @@ -67,4 +72,4 @@ public class AuthenticationTest response.StatusCode.ShouldBe(HttpStatusCode.Unauthorized); } } -} \ No newline at end of file +} diff --git a/test/Dapr.AspNetCore.IntegrationTest/ControllerIntegrationTest.cs b/test/Dapr.AspNetCore.IntegrationTest/ControllerIntegrationTest.cs index 8bf7316e..e7315ab1 100644 --- a/test/Dapr.AspNetCore.IntegrationTest/ControllerIntegrationTest.cs +++ b/test/Dapr.AspNetCore.IntegrationTest/ControllerIntegrationTest.cs @@ -11,15 +11,15 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.AspNetCore.IntegrationTest; - +using System.Text.Json; using System.Net.Http; using System.Threading.Tasks; using Dapr.AspNetCore.IntegrationTest.App; using Shouldly; -using Newtonsoft.Json; using Xunit; +namespace Dapr.AspNetCore.IntegrationTest; + public class ControllerIntegrationTest { [Fact] @@ -55,7 +55,7 @@ public class ControllerIntegrationTest var response = await httpClient.SendAsync(request); response.EnsureSuccessStatusCode(); var responseContent = await response.Content.ReadAsStringAsync(); - var responseWidget = JsonConvert.DeserializeObject(responseContent); + var responseWidget = JsonSerializer.Deserialize(responseContent); responseWidget.Size.ShouldBe(widget.Size); responseWidget.Count.ShouldBe(widget.Count); } @@ -72,7 +72,8 @@ public class ControllerIntegrationTest var response = await httpClient.SendAsync(request); response.EnsureSuccessStatusCode(); var responseContent = await response.Content.ReadAsStringAsync(); - var responseWidget = JsonConvert.DeserializeObject(responseContent); + var responseWidget = + !string.IsNullOrWhiteSpace(responseContent) ? JsonSerializer.Deserialize(responseContent) : null; Assert.Null(responseWidget); } } @@ -129,7 +130,7 @@ public class ControllerIntegrationTest var response = await httpClient.SendAsync(request); response.EnsureSuccessStatusCode(); var responseContent = await response.Content.ReadAsStringAsync(); - var responseWidget = JsonConvert.DeserializeObject(responseContent); + var responseWidget = JsonSerializer.Deserialize(responseContent); responseWidget.Size.ShouldBe(widget.Size); responseWidget.Count.ShouldBe(widget.Count); } @@ -146,7 +147,7 @@ public class ControllerIntegrationTest var response = await httpClient.SendAsync(request); response.EnsureSuccessStatusCode(); var responseContent = await response.Content.ReadAsStringAsync(); - var responseWidget = JsonConvert.DeserializeObject(responseContent); + var responseWidget = !string.IsNullOrWhiteSpace(responseContent) ? JsonSerializer.Deserialize(responseContent) : null; Assert.Null(responseWidget); } } @@ -163,4 +164,4 @@ public class ControllerIntegrationTest response.EnsureSuccessStatusCode(); } } -} \ No newline at end of file +} From 49992f4a8631e901f89526eb6a8aa071167b9d2a Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Fri, 2 May 2025 15:39:12 -0500 Subject: [PATCH 14/22] Add Dapr.Cryptography package + fix for large files (#1527) * Implementation of the new crypto client Signed-off-by: Whit Waldo Co-authored-by: Christopher Watford <83599748+watfordsuzy@users.noreply.github.com> --- all.sln | 39 +++- .../dotnet-ai/dotnet-ai-conversation-howto.md | 4 - .../dotnet-ai/dotnet-ai-conversation-usage.md | 2 +- .../dotnet-cryptography/_index.md | 12 + .../dotnet-cryptography-howto.md | 74 +++++++ .../dotnet-cryptography-usage.md | 131 +++++++++++ examples/Client/Cryptography/Program.cs | 48 ---- .../Components/local-storage.yaml | 0 .../Cryptography/Cryptography.csproj | 15 +- .../EncryptDecryptFileStreamExample.cs | 37 ++-- .../EncryptDecryptLargeFileExample.cs | 105 +++++++++ .../Examples/EncryptDecryptStringExample.cs | 24 +- .../Example.cs => Cryptography/IExample.cs} | 8 +- examples/Cryptography/Program.cs | 64 ++++++ examples/{Client => }/Cryptography/README.md | 0 examples/{Client => }/Cryptography/file.txt | 0 .../Cryptography/keys/rsa-private-key.pem | 0 .../StreamingSubscriptionExample/Program.cs | 4 +- .../StreamingSubscriptionExample.csproj | 3 +- examples/README.md | 1 + src/Dapr.Common/AssemblyInfo.cs | 2 + src/Dapr.Common/DaprGenericClientBuilder.cs | 2 +- .../Dapr.Cryptography.csproj | 28 +++ .../Encryption/DaprEncryptionClient.cs | 143 ++++++++++++ .../Encryption/DaprEncryptionClientBuilder.cs | 36 +++ .../Encryption/DaprEncryptionGrpcClient.cs | 208 ++++++++++++++++++ .../Encryption/DecryptionStreamProcessor.cs | 147 +++++++++++++ .../Encryption/EncryptionStreamProcessor.cs | 148 +++++++++++++ .../Extensions/DaprCryptographyBuilder.cs | 28 +++ ...CryptographyServiceCollectionExtensions.cs | 42 ++++ .../Encryption/IDaprEncryptionBuilder.cs | 19 ++ .../Encryption/IDaprEncryptionClient.cs | 77 +++++++ .../Encryption/IDecryptionStreamProcessor.cs | 40 ++++ .../Encryption/IEncryptionStreamProcessor.cs | 40 ++++ .../Encryption/Models/DataEncryptionCipher.cs | 33 +++ .../Encryption/Models/DecryptionOptions.cs | 34 +++ .../Encryption/Models/EncryptionOptions.cs | 52 +++++ .../Encryption/Models/KeyWrapAlgorithm.cs | 58 +++++ .../Extensions/ReadOnlyMemoryExtensions.cs | 34 +++ .../IDaprCryptographyBuilder.cs | 22 ++ .../Dapr.Cryptography.Test.csproj | 28 +++ .../DaprCryptographyBuilderTests.cs | 29 +++ ...ographyServiceCollectionExtensionsTests.cs | 66 ++++++ .../Models/DecryptionOptionsTests.cs | 35 +++ .../Models/EncryptionOptionsTests.cs | 72 ++++++ .../ReadOnlyMemoryExtensionsTests.cs | 59 +++++ test/Dapr.E2E.Test/Dapr.E2E.Test.csproj | 7 + test/Dapr.E2E.Test/components/crypto.yaml | 11 + test/Dapr.E2E.Test/keys/rsa-private-key.pem | 52 +++++ 49 files changed, 2016 insertions(+), 107 deletions(-) create mode 100644 daprdocs/content/en/dotnet-sdk-docs/dotnet-cryptography/_index.md create mode 100644 daprdocs/content/en/dotnet-sdk-docs/dotnet-cryptography/dotnet-cryptography-howto.md create mode 100644 daprdocs/content/en/dotnet-sdk-docs/dotnet-cryptography/dotnet-cryptography-usage.md delete mode 100644 examples/Client/Cryptography/Program.cs rename examples/{Client => }/Cryptography/Components/local-storage.yaml (100%) rename examples/{Client => }/Cryptography/Cryptography.csproj (59%) rename examples/{Client => }/Cryptography/Examples/EncryptDecryptFileStreamExample.cs (70%) create mode 100644 examples/Cryptography/Examples/EncryptDecryptLargeFileExample.cs rename examples/{Client => }/Cryptography/Examples/EncryptDecryptStringExample.cs (54%) rename examples/{Client/Cryptography/Example.cs => Cryptography/IExample.cs} (83%) create mode 100644 examples/Cryptography/Program.cs rename examples/{Client => }/Cryptography/README.md (100%) rename examples/{Client => }/Cryptography/file.txt (100%) rename examples/{Client => }/Cryptography/keys/rsa-private-key.pem (100%) rename examples/{Client/PublishSubscribe => Messaging}/StreamingSubscriptionExample/Program.cs (98%) rename examples/{Client/PublishSubscribe => Messaging}/StreamingSubscriptionExample/StreamingSubscriptionExample.csproj (62%) create mode 100644 src/Dapr.Cryptography/Dapr.Cryptography.csproj create mode 100644 src/Dapr.Cryptography/Encryption/DaprEncryptionClient.cs create mode 100644 src/Dapr.Cryptography/Encryption/DaprEncryptionClientBuilder.cs create mode 100644 src/Dapr.Cryptography/Encryption/DaprEncryptionGrpcClient.cs create mode 100644 src/Dapr.Cryptography/Encryption/DecryptionStreamProcessor.cs create mode 100644 src/Dapr.Cryptography/Encryption/EncryptionStreamProcessor.cs create mode 100644 src/Dapr.Cryptography/Encryption/Extensions/DaprCryptographyBuilder.cs create mode 100644 src/Dapr.Cryptography/Encryption/Extensions/DaprCryptographyServiceCollectionExtensions.cs create mode 100644 src/Dapr.Cryptography/Encryption/IDaprEncryptionBuilder.cs create mode 100644 src/Dapr.Cryptography/Encryption/IDaprEncryptionClient.cs create mode 100644 src/Dapr.Cryptography/Encryption/IDecryptionStreamProcessor.cs create mode 100644 src/Dapr.Cryptography/Encryption/IEncryptionStreamProcessor.cs create mode 100644 src/Dapr.Cryptography/Encryption/Models/DataEncryptionCipher.cs create mode 100644 src/Dapr.Cryptography/Encryption/Models/DecryptionOptions.cs create mode 100644 src/Dapr.Cryptography/Encryption/Models/EncryptionOptions.cs create mode 100644 src/Dapr.Cryptography/Encryption/Models/KeyWrapAlgorithm.cs create mode 100644 src/Dapr.Cryptography/Extensions/ReadOnlyMemoryExtensions.cs create mode 100644 src/Dapr.Cryptography/IDaprCryptographyBuilder.cs create mode 100644 test/Dapr.Cryptography.Test/Dapr.Cryptography.Test.csproj create mode 100644 test/Dapr.Cryptography.Test/Encryption/Extensions/DaprCryptographyBuilderTests.cs create mode 100644 test/Dapr.Cryptography.Test/Encryption/Extensions/DaprCryptographyServiceCollectionExtensionsTests.cs create mode 100644 test/Dapr.Cryptography.Test/Encryption/Models/DecryptionOptionsTests.cs create mode 100644 test/Dapr.Cryptography.Test/Encryption/Models/EncryptionOptionsTests.cs create mode 100644 test/Dapr.Cryptography.Test/Extensions/ReadOnlyMemoryExtensionsTests.cs create mode 100644 test/Dapr.E2E.Test/components/crypto.yaml create mode 100644 test/Dapr.E2E.Test/keys/rsa-private-key.pem diff --git a/all.sln b/all.sln index ae51012e..706cf896 100644 --- a/all.sln +++ b/all.sln @@ -111,8 +111,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.Actors.Generators.Test EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.E2E.Test.Actors.Generators", "test\Dapr.E2E.Test.Actors.Generators\Dapr.E2E.Test.Actors.Generators.csproj", "{B5CDB0DC-B26D-48F1-B934-FE5C1C991940}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cryptography", "examples\Client\Cryptography\Cryptography.csproj", "{C74FBA78-13E8-407F-A173-4555AEE41FF3}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Protos", "src\Dapr.Protos\Dapr.Protos.csproj", "{DFBABB04-50E9-42F6-B470-310E1B545638}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Common", "src\Dapr.Common\Dapr.Common.csproj", "{B445B19C-A925-4873-8CB7-8317898B6970}" @@ -143,8 +141,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Messaging.Test", "test EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Messaging", "src\Dapr.Messaging\Dapr.Messaging.csproj", "{0EAE36A1-B578-4F13-A113-7A477ECA1BDA}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StreamingSubscriptionExample", "examples\Client\PublishSubscribe\StreamingSubscriptionExample\StreamingSubscriptionExample.csproj", "{290D1278-F613-4DF3-9DF5-F37E38CDC363}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Jobs", "src\Dapr.Jobs\Dapr.Jobs.csproj", "{C8BB6A85-A7EA-40C0-893D-F36F317829B3}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Jobs.Test", "test\Dapr.Jobs.Test\Dapr.Jobs.Test.csproj", "{BF9828E9-5597-4D42-AA6E-6E6C12214204}" @@ -169,6 +165,18 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Actors.Analyzers.Test" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Analyzers.Common", "test\Dapr.Analyzers.Common\Dapr.Analyzers.Common.csproj", "{7E23E229-6823-4D84-AF3A-AE14CEAEF52A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Cryptography", "src\Dapr.Cryptography\Dapr.Cryptography.csproj", "{160EFFA0-F6B9-49E4-B62B-68C0D53DB425}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Cryptography.Test", "test\Dapr.Cryptography.Test\Dapr.Cryptography.Test.csproj", "{B508EBD6-0F14-480C-A446-45A09052733B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StreamingSubscriptionExample", "examples\Messaging\StreamingSubscriptionExample\StreamingSubscriptionExample.csproj", "{E070F694-335D-4D96-8951-F41D0A5F2A8B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Cryptography", "Cryptography", "{6843B5B3-9E95-4022-B792-8A1DE6BFEFEC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cryptography", "examples\Cryptography\Cryptography.csproj", "{097D5F6F-D26F-4BFB-9074-FA52577EB442}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Messaging", "Messaging", "{442E80E5-8040-4123-B88A-26FD36BA95D9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -445,6 +453,22 @@ Global {7E23E229-6823-4D84-AF3A-AE14CEAEF52A}.Debug|Any CPU.Build.0 = Debug|Any CPU {7E23E229-6823-4D84-AF3A-AE14CEAEF52A}.Release|Any CPU.ActiveCfg = Release|Any CPU {7E23E229-6823-4D84-AF3A-AE14CEAEF52A}.Release|Any CPU.Build.0 = Release|Any CPU + {160EFFA0-F6B9-49E4-B62B-68C0D53DB425}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {160EFFA0-F6B9-49E4-B62B-68C0D53DB425}.Debug|Any CPU.Build.0 = Debug|Any CPU + {160EFFA0-F6B9-49E4-B62B-68C0D53DB425}.Release|Any CPU.ActiveCfg = Release|Any CPU + {160EFFA0-F6B9-49E4-B62B-68C0D53DB425}.Release|Any CPU.Build.0 = Release|Any CPU + {B508EBD6-0F14-480C-A446-45A09052733B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B508EBD6-0F14-480C-A446-45A09052733B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B508EBD6-0F14-480C-A446-45A09052733B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B508EBD6-0F14-480C-A446-45A09052733B}.Release|Any CPU.Build.0 = Release|Any CPU + {E070F694-335D-4D96-8951-F41D0A5F2A8B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E070F694-335D-4D96-8951-F41D0A5F2A8B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E070F694-335D-4D96-8951-F41D0A5F2A8B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E070F694-335D-4D96-8951-F41D0A5F2A8B}.Release|Any CPU.Build.0 = Release|Any CPU + {097D5F6F-D26F-4BFB-9074-FA52577EB442}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {097D5F6F-D26F-4BFB-9074-FA52577EB442}.Debug|Any CPU.Build.0 = Debug|Any CPU + {097D5F6F-D26F-4BFB-9074-FA52577EB442}.Release|Any CPU.ActiveCfg = Release|Any CPU + {097D5F6F-D26F-4BFB-9074-FA52577EB442}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -497,7 +521,6 @@ Global {7C06FE2D-6C62-48F5-A505-F0D715C554DE} = {7592AFA4-426B-42F3-AE82-957C86814482} {AF89083D-4715-42E6-93E9-38497D12A8A6} = {DD020B34-460F-455F-8D17-CF4A949F100B} {B5CDB0DC-B26D-48F1-B934-FE5C1C991940} = {DD020B34-460F-455F-8D17-CF4A949F100B} - {C74FBA78-13E8-407F-A173-4555AEE41FF3} = {A7F41094-8648-446B-AECD-DCC2CC871F73} {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} @@ -525,6 +548,12 @@ Global {E49C822C-E921-48DF-897B-3E603CA596D2} = {27C5D71D-0721-4221-9286-B94AB07B58CF} {A2C0F203-11FF-4B7F-A94F-B9FD873573FE} = {DD020B34-460F-455F-8D17-CF4A949F100B} {7E23E229-6823-4D84-AF3A-AE14CEAEF52A} = {DD020B34-460F-455F-8D17-CF4A949F100B} + {160EFFA0-F6B9-49E4-B62B-68C0D53DB425} = {27C5D71D-0721-4221-9286-B94AB07B58CF} + {B508EBD6-0F14-480C-A446-45A09052733B} = {DD020B34-460F-455F-8D17-CF4A949F100B} + {6843B5B3-9E95-4022-B792-8A1DE6BFEFEC} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78} + {097D5F6F-D26F-4BFB-9074-FA52577EB442} = {6843B5B3-9E95-4022-B792-8A1DE6BFEFEC} + {442E80E5-8040-4123-B88A-26FD36BA95D9} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78} + {E070F694-335D-4D96-8951-F41D0A5F2A8B} = {442E80E5-8040-4123-B88A-26FD36BA95D9} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40} 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 index 8826f2af..62535dab 100644 --- 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 @@ -77,7 +77,3 @@ Put the Dapr AI .NET SDK to the test. Walk through the samples to see Dapr in ac 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 index b4917e02..444a59a0 100644 --- 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 @@ -17,7 +17,7 @@ It maintains access to networking resources in the form of TCP sockets used to c 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 +This can be aided by utilizing the dependency injection functionality. The registration method supports registration 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. diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-cryptography/_index.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-cryptography/_index.md new file mode 100644 index 00000000..3fe574f1 --- /dev/null +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-cryptography/_index.md @@ -0,0 +1,12 @@ +--- +type: docs +title: "Dapr Cryptography .NET SDK" +linkTitle: "Cryptography" +weight: 51000 +description: Get up and running with the Dapr Cryptography .NET SDK +--- + +With the Dapr Cryptography package, you can perform high-performance encryption and decryption operations with Dapr. + +To get started with this functionality, walk through the [Dapr Cryptography({{< ref dotnet-cryptography-howto.md >}}) +how-to guide. \ No newline at end of file diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-cryptography/dotnet-cryptography-howto.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-cryptography/dotnet-cryptography-howto.md new file mode 100644 index 00000000..1bf4f20f --- /dev/null +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-cryptography/dotnet-cryptography-howto.md @@ -0,0 +1,74 @@ +--- +type: docs +title: "How to: Create an use Dapr Cryptography in the .NET SDK" +linkTitle: "How to: Use the Cryptography client" +weight: 510100 +description: Learn how to create and use the Dapr Cryptography client using the .NET SDK +--- + +## Prerequisites +- [.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) + +## Installation +To get started with the Dapr Cryptography client, install the [Dapr.Cryptography package](https://www.nuget.org/packages/Dapr.Cryptography) from NuGet: +```sh +dotnet add package Dapr.Cryptography +``` + +A `DaprEncryptionClient` maintains access to networking resources in the form of TCP sockets used to communicate with +the Dapr sidecar. + +### Dependency Injection + +The `AddDaprEncryptionClient()` method will register the Dapr client with dependency injection and is the recommended approach +for using this package. This method accepts an optional options delegate for configuring the `DaprEncryptionClient` 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 `DaprEncryptionClient`: + +```csharp +services.AddDaprEncryptionClient(); +``` + +The optional configuration delegate is used to configure the `DaprEncryptionClient` by specifying options on the +`DaprEncryptionClientBuilder` as in the following example: +```csharp +services.AddSingleton(); +services.AddDaprEncryptionClient((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 `DaprEncryptionClient` can also be built using the static client builder. + +For best performance, create a single long-lived instance of `DaprEncryptionClient` and provide access to that shared instance throughout +your application. `DaprEncryptionClient` instances are thread-safe and intended to be shared. + +Avoid creating a `DaprEncryptionClient` per-operation. + +A `DaprEncryptionClient` can be configured by invoking methods on the `DaprEncryptionClientBuilder` class before calling `.Build()` +to create the client. The settings for each `DaprEncryptionClient` are separate and cannot be changed after calling `.Build()`. + +```csharp +var daprEncryptionClient = new DaprEncryptionClientBuilder() + .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/Cryptography) | Clone the SDK repo to try out some examples and get started. | diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-cryptography/dotnet-cryptography-usage.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-cryptography/dotnet-cryptography-usage.md new file mode 100644 index 00000000..760e3965 --- /dev/null +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-cryptography/dotnet-cryptography-usage.md @@ -0,0 +1,131 @@ +--- +type: docs +title: "Dapr Cryptography Client" +linkTitle: "Cryptography client" +weight: 510005 +description: Learn how to create Dapr Crytography clients +--- + +The Dapr Cryptography package allows you to perform encryption and decryption operations provided by the Dapr sidecar. + +## Lifetime management +A `DaprEncryptionClient` is a version of the Dapr client that is dedicated to interacting with the Dapr Cryptography 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 `DaprEncryptionClient` and provide access to that shared +instance throughout your application. `DaprEncryptionClient` instances are thread-safe and intended to be shared. + +This can be aided by utilizing the dependency injection functionality. The registration method supports registration +as a singleton, a scoped instance, or as a 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 `DaprEncryptionClient` for each operation. + +## Configuring `DaprEncryptionClient` via `DaprEncryptionClientBuilder` +A `DaprCryptographyClient` can be configured by invoking methods on the `DaprEncryptionClientBuilder` class before calling +`.Build()` to create the client itself. The settings for each `DaprEncryptionClientBuilder` are separate can cannot be +changed after calling `.Build()`. + +```cs +var daprEncryptionClient = new DaprEncryptionClientBuilder() + .UseDaprApiToken("abc123") //Specify the API token used to authenticate to the Dapr sidecar + .Build(); +``` + +The `DaprEncryptionClientBuilder` 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 daprEncryptionClient = new DaprEncryptionClientBuilder() + .UseGrpcChannelOptions(new GrpcChannelOptions { .. ThrowOperationCanceledOnCancellation = true }) + .Build(); +``` + +## Using cancellation with `DaprEncryptionClient` +The APIs on `DaprEncryptionClient` 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 `DaprEncryptionClient` via dependency injection +Using the built-in extension methods for registering the `DaprEncryptionClient` 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 `DaprEncryptionClientBuilder` 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 `DaprEncryptionClient` is configured with the +default settings. + +```cs +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddDaprEncryptionClent(); //Registers the `DaprEncryptionClient` 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 `DaprEncryptionClientBuiler` and exposes methods for configuring +the necessary options. + +```cs +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddDaprEncryptionClient((_, daprEncrpyptionClientBuilder) => { + //Set the API token + daprEncryptionClientBuilder.UseDaprApiToken("abc123"); + //Specify a non-standard HTTP endpoint + daprEncryptionClientBuilder.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.AddDaprEncryptionClient((serviceProvider, daprEncryptionClientBuilder) => { + //Retrieve an instance of the `SecretService` from the service provider + var secretService = serviceProvider.GetRequiredService(); + var daprApiToken = secretService.GetSecret("DaprApiToken").Value; + + //Configure the `DaprEncryptionClientBuilder` + daprEncryptionClientBuilder.UseDaprApiToken(daprApiToken); +}); + +var app = builder.Build(); +``` \ No newline at end of file diff --git a/examples/Client/Cryptography/Program.cs b/examples/Client/Cryptography/Program.cs deleted file mode 100644 index 9fe3ec97..00000000 --- a/examples/Client/Cryptography/Program.cs +++ /dev/null @@ -1,48 +0,0 @@ -// ------------------------------------------------------------------------ -// Copyright 2023 The Dapr Authors -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// ------------------------------------------------------------------------ - -using Cryptography.Examples; - -namespace Cryptography; - -class Program -{ - private const string ComponentName = "localstorage"; - private const string KeyName = "rsa-private-key.pem"; //This should match the name of your generated key - this sample expects an RSA symmetrical key. - - private static readonly Example[] Examples = new Example[] - { - new EncryptDecryptStringExample(ComponentName, KeyName), - new EncryptDecryptFileStreamExample(ComponentName, KeyName) - }; - - static async Task Main(string[] args) - { - if (args.Length > 0 && int.TryParse(args[0], out var index) && index >= 0 && index < Examples.Length) - { - var cts = new CancellationTokenSource(); - Console.CancelKeyPress += (object? sender, ConsoleCancelEventArgs e) => cts.Cancel(); - - await Examples[index].RunAsync(cts.Token); - return 0; - } - - Console.WriteLine("Hello, please choose a sample to run by passing your selection's number into the arguments, e.g. 'dotnet run 0':"); - for (var i = 0; i < Examples.Length; i++) - { - Console.WriteLine($"{i}: {Examples[i].DisplayName}"); - } - Console.WriteLine(); - return 1; - } -} \ No newline at end of file diff --git a/examples/Client/Cryptography/Components/local-storage.yaml b/examples/Cryptography/Components/local-storage.yaml similarity index 100% rename from examples/Client/Cryptography/Components/local-storage.yaml rename to examples/Cryptography/Components/local-storage.yaml diff --git a/examples/Client/Cryptography/Cryptography.csproj b/examples/Cryptography/Cryptography.csproj similarity index 59% rename from examples/Client/Cryptography/Cryptography.csproj rename to examples/Cryptography/Cryptography.csproj index 1a1b1eeb..2c942d3a 100644 --- a/examples/Client/Cryptography/Cryptography.csproj +++ b/examples/Cryptography/Cryptography.csproj @@ -1,4 +1,4 @@ - + Exe @@ -7,18 +7,15 @@ latest - - - - - - - - PreserveNewest + + + + + \ No newline at end of file diff --git a/examples/Client/Cryptography/Examples/EncryptDecryptFileStreamExample.cs b/examples/Cryptography/Examples/EncryptDecryptFileStreamExample.cs similarity index 70% rename from examples/Client/Cryptography/Examples/EncryptDecryptFileStreamExample.cs rename to examples/Cryptography/Examples/EncryptDecryptFileStreamExample.cs index 43cf2dc9..60fea01f 100644 --- a/examples/Client/Cryptography/Examples/EncryptDecryptFileStreamExample.cs +++ b/examples/Cryptography/Examples/EncryptDecryptFileStreamExample.cs @@ -12,18 +12,20 @@ // ------------------------------------------------------------------------ using System.Buffers; -using Dapr.Client; +using System.Text; +using Dapr.Cryptography.Encryption; +using Dapr.Cryptography.Encryption.Models; + #pragma warning disable CS0618 // Type or member is obsolete namespace Cryptography.Examples; -internal class EncryptDecryptFileStreamExample(string componentName, string keyName) : Example +internal sealed class EncryptDecryptFileStreamExample(DaprEncryptionClient daprClient) : IExample { - public override string DisplayName => "Use Cryptography to encrypt and decrypt a file"; - public override async Task RunAsync(CancellationToken cancellationToken) - { - using var client = new DaprClientBuilder().Build(); + public static string DisplayName => "Use Cryptography to encrypt and decrypt a file"; + public async Task RunAsync(string componentName, string keyName, CancellationToken cancellationToken) + { // The name of the file we're using as an example const string fileName = "file.txt"; @@ -37,36 +39,35 @@ internal class EncryptDecryptFileStreamExample(string componentName, string keyN await using var encryptFs = new FileStream(fileName, FileMode.Open); var bufferedEncryptedBytes = new ArrayBufferWriter(); - await foreach (var bytes in (await client.EncryptAsync(componentName, encryptFs, keyName, - new EncryptionOptions(KeyWrapAlgorithm.Rsa), cancellationToken)) - .WithCancellation(cancellationToken)) + await foreach (var bytes in ((daprClient.EncryptAsync(componentName, encryptFs, keyName, + new EncryptionOptions(KeyWrapAlgorithm.Rsa), cancellationToken)))) { bufferedEncryptedBytes.Write(bytes.Span); } - Console.WriteLine("Encrypted bytes:"); + Console.WriteLine($"Encrypted bytes ({bufferedEncryptedBytes.WrittenMemory.Length} bytes):"); Console.WriteLine(Convert.ToBase64String(bufferedEncryptedBytes.WrittenMemory.ToArray())); - + //We'll write to a temporary file via a FileStream var tempDecryptedFile = Path.GetTempFileName(); await using var decryptFs = new FileStream(tempDecryptedFile, FileMode.Create); - + //We'll stream the decrypted bytes from a MemoryStream into the above temporary file await using var encryptedMs = new MemoryStream(bufferedEncryptedBytes.WrittenMemory.ToArray()); - await foreach (var result in (await client.DecryptAsync(componentName, encryptedMs, keyName, - cancellationToken)).WithCancellation(cancellationToken)) + await foreach (var result in ((daprClient.DecryptAsync(componentName, encryptedMs, keyName, + cancellationToken: cancellationToken)))) { decryptFs.Write(result.Span); } decryptFs.Close(); - + //Let's confirm the value as written to the file var decryptedValue = await File.ReadAllTextAsync(tempDecryptedFile, cancellationToken); - Console.WriteLine("Decrypted value: "); + Console.WriteLine($"Decrypted value ({Encoding.UTF8.GetByteCount(decryptedValue)} bytes): "); Console.WriteLine(decryptedValue); - + //And some cleanup to delete our temp file File.Delete(tempDecryptedFile); } -} \ No newline at end of file +} diff --git a/examples/Cryptography/Examples/EncryptDecryptLargeFileExample.cs b/examples/Cryptography/Examples/EncryptDecryptLargeFileExample.cs new file mode 100644 index 00000000..f892a179 --- /dev/null +++ b/examples/Cryptography/Examples/EncryptDecryptLargeFileExample.cs @@ -0,0 +1,105 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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.Security.Cryptography;using Dapr.Cryptography.Encryption; +using Dapr.Cryptography.Encryption.Models; + +#pragma warning disable CS0618 // Type or member is obsolete + +namespace Cryptography.Examples; + +internal sealed class EncryptDecryptLargeFileExample(DaprEncryptionClient daprClient) : IExample +{ + public static string DisplayName => "Use Cryptography to encrypt and decrypt a large file"; + + public async Task RunAsync(string componentName, string keyName, CancellationToken cancellationToken) + { + //Create our large file locally and fill with random bytes + const string fileName = "templargefile.txt"; + const long sizeLimitInBytes = 1L * 1024 * 1024 * 1024; //1 GB + await WriteLargeFileAsync(fileName, sizeLimitInBytes); + + //Get the starting hash of the file for comparison reasons + var startingFileHash = await GetFileHashAsync(fileName); + var startingFileLength = new FileInfo(fileName).Length; + Console.WriteLine($"Starting with a file spanning {startingFileLength} bytes called '{fileName}' filled with random bytes with MD5 hash of: '{startingFileHash}'"); + + //Encrypt from the file stream and write the result to another file + const string encryptedFileName = "enc_templargefile.txt"; + await using (var encryptFs = new FileStream(fileName, FileMode.Open)) + { + await using (var encryptedFs = new FileStream(encryptedFileName, FileMode.Create)) + { + await foreach (var encryptedBytes in ((daprClient.EncryptAsync(componentName, encryptFs, keyName, + new EncryptionOptions(KeyWrapAlgorithm.Rsa), cancellationToken)))) + { + await encryptedFs.WriteAsync(encryptedBytes, cancellationToken); + } + + encryptedFs.Close(); + } + } + + //Get a hash and length of our newly encrypted file + var encryptedFileHash = await GetFileHashAsync(encryptedFileName); + var encryptedFileLength = new FileInfo(encryptedFileName).Length; + Console.WriteLine($"Encrypted file spanning {encryptedFileLength} bytes called '{encryptedFileName}' with MD5 hash of: '{encryptedFileHash}'"); + + //Now we'll decrypt the file back into another file + const string decryptedFileName = "dec_templargefile.txt"; + await using (var decryptFs = new FileStream(encryptedFileName, FileMode.Open)) + { + await using (var decryptedFs = new FileStream(decryptedFileName, FileMode.Create)) + { + await foreach (var decryptedBytes in ((daprClient.DecryptAsync(componentName, decryptFs, keyName, + cancellationToken: cancellationToken)))) + { + await decryptedFs.WriteAsync(decryptedBytes, cancellationToken); + } + + decryptedFs.Close(); + } + } + + //Get the hash and length of the decrypted file + var decryptedFileHash = await GetFileHashAsync(decryptedFileName); + var decryptedFileLength = new FileInfo(decryptedFileName).Length; + Console.WriteLine($"Decrypted file spanning {decryptedFileLength} bytes called '{decryptedFileName}' with MD5 hash of: '{decryptedFileHash}'"); + + var match = string.Equals(startingFileHash, decryptedFileHash); + Console.WriteLine($"The hash of the original and decrypted file are {(match ? "": "NOT ")}the same!"); + + //Clean up our large files + File.Delete(fileName); + File.Delete(encryptedFileName); + File.Delete(decryptedFileName); + } + + private static async Task WriteLargeFileAsync(string fileName, long sizeLimit) + { + var buffer = new byte[5 * 1024 * 1024]; // 5 MB buffer + await using var fs = new FileStream(fileName, FileMode.Create, FileAccess.Write, FileShare.Read); + for (var written = 0; written < sizeLimit; written += buffer.Length) + { + Random.Shared.NextBytes(buffer); + await fs.WriteAsync(buffer); + } + } + + private static async Task GetFileHashAsync(string fileName) + { + using var md5 = MD5.Create(); + await using var stream = File.OpenRead(fileName); + return Convert.ToBase64String(await md5.ComputeHashAsync(stream)); + } +} diff --git a/examples/Client/Cryptography/Examples/EncryptDecryptStringExample.cs b/examples/Cryptography/Examples/EncryptDecryptStringExample.cs similarity index 54% rename from examples/Client/Cryptography/Examples/EncryptDecryptStringExample.cs rename to examples/Cryptography/Examples/EncryptDecryptStringExample.cs index 1a31e915..b2f45c26 100644 --- a/examples/Client/Cryptography/Examples/EncryptDecryptStringExample.cs +++ b/examples/Cryptography/Examples/EncryptDecryptStringExample.cs @@ -12,31 +12,29 @@ // ------------------------------------------------------------------------ using System.Text; -using Dapr.Client; +using Dapr.Cryptography.Encryption; +using Dapr.Cryptography.Encryption.Models; + #pragma warning disable CS0618 // Type or member is obsolete namespace Cryptography.Examples; -internal class EncryptDecryptStringExample(string componentName, string keyName) : Example +internal sealed class EncryptDecryptStringExample(DaprEncryptionClient daprClient) : IExample { - public override string DisplayName => "Using Cryptography to encrypt and decrypt a string"; - - public override async Task RunAsync(CancellationToken cancellationToken) + public static string DisplayName => "Using Cryptography to encrypt and decrypt a string"; + public async Task RunAsync(string componentName, string keyName, CancellationToken cancellationToken) { - using var client = new DaprClientBuilder().Build(); - const string plaintextStr = "This is the value we're going to encrypt today"; Console.WriteLine($"Original string value: '{plaintextStr}'"); //Encrypt the string var plaintextBytes = Encoding.UTF8.GetBytes(plaintextStr); - var encryptedBytesResult = await client.EncryptAsync(componentName, plaintextBytes, keyName, new EncryptionOptions(KeyWrapAlgorithm.Rsa), - cancellationToken); + var encryptedBytesResult = await daprClient.EncryptAsync(componentName, plaintextBytes, keyName, new EncryptionOptions(KeyWrapAlgorithm.Rsa), cancellationToken); - Console.WriteLine($"Encrypted bytes: '{Convert.ToBase64String(encryptedBytesResult.Span)}'"); + Console.WriteLine($"Encrypted bytes ({encryptedBytesResult.Length} bytes): '{Convert.ToBase64String(encryptedBytesResult.Span)}'"); //Decrypt the string - var decryptedBytes = await client.DecryptAsync(componentName, encryptedBytesResult, keyName, cancellationToken); - Console.WriteLine($"Decrypted string: '{Encoding.UTF8.GetString(decryptedBytes.ToArray())}'"); + var decryptedBytes = await daprClient.DecryptAsync(componentName, encryptedBytesResult, keyName, cancellationToken: cancellationToken); + Console.WriteLine($"Decrypted string ({decryptedBytes.Length} bytes): '{Encoding.UTF8.GetString(decryptedBytes.ToArray())}'"); } -} \ No newline at end of file +} diff --git a/examples/Client/Cryptography/Example.cs b/examples/Cryptography/IExample.cs similarity index 83% rename from examples/Client/Cryptography/Example.cs rename to examples/Cryptography/IExample.cs index ac3d525b..276eb761 100644 --- a/examples/Client/Cryptography/Example.cs +++ b/examples/Cryptography/IExample.cs @@ -13,9 +13,7 @@ namespace Cryptography; -internal abstract class Example +internal interface IExample { - public abstract string DisplayName { get; } - - public abstract Task RunAsync(CancellationToken cancellationToken); -} \ No newline at end of file + public Task RunAsync(string componentName, string keyName, CancellationToken cancellationToken); +} diff --git a/examples/Cryptography/Program.cs b/examples/Cryptography/Program.cs new file mode 100644 index 00000000..d97c7aca --- /dev/null +++ b/examples/Cryptography/Program.cs @@ -0,0 +1,64 @@ +// ------------------------------------------------------------------------ +// Copyright 2023 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Cryptography.Examples; +using Dapr.Cryptography.Encryption.Extensions; + +const string ComponentName = "localstorage"; +const string KeyName = "rsa-private-key.pem"; //This should match the name of your generated key - this sample expects an RSA symmetrical key. + +if (int.TryParse(args[0], out var exampleId)) +{ + var builder = WebApplication.CreateBuilder(args); + builder.Services.AddDaprEncryptionClient((sp, opt) => + { + opt.UseHttpEndpoint("http://localhost:6552"); + opt.UseGrpcEndpoint("http://localhost:6551"); + }); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + var app = builder.Build(); + + var ctx = new CancellationTokenSource(); + Console.CancelKeyPress += (object? sender, ConsoleCancelEventArgs e) => ctx.Cancel(); + + switch (exampleId) + { + case 0: + { + var ex0 = app.Services.GetRequiredService(); + await ex0.RunAsync(ComponentName, KeyName, ctx.Token); + return 0; + } + case 1: + { + var ex1 = app.Services.GetRequiredService(); + await ex1.RunAsync(ComponentName, KeyName, ctx.Token); + return 0; + } + case 2: + { + var ex2 = app.Services.GetRequiredService(); + await ex2.RunAsync(ComponentName, KeyName, ctx.Token); + return 0; + } + } +} + +Console.WriteLine("Please choose a sample to run by passing your selection's number into the arguments, e.g. 'dotnet run 0':"); +Console.WriteLine($"0: {EncryptDecryptStringExample.DisplayName}"); +Console.WriteLine($"1: {EncryptDecryptFileStreamExample.DisplayName}"); +Console.WriteLine($"2: {EncryptDecryptLargeFileExample.DisplayName}"); +Console.WriteLine(); +return 1; diff --git a/examples/Client/Cryptography/README.md b/examples/Cryptography/README.md similarity index 100% rename from examples/Client/Cryptography/README.md rename to examples/Cryptography/README.md diff --git a/examples/Client/Cryptography/file.txt b/examples/Cryptography/file.txt similarity index 100% rename from examples/Client/Cryptography/file.txt rename to examples/Cryptography/file.txt diff --git a/examples/Client/Cryptography/keys/rsa-private-key.pem b/examples/Cryptography/keys/rsa-private-key.pem similarity index 100% rename from examples/Client/Cryptography/keys/rsa-private-key.pem rename to examples/Cryptography/keys/rsa-private-key.pem diff --git a/examples/Client/PublishSubscribe/StreamingSubscriptionExample/Program.cs b/examples/Messaging/StreamingSubscriptionExample/Program.cs similarity index 98% rename from examples/Client/PublishSubscribe/StreamingSubscriptionExample/Program.cs rename to examples/Messaging/StreamingSubscriptionExample/Program.cs index ac00d879..00431caf 100644 --- a/examples/Client/PublishSubscribe/StreamingSubscriptionExample/Program.cs +++ b/examples/Messaging/StreamingSubscriptionExample/Program.cs @@ -1,4 +1,6 @@ -using System.Text; + + +using System.Text; using Dapr.Messaging.PublishSubscribe; using Dapr.Messaging.PublishSubscribe.Extensions; diff --git a/examples/Client/PublishSubscribe/StreamingSubscriptionExample/StreamingSubscriptionExample.csproj b/examples/Messaging/StreamingSubscriptionExample/StreamingSubscriptionExample.csproj similarity index 62% rename from examples/Client/PublishSubscribe/StreamingSubscriptionExample/StreamingSubscriptionExample.csproj rename to examples/Messaging/StreamingSubscriptionExample/StreamingSubscriptionExample.csproj index 77e84dbb..4a242799 100644 --- a/examples/Client/PublishSubscribe/StreamingSubscriptionExample/StreamingSubscriptionExample.csproj +++ b/examples/Messaging/StreamingSubscriptionExample/StreamingSubscriptionExample.csproj @@ -1,13 +1,12 @@  - Exe enable enable - + diff --git a/examples/README.md b/examples/README.md index 44eb57a8..0e3f3d96 100644 --- a/examples/README.md +++ b/examples/README.md @@ -8,3 +8,4 @@ This repository contains a samples that highlight the Dapr .NET SDK capabilities | [2. Actor](./Actor) | Demonstrates creating virtual actors that encapsulate code and state. | | [3. ASP.NET Core](./AspNetCore) | Demonstrates ASP.NET Core integration with Dapr by creating Controllers and Routes. | | [4. Workflow](./Workflow) | Demonstrates creating durable, long-running Dapr workflows using code. | +| [5. Cryptography](./Cryptography) | Demonstrates encryption and decryption operations using Dapr. | \ No newline at end of file diff --git a/src/Dapr.Common/AssemblyInfo.cs b/src/Dapr.Common/AssemblyInfo.cs index 3037485a..f4dccf42 100644 --- a/src/Dapr.Common/AssemblyInfo.cs +++ b/src/Dapr.Common/AssemblyInfo.cs @@ -19,6 +19,7 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Dapr.AI, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.AspNetCore, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.Client, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.Cryptography, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.Jobs, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.Messaging, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.Extensions.Configuration, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] @@ -34,6 +35,7 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Dapr.AspNetCore.IntegrationTest.App, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.AspNetCore.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.Client.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.Cryptography.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.Common.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.E2E.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.E2E.Test.Actors, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] diff --git a/src/Dapr.Common/DaprGenericClientBuilder.cs b/src/Dapr.Common/DaprGenericClientBuilder.cs index 8ff59490..3e29a2ef 100644 --- a/src/Dapr.Common/DaprGenericClientBuilder.cs +++ b/src/Dapr.Common/DaprGenericClientBuilder.cs @@ -200,7 +200,7 @@ public abstract class DaprGenericClientBuilder where TClientBuil 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."); diff --git a/src/Dapr.Cryptography/Dapr.Cryptography.csproj b/src/Dapr.Cryptography/Dapr.Cryptography.csproj new file mode 100644 index 00000000..4cc2fc6f --- /dev/null +++ b/src/Dapr.Cryptography/Dapr.Cryptography.csproj @@ -0,0 +1,28 @@ + + + + enable + enable + Dapr.Cryptography + Dapr Cryptography SDK + Dapr Cryptography SDK for performing encryption and decryption operations with Dapr + alpha + + + + + + + + + + + + + + + + + + + diff --git a/src/Dapr.Cryptography/Encryption/DaprEncryptionClient.cs b/src/Dapr.Cryptography/Encryption/DaprEncryptionClient.cs new file mode 100644 index 00000000..c99ed571 --- /dev/null +++ b/src/Dapr.Cryptography/Encryption/DaprEncryptionClient.cs @@ -0,0 +1,143 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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.Cryptography.Encryption.Models; +using Autogenerated = Dapr.Client.Autogen.Grpc.v1.Dapr; + +namespace Dapr.Cryptography.Encryption; + +/// +/// +/// Defines client operations for performing cryptography operations with Dapr.. +/// Use to create a or register +/// for use with dependency injection via +/// DaprJobsServiceCollectionExtensions.AddDaprJobsClient. +/// +/// +/// Implementations of implement because the +/// client accesses network resources. For best performance, create a single long-lived client instance +/// and share it for the lifetime of the application. This is done for you if created via the DI extensions. Avoid +/// creating a disposing a client instance for each operation that the application performs - this can lead to socket +/// exhaustion and other problems. +/// +/// +public abstract class DaprEncryptionClient(Autogenerated.DaprClient client, HttpClient httpClient, string? daprApiToken = null) : IDaprEncryptionClient +{ + private bool disposed; + + /// + /// The HTTP client used by the client for calling the Dapr runtime. + /// + /// + /// Property exposed for testing purposes. + /// + internal readonly HttpClient HttpClient = httpClient; + + /// + /// The Dapr API token value. + /// + /// + /// Property exposed for testing purposes. + /// + internal readonly string? DaprApiToken = daprApiToken; + + /// + /// The autogenerated Dapr client. + /// + /// + /// Property exposed for testing purposes. + /// + internal Autogenerated.DaprClient Client { get; } = client; + + /// + /// Encrypts an array of bytes using the Dapr Cryptography encryption functionality. + /// + /// The name of the vault resource used by the operation. + /// The bytes of the plaintext value to encrypt. + /// The name of the key to use from the Vault for the encryption operation. + /// Options informing how the encryption operation should be configured. + /// A that can be used to cancel the operation. + /// An array of encrypted bytes. + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public abstract Task> EncryptAsync(string vaultResourceName, + ReadOnlyMemory plaintextBytes, string keyName, EncryptionOptions encryptionOptions, + CancellationToken cancellationToken = default); + + /// + /// Encrypts a stream using the Dapr Cryptography encryption functionality. + /// + /// The name of the vault resource used by the operation. + /// The stream containing the bytes of the plaintext value to encrypt. + /// The name of the key to use from the Vault for the encryption operation. + /// Options informing how the encryption operation should be configured. + /// A that can be used to cancel the operation. + /// An array of encrypted bytes. + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public abstract IAsyncEnumerable> EncryptAsync(string vaultResourceName, Stream plaintextStream, string keyName, + EncryptionOptions encryptionOptions, CancellationToken cancellationToken = default); + + /// + /// Decrypts the specified ciphertext bytes using the Dapr Cryptography encryption functionality. + /// + /// The name of the vault resource used by the operation. + /// The bytes of the ciphertext value to decrypt. + /// The name of the key to use from the Vault for the decryption operation. + /// Options informing how the decryption operation should be configured. + /// A that can be used to cancel the operation. + /// An array of decrypted bytes. + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public abstract Task> DecryptAsync(string vaultResourceName, ReadOnlyMemory ciphertextBytes, string keyName, DecryptionOptions? options = null, + CancellationToken cancellationToken = default); + + /// + /// Decrypts the specified stream of ciphertext using the Dapr Cryptography encryption functionality. + /// + /// The name of the vault resource used by the operation. + /// The stream containing the bytes of the ciphertext value to decrypt. + /// The name of the key to use from the Vault for the decryption operation. + /// Options informing how the decryption operation should be configured. + /// A that can be used to cancel the operation. + /// An asynchronously enumerable array of decrypted bytes. + [Obsolete( + "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public abstract IAsyncEnumerable> DecryptAsync(string vaultResourceName, Stream ciphertextStream, + string keyName, DecryptionOptions? options = null, CancellationToken cancellationToken = default); + + internal static KeyValuePair? GetDaprApiTokenHeader(string apiToken) + { + if (string.IsNullOrWhiteSpace(apiToken)) + { + return null; + } + + return new KeyValuePair("dapr-api-token", apiToken); + } + + /// + public void Dispose() + { + if (!this.disposed) + { + Dispose(disposing: true); + this.disposed = true; + } + } + + /// + /// Disposes the resources associated with the object. + /// + /// true if called by a call to the Dispose method; otherwise false. + protected virtual void Dispose(bool disposing) + { + } +} diff --git a/src/Dapr.Cryptography/Encryption/DaprEncryptionClientBuilder.cs b/src/Dapr.Cryptography/Encryption/DaprEncryptionClientBuilder.cs new file mode 100644 index 00000000..65ff2391 --- /dev/null +++ b/src/Dapr.Cryptography/Encryption/DaprEncryptionClientBuilder.cs @@ -0,0 +1,36 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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; + +namespace Dapr.Cryptography.Encryption; + +/// +/// Builds a . +/// +/// An optional instance of . +public sealed class DaprEncryptionClientBuilder(IConfiguration? configuration = null) : DaprGenericClientBuilder(configuration) +{ + /// + /// Builds the client instance from the properties of the builder. + /// + /// The Dapr client instance. + public override DaprEncryptionClient Build() + { + var daprClientDependencies = this.BuildDaprClientDependencies(typeof(DaprEncryptionClient).Assembly); + var client = new Autogenerated.Dapr.DaprClient(daprClientDependencies.channel); + return new DaprEncryptionGrpcClient(client, daprClientDependencies.httpClient, daprClientDependencies.daprApiToken); + } +} diff --git a/src/Dapr.Cryptography/Encryption/DaprEncryptionGrpcClient.cs b/src/Dapr.Cryptography/Encryption/DaprEncryptionGrpcClient.cs new file mode 100644 index 00000000..c2d00fed --- /dev/null +++ b/src/Dapr.Cryptography/Encryption/DaprEncryptionGrpcClient.cs @@ -0,0 +1,208 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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.Buffers; +using System.Runtime.CompilerServices; +using Dapr.Common; +using Dapr.Cryptography.Extensions; +using Dapr.Common.Extensions; +using Dapr.Cryptography.Encryption.Models; +using Autogenerated = Dapr.Client.Autogen.Grpc.v1; + +namespace Dapr.Cryptography.Encryption; + +/// +/// A client for performing cryptography operations with Dapr. +/// +internal sealed class DaprEncryptionGrpcClient(Autogenerated.Dapr.DaprClient client, HttpClient httpClient, string? daprApiToken = null) : DaprEncryptionClient(client, httpClient, daprApiToken: daprApiToken) +{ + /// + /// Encrypts an array of bytes using the Dapr Cryptography encryption functionality. + /// + /// The name of the vault resource used by the operation. + /// The bytes of the plaintext value to encrypt. + /// The name of the key to use from the Vault for the encryption operation. + /// Options informing how the encryption operation should be configured. + /// A that can be used to cancel the operation. + /// An array of encrypted bytes. + [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) + { + using var memoryStream = plaintextBytes.CreateMemoryStream(true); + var encryptionResult = EncryptAsync(vaultResourceName, memoryStream, keyName, encryptionOptions, cancellationToken); + + var bufferedResult = new ArrayBufferWriter(); + await foreach (var item in encryptionResult) + { + bufferedResult.Write(item.Span); + } + + return bufferedResult.WrittenMemory; + } + + /// + /// Encrypts a stream using the Dapr Cryptography encryption functionality. + /// + /// The name of the vault resource used by the operation. + /// The stream containing the bytes of the plaintext value to encrypt. + /// The name of the key to use from the Vault for the encryption operation. + /// Options informing how the encryption operation should be configured. + /// A that can be used to cancel the operation. + /// An array of encrypted bytes. + [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 IAsyncEnumerable> EncryptAsync( + string vaultResourceName, + Stream plaintextStream, + string keyName, + EncryptionOptions encryptionOptions, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); + ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); + ArgumentVerifier.ThrowIfNull(plaintextStream, nameof(plaintextStream)); + ArgumentVerifier.ThrowIfNull(encryptionOptions, nameof(encryptionOptions)); + + EventHandler exceptionHandler = (_, ex) => throw ex; + + 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 grpcCallOptions = DaprClientUtilities.ConfigureGrpcCallOptions(typeof(DaprEncryptionClient).Assembly, + this.DaprApiToken, cancellationToken); + var duplexStream = Client.EncryptAlpha1(grpcCallOptions); + + using var streamProcessor = new EncryptionStreamProcessor(); + try + { + streamProcessor.OnException += exceptionHandler; + await streamProcessor.ProcessStreamAsync(plaintextStream, duplexStream, encryptRequestOptions, + encryptionOptions.StreamingBlockSizeInBytes, + cancellationToken); + + await foreach (var value in streamProcessor.GetProcessedDataAsync(cancellationToken)) + { + yield return value; + } + } + finally + { + streamProcessor.OnException -= exceptionHandler; + } + } + + /// + /// Decrypts the specified ciphertext bytes using the Dapr Cryptography encryption functionality. + /// + /// The name of the vault resource used by the operation. + /// The bytes of the ciphertext value to decrypt. + /// The name of the key to use from the Vault for the decryption operation. + /// Options informing how the decryption operation should be configured. + /// A that can be used to cancel the operation. + /// An array of decrypted bytes. + [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? options = null, + CancellationToken cancellationToken = default) + { + using var memoryStream = ciphertextBytes.CreateMemoryStream(true); + var decryptionResult = DecryptAsync(vaultResourceName, memoryStream, keyName, options, cancellationToken); + + var bufferedResult = new ArrayBufferWriter(); + await foreach (var item in decryptionResult) + { + bufferedResult.Write(item.Span); + } + + return bufferedResult.WrittenMemory; + } + + /// + /// Decrypts the specified stream of ciphertext using the Dapr Cryptography encryption functionality. + /// + /// The name of the vault resource used by the operation. + /// The stream containing the bytes of the ciphertext value to decrypt. + /// The name of the key to use from the Vault for the decryption operation. + /// Options informing how the decryption operation should be configured. + /// A that can be used to cancel the operation. + /// An asynchronously enumerable array of decrypted bytes. + [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 IAsyncEnumerable> DecryptAsync( + string vaultResourceName, + Stream ciphertextStream, + string keyName, + DecryptionOptions? options = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); + ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); + ArgumentVerifier.ThrowIfNull(ciphertextStream, nameof(ciphertextStream)); + options = options ?? new DecryptionOptions(); + + EventHandler exceptionHandler = (_, ex) => throw ex; + + var decryptRequestOptions = new Autogenerated.DecryptRequestOptions + { + ComponentName = vaultResourceName, + KeyName = keyName + }; + + var grpcCallOptions = DaprClientUtilities.ConfigureGrpcCallOptions(typeof(DaprEncryptionClient).Assembly, + this.DaprApiToken, cancellationToken); + var duplexStream = Client.DecryptAlpha1(grpcCallOptions); + + using var streamProcessor = new DecryptionStreamProcessor(); + try + { + streamProcessor.OnException += exceptionHandler; + await streamProcessor.ProcessStreamAsync(ciphertextStream, duplexStream, options.StreamingBlockSizeInBytes, + decryptRequestOptions, + cancellationToken); + + await foreach (var value in streamProcessor.GetProcessedDataAsync(cancellationToken)) + { + yield return value; + } + } + finally + { + streamProcessor.OnException -= exceptionHandler; + } + } +} diff --git a/src/Dapr.Cryptography/Encryption/DecryptionStreamProcessor.cs b/src/Dapr.Cryptography/Encryption/DecryptionStreamProcessor.cs new file mode 100644 index 00000000..4df07f10 --- /dev/null +++ b/src/Dapr.Cryptography/Encryption/DecryptionStreamProcessor.cs @@ -0,0 +1,147 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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; +using System.Threading.Channels; +using Google.Protobuf; +using Grpc.Core; +using Autogenerated = Dapr.Client.Autogen.Grpc.v1; +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + +namespace Dapr.Cryptography.Encryption; + +/// +/// Provides the implementation to decrypt a stream of plaintext data with the Dapr runtime. +/// +internal sealed class DecryptionStreamProcessor : IDecryptionStreamProcessor, IDisposable +{ + private bool disposed; + private readonly Channel> outputChannel = Channel.CreateUnbounded>(); + + /// + /// Surfaces any exceptions encountered while asynchronously processing the inbound and outbound streams. + /// + internal event EventHandler? OnException; + + /// + /// Sends the provided bytes in chunks to the sidecar for the encryption operation. + /// + /// The stream containing the bytes to decrypt. + /// The call to make to the sidecar to process the encryption operation. + /// The size, in bytes, of the streaming blocks. + /// The decryption options. + /// Token used to cancel the ongoing request. + public async Task ProcessStreamAsync( + Stream inputStream, + AsyncDuplexStreamingCall call, + int streamingBlockSizeInBytes, + Autogenerated.DecryptRequestOptions options, + CancellationToken cancellationToken) + { + //Read from the input stream and write to the gRPC call + _ = Task.Run(async () => + { + try + { + await using var bufferedStream = new BufferedStream(inputStream, streamingBlockSizeInBytes); + var buffer = new byte[streamingBlockSizeInBytes]; + int bytesRead; + ulong sequenceNumber = 0; + + while ((bytesRead = await bufferedStream.ReadAsync(buffer, cancellationToken)) > 0) + { + var request = new Autogenerated.DecryptRequest + { + Payload = new Autogenerated.StreamPayload + { + Data = ByteString.CopyFrom(buffer, 0, bytesRead), Seq = sequenceNumber + } + }; + + //Only include the options in the first message + if (sequenceNumber == 0) + { + request.Options = options; + } + + await call.RequestStream.WriteAsync(request, cancellationToken); + + //Increment the sequence number + sequenceNumber++; + } + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + // Expected cancellation exception + } + catch (Exception ex) + { + OnException?.Invoke(this, ex); + } + finally + { + await call.RequestStream.CompleteAsync(); + } + }, cancellationToken); + + //Start reading from the gRPC call and writing to the output channel + _ = Task.Run(async () => + { + try + { + await foreach (var response in call.ResponseStream.ReadAllAsync(cancellationToken)) + { + await outputChannel.Writer.WriteAsync(response.Payload.Data.Memory, cancellationToken); + } + } + catch (Exception ex) + { + OnException?.Invoke(this, ex); + } + finally + { + outputChannel.Writer.Complete(); + } + }, cancellationToken); + } + + /// + /// Retrieves the processed bytes from the operation from the sidecar and + /// returns as an enumerable stream. + /// + public async IAsyncEnumerable> GetProcessedDataAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + await foreach (var data in outputChannel.Reader.ReadAllAsync(cancellationToken)) + { + yield return data; + } + } + + public void Dispose() + { + Dispose(true); + } + + private void Dispose(bool disposing) + { + if (!disposed) + { + if (disposing) + { + outputChannel.Writer.TryComplete(); + } + + disposed = true; + } + } +} diff --git a/src/Dapr.Cryptography/Encryption/EncryptionStreamProcessor.cs b/src/Dapr.Cryptography/Encryption/EncryptionStreamProcessor.cs new file mode 100644 index 00000000..bd8b9a26 --- /dev/null +++ b/src/Dapr.Cryptography/Encryption/EncryptionStreamProcessor.cs @@ -0,0 +1,148 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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; +using System.Threading.Channels; +using Google.Protobuf; +using Grpc.Core; +using Autogenerated = Dapr.Client.Autogen.Grpc.v1; +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + +namespace Dapr.Cryptography.Encryption; + +/// +/// Provides the implementation to encrypt a stream of plaintext data with the Dapr runtime. +/// +internal sealed class EncryptionStreamProcessor : IEncryptionStreamProcessor, IDisposable +{ + private bool disposed; + private readonly Channel> outputChannel = Channel.CreateUnbounded>(); + + /// + /// Surfaces any exceptions encountered while asynchronously processing the inbound and outbound streams. + /// + internal event EventHandler? OnException; + + /// + /// Sends the provided bytes in chunks to the sidecar for the encryption operation. + /// + /// The stream containing the bytes to encrypt. + /// The call to make to the sidecar to process the encryption operation. + /// The encryption options. + /// The size, in bytes, of the streaming blocks. + /// Token used to cancel the ongoing request. + public async Task ProcessStreamAsync( + Stream inputStream, + AsyncDuplexStreamingCall call, + Autogenerated.EncryptRequestOptions options, + int streamingBlockSizeInBytes, + CancellationToken cancellationToken) + { + //Read from the input stream and write to the gRPC call + _ = Task.Run(async () => + { + try + { + await using var bufferedStream = new BufferedStream(inputStream, streamingBlockSizeInBytes); + var buffer = new byte[streamingBlockSizeInBytes]; + int bytesRead; + ulong sequenceNumber = 0; + + while ((bytesRead = await bufferedStream.ReadAsync(buffer, cancellationToken)) > 0) + { + var request = new Autogenerated.EncryptRequest + { + Payload = new Autogenerated.StreamPayload + { + Data = ByteString.CopyFrom(buffer, 0, bytesRead), Seq = sequenceNumber + } + }; + + //Only include the options in the first message + if (sequenceNumber == 0) + { + request.Options = options; + } + + await call.RequestStream.WriteAsync(request, cancellationToken); + + //Increment the sequence number + sequenceNumber++; + } + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + // Expected cancellation exception + } + catch (Exception ex) + { + OnException?.Invoke(this, ex); + } + finally + { + await call.RequestStream.CompleteAsync(); + } + }, cancellationToken); + + //Start reading from the gRPC call and writing to the output channel + _ = Task.Run(async () => + { + try + { + await foreach (var response in call.ResponseStream.ReadAllAsync(cancellationToken)) + { + await outputChannel.Writer.WriteAsync(response.Payload.Data.Memory, cancellationToken); + } + } + catch (Exception ex) + { + OnException?.Invoke(this, ex); + } + finally + { + outputChannel.Writer.Complete(); + } + }, cancellationToken); + } + + /// + /// Retrieves the processed bytes from the operation from the sidecar and + /// returns as an enumerable stream. + /// + public async IAsyncEnumerable> GetProcessedDataAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + await foreach (var data in outputChannel.Reader.ReadAllAsync(cancellationToken)) + { + yield return data; + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (!disposed) + { + if (disposing) + { + outputChannel.Writer.TryComplete(); + } + + disposed = true; + } + } +} diff --git a/src/Dapr.Cryptography/Encryption/Extensions/DaprCryptographyBuilder.cs b/src/Dapr.Cryptography/Encryption/Extensions/DaprCryptographyBuilder.cs new file mode 100644 index 00000000..9333d481 --- /dev/null +++ b/src/Dapr.Cryptography/Encryption/Extensions/DaprCryptographyBuilder.cs @@ -0,0 +1,28 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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.Cryptography.Encryption.Extensions; + +/// +/// Used by the fluent registration builder to configure a Dapr crypto client. +/// +/// +public sealed class DaprCryptographyBuilder(IServiceCollection services) : IDaprCryptographyBuilder +{ + /// + /// The registered services on the builder. + /// + public IServiceCollection Services { get; } = services; +} diff --git a/src/Dapr.Cryptography/Encryption/Extensions/DaprCryptographyServiceCollectionExtensions.cs b/src/Dapr.Cryptography/Encryption/Extensions/DaprCryptographyServiceCollectionExtensions.cs new file mode 100644 index 00000000..152bbab4 --- /dev/null +++ b/src/Dapr.Cryptography/Encryption/Extensions/DaprCryptographyServiceCollectionExtensions.cs @@ -0,0 +1,42 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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.Extensions; +using Microsoft.Extensions.DependencyInjection; + +namespace Dapr.Cryptography.Encryption.Extensions; + +/// +/// Contains extension methods for using Dapr cryptography with dependency injection. +/// +public static class DaprCryptographyServiceCollectionExtensions +{ + /// + /// Adds Dapr encryption/decryption support to the service collection. + /// + /// The . + /// Optionally allows greater configuration of the using injected services. + /// The lifetime of the registered services. + /// + public static IDaprCryptographyBuilder AddDaprEncryptionClient( + this IServiceCollection services, + Action? configure = null, + ServiceLifetime lifetime = ServiceLifetime.Singleton) + { + services.AddTransient(); + services.AddTransient(); + return services + .AddDaprClient(configure, lifetime); + } +} diff --git a/src/Dapr.Cryptography/Encryption/IDaprEncryptionBuilder.cs b/src/Dapr.Cryptography/Encryption/IDaprEncryptionBuilder.cs new file mode 100644 index 00000000..3b30a003 --- /dev/null +++ b/src/Dapr.Cryptography/Encryption/IDaprEncryptionBuilder.cs @@ -0,0 +1,19 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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.Cryptography.Encryption; + +/// +/// Provides a Dapr client specifically for encryption and decryption operations using Dapr. +/// +public interface IDaprEncryptionBuilder : IDaprCryptographyBuilder; diff --git a/src/Dapr.Cryptography/Encryption/IDaprEncryptionClient.cs b/src/Dapr.Cryptography/Encryption/IDaprEncryptionClient.cs new file mode 100644 index 00000000..ef2f2128 --- /dev/null +++ b/src/Dapr.Cryptography/Encryption/IDaprEncryptionClient.cs @@ -0,0 +1,77 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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.Cryptography.Encryption.Models; + +namespace Dapr.Cryptography.Encryption; + +/// +/// Provides the implementation shape for the Dapr encryption client. +/// +public interface IDaprEncryptionClient : IDaprClient +{ + /// + /// Encrypts an array of bytes using the Dapr Cryptography encryption functionality. + /// + /// The name of the vault resource used by the operation. + /// The bytes of the plaintext value to encrypt. + /// The name of the key to use from the Vault for the encryption operation. + /// Options informing how the encryption operation should be configured. + /// A that can be used to cancel the operation. + /// An array of encrypted bytes. + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public Task> EncryptAsync(string vaultResourceName, + ReadOnlyMemory plaintextBytes, string keyName, EncryptionOptions encryptionOptions, + CancellationToken cancellationToken = default); + + /// + /// Encrypts a stream using the Dapr Cryptography encryption functionality. + /// + /// The name of the vault resource used by the operation. + /// The stream containing the bytes of the plaintext value to encrypt. + /// The name of the key to use from the Vault for the encryption operation. + /// Options informing how the encryption operation should be configured. + /// A that can be used to cancel the operation. + /// An array of encrypted bytes. + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public IAsyncEnumerable> EncryptAsync(string vaultResourceName, Stream plaintextStream, string keyName, + EncryptionOptions encryptionOptions, CancellationToken cancellationToken = default); + + /// + /// Decrypts the specified ciphertext bytes using the Dapr Cryptography encryption functionality. + /// + /// The name of the vault resource used by the operation. + /// The bytes of the ciphertext value to decrypt. + /// The name of the key to use from the Vault for the decryption operation. + /// Options informing how the decryption operation should be configured. + /// A that can be used to cancel the operation. + /// An array of decrypted bytes. + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public Task> DecryptAsync(string vaultResourceName, ReadOnlyMemory ciphertextBytes, string keyName, DecryptionOptions? options = null, + CancellationToken cancellationToken = default); + + /// + /// Decrypts the specified stream of ciphertext using the Dapr Cryptography encryption functionality. + /// + /// The name of the vault resource used by the operation. + /// The stream containing the bytes of the ciphertext value to decrypt. + /// The name of the key to use from the Vault for the decryption operation. + /// Options informing how the decryption operation should be configured. + /// A that can be used to cancel the operation. + /// An asynchronously enumerable array of decrypted bytes. + [Obsolete( + "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public IAsyncEnumerable> DecryptAsync(string vaultResourceName, Stream ciphertextStream, + string keyName, DecryptionOptions? options = null, CancellationToken cancellationToken = default); +} diff --git a/src/Dapr.Cryptography/Encryption/IDecryptionStreamProcessor.cs b/src/Dapr.Cryptography/Encryption/IDecryptionStreamProcessor.cs new file mode 100644 index 00000000..d7538d70 --- /dev/null +++ b/src/Dapr.Cryptography/Encryption/IDecryptionStreamProcessor.cs @@ -0,0 +1,40 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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 Grpc.Core; + +namespace Dapr.Cryptography.Encryption; + +internal interface IDecryptionStreamProcessor +{ + /// + /// Sends the provided bytes in chunks to the sidecar for the encryption operation. + /// + /// The stream containing the bytes to decrypt. + /// The call to make to the sidecar to process the encryption operation. + /// The size, in bytes, of the streaming blocks. + /// The decryption options. + /// Token used to cancel the ongoing request. + Task ProcessStreamAsync( + Stream inputStream, + AsyncDuplexStreamingCall call, + int streamingBlockSizeInBytes, + Client.Autogen.Grpc.v1.DecryptRequestOptions options, + CancellationToken cancellationToken); + + /// + /// Retrieves the processed bytes from the operation from the sidecar and + /// returns as an enumerable stream. + /// + IAsyncEnumerable> GetProcessedDataAsync(CancellationToken cancellationToken); +} diff --git a/src/Dapr.Cryptography/Encryption/IEncryptionStreamProcessor.cs b/src/Dapr.Cryptography/Encryption/IEncryptionStreamProcessor.cs new file mode 100644 index 00000000..b4fc6032 --- /dev/null +++ b/src/Dapr.Cryptography/Encryption/IEncryptionStreamProcessor.cs @@ -0,0 +1,40 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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 Grpc.Core; + +namespace Dapr.Cryptography.Encryption; + +internal interface IEncryptionStreamProcessor +{ + /// + /// Sends the provided bytes in chunks to the sidecar for the encryption operation. + /// + /// The stream containing the bytes to encrypt. + /// The call to make to the sidecar to process the encryption operation. + /// The encryption options. + /// The size, in bytes, of the streaming blocks. + /// Token used to cancel the ongoing request. + Task ProcessStreamAsync( + Stream inputStream, + AsyncDuplexStreamingCall call, + Client.Autogen.Grpc.v1.EncryptRequestOptions options, + int streamingBlockSizeInBytes, + CancellationToken cancellationToken); + + /// + /// Retrieves the processed bytes from the operation from the sidecar and + /// returns as an enumerable stream. + /// + IAsyncEnumerable> GetProcessedDataAsync(CancellationToken cancellationToken); +} diff --git a/src/Dapr.Cryptography/Encryption/Models/DataEncryptionCipher.cs b/src/Dapr.Cryptography/Encryption/Models/DataEncryptionCipher.cs new file mode 100644 index 00000000..df8fd283 --- /dev/null +++ b/src/Dapr.Cryptography/Encryption/Models/DataEncryptionCipher.cs @@ -0,0 +1,33 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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.Serialization; + +namespace Dapr.Cryptography.Encryption.Models; + +/// +/// The cipher used for data encryption operations. +/// +public enum DataEncryptionCipher +{ + /// + /// The default data encryption cipher used, this represents AES GCM. + /// + [EnumMember(Value = "aes-gcm")] + AesGcm, + /// + /// Represents the ChaCha20-Poly1305 data encryption cipher. + /// + [EnumMember(Value = "chacha20-poly1305")] + ChaCha20Poly1305 +} diff --git a/src/Dapr.Cryptography/Encryption/Models/DecryptionOptions.cs b/src/Dapr.Cryptography/Encryption/Models/DecryptionOptions.cs new file mode 100644 index 00000000..b33e3486 --- /dev/null +++ b/src/Dapr.Cryptography/Encryption/Models/DecryptionOptions.cs @@ -0,0 +1,34 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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.Cryptography.Encryption.Models; + +/// +/// A collection of options used to configure how decryption cryptographic operations are performed. +/// +public sealed class DecryptionOptions +{ + private int streamingBlockSizeInBytes = 4 * 1024; // 4KB + /// + /// The size of the block in bytes used to send data to the sidecar for cryptography operations. + /// + public int StreamingBlockSizeInBytes + { + get => streamingBlockSizeInBytes; + set + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(value); + streamingBlockSizeInBytes = value; + } + } +} diff --git a/src/Dapr.Cryptography/Encryption/Models/EncryptionOptions.cs b/src/Dapr.Cryptography/Encryption/Models/EncryptionOptions.cs new file mode 100644 index 00000000..223518a8 --- /dev/null +++ b/src/Dapr.Cryptography/Encryption/Models/EncryptionOptions.cs @@ -0,0 +1,52 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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.Cryptography.Encryption.Models; + +/// +/// A collection of options used to configure how encryption cryptographic operations are performed. +/// +public sealed class EncryptionOptions(KeyWrapAlgorithm keyWrapAlgorithm) +{ + /// + /// The name of the algorithm used to wrap the encryption key. + /// + public KeyWrapAlgorithm KeyWrapAlgorithm { get; set; } = keyWrapAlgorithm; + + private int streamingBlockSizeInBytes = 4 * 1024; // 4 KB + /// + /// The size of the block in bytes used to send data to the sidecar for cryptography operations. + /// + /// + /// This defaults to 4KB and generally should not exceed 64KB. + /// + public int StreamingBlockSizeInBytes + { + get => streamingBlockSizeInBytes; + set + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(value); + streamingBlockSizeInBytes = value; + } + } + + /// + /// The optional name (and optionally a version) of the key specified to use during decryption. + /// + public string? DecryptionKeyName { get; set; } = null; + + /// + /// The name of the cipher to use for the encryption operation. + /// + public DataEncryptionCipher EncryptionCipher { get; set; } = DataEncryptionCipher.AesGcm; +} diff --git a/src/Dapr.Cryptography/Encryption/Models/KeyWrapAlgorithm.cs b/src/Dapr.Cryptography/Encryption/Models/KeyWrapAlgorithm.cs new file mode 100644 index 00000000..50fb47e2 --- /dev/null +++ b/src/Dapr.Cryptography/Encryption/Models/KeyWrapAlgorithm.cs @@ -0,0 +1,58 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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.Serialization; + +namespace Dapr.Cryptography.Encryption.Models; + +/// +/// The algorithm used for key wrapping cryptographic operations. +/// +public enum KeyWrapAlgorithm +{ + /// + /// Represents the AES key wrap algorithm. + /// + [EnumMember(Value="A256KW")] + Aes, + /// + /// An alias for the AES key wrap algorithm. + /// + [EnumMember(Value="A256KW")] + A256kw, + /// + /// Represents the AES 128 CBC key wrap algorithm. + /// + [EnumMember(Value="A128CBC")] + A128cbc, + /// + /// Represents the AES 192 CBC key wrap algorithm. + /// + [EnumMember(Value="A192CBC")] + A192cbc, + /// + /// Represents the AES 256 CBC key wrap algorithm. + /// + [EnumMember(Value="A256CBC")] + A256cbc, + /// + /// Represents the RSA key wrap algorithm. + /// + [EnumMember(Value= "RSA-OAEP-256")] + Rsa, + /// + /// An alias for the RSA key wrap algorithm. + /// + [EnumMember(Value= "RSA-OAEP-256")] + RsaOaep256 //Alias for RSA +} diff --git a/src/Dapr.Cryptography/Extensions/ReadOnlyMemoryExtensions.cs b/src/Dapr.Cryptography/Extensions/ReadOnlyMemoryExtensions.cs new file mode 100644 index 00000000..58347c6d --- /dev/null +++ b/src/Dapr.Cryptography/Extensions/ReadOnlyMemoryExtensions.cs @@ -0,0 +1,34 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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.InteropServices; + +namespace Dapr.Cryptography.Extensions; + +internal static class ReadOnlyMemoryExtensions +{ + public static MemoryStream CreateMemoryStream(this ReadOnlyMemory memory, bool isReadOnly) + { + if (memory.IsEmpty) + { + return new MemoryStream([], !isReadOnly); + } + + if (MemoryMarshal.TryGetArray(memory, out ArraySegment segment) && segment.Array is not null) + { + return new MemoryStream(segment.Array, segment.Offset, segment.Count, !isReadOnly); + } + + throw new ArgumentException("Unable to create MemoryStream from provided memory value", nameof(memory)); + } +} diff --git a/src/Dapr.Cryptography/IDaprCryptographyBuilder.cs b/src/Dapr.Cryptography/IDaprCryptographyBuilder.cs new file mode 100644 index 00000000..a00c5770 --- /dev/null +++ b/src/Dapr.Cryptography/IDaprCryptographyBuilder.cs @@ -0,0 +1,22 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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; + +namespace Dapr.Cryptography; + +/// +/// Provides a root builder for the Dapr cryptography operations facilitating a new fluent-style registration that looks +/// ahead to additional Dapr capabilities in this space. +/// +public interface IDaprCryptographyBuilder : IDaprServiceBuilder; diff --git a/test/Dapr.Cryptography.Test/Dapr.Cryptography.Test.csproj b/test/Dapr.Cryptography.Test/Dapr.Cryptography.Test.csproj new file mode 100644 index 00000000..1d84c9f9 --- /dev/null +++ b/test/Dapr.Cryptography.Test/Dapr.Cryptography.Test.csproj @@ -0,0 +1,28 @@ + + + enable + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + diff --git a/test/Dapr.Cryptography.Test/Encryption/Extensions/DaprCryptographyBuilderTests.cs b/test/Dapr.Cryptography.Test/Encryption/Extensions/DaprCryptographyBuilderTests.cs new file mode 100644 index 00000000..5ed90e4f --- /dev/null +++ b/test/Dapr.Cryptography.Test/Encryption/Extensions/DaprCryptographyBuilderTests.cs @@ -0,0 +1,29 @@ +using Dapr.Cryptography.Encryption.Extensions; +using Microsoft.Extensions.DependencyInjection; + +namespace Dapr.Cryptography.Test.Encryption.Extensions; + +public class DaprCryptographyBuilderTests +{ + [Fact] + public void Constructor_InitializesServiceProperty() + { + var services = new ServiceCollection(); + var builder = new DaprCryptographyBuilder(services); + + Assert.NotNull(builder.Services); + Assert.Equal(services, builder.Services); + } + + [Fact] + public void ServicesProperty_ReturnsRegisteredServices() + { + var services = new ServiceCollection(); + services.AddSingleton(); + + var builder = new DaprCryptographyBuilder(services); + + Assert.NotNull(builder.Services); + Assert.Single(builder.Services); + } +} diff --git a/test/Dapr.Cryptography.Test/Encryption/Extensions/DaprCryptographyServiceCollectionExtensionsTests.cs b/test/Dapr.Cryptography.Test/Encryption/Extensions/DaprCryptographyServiceCollectionExtensionsTests.cs new file mode 100644 index 00000000..1372bd7a --- /dev/null +++ b/test/Dapr.Cryptography.Test/Encryption/Extensions/DaprCryptographyServiceCollectionExtensionsTests.cs @@ -0,0 +1,66 @@ +using System; +using System.Linq; +using Dapr.Cryptography.Encryption; +using Dapr.Cryptography.Encryption.Extensions; +using Microsoft.Extensions.DependencyInjection; + +namespace Dapr.Cryptography.Test.Encryption.Extensions; + +public class DaprCryptographyServiceCollectionExtensionsTests +{ + [Fact] + public void AddDaprEncryptionClient_RegistersServicesWithSingletonLifetime() + { + var services = new ServiceCollection(); + var builder = services.AddDaprEncryptionClient(); + var servicesProvider = services.BuildServiceProvider(); + var daprClient = servicesProvider.GetService(); + + Assert.NotNull(builder); + Assert.NotNull(daprClient); + Assert.Equal(ServiceLifetime.Singleton, services.First(sd => sd.ServiceType == typeof(DaprEncryptionClient)).Lifetime); + } + + [Fact] + public void AddDaprEncryptionClient_RegistersServicesWithScopedLifetime() + { + var services = new ServiceCollection(); + var builder = services.AddDaprEncryptionClient(lifetime: ServiceLifetime.Scoped); + var serviceProvider = services.BuildServiceProvider(); + var daprClient = serviceProvider.GetService(); + + Assert.NotNull(builder); + Assert.NotNull(daprClient); + Assert.Equal(ServiceLifetime.Scoped, services.First(sd => sd.ServiceType == typeof(DaprEncryptionClient)).Lifetime); + } + + [Fact] + public void AddDaprEncryptionClient_RegistersServicesWithTransientLifetime() + { + var services = new ServiceCollection(); + var builder = services.AddDaprEncryptionClient(lifetime: ServiceLifetime.Transient); + var serviceProvider = services.BuildServiceProvider(); + var daprClient = serviceProvider.GetService(); + + Assert.NotNull(builder); + Assert.NotNull(daprClient); + Assert.Equal(ServiceLifetime.Transient, services.First(sd => sd.ServiceType == typeof(DaprEncryptionClient)).Lifetime); + } + + [Fact] + public void AddDaprEncryptionClient_WithConfigureAction_ExecutesConfiguation() + { + var services = new ServiceCollection(); + var configureCalled = false; + + Action configure = (sp, builder) => configureCalled = true; + + var builder = services.AddDaprEncryptionClient(configure); + var serviceProvider = services.BuildServiceProvider(); + var daprClient = serviceProvider.GetService(); + + Assert.NotNull(builder); + Assert.NotNull(daprClient); + Assert.True(configureCalled); + } +} diff --git a/test/Dapr.Cryptography.Test/Encryption/Models/DecryptionOptionsTests.cs b/test/Dapr.Cryptography.Test/Encryption/Models/DecryptionOptionsTests.cs new file mode 100644 index 00000000..0e086901 --- /dev/null +++ b/test/Dapr.Cryptography.Test/Encryption/Models/DecryptionOptionsTests.cs @@ -0,0 +1,35 @@ +using System; +using Dapr.Cryptography.Encryption.Models; + +namespace Dapr.Cryptography.Test.Encryption.Models; + +public class DecryptionOptionsTests +{ + [Fact] + public void DefaultStreamingBlockSizeInBytes_Is4KB() + { + var options = new DecryptionOptions(); + var defaultBlockSize = options.StreamingBlockSizeInBytes; + Assert.Equal(4 * 1024, defaultBlockSize); + } + + [Theory] + [InlineData(1024)] + [InlineData(2048)] + [InlineData(8192)] + public void StreamingBlockSizeInBytes_SetValidValue_UpdatesBlockSize(int newBlockSize) + { + var options = new DecryptionOptions(); + options.StreamingBlockSizeInBytes = newBlockSize; + Assert.Equal(newBlockSize, options.StreamingBlockSizeInBytes); + } + + [Theory] + [InlineData(0)] + [InlineData(-1024)] + public void StreamingBlockSizeInBytes_SetInvalidValue_ThrowsArgumentOutOfRangeException(int invalidBlockSize) + { + var options = new DecryptionOptions(); + Assert.Throws(() => options.StreamingBlockSizeInBytes = invalidBlockSize); + } +} diff --git a/test/Dapr.Cryptography.Test/Encryption/Models/EncryptionOptionsTests.cs b/test/Dapr.Cryptography.Test/Encryption/Models/EncryptionOptionsTests.cs new file mode 100644 index 00000000..53529825 --- /dev/null +++ b/test/Dapr.Cryptography.Test/Encryption/Models/EncryptionOptionsTests.cs @@ -0,0 +1,72 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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 Dapr.Cryptography.Encryption.Models; + +namespace Dapr.Cryptography.Test.Encryption.Models; + +public class EncryptionOptionsTests +{ + [Fact] + public void Constructor_InitializesKeyWrapAlgorithm() + { + var keyWrapAlgorithm = KeyWrapAlgorithm.RsaOaep256; + var options = new EncryptionOptions(keyWrapAlgorithm); + Assert.Equal(keyWrapAlgorithm, options.KeyWrapAlgorithm); + } + + [Fact] + public void DefaultStreamingBlockSizeInBytes_Is4Kb() + { + var options = new EncryptionOptions(KeyWrapAlgorithm.A256kw); + var defaultBlockSize = options.StreamingBlockSizeInBytes; + Assert.Equal(4 * 1024, defaultBlockSize); + } + + [Theory] + [InlineData(1024)] + [InlineData(2048)] + [InlineData(4092)] + public void StreamingBlockSizeInBytes_SetValidValue_UpdatesBlockSize(int newBlockSize) + { + var options = new EncryptionOptions(KeyWrapAlgorithm.A192cbc); + options.StreamingBlockSizeInBytes = newBlockSize; + Assert.Equal(newBlockSize, options.StreamingBlockSizeInBytes); + } + + [Theory] + [InlineData(0)] + [InlineData(-1024)] + public void StreamingBlockSizeInBytes_SetInvalidValue_ThrowsArgumentOutOfRangeException(int invalidBlockSize) + { + var options = new EncryptionOptions(KeyWrapAlgorithm.Rsa); + Assert.Throws(() => options.StreamingBlockSizeInBytes = invalidBlockSize); + } + + [Fact] + public void DefaultDecryptionKeyName_IsNull() + { + var options = new EncryptionOptions(KeyWrapAlgorithm.A256cbc); + string? defaultKeyName = options.DecryptionKeyName; + Assert.Null(defaultKeyName); + } + + [Fact] + public void DefaultEncryptionCipher_IsAes() + { + var options = new EncryptionOptions(KeyWrapAlgorithm.Aes); + var defaultCipher = options.EncryptionCipher; + Assert.Equal(DataEncryptionCipher.AesGcm, defaultCipher); + } +} diff --git a/test/Dapr.Cryptography.Test/Extensions/ReadOnlyMemoryExtensionsTests.cs b/test/Dapr.Cryptography.Test/Extensions/ReadOnlyMemoryExtensionsTests.cs new file mode 100644 index 00000000..72ecfc80 --- /dev/null +++ b/test/Dapr.Cryptography.Test/Extensions/ReadOnlyMemoryExtensionsTests.cs @@ -0,0 +1,59 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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 Dapr.Cryptography.Extensions; + +namespace Dapr.Cryptography.Test.Extensions; + +public class ReadOnlyMemoryExtensionsTests +{ + [Fact] + public void CreateMemoryStream_EmptyMemory_ReturnsEmptyMemoryStream() + { + var emptyMemory = ReadOnlyMemory.Empty; + var result = emptyMemory.CreateMemoryStream(isReadOnly: true); + Assert.NotNull(result); + Assert.Equal(0, result.Length); + Assert.True(result.CanRead); + Assert.False(result.CanWrite); + } + + [Fact] + public void CreateMemoryStream_NonEmptyMemory_ReturnsMemoryStream() + { + byte[] data = { 1, 2, 3, 4, 5 }; + var memory = new ReadOnlyMemory(data); + + var result = memory.CreateMemoryStream(isReadOnly: true); + + Assert.NotNull(result); + Assert.Equal(data.Length, result.Length); + Assert.True(result.CanRead); + Assert.False(result.CanWrite); + } + + [Fact] + public void CreateMemoryStream_NonEmptyMemory_ReturnsWriteableMemoryStream() + { + byte[] data = { 1, 2, 3, 4, 5 }; + var memory = new ReadOnlyMemory(data); + + var result = memory.CreateMemoryStream(isReadOnly: false); + + Assert.NotNull(result); + Assert.Equal(data.Length, result.Length); + Assert.True(result.CanRead); + Assert.True(result.CanWrite); + } +} diff --git a/test/Dapr.E2E.Test/Dapr.E2E.Test.csproj b/test/Dapr.E2E.Test/Dapr.E2E.Test.csproj index 50379223..93b21ee7 100644 --- a/test/Dapr.E2E.Test/Dapr.E2E.Test.csproj +++ b/test/Dapr.E2E.Test/Dapr.E2E.Test.csproj @@ -13,6 +13,7 @@ + @@ -24,4 +25,10 @@ + + + + Always + + diff --git a/test/Dapr.E2E.Test/components/crypto.yaml b/test/Dapr.E2E.Test/components/crypto.yaml new file mode 100644 index 00000000..4a6640fe --- /dev/null +++ b/test/Dapr.E2E.Test/components/crypto.yaml @@ -0,0 +1,11 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: localstorage +spec: + type: crypto.dapr.localstorage + version: v1 + metadata: + - name: path + # Path is relative to the folder where the example is located + value: ./keys diff --git a/test/Dapr.E2E.Test/keys/rsa-private-key.pem b/test/Dapr.E2E.Test/keys/rsa-private-key.pem new file mode 100644 index 00000000..f4508f7a --- /dev/null +++ b/test/Dapr.E2E.Test/keys/rsa-private-key.pem @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQC0URLpxZCqDv7S +WfROh2Kei4VCEayNu/TK3NaD/QlIpip1rrsPKgTfTOZoRmkmG0Qj59srEJi2GEhL +xpjvRQpA/C/OS+KELU8AeGrqHw7uN/a99NkoAr+zYDCyY9yckPeC5wGxc0/Q6HQT +mWp+YcpR9wFO0PmTVlObssibagjjRNX7z/ZosecOOqjnAqlnYoHMavvoCD5fxM7y +cm7so0JWooXwVaZKgehBEBg1W5F0q5e9ssAQk3lY6IUd5sOskiylTNf/+3r1JU0j +YM8ik3a1/dyDALVXpLSfz7FM9VEj4QjiPF4UuXeBHPDFFiKWbiKfbjqvZ2Sz7Gl7 +c5rTk1Fozpr70E/wihrrv22Mxs0sEPdtemQgHXroQfRW8K4FhI0WHs7tR2gVxLHu +OAU9LzCngz4yITh1eixVDmm/B5ZtNVrTQmaY84vGqhrFp+asyFNiXbhUAcT7D/q6 +w/c4aQ635ntCFSPYpWvhKqrqVDsoanD/5AWfc3+6Ek2/GVMyEQq+9tnCMM10EVSX +8PsoAWHESDFude5zkHzn7IKy8mh6lfheEbBI5zN9z7WGexyiBgljmyUHXx6Pd8Uc +yxpLRm94kynkDXD9SapQLzXmz+D+X/OYeADMIDWlbdXiIb1+2Q62H1lo6n10KVP7 +oEr8BHvcMFY89kwK4lKscUupn8xkzwIDAQABAoICACDuu78Rc8Hzeivt/PZIuMTP +I5f1BWhffy571fwGP2dS3edfcc+rs3cbIuvBjFvG2BOcuYUsg0+isLWSQIVWvTAw +PwT1DBpq8gZad+Bpqr7sXrbD3NN3aQ64TzyNi5HW0jXIviDsOBQmGGkp+G67qol8 +zPLZrPNxbVS++u+Tlqr3fAOBMHZfo50QLp/+dvUoYx90HKz8sHOqTMewCb1Tdf6/ +sSm7YuMxxbr4VwuLvU2rN0wQtQ5x+NQ5p3JWHr/KdLf+CGc6xXK3jNaczEf62dAU +XO1aOESZEtorQy0Ukuy0IXy8XMx5MS/WGs1MJSYHWHB43+QARL6tu3guHYVt3wyv +W6YTglQsSKc6uuK4JTZOx1VYZjjnSdeY/xiUmZGYp4ZiC9p8b9NvXmZT2EwqhCVt +4OTcX4lkwGAsKcoEdLHi0K5CbBfYJsRgVVheDjP0xUFjCJCYqfqo2rE5YMXMTeY7 +clYEOXKGxwuy1Iu8nKqtWAV5r/eSmXBdxBqEBW9oxJfnnwNPG+yOk0Qkd1vaRj00 +mdKCOjgB2fOuPX2JRZ2z41Cem3gqhH0NQGrx3APV4egGrYAMClasgtZkUeUOIgK5 +xLlC/6svuHNyKXAKFpOubEy1FM8jz7111eNHxHRDP3+vH3u4CfAD2Sl+VDZdg51i +WmVpT+B/DrnlHVSP2/XNAoIBAQD7F49oSdveKuO/lAyqkE9iF61i09G0b0ouDGUI +qx+pd5/8vUcqi4upCxz+3AqMPWZRIqOyo8EUP7f4rSJrXn8U2SwnFfi4k2jiqmEA +Wr0b8z5P1q5MH6BtVDa0Sr1R8xI9s3UgIs4pUKgBoQu9+U4Du4NSucQFcea8nIVY +lLCqQcRhz8bCJPCNuHay5c77kK3Te197KPMasNurTNMOJcPMG95CZLB8Clf4A+pw +fixvA1/fE4mFo1L7Ymxoz5lFYVWOTY9hh50Kqz57wxw4laU4ii+MaJj+YHuNR83N +cO6FztUYKMR8BPgtl3/POTHTofSg7eIOiUYwcfRr6jbMWlsDAoIBAQC311xiMpho +Hvdcvp3/urrIp2QhdD05n6TnZOPkpnd9kwGku2RA+occDQOg/BzADVwJaR/aE97F +jbfRlfBesTZlUec0EwjKIFbeYh+QS/RmjQe9zpPQWMo1M7y0fMWU+yXRUcNBpcuy +R6KlphK0k4xFkIAdC3QHmJQ0XvOpqvrhFy3i/Prc5Wlg29FYBBTAF0WZCZ4uCG34 +D0eG0CNaf8w9g9ClbU6nGLBCMcgjEOPYfyrJaedM+jXennLDPG6ySytrGwnwLAQc +Okx+SrIiNHUpQGKteT88Kdpgo3F4KUX/pm84uGdxrOpDS7L0T9/G4CbjzCe1nHeS +fJJsw5JN+Z9FAoIBAGn5S6FsasudtnnI9n+WYKq564fmdn986QX+XTYHY1mXD4MQ +L9UZCFzUP+yg2iLOVzyvLf/bdUYijnb6O6itPV2DO0tTzqG4NXBVEJOhuGbvhsET +joS6ZG9AN8ZoNPc9a9l2wFxL1E9Dp2Ton5gSfIa+wXJMzRqvM/8u4Gi+eMGi+Et/ +8hdGl/B4hkCDFZS/P14el/HXGqONOWlXB0zVS4n9yRSkgogXpYEbxfqshfxkpDX2 +fPhWMlO++ppR5BKQPhfNTFKRdgpms/xwIJ0RK6ZtTBwqmUfjWMIMKCQpIcJ/xRhp +PGRLhKNZaawAK7Nyi1jQjbQs497WeZ6CP5aIHBkCggEALHyl83FQ5ilQLJZH/6E9 +H9854MqTIkWajxAgAa2yzqVrSWS7XuoBFe2kSimX/3V8Jx7UQV57kwy3RbVl5FQ3 +2I7YRwawItFulAPkpXNr4gEQtYKuzEUgMX2ilX54BZQ804lYmaM4Rp0FI9arQh1O +XWsZRW4HFut6Oa4cgptIeH22ce5L+nZdaL3oy8a5Cr7W7bChIXySt+tioKHvXC/+ +yYgDTnTECrVzuaD4UFv+9t3XCcRh34PQ010+YjZWhzifehyh7AeKuxX0er8ymgpd +q6zT9CyZ+8IZATer9qruMG4jDfO5vI1eZwiDdpF5klOdtZQqq80ANmeEu2McHVhh +jQKCAQBbohPxMb3QYdukGp8IsIF04GfnTgaDbRgl4KeUyzdBN3nzvCKK0HDluptR +4Ua64JksGG24gsTBy6yuQoGRCG0LJe0Ty3TRRnvZ8MpADoNMObspMSC8n8kk6ps+ +SoG1U9t6HYlIgQagvTc7mTmCmwYX1zlCoZp24yz5pDkKxqoPFDtrGlXxeUgOhpDT +Mzi+DNTz9sH9vod4ibQiOseUxITwQpXHTJVrtNfvva6xjlhq+GGCuKIUwkUKOvBC +ds7SR9demn69aWCyzXqD1cTnmxtn6bNPukwowg7a07ieUyKftcJ1icOWQ/bdQkEf +dV1dhNiQEnqs4vDBVn40dnTKSSG2 +-----END PRIVATE KEY----- From 326ad37ed768f265b836c34b63c96e49b428fa66 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Fri, 16 May 2025 10:54:02 -0500 Subject: [PATCH 15/22] Removed the Project Tye deployment documentation (#1540) Signed-off-by: Whit Waldo --- .../dotnet-development-tye.md | 67 ------------------- 1 file changed, 67 deletions(-) delete mode 100644 daprdocs/content/en/dotnet-sdk-docs/dotnet-development/dotnet-development-tye.md 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 deleted file mode 100644 index 0077bd56..00000000 --- a/daprdocs/content/en/dotnet-sdk-docs/dotnet-development/dotnet-development-tye.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -type: docs -title: "Dapr .NET SDK Development with Project Tye" -linkTitle: "Project Tye" -weight: 50000 -description: Learn about local development with Project Tye ---- - -## Project Tye - -[.NET Project Tye](https://github.com/dotnet/tye/) is a microservices development tool designed to make running many .NET services easy. Tye enables you to store a configuration of multiple .NET services, processes, and container images as a runnable application. - -Tye is advantageous for a .NET Dapr developer because: - -- Tye has the ability to automate the dapr CLI built-in -- Tye understands .NET's conventions and requires almost no configuration for .NET services -- Tye can manage the lifetime of your dependencies in containers - -Pros/cons: -- **Pro:** Tye can automate all of the steps described above. You no longer need to think about concepts like ports or app-ids. -- **Pro:** Since Tye can also manage containers for you, you can make those part of the application definition and stop the long-running containers on your machine. - -### Using Tye - -Follow the [Tye Getting Started](https://github.com/dotnet/tye/blob/master/docs/getting_started.md) to install the `tye` CLI and create a `tye.yaml` for your application. - -Next follow the steps in the [Tye Dapr recipe](https://github.com/dotnet/tye/blob/master/docs/recipes/dapr.md) to add Dapr. Make sure to specify the relative path to your components folder with `components-path` in `tye.yaml`. - -Next add any additional container dependencies and add component definitions to the folder you created earlier. - -You should end up with something like this: - -```yaml -name: store-application -extensions: - - # Configuration for dapr goes here. -- name: dapr - components-path: - -# Services to run go here. -services: - - # The name will be used as the app-id. For a .NET project, Tye only needs the path to the project file. -- name: orders - project: orders/orders.csproj -- name: products - project: products/products.csproj -- name: store - project: store/store.csproj - - # Containers you want to run need an image name and set of ports to expose. -- name: redis - image: redis - bindings: - - port: 6973 -``` - -Checkin `tye.yaml` in source control with the application code. - -You can now use `tye run` to launch the whole application from one terminal. When running, Tye has a dashboard at `http://localhost:8000` to view application status and logs. - -### Next steps - -Tye runs your services locally as normal .NET process. If you need to debug, then use the attach feature of your debugger to attach to one of the running processes. Since Tye is .NET aware, it has the ability to [start a process suspended](https://github.com/dotnet/tye/blob/master/docs/reference/commandline/tye-run.md#options) for startup debugging. - -Tye also has an [option](https://github.com/dotnet/tye/blob/master/docs/reference/commandline/tye-run.md#options) to run your services in containers if you wish to test locally in containers. From 8aa6a7fe8699cd331d699d41417a01c79f410512 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Fri, 20 Jun 2025 03:44:09 -0500 Subject: [PATCH 16/22] Jobs overwrite/failure policy updates (#1558) Updated Jobs SDK to reflect both the overwrite and failure policy additions --- Directory.Packages.props | 70 ++++++++++--------- .../DaprConfigurationStoreProvider.cs | 4 +- .../DaprSecretStoreConfigurationProvider.cs | 4 +- src/Dapr.Jobs/DaprJobsClient.cs | 5 +- src/Dapr.Jobs/DaprJobsGrpcClient.cs | 43 +++++++++++- .../Extensions/DaprSerializationExtensions.cs | 15 +++- src/Dapr.Jobs/Models/DaprJobSchedule.cs | 8 +-- .../Models/IJobFailurePolicyOptions.cs | 25 +++++++ src/Dapr.Jobs/Models/JobFailurePolicy.cs | 29 ++++++++ .../Models/JobFailurePolicyConstantOptions.cs | 33 +++++++++ .../Models/JobFailurePolicyDropOptions.cs | 25 +++++++ .../Models/Responses/DaprJobDetails.cs | 11 +++ .../Protos/dapr/proto/common/v1/common.proto | 28 +++++++- .../Protos/dapr/proto/runtime/v1/dapr.proto | 17 ++++- test/Dapr.Client.Test/StateApiTest.cs | 8 +-- .../CronExpressionBuilderTests.cs | 1 - .../DaprJobsClientBuilderTests.cs | 1 - .../Dapr.Jobs.Test/DaprJobsGrpcClientTests.cs | 1 - ...aprJobsServiceCollectionExtensionsTests.cs | 1 - .../EndpointRouteBuilderExtensionsTests.cs | 9 --- .../Extensions/StringExtensionsTests.cs | 1 - .../Extensions/TimeSpanExtensionsTests.cs | 1 - .../Models/DaprJobScheduleTests.cs | 1 - 23 files changed, 266 insertions(+), 75 deletions(-) create mode 100644 src/Dapr.Jobs/Models/IJobFailurePolicyOptions.cs create mode 100644 src/Dapr.Jobs/Models/JobFailurePolicy.cs create mode 100644 src/Dapr.Jobs/Models/JobFailurePolicyConstantOptions.cs create mode 100644 src/Dapr.Jobs/Models/JobFailurePolicyDropOptions.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index eee08679..f0971244 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,51 +4,53 @@ true - - - + + + - - - + + + - - - - - - - - + + + + + + + + + + - - + + - - - - - - - - - - - + + + + + + + + + + + - - - - - - + + + + + + - + \ No newline at end of file diff --git a/src/Dapr.Extensions.Configuration/DaprConfigurationStoreProvider.cs b/src/Dapr.Extensions.Configuration/DaprConfigurationStoreProvider.cs index b5f2900d..3e859b8d 100644 --- a/src/Dapr.Extensions.Configuration/DaprConfigurationStoreProvider.cs +++ b/src/Dapr.Extensions.Configuration/DaprConfigurationStoreProvider.cs @@ -89,7 +89,7 @@ internal class DaprConfigurationStoreProvider : ConfigurationProvider, IDisposab var subscribeConfigurationResponse = await daprClient.SubscribeConfiguration(store, keys, metadata, cts.Token); await foreach (var items in subscribeConfigurationResponse.Source.WithCancellation(cts.Token)) { - var data = new Dictionary(Data, StringComparer.OrdinalIgnoreCase); + var data = new Dictionary(Data, StringComparer.OrdinalIgnoreCase); foreach (var item in items) { id = subscribeConfigurationResponse.Id; @@ -121,4 +121,4 @@ internal class DaprConfigurationStoreProvider : ConfigurationProvider, IDisposab } } } -} \ No newline at end of file +} diff --git a/src/Dapr.Extensions.Configuration/DaprSecretStoreConfigurationProvider.cs b/src/Dapr.Extensions.Configuration/DaprSecretStoreConfigurationProvider.cs index b2d4dc41..31aa5b1f 100644 --- a/src/Dapr.Extensions.Configuration/DaprSecretStoreConfigurationProvider.cs +++ b/src/Dapr.Extensions.Configuration/DaprSecretStoreConfigurationProvider.cs @@ -189,7 +189,7 @@ internal class DaprSecretStoreConfigurationProvider : ConfigurationProvider private async Task LoadAsync() { - var data = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + var data = new Dictionary(StringComparer.InvariantCultureIgnoreCase); // Wait for the Dapr Sidecar to report healthy before attempting to fetch secrets. using (var tokenSource = new CancellationTokenSource(sidecarWaitTimeout)) @@ -259,4 +259,4 @@ internal class DaprSecretStoreConfigurationProvider : ConfigurationProvider Data = data; } } -} \ No newline at end of file +} diff --git a/src/Dapr.Jobs/DaprJobsClient.cs b/src/Dapr.Jobs/DaprJobsClient.cs index 2aecf816..c30c34ba 100644 --- a/src/Dapr.Jobs/DaprJobsClient.cs +++ b/src/Dapr.Jobs/DaprJobsClient.cs @@ -70,12 +70,15 @@ public abstract class DaprJobsClient(Autogenerated.DaprClient client, HttpClient /// The optional point-in-time from which the job schedule should start. /// The optional number of times the job should be triggered. /// Represents when the job should expire. If both this and DueTime are set, TTL needs to represent a later point in time. + /// A flag indicating whether the job should be overwritten when submitted (true); otherwise false to require + /// to require that an existing job with the same name be deleted first. + /// The characteristics of the policy to apply when a job fails to trigger. /// Cancellation token. [Obsolete( "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] public abstract Task ScheduleJobAsync(string jobName, DaprJobSchedule schedule, ReadOnlyMemory? payload = null, DateTimeOffset? startingFrom = null, int? repeats = null, - DateTimeOffset? ttl = null, + DateTimeOffset? ttl = null, bool overwrite = false, IJobFailurePolicyOptions? failurePolicyOptions = null, CancellationToken cancellationToken = default); /// diff --git a/src/Dapr.Jobs/DaprJobsGrpcClient.cs b/src/Dapr.Jobs/DaprJobsGrpcClient.cs index 182db48c..31811ee8 100644 --- a/src/Dapr.Jobs/DaprJobsGrpcClient.cs +++ b/src/Dapr.Jobs/DaprJobsGrpcClient.cs @@ -35,17 +35,19 @@ internal sealed class DaprJobsGrpcClient(Autogenerated.Dapr.DaprClient client, H /// The optional point-in-time from which the job schedule should start. /// The optional number of times the job should be triggered. /// Represents when the job should expire. If both this and DueTime are set, TTL needs to represent a later point in time. + /// A flag indicating whether the job should be overwritten when submitted (true); otherwise false to require that an existing job with the same name be deleted first. + /// The characteristics of the policy to apply when a job fails to trigger. /// Cancellation token. [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 ScheduleJobAsync(string jobName, DaprJobSchedule schedule, ReadOnlyMemory? payload = null, DateTimeOffset? startingFrom = null, int? repeats = null, - DateTimeOffset? ttl = null, + DateTimeOffset? ttl = null, bool overwrite = false, IJobFailurePolicyOptions? failurePolicyOptions = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(jobName, nameof(jobName)); ArgumentNullException.ThrowIfNull(schedule, nameof(schedule)); - var job = new Autogenerated.Job { Name = jobName }; + var job = new Autogenerated.Job { Name = jobName, Overwrite = overwrite }; //Set up the schedule (recurring or point in time) if (schedule.IsPointInTimeExpression) @@ -88,6 +90,43 @@ internal sealed class DaprJobsGrpcClient(Autogenerated.Dapr.DaprClient client, H job.Ttl = ((DateTimeOffset)ttl).ToString("O"); } + if (failurePolicyOptions is not null) + { + switch (failurePolicyOptions.Policy) + { + case JobFailurePolicy.Drop: + job.FailurePolicy = new Autogenerated.JobFailurePolicy + { + Drop = new Autogenerated.JobFailurePolicyDrop() + }; + break; + case JobFailurePolicy.Constant: + var constantOptions = (JobFailurePolicyConstantOptions)failurePolicyOptions; + if (constantOptions.MaxRetries is null) + { + job.FailurePolicy = new Autogenerated.JobFailurePolicy + { + Constant = new Autogenerated.JobFailurePolicyConstant + { + Interval = constantOptions.Interval.ToDuration() + } + }; + } + else + { + job.FailurePolicy = new Autogenerated.JobFailurePolicy + { + Constant = new Autogenerated.JobFailurePolicyConstant + { + Interval = constantOptions.Interval.ToDuration(), + MaxRetries = (uint)constantOptions.MaxRetries + } + }; + } + break; + } + } + var envelope = new Autogenerated.ScheduleJobRequest { Job = job }; var grpcCallOptions = DaprClientUtilities.ConfigureGrpcCallOptions(typeof(DaprJobsClient).Assembly, this.DaprApiToken, cancellationToken); diff --git a/src/Dapr.Jobs/Extensions/DaprSerializationExtensions.cs b/src/Dapr.Jobs/Extensions/DaprSerializationExtensions.cs index 0a612e3a..2fd27800 100644 --- a/src/Dapr.Jobs/Extensions/DaprSerializationExtensions.cs +++ b/src/Dapr.Jobs/Extensions/DaprSerializationExtensions.cs @@ -38,10 +38,14 @@ public static class DaprJobsSerializationExtensions /// The optional number of times the job should be triggered. /// Optional JSON serialization options. /// Represents when the job should expire. If both this and DueTime are set, TTL needs to represent a later point in time. + /// A flag indicating whether the job should be overwritten when submitted (true); otherwise false to require that an existing job with the same name be deleted first. + /// The characteristics of the policy to apply when a job fails to trigger. /// Cancellation token. [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] public static async Task ScheduleJobWithPayloadAsync(this DaprJobsClient client, string jobName, DaprJobSchedule schedule, - object payload, DateTime? startingFrom = null, int? repeats = null, JsonSerializerOptions? jsonSerializerOptions = null, DateTimeOffset? ttl = null, + object payload, DateTime? startingFrom = null, int? repeats = null, + JsonSerializerOptions? jsonSerializerOptions = null, DateTimeOffset? ttl = null, + bool overwrite = false, IJobFailurePolicyOptions? failurePolicyOptions = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(payload, nameof(payload)); @@ -50,7 +54,8 @@ public static class DaprJobsSerializationExtensions var payloadBytes = JsonSerializer.SerializeToUtf8Bytes(payload, serializerOptions); - await client.ScheduleJobAsync(jobName, schedule, payloadBytes, startingFrom, repeats, ttl, cancellationToken); + await client.ScheduleJobAsync(jobName, schedule, payloadBytes, startingFrom, repeats, ttl, overwrite, + failurePolicyOptions, cancellationToken); } /// @@ -63,16 +68,20 @@ public static class DaprJobsSerializationExtensions /// The optional point-in-time from which the job schedule should start. /// The optional number of times the job should be triggered. /// Represents when the job should expire. If both this and DueTime are set, TTL needs to represent a later point in time. + /// A flag indicating whether the job should be overwritten when submitted (true); otherwise false to require that an existing job with the same name be deleted first. + /// The characteristics of the policy to apply when a job fails to trigger. /// Cancellation token. [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] public static async Task ScheduleJobWithPayloadAsync(this DaprJobsClient client, string jobName, DaprJobSchedule schedule, string payload, DateTime? startingFrom = null, int? repeats = null, DateTimeOffset? ttl = null, + bool overwrite = false, IJobFailurePolicyOptions? failurePolicyOptions = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(payload, nameof(payload)); var payloadBytes = Encoding.UTF8.GetBytes(payload); - await client.ScheduleJobAsync(jobName, schedule, payloadBytes, startingFrom, repeats, ttl, cancellationToken); + await client.ScheduleJobAsync(jobName, schedule, payloadBytes, startingFrom, repeats, ttl, overwrite, + failurePolicyOptions, cancellationToken); } } diff --git a/src/Dapr.Jobs/Models/DaprJobSchedule.cs b/src/Dapr.Jobs/Models/DaprJobSchedule.cs index f897bd6b..06663b11 100644 --- a/src/Dapr.Jobs/Models/DaprJobSchedule.cs +++ b/src/Dapr.Jobs/Models/DaprJobSchedule.cs @@ -71,13 +71,7 @@ public sealed class DaprJobSchedule /// Specifies a schedule using a Cron-like expression or '@' prefixed period strings. /// /// The systemd Cron-like expression indicating when the job should be triggered. - public static DaprJobSchedule FromExpression(string expression) - { -#if NET6_0 - ArgumentNullException.ThrowIfNull(expression, nameof(expression)); -#endif - return new DaprJobSchedule(expression); - } + public static DaprJobSchedule FromExpression(string expression) => new(expression); /// /// Specifies a schedule using a duration interval articulated via a . diff --git a/src/Dapr.Jobs/Models/IJobFailurePolicyOptions.cs b/src/Dapr.Jobs/Models/IJobFailurePolicyOptions.cs new file mode 100644 index 00000000..5f45b0f0 --- /dev/null +++ b/src/Dapr.Jobs/Models/IJobFailurePolicyOptions.cs @@ -0,0 +1,25 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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.Jobs.Models; + +/// +/// Interface for specifying job failure policy options. +/// +public interface IJobFailurePolicyOptions +{ + /// + /// The type of policy to apply. + /// + public JobFailurePolicy Policy { get; } +} diff --git a/src/Dapr.Jobs/Models/JobFailurePolicy.cs b/src/Dapr.Jobs/Models/JobFailurePolicy.cs new file mode 100644 index 00000000..fff575c0 --- /dev/null +++ b/src/Dapr.Jobs/Models/JobFailurePolicy.cs @@ -0,0 +1,29 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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.Jobs.Models; + +/// +/// Specifies the policy the apply when a job fails to trigger. +/// +public enum JobFailurePolicy +{ + /// + /// Drops the job tick with the job fails to trigger. + /// + Drop, + /// + /// Retries the job at a consistent interval when the job fails to trigger. + /// + Constant +} diff --git a/src/Dapr.Jobs/Models/JobFailurePolicyConstantOptions.cs b/src/Dapr.Jobs/Models/JobFailurePolicyConstantOptions.cs new file mode 100644 index 00000000..df0b33ae --- /dev/null +++ b/src/Dapr.Jobs/Models/JobFailurePolicyConstantOptions.cs @@ -0,0 +1,33 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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.Jobs.Models; + +/// +/// A policy which retries the job at a consistent interval when the job +/// fails to trigger. +/// +/// The constant delay to wait before trying the job. +public sealed record JobFailurePolicyConstantOptions(TimeSpan Interval) : IJobFailurePolicyOptions +{ + /// + /// An optional maximum number of retries to attempt before giving up. If unset, + /// the Job will be retried indefinitely. + /// + public uint? MaxRetries { get; init; } = null; + + /// + /// The type of policy to apply. + /// + public JobFailurePolicy Policy => JobFailurePolicy.Constant; +} diff --git a/src/Dapr.Jobs/Models/JobFailurePolicyDropOptions.cs b/src/Dapr.Jobs/Models/JobFailurePolicyDropOptions.cs new file mode 100644 index 00000000..562c09c8 --- /dev/null +++ b/src/Dapr.Jobs/Models/JobFailurePolicyDropOptions.cs @@ -0,0 +1,25 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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.Jobs.Models; + +/// +/// A policy which drops the job tick when the job fails to trigger. +/// +public sealed record JobFailurePolicyDropOptions : IJobFailurePolicyOptions +{ + /// + /// The type of policy to apply. + /// + public JobFailurePolicy Policy => JobFailurePolicy.Drop; +} diff --git a/src/Dapr.Jobs/Models/Responses/DaprJobDetails.cs b/src/Dapr.Jobs/Models/Responses/DaprJobDetails.cs index 1e53d95e..77911c7b 100644 --- a/src/Dapr.Jobs/Models/Responses/DaprJobDetails.cs +++ b/src/Dapr.Jobs/Models/Responses/DaprJobDetails.cs @@ -42,4 +42,15 @@ public sealed record DaprJobDetails(DaprJobSchedule Schedule) /// Stores the main payload of the job which is passed to the trigger function. /// public byte[]? Payload { get; init; } = null; + + /// + /// True if the job should be overwritten when submitted; false to require an existing job with the same name + /// to be deleted first. + /// + public bool Overwrite { get; init; } = false; + + /// + /// Defines the characteristics of the policy to apply when a job fails to trigger. + /// + public JobFailurePolicy? FailurePolicy { get; init; } } 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 fc5e9983..c46ad4db 100644 --- a/src/Dapr.Protos/Protos/dapr/proto/common/v1/common.proto +++ b/src/Dapr.Protos/Protos/dapr/proto/common/v1/common.proto @@ -16,6 +16,7 @@ syntax = "proto3"; package dapr.proto.common.v1; import "google/protobuf/any.proto"; +import "google/protobuf/duration.proto"; option csharp_namespace = "Dapr.Client.Autogen.Grpc.v1"; option java_outer_classname = "CommonProtos"; @@ -24,10 +25,10 @@ option go_package = "github.com/dapr/dapr/pkg/proto/common/v1;common"; // HTTPExtension includes HTTP verb and querystring // when Dapr runtime delivers HTTP content. -// +// // For example, when callers calls http invoke api // `POST http://localhost:3500/v1.0/invoke//method/?query1=value1&query2=value2` -// +// // Dapr runtime will parse POST as a verb and extract querystring to quersytring map. message HTTPExtension { // Type of HTTP 1.1 Methods @@ -158,3 +159,26 @@ message ConfigurationItem { // the metadata which will be passed to/from configuration store component. map metadata = 3; } + +// JobFailurePolicy defines the policy to apply when a job fails to trigger. +message JobFailurePolicy { + // policy is the policy to apply when a job fails to trigger. + oneof policy { + JobFailurePolicyDrop drop = 1; + JobFailurePolicyConstant constant = 2; + } +} + +// JobFailurePolicyDrop is a policy which drops the job tick when the job fails to trigger. +message JobFailurePolicyDrop {} + +// JobFailurePolicyConstant is a policy which retries the job at a consistent interval when the job fails to trigger. +message JobFailurePolicyConstant { + // interval is the constant delay to wait before retrying the job. + google.protobuf.Duration interval = 1; + + // max_retries is the optional maximum number of retries to attempt before giving up. + // If unset, the Job will be retried indefinitely. + optional uint32 max_retries = 2; +} + 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 0ae51d6b..a0a7b124 100644 --- a/src/Dapr.Protos/Protos/dapr/proto/runtime/v1/dapr.proto +++ b/src/Dapr.Protos/Protos/dapr/proto/runtime/v1/dapr.proto @@ -694,7 +694,14 @@ message GetMetadataResponse { string runtime_version = 8 [json_name = "runtimeVersion"]; repeated string enabled_features = 9 [json_name = "enabledFeatures"]; ActorRuntime actor_runtime = 10 [json_name = "actorRuntime"]; - //TODO: Cassie: probably add scheduler runtime status + optional MetadataScheduler scheduler = 11 [json_name = "scheduler"]; +} + +// MetadataScheduler is a message that contains the list of addresses of the +// scheduler connections. +message MetadataScheduler { + // connected_addresses the list of addresses of the scheduler connections. + repeated string connected_addresses = 1; } message ActorRuntime { @@ -1258,6 +1265,12 @@ message Job { // payload is the serialized job payload that will be sent to the recipient // when the job is triggered. google.protobuf.Any data = 6 [json_name = "data"]; + + // If true, allows this job to overwrite an existing job with the same name. + bool overwrite = 7 [json_name = "overwrite"]; + + // failure_policy is the optional policy for handling job failures. + optional common.v1.JobFailurePolicy failure_policy = 8 [json_name = "failurePolicy"]; } // ScheduleJobRequest is the message to create/schedule the job. @@ -1344,4 +1357,4 @@ message ConversationResponse { // An array of results. repeated ConversationResult outputs = 2; -} \ No newline at end of file +} diff --git a/test/Dapr.Client.Test/StateApiTest.cs b/test/Dapr.Client.Test/StateApiTest.cs index 7fcc44a4..5e9b5e24 100644 --- a/test/Dapr.Client.Test/StateApiTest.cs +++ b/test/Dapr.Client.Test/StateApiTest.cs @@ -334,15 +334,15 @@ public class StateApiTest envelope.States[0].Key.ShouldBe("testKey1"); envelope.States[0].Value.ShouldBe(ByteString.CopyFromUtf8(JsonSerializer.Serialize("testValue1"))); - envelope.States[0].Metadata.ShouldContainKey("partitionKey1"); + ((IDictionary)envelope.States[0].Metadata).ShouldContainKey("partitionKey1"); envelope.States[1].Key.ShouldBe("testKey2"); envelope.States[1].Value.ShouldBe(ByteString.CopyFromUtf8(JsonSerializer.Serialize("testValue2"))); - envelope.States[1].Metadata.ShouldContainKey("partitionKey2"); + ((IDictionary)envelope.States[1].Metadata).ShouldContainKey("partitionKey2"); envelope.States[2].Key.ShouldBe("testKey3"); envelope.States[2].Value.ShouldBe(ByteString.CopyFromUtf8(JsonSerializer.Serialize("testValue3"))); - envelope.States[2].Metadata.ShouldContainKey("partitionKey3"); + ((IDictionary)envelope.States[2].Metadata).ShouldContainKey("partitionKey3"); } [Fact] @@ -1004,7 +1004,7 @@ public class StateApiTest envelope.StoreName.ShouldBe("testStore"); envelope.States.Count.ShouldBe(1); envelope.States[0].Key.ShouldBe(key); - envelope.States[0].Metadata.ShouldContainKey("partitionKey"); + ((IDictionary)envelope.States[0].Metadata).ShouldContainKey("partitionKey"); } [Fact] diff --git a/test/Dapr.Jobs.Test/CronExpressionBuilderTests.cs b/test/Dapr.Jobs.Test/CronExpressionBuilderTests.cs index 38031e0e..117e5852 100644 --- a/test/Dapr.Jobs.Test/CronExpressionBuilderTests.cs +++ b/test/Dapr.Jobs.Test/CronExpressionBuilderTests.cs @@ -12,7 +12,6 @@ // ------------------------------------------------------------------------ using System; -using Xunit; using ArgumentException = System.ArgumentException; namespace Dapr.Jobs.Test; diff --git a/test/Dapr.Jobs.Test/DaprJobsClientBuilderTests.cs b/test/Dapr.Jobs.Test/DaprJobsClientBuilderTests.cs index bdfa2d8d..1b299a58 100644 --- a/test/Dapr.Jobs.Test/DaprJobsClientBuilderTests.cs +++ b/test/Dapr.Jobs.Test/DaprJobsClientBuilderTests.cs @@ -14,7 +14,6 @@ using System; using System.Text.Json; using Grpc.Net.Client; -using Xunit; namespace Dapr.Jobs.Test; diff --git a/test/Dapr.Jobs.Test/DaprJobsGrpcClientTests.cs b/test/Dapr.Jobs.Test/DaprJobsGrpcClientTests.cs index 4f616883..0e04d130 100644 --- a/test/Dapr.Jobs.Test/DaprJobsGrpcClientTests.cs +++ b/test/Dapr.Jobs.Test/DaprJobsGrpcClientTests.cs @@ -15,7 +15,6 @@ using System; using System.Net.Http; using Dapr.Jobs.Models; using Moq; -using Xunit; namespace Dapr.Jobs.Test; diff --git a/test/Dapr.Jobs.Test/Extensions/DaprJobsServiceCollectionExtensionsTests.cs b/test/Dapr.Jobs.Test/Extensions/DaprJobsServiceCollectionExtensionsTests.cs index 814e5279..dd40287f 100644 --- a/test/Dapr.Jobs.Test/Extensions/DaprJobsServiceCollectionExtensionsTests.cs +++ b/test/Dapr.Jobs.Test/Extensions/DaprJobsServiceCollectionExtensionsTests.cs @@ -11,7 +11,6 @@ // limitations under the License. // ------------------------------------------------------------------------ -using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; diff --git a/test/Dapr.Jobs.Test/Extensions/EndpointRouteBuilderExtensionsTests.cs b/test/Dapr.Jobs.Test/Extensions/EndpointRouteBuilderExtensionsTests.cs index 4220db7b..77b60fe0 100644 --- a/test/Dapr.Jobs.Test/Extensions/EndpointRouteBuilderExtensionsTests.cs +++ b/test/Dapr.Jobs.Test/Extensions/EndpointRouteBuilderExtensionsTests.cs @@ -14,26 +14,17 @@ #nullable enable using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Net.Http; using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Dapr.Jobs.Extensions; -using Dapr.Jobs.Models; -using Dapr.Jobs.Models.Responses; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Moq; -using Xunit; namespace Dapr.Jobs.Test.Extensions; diff --git a/test/Dapr.Jobs.Test/Extensions/StringExtensionsTests.cs b/test/Dapr.Jobs.Test/Extensions/StringExtensionsTests.cs index 8c25de11..2e27025c 100644 --- a/test/Dapr.Jobs.Test/Extensions/StringExtensionsTests.cs +++ b/test/Dapr.Jobs.Test/Extensions/StringExtensionsTests.cs @@ -14,7 +14,6 @@ using System; using System.Collections.Generic; using Dapr.Jobs.Extensions; -using Xunit; namespace Dapr.Jobs.Test.Extensions; diff --git a/test/Dapr.Jobs.Test/Extensions/TimeSpanExtensionsTests.cs b/test/Dapr.Jobs.Test/Extensions/TimeSpanExtensionsTests.cs index 1e888a84..c8aff977 100644 --- a/test/Dapr.Jobs.Test/Extensions/TimeSpanExtensionsTests.cs +++ b/test/Dapr.Jobs.Test/Extensions/TimeSpanExtensionsTests.cs @@ -12,7 +12,6 @@ // ------------------------------------------------------------------------ using System; -using Xunit; namespace Dapr.Jobs.Test.Extensions; diff --git a/test/Dapr.Jobs.Test/Models/DaprJobScheduleTests.cs b/test/Dapr.Jobs.Test/Models/DaprJobScheduleTests.cs index da0afb43..32f1eef7 100644 --- a/test/Dapr.Jobs.Test/Models/DaprJobScheduleTests.cs +++ b/test/Dapr.Jobs.Test/Models/DaprJobScheduleTests.cs @@ -13,7 +13,6 @@ using System; using Dapr.Jobs.Models; -using Xunit; namespace Dapr.Jobs.Test.Models; From 5e8e88de77b7b83fb6f2847efbf769b8fea9bc7c Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sat, 21 Jun 2025 04:42:41 -0500 Subject: [PATCH 17/22] Replace [Obsolete] with [Experimental] attributes (#1564) * Replaced Obsolete with Experimental attributes throughout solution, added documentation and updated unit tests --- ...net-development-experimental-attributes.md | 138 ++++++++++++++++++ .../Controllers/BindingController.cs | 82 +++++------ examples/Directory.Build.props | 1 + .../Conversation/DaprConversationClient.cs | 2 + .../DaprConversationClientBuilder.cs | 3 + .../DaprConversationGrpcClient.cs | 2 + .../DaprAiConversationBuilderExtensions.cs | 2 + src/Dapr.Client/DaprClient.cs | 48 +++--- src/Dapr.Client/DaprClientGrpc.cs | 46 +++--- src/Dapr.Client/TryLockResponse.cs | 3 +- .../Encryption/DaprEncryptionClient.cs | 7 +- .../Encryption/DaprEncryptionClientBuilder.cs | 2 + .../Encryption/DaprEncryptionGrpcClient.cs | 8 +- ...CryptographyServiceCollectionExtensions.cs | 2 + .../Encryption/IDaprEncryptionClient.cs | 7 +- src/Dapr.Jobs/DaprJobsClient.cs | 6 +- src/Dapr.Jobs/DaprJobsClientBuilder.cs | 2 + src/Dapr.Jobs/DaprJobsGrpcClient.cs | 5 +- .../DaprJobsServiceCollectionExtensions.cs | 2 + .../Extensions/DaprSerializationExtensions.cs | 4 +- .../MapDaprScheduledJobHandlerTest.cs | 22 ++- test/Directory.Build.props | 1 + 22 files changed, 269 insertions(+), 126 deletions(-) create mode 100644 daprdocs/content/en/dotnet-sdk-docs/dotnet-development/dotnet-development-experimental-attributes.md diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-development/dotnet-development-experimental-attributes.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-development/dotnet-development-experimental-attributes.md new file mode 100644 index 00000000..12825be2 --- /dev/null +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-development/dotnet-development-experimental-attributes.md @@ -0,0 +1,138 @@ +--- +type: docs +title: "Dapr .NET SDK Development with Dapr CLI" +linkTitle: "Experimental Attributes" +weight: 61000 +description: Learn about local development with the Dapr CLI +--- + +## Experimental Attributes + +### Introduction to Experimental Attributes + +With the release of .NET 8, C# 12 introduced the `[Experimental]` attribute, which provides a standardized way to mark +APIs that are still in development or experimental. This attribute is defined in the `System.Diagnostics.CodeAnalysis` +namespace and requires a diagnostic ID parameter used to generate compiler warnings when the experimental API +is used. + +In the Dapr .NET SDK, we now use the `[Experimental]` attribute instead of `[Obsolete]` to mark building blocks and +components that have not yet passed the stable lifecycle certification. This approach provides a clearer distinction +between: + +1. **Experimental APIs** - Features that are available but still evolving and have not yet been certified as stable +according to the [Dapr Component Certification Lifecycle](https://docs.dapr.io/operations/components/certification-lifecycle/). + +2. **Obsolete APIs** - Features that are truly deprecated and will be removed in a future release. + +### Usage in the Dapr .NET SDK + +In the Dapr .NET SDK, we apply the `[Experimental]` attribute at the class level for building blocks that are still in +the Alpha or Beta stages of the [Component Certification Lifecycle](https://docs.dapr.io/operations/components/certification-lifecycle/). +The attribute includes: + +- A diagnostic ID that identifies the experimental building block +- A URL that points to the relevant documentation for that block + +For example: + +```csharp +using System.Diagnostics.CodeAnalysis; +namespace Dapr.Cryptography.Encryption +{ + [Experimental("DAPR_CRYPTOGRAPHY", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/cryptography/cryptography-overview/")] + public class DaprEncryptionClient + { + // Implementation + } +} +``` + +The diagnostic IDs follow a naming convention of `DAPR_[BUILDING_BLOCK_NAME]`, such as: + +- `DAPR_CONVERSATION` - For the Conversation building block +- `DAPR_CRYPTOGRAPHY` - For the Cryptography building block +- `DAPR_JOBS` - For the Jobs building block +- `DAPR_DISTRIBUTEDLOCK` - For the Distributed Lock building block + +### Suppressing Experimental Warnings + +When you use APIs marked with the `[Experimental]` attribute, the compiler will generate errors. +To build your solution without marking your own code as experimental, you will need to suppress these errors. Here are +several approaches to do this: + +#### Option 1: Using #pragma directive + +You can use the `#pragma warning` directive to suppress the warning for specific sections of code: + +```csharp +// Disable experimental warning +#pragma warning disable DAPR_CRYPTOGRAPHY +// Your code using the experimental API +var client = new DaprEncryptionClient(); +// Re-enable the warning +#pragma warning restore DAPR_CRYPTOGRAPHY +``` + +This approach is useful when you want to suppress warnings only for specific sections of your code. + +#### Option 2: Project-level suppression + +To suppress warnings for an entire project, add the following to your `.csproj` file. +file. + +```xml + + $(NoWarn);DAPR_CRYPTOGRAPHY + +``` + +You can include multiple diagnostic IDs separated by semicolons: + +```xml + + $(NoWarn);DAPR_CONVERSATION;DAPR_JOBS;DAPR_DISTRIBUTEDLOCK;DAPR_CRYPTOGRAPHY + +``` + +This approach is particularly useful for test projects that need to use experimental APIs. + +#### Option 3: Directory-level suppression + +For suppressing warnings across multiple projects in a directory, add a `Directory.Build.props` file: + +```xml + + $(NoWarn);DAPR_CONVERSATION;DAPR_JOBS;DAPR_DISTRIBUTEDLOCK;DAPR_CRYPTOGRAPHY + +``` + +This file should be placed in the root directory of your test projects. You can learn more about using +`Directory.Build.props` files in the +[MSBuild documentation](https://learn.microsoft.com/en-us/visualstudio/msbuild/customize-by-directory). + +### Lifecycle of Experimental APIs + +As building blocks move through the certification lifecycle and reach the "Stable" stage, the `[Experimental]` attribute will be removed. No migration or code changes will be required from users when this happens, except for the removal of any warning suppressions if they were added. + +Conversely, the `[Obsolete]` attribute will now be reserved exclusively for APIs that are truly deprecated and scheduled for removal. When you see a method or class marked with `[Obsolete]`, you should plan to migrate away from it according to the migration guidance provided in the attribute message. + +### Best Practices + +1. **In application code:** + - Be cautious when using experimental APIs, as they may change in future releases + - Consider isolating usage of experimental APIs to make future updates easier + - Document your use of experimental APIs for team awareness + +2. **In test code:** + - Use project-level suppression to avoid cluttering test code with warning suppressions + - Regularly review which experimental APIs you're using and check if they've been stabilized + +3. **When contributing to the SDK:** + - Use `[Experimental]` for new building blocks that haven't completed certification + - Use `[Obsolete]` only for truly deprecated APIs + - Provide clear documentation links in the `UrlFormat` parameter + +### Additional Resources + +- [Dapr Component Certification Lifecycle](https://docs.dapr.io/operations/components/certification-lifecycle/) +- [C# Experimental Attribute Documentation](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-12.0/experimental-attribute) diff --git a/examples/Client/DistributedLock/Controllers/BindingController.cs b/examples/Client/DistributedLock/Controllers/BindingController.cs index 37fbe637..64228df4 100644 --- a/examples/Client/DistributedLock/Controllers/BindingController.cs +++ b/examples/Client/DistributedLock/Controllers/BindingController.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text.Json; using System.Threading.Tasks; @@ -11,24 +12,15 @@ using Microsoft.Extensions.Logging; namespace DistributedLock.Controllers; [ApiController] -public class BindingController : ControllerBase +[Experimental("DAPR_DISTRIBUTEDLOCK", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/distributed-lock/distributed-lock-api-overview/")] +public class BindingController(DaprClient client, ILogger logger) : ControllerBase { - private DaprClient client; - private ILogger logger; - private string appId; - - public BindingController(DaprClient client, ILogger logger) - { - this.client = client; - this.logger = logger; - this.appId = Environment.GetEnvironmentVariable("APP_ID"); - } - + private string appId = Environment.GetEnvironmentVariable("APP_ID"); + [HttpPost("cronbinding")] - [Obsolete] public async Task HandleBindingEvent() { - logger.LogInformation($"Received binding event on {appId}, scanning for work."); + logger.LogInformation("Received binding event on {appId}, scanning for work.", appId); var request = new BindingRequest("localstorage", "list"); var result = client.InvokeBindingAsync(request); @@ -47,44 +39,40 @@ public class BindingController : ControllerBase return Ok(); } - - [Obsolete] private async Task AttemptToProcessFile(string fileName) { // Locks are Disposable and will automatically unlock at the end of a 'using' statement. - logger.LogInformation($"Attempting to lock: {fileName}"); - await using (var fileLock = await client.Lock("redislock", fileName, appId, 60)) + logger.LogInformation("Attempting to lock: {fileName}", fileName); + await using var fileLock = await client.Lock("redislock", fileName, appId, 60); + if (fileLock.Success) { - if (fileLock.Success) + logger.LogInformation("Successfully locked file: {fileName}", fileName); + + // Get the file after we've locked it, we're safe here because of the lock. + var fileState = await GetFile(fileName); + + if (fileState == null) { - logger.LogInformation($"Successfully locked file: {fileName}"); - - // Get the file after we've locked it, we're safe here because of the lock. - var fileState = await GetFile(fileName); - - if (fileState == null) - { - logger.LogWarning($"File {fileName} has already been processed!"); - return; - } - - // "Analyze" the file before committing it to our remote storage. - fileState.Analysis = fileState.Number > 50 ? "High" : "Low"; - - // Save it to remote storage. - await client.SaveStateAsync("redisstore", fileName, fileState); - - // Remove it from local storage. - var bindingDeleteRequest = new BindingRequest("localstorage", "delete"); - bindingDeleteRequest.Metadata["fileName"] = fileName; - await client.InvokeBindingAsync(bindingDeleteRequest); - - logger.LogInformation($"Done processing {fileName}"); - } - else - { - logger.LogWarning($"Failed to lock {fileName}."); + logger.LogWarning("File {fileName} has already been processed!", fileName); + return; } + + // "Analyze" the file before committing it to our remote storage. + fileState.Analysis = fileState.Number > 50 ? "High" : "Low"; + + // Save it to remote storage. + await client.SaveStateAsync("redisstore", fileName, fileState); + + // Remove it from local storage. + var bindingDeleteRequest = new BindingRequest("localstorage", "delete"); + bindingDeleteRequest.Metadata["fileName"] = fileName; + await client.InvokeBindingAsync(bindingDeleteRequest); + + logger.LogInformation("Done processing {fileName}", fileName); + } + else + { + logger.LogWarning("Failed to lock {fileName}.", fileName); } } @@ -103,4 +91,4 @@ public class BindingController : ControllerBase return null; } } -} \ No newline at end of file +} diff --git a/examples/Directory.Build.props b/examples/Directory.Build.props index 95483b7a..0c8af6ea 100644 --- a/examples/Directory.Build.props +++ b/examples/Directory.Build.props @@ -7,5 +7,6 @@ $(RepoRoot)bin\$(Configuration)\examples\$(MSBuildProjectName)\ false + $(NoWarn);DAPR_CONVERSATION;DAPR_JOBS;DAPR_DISTRIBUTEDLOCK;DAPR_CRYPTOGRAPHY \ No newline at end of file diff --git a/src/Dapr.AI/Conversation/DaprConversationClient.cs b/src/Dapr.AI/Conversation/DaprConversationClient.cs index b081427b..a3bd58f8 100644 --- a/src/Dapr.AI/Conversation/DaprConversationClient.cs +++ b/src/Dapr.AI/Conversation/DaprConversationClient.cs @@ -11,6 +11,7 @@ // limitations under the License. // ------------------------------------------------------------------------ +using System.Diagnostics.CodeAnalysis; using Autogenerated = Dapr.Client.Autogen.Grpc.v1.Dapr; namespace Dapr.AI.Conversation; @@ -30,6 +31,7 @@ namespace Dapr.AI.Conversation; /// exhaustion and other problems. /// /// +[Experimental("DAPR_CONVERSATION", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/conversation/conversation-overview/")] public abstract class DaprConversationClient : DaprAIClient { /// diff --git a/src/Dapr.AI/Conversation/DaprConversationClientBuilder.cs b/src/Dapr.AI/Conversation/DaprConversationClientBuilder.cs index ae897fcd..a8a7983f 100644 --- a/src/Dapr.AI/Conversation/DaprConversationClientBuilder.cs +++ b/src/Dapr.AI/Conversation/DaprConversationClientBuilder.cs @@ -11,6 +11,7 @@ // limitations under the License. // ------------------------------------------------------------------------ +using System.Diagnostics.CodeAnalysis; using Dapr.Common; using Microsoft.Extensions.Configuration; using Autogenerated = Dapr.Client.Autogen.Grpc.v1.Dapr; @@ -21,6 +22,7 @@ namespace Dapr.AI.Conversation; /// Used to create a new instance of a . /// /// An optional to configure the client with. +[Experimental("DAPR_CONVERSATION", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/conversation/conversation-overview/")] public sealed class DaprConversationClientBuilder(IConfiguration? configuration = null) : DaprGenericClientBuilder(configuration) { /// @@ -30,6 +32,7 @@ public sealed class DaprConversationClientBuilder(IConfiguration? configuration /// /// Builds the client instance from the properties of the builder. /// + [Experimental("DAPR_CONVERSATION", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/conversation/conversation-overview/")] public override DaprConversationClient Build() { var daprClientDependencies = BuildDaprClientDependencies(typeof(DaprConversationClient).Assembly); diff --git a/src/Dapr.AI/Conversation/DaprConversationGrpcClient.cs b/src/Dapr.AI/Conversation/DaprConversationGrpcClient.cs index 6a1a5f43..a95dfd71 100644 --- a/src/Dapr.AI/Conversation/DaprConversationGrpcClient.cs +++ b/src/Dapr.AI/Conversation/DaprConversationGrpcClient.cs @@ -11,6 +11,7 @@ // limitations under the License. // ------------------------------------------------------------------------ +using System.Diagnostics.CodeAnalysis; using Dapr.Common; using Dapr.Common.Extensions; using Autogenerated = Dapr.Client.Autogen.Grpc.v1; @@ -23,6 +24,7 @@ namespace Dapr.AI.Conversation; /// 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. +[Experimental("DAPR_CONVERSATION", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/conversation/conversation-overview/")] internal sealed class DaprConversationGrpcClient(Autogenerated.Dapr.DaprClient client, HttpClient httpClient, string? daprApiToken = null) : DaprConversationClient(client, httpClient, daprApiToken: daprApiToken) { /// diff --git a/src/Dapr.AI/Conversation/Extensions/DaprAiConversationBuilderExtensions.cs b/src/Dapr.AI/Conversation/Extensions/DaprAiConversationBuilderExtensions.cs index 42bd9180..ba7c7b91 100644 --- a/src/Dapr.AI/Conversation/Extensions/DaprAiConversationBuilderExtensions.cs +++ b/src/Dapr.AI/Conversation/Extensions/DaprAiConversationBuilderExtensions.cs @@ -11,6 +11,7 @@ // limitations under the License. // ------------------------------------------------------------------------ +using System.Diagnostics.CodeAnalysis; using Dapr.Common.Extensions; using Microsoft.Extensions.DependencyInjection; @@ -24,6 +25,7 @@ public static class DaprAiConversationBuilderExtensions /// /// Registers the necessary functionality for the Dapr AI Conversation functionality. /// + [Experimental("DAPR_CONVERSATION", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/conversation/conversation-overview/")] public static IDaprAiConversationBuilder AddDaprConversationClient( this IServiceCollection services, Action? configure = null, diff --git a/src/Dapr.Client/DaprClient.cs b/src/Dapr.Client/DaprClient.cs index 96fe0cfd..3e614324 100644 --- a/src/Dapr.Client/DaprClient.cs +++ b/src/Dapr.Client/DaprClient.cs @@ -14,6 +14,7 @@ #nullable enable using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Net.Http; @@ -1089,7 +1090,7 @@ public abstract class DaprClient : IDisposable /// Options informing how the encryption operation should be configured. /// A that can be used to cancel the operation. /// An array of encrypted bytes. - [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + [Experimental("DAPR_CRYPTOGRAPHY", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/cryptography/cryptography-overview/")] public abstract Task> EncryptAsync(string vaultResourceName, ReadOnlyMemory plaintextBytes, string keyName, EncryptionOptions encryptionOptions, CancellationToken cancellationToken = default); @@ -1103,7 +1104,7 @@ public abstract class DaprClient : IDisposable /// Options informing how the encryption operation should be configured. /// A that can be used to cancel the operation. /// An array of encrypted bytes. - [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + [Experimental("DAPR_CRYPTOGRAPHY", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/cryptography/cryptography-overview/")] public abstract Task>> EncryptAsync(string vaultResourceName, Stream plaintextStream, string keyName, EncryptionOptions encryptionOptions, CancellationToken cancellationToken = default); @@ -1116,7 +1117,7 @@ public abstract class DaprClient : IDisposable /// Options informing how the decryption operation should be configured. /// A that can be used to cancel the operation. /// An array of decrypted bytes. - [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + [Experimental("DAPR_CRYPTOGRAPHY", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/cryptography/cryptography-overview/")] public abstract Task> DecryptAsync(string vaultResourceName, ReadOnlyMemory ciphertextBytes, string keyName, DecryptionOptions options, CancellationToken cancellationToken = default); @@ -1128,7 +1129,7 @@ public abstract class DaprClient : IDisposable /// The name of the key to use from the Vault for the decryption operation. /// A that can be used to cancel the operation. /// An array of decrypted bytes. - [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + [Experimental("DAPR_CRYPTOGRAPHY", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/cryptography/cryptography-overview/")] public abstract Task> DecryptAsync(string vaultResourceName, ReadOnlyMemory ciphertextBytes, string keyName, CancellationToken cancellationToken = default); @@ -1141,8 +1142,7 @@ public abstract class DaprClient : IDisposable /// Options informing how the decryption operation should be configured. /// A that can be used to cancel the operation. /// An asynchronously enumerable array of decrypted bytes. - [Obsolete( - "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + [Experimental("DAPR_CRYPTOGRAPHY", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/cryptography/cryptography-overview/")] public abstract Task>> DecryptAsync(string vaultResourceName, Stream ciphertextStream, string keyName, DecryptionOptions options, CancellationToken cancellationToken = default); @@ -1154,8 +1154,7 @@ public abstract class DaprClient : IDisposable /// The name of the key to use from the Vault for the decryption operation. /// A that can be used to cancel the operation. /// An asynchronously enumerable array of decrypted bytes. - [Obsolete( - "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + [Experimental("DAPR_CRYPTOGRAPHY", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/cryptography/cryptography-overview/")] public abstract Task>> DecryptAsync(string vaultResourceName, Stream ciphertextStream, string keyName, CancellationToken cancellationToken = default); @@ -1171,7 +1170,7 @@ public abstract class DaprClient : IDisposable ///// The format to use for the key result. ///// A that can be used to cancel the operation. ///// The name (and possibly version as name/version) of the key and its public key. - //[Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //[Experimental("DAPR_CRYPTOGRAPHY", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/cryptography/cryptography-overview/")] //public abstract Task<(string Name, string PublicKey)> GetKeyAsync(string vaultResourceName, string keyName, SubtleGetKeyRequest.Types.KeyFormat keyFormat, // CancellationToken cancellationToken = default); @@ -1186,7 +1185,7 @@ public abstract class DaprClient : IDisposable ///// Any associated data when using AEAD ciphers. ///// A that can be used to cancel the operation. ///// The array of encrypted bytes. - //[Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //[Experimental("DAPR_CRYPTOGRAPHY", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/cryptography/cryptography-overview/")] //public abstract Task<(byte[] CipherTextBytes, byte[] AuthenticationTag)> EncryptAsync( // string vaultResourceName, // byte[] plainTextBytes, @@ -1206,7 +1205,7 @@ public abstract class DaprClient : IDisposable ///// The bytes comprising the nonce. ///// A that can be used to cancel the operation. ///// The array of encrypted bytes. - //[Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //[Experimental("DAPR_CRYPTOGRAPHY", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/cryptography/cryptography-overview/")] //public async Task<(byte[] CipherTextBytes, byte[] AuthenticationTag)> EncryptAsync( // string vaultResourceName, // byte[] plainTextBytes, @@ -1229,7 +1228,7 @@ public abstract class DaprClient : IDisposable ///// ///// Any associated data when using AEAD ciphers. ///// The array of plaintext bytes. - //[Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //[Experimental("DAPR_CRYPTOGRAPHY", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/cryptography/cryptography-overview/")] //public abstract Task DecryptAsync(string vaultResourceName, byte[] cipherTextBytes, // string algorithm, string keyName, byte[] nonce, byte[] tag, byte[] associatedData, // CancellationToken cancellationToken = default); @@ -1245,8 +1244,7 @@ public abstract class DaprClient : IDisposable ///// The nonce value used. ///// ///// The array of plaintext bytes. - //[Obsolete( - // "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //[Experimental("DAPR_CRYPTOGRAPHY", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/cryptography/cryptography-overview/")] //public async Task DecryptAsync(string vaultResourceName, byte[] cipherTextBytes, // string algorithm, string keyName, byte[] nonce, byte[] tag, CancellationToken cancellationToken = default) => // await DecryptAsync(vaultResourceName, cipherTextBytes, algorithm, keyName, nonce, tag, Array.Empty(), cancellationToken); @@ -1262,7 +1260,7 @@ public abstract class DaprClient : IDisposable ///// Any associated data when using AEAD ciphers. ///// A that can be used to cancel the operation. ///// The bytes comprising the wrapped plain-text key and the authentication tag, if applicable. - //[Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //[Experimental("DAPR_CRYPTOGRAPHY", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/cryptography/cryptography-overview/")] //public abstract Task<(byte[] WrappedKey, byte[] AuthenticationTag)> WrapKeyAsync(string vaultResourceName, byte[] plainTextKey, string keyName, string algorithm, byte[] nonce, byte[] associatedData, // CancellationToken cancellationToken = default); @@ -1276,7 +1274,7 @@ public abstract class DaprClient : IDisposable ///// The none used. ///// A that can be used to cancel the operation. ///// The bytes comprising the unwrapped key and the authentication tag, if applicable. - //[Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //[Experimental("DAPR_CRYPTOGRAPHY", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/cryptography/cryptography-overview/")] //public async Task<(byte[] WrappedKey, byte[] AuthenticationTag)> WrapKeyAsync(string vaultResourceName, byte[] plainTextKey, string keyName, string algorithm, // byte[] nonce, CancellationToken cancellationToken = default) => await WrapKeyAsync(vaultResourceName, plainTextKey, // keyName, algorithm, nonce, Array.Empty(), cancellationToken); @@ -1293,7 +1291,7 @@ public abstract class DaprClient : IDisposable ///// Any associated data when using AEAD ciphers. ///// A that can be used to cancel the operation. ///// The bytes comprising the unwrapped key. - //[Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //[Experimental("DAPR_CRYPTOGRAPHY", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/cryptography/cryptography-overview/")] //public abstract Task UnwrapKeyAsync(string vaultResourceName, byte[] wrappedKey, string algorithm, string keyName, byte[] nonce, byte[] tag, byte[] associatedData, // CancellationToken cancellationToken = default); @@ -1308,7 +1306,7 @@ public abstract class DaprClient : IDisposable ///// The bytes comprising the authentication tag. ///// A that can be used to cancel the operation. ///// The bytes comprising the unwrapped key. - //[Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //[Experimental("DAPR_CRYPTOGRAPHY", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/cryptography/cryptography-overview/")] //public async Task UnwrapKeyAsync(string vaultResourceName, byte[] wrappedKey, string algorithm, string keyName, // byte[] nonce, byte[] tag, // CancellationToken cancellationToken = default) => await UnwrapKeyAsync(vaultResourceName, @@ -1324,7 +1322,7 @@ public abstract class DaprClient : IDisposable ///// The nonce value. ///// A that can be used to cancel the operation. ///// The bytes comprising the unwrapped key. - //[Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //[Experimental("DAPR_CRYPTOGRAPHY", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/cryptography/cryptography-overview/")] //public async Task UnwrapKeyAsync(string vaultResourceName, byte[] wrappedKey, string algorithm, string keyName, // byte[] nonce, CancellationToken cancellationToken = default) => await UnwrapKeyAsync(vaultResourceName, // wrappedKey, algorithm, keyName, nonce, Array.Empty(), Array.Empty(), cancellationToken); @@ -1338,7 +1336,7 @@ public abstract class DaprClient : IDisposable ///// The name of the key used. ///// A that can be used to cancel the operation. ///// The bytes comprising the signature. - //[Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //[Experimental("DAPR_CRYPTOGRAPHY", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/cryptography/cryptography-overview/")] //public abstract Task SignAsync(string vaultResourceName, byte[] digest, string algorithm, string keyName, // CancellationToken cancellationToken = default); @@ -1352,12 +1350,14 @@ public abstract class DaprClient : IDisposable ///// The name of the key used. ///// A that can be used to cancel the operation. ///// True if the signature verification is successful; otherwise false. - //[Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //[Experimental("DAPR_CRYPTOGRAPHY", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/cryptography/cryptography-overview/")] //public abstract Task VerifyAsync(string vaultResourceName, byte[] digest, byte[] signature, string algorithm, string keyName, // CancellationToken cancellationToken = default); #endregion + #region Distributed Lock + /// /// Attempt to lock the given resourceId with response indicating success. /// @@ -1367,7 +1367,7 @@ public abstract class DaprClient : IDisposable /// The time after which the lock gets expired. /// A that can be used to cancel the operation. /// A containing a - [Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + [Experimental("DAPR_DISTRIBUTEDLOCK", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/distributed-lock/distributed-lock-api-overview/")] public abstract Task Lock( string storeName, string resourceId, @@ -1384,12 +1384,14 @@ public abstract class DaprClient : IDisposable /// Indicates the identifier of lock owner. /// A that can be used to cancel the operation. /// A containing a - [Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + [Experimental("DAPR_DISTRIBUTEDLOCK", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/distributed-lock/distributed-lock-api-overview/")] public abstract Task Unlock( string storeName, string resourceId, string lockOwner, CancellationToken cancellationToken = default); + + #endregion /// public void Dispose() diff --git a/src/Dapr.Client/DaprClientGrpc.cs b/src/Dapr.Client/DaprClientGrpc.cs index 70fe750a..50f048ad 100644 --- a/src/Dapr.Client/DaprClientGrpc.cs +++ b/src/Dapr.Client/DaprClientGrpc.cs @@ -11,6 +11,8 @@ // limitations under the License. // ------------------------------------------------------------------------ +using System.Diagnostics.CodeAnalysis; + namespace Dapr.Client; using System; @@ -1666,8 +1668,7 @@ internal class DaprClientGrpc : DaprClient #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.")] + [Experimental("DAPR_CRYPTOGRAPHY", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/cryptography/cryptography-overview/")] public override async Task> EncryptAsync(string vaultResourceName, ReadOnlyMemory plaintextBytes, string keyName, EncryptionOptions encryptionOptions, CancellationToken cancellationToken = default) @@ -1687,8 +1688,7 @@ internal class DaprClientGrpc : DaprClient } /// - [Obsolete( - "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + [Experimental("DAPR_CRYPTOGRAPHY", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/cryptography/cryptography-overview/")] public override async Task>> EncryptAsync(string vaultResourceName, Stream plaintextStream, string keyName, EncryptionOptions encryptionOptions, CancellationToken cancellationToken = default) @@ -1734,7 +1734,7 @@ internal class DaprClientGrpc : DaprClient /// /// Sends the plaintext bytes in chunks to the sidecar to be encrypted. /// - private async Task SendPlaintextStreamAsync(Stream plaintextStream, + private static async Task SendPlaintextStreamAsync(Stream plaintextStream, int streamingBlockSizeInBytes, AsyncDuplexStreamingCall duplexStream, Autogenerated.EncryptRequestOptions encryptRequestOptions, @@ -1777,7 +1777,7 @@ internal class DaprClientGrpc : DaprClient /// /// Retrieves the encrypted bytes from the encryption operation on the sidecar and returns as an enumerable stream. /// - private async IAsyncEnumerable> RetrieveEncryptedStreamAsync( + private static async IAsyncEnumerable> RetrieveEncryptedStreamAsync( AsyncDuplexStreamingCall duplexStream, [EnumeratorCancellation] CancellationToken cancellationToken) { @@ -1789,8 +1789,7 @@ internal class DaprClientGrpc : DaprClient } /// - [Obsolete( - "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + [Experimental("DAPR_CRYPTOGRAPHY", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/cryptography/cryptography-overview/")] public override async Task>> DecryptAsync(string vaultResourceName, Stream ciphertextStream, string keyName, DecryptionOptions decryptionOptions, CancellationToken cancellationToken = default) @@ -1821,8 +1820,7 @@ internal class DaprClientGrpc : DaprClient } /// - [Obsolete( - "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + [Experimental("DAPR_CRYPTOGRAPHY", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/cryptography/cryptography-overview/")] public override Task>> DecryptAsync(string vaultResourceName, Stream ciphertextStream, string keyName, CancellationToken cancellationToken = default) => DecryptAsync(vaultResourceName, ciphertextStream, keyName, new DecryptionOptions(), @@ -1831,7 +1829,7 @@ internal class DaprClientGrpc : DaprClient /// /// Sends the ciphertext bytes in chunks to the sidecar to be decrypted. /// - private async Task SendCiphertextStreamAsync(Stream ciphertextStream, + private static async Task SendCiphertextStreamAsync(Stream ciphertextStream, int streamingBlockSizeInBytes, AsyncDuplexStreamingCall duplexStream, Autogenerated.DecryptRequestOptions decryptRequestOptions, @@ -1873,7 +1871,7 @@ internal class DaprClientGrpc : DaprClient /// /// Retrieves the decrypted bytes from the decryption operation on the sidecar and returns as an enumerable stream. /// - private async IAsyncEnumerable> RetrieveDecryptedStreamAsync( + private static async IAsyncEnumerable> RetrieveDecryptedStreamAsync( AsyncDuplexStreamingCall duplexStream, [EnumeratorCancellation] CancellationToken cancellationToken) { @@ -1885,8 +1883,7 @@ internal class DaprClientGrpc : DaprClient } /// - [Obsolete( - "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + [Experimental("DAPR_CRYPTOGRAPHY", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/cryptography/cryptography-overview/")] public override async Task> DecryptAsync(string vaultResourceName, ReadOnlyMemory ciphertextBytes, string keyName, DecryptionOptions decryptionOptions, CancellationToken cancellationToken = default) @@ -1906,8 +1903,7 @@ internal class DaprClientGrpc : DaprClient } /// - [Obsolete( - "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + [Experimental("DAPR_CRYPTOGRAPHY", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/cryptography/cryptography-overview/")] public override async Task> DecryptAsync(string vaultResourceName, ReadOnlyMemory ciphertextBytes, string keyName, CancellationToken cancellationToken = default) => await DecryptAsync(vaultResourceName, ciphertextBytes, keyName, @@ -1916,7 +1912,7 @@ internal class DaprClientGrpc : DaprClient #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.")] + //[Experimental("DAPR_CRYPTOGRAPHY", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/cryptography/cryptography-overview/")] //public override async Task<(string Name, string PublicKey)> GetKeyAsync(string vaultResourceName, string keyName, Autogenerated.SubtleGetKeyRequest.Types.KeyFormat keyFormat, // CancellationToken cancellationToken = default) //{ @@ -1945,7 +1941,7 @@ internal class DaprClientGrpc : DaprClient //} ///// - //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //[Experimental("DAPR_CRYPTOGRAPHY", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/cryptography/cryptography-overview/")] //public override async Task<(byte[] CipherTextBytes, byte[] AuthenticationTag)> EncryptAsync(string vaultResourceName, byte[] plainTextBytes, string algorithm, // string keyName, byte[] nonce, byte[] associatedData, CancellationToken cancellationToken = default) //{ @@ -1981,7 +1977,7 @@ internal class DaprClientGrpc : DaprClient //} ///// - //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //[Experimental("DAPR_CRYPTOGRAPHY", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/cryptography/cryptography-overview/")] //public override async Task DecryptAsync(string vaultResourceName, byte[] cipherTextBytes, string algorithm, string keyName, byte[] nonce, byte[] tag, // byte[] associatedData, CancellationToken cancellationToken = default) //{ @@ -2017,7 +2013,7 @@ internal class DaprClientGrpc : DaprClient //} ///// - //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //[Experimental("DAPR_CRYPTOGRAPHY", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/cryptography/cryptography-overview/")] //public override async Task<(byte[] WrappedKey, byte[] AuthenticationTag)> WrapKeyAsync(string vaultResourceName, byte[] plainTextKey, string keyName, // string algorithm, byte[] nonce, byte[] associatedData, CancellationToken cancellationToken = default) //{ @@ -2053,7 +2049,7 @@ internal class DaprClientGrpc : DaprClient //} ///// - //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //[Experimental("DAPR_CRYPTOGRAPHY", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/cryptography/cryptography-overview/")] //public override async Task UnwrapKeyAsync(string vaultResourceName, byte[] wrappedKey, string algorithm, // string keyName, byte[] nonce, byte[] tag, byte[] associatedData, CancellationToken cancellationToken = default) //{ @@ -2090,7 +2086,7 @@ internal class DaprClientGrpc : DaprClient //} ///// - //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //[Experimental("DAPR_CRYPTOGRAPHY", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/cryptography/cryptography-overview/")] //public override async Task SignAsync(string vaultResourceName, byte[] digest, string algorithm, string keyName, CancellationToken cancellationToken = default) //{ // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); @@ -2123,7 +2119,7 @@ internal class DaprClientGrpc : DaprClient //} ///// - //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //[Experimental("DAPR_CRYPTOGRAPHY", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/cryptography/cryptography-overview/")] //public override async Task VerifyAsync(string vaultResourceName, byte[] digest, byte[] signature, // string algorithm, string keyName, CancellationToken cancellationToken = default) //{ @@ -2165,7 +2161,7 @@ internal class DaprClientGrpc : DaprClient #region Distributed Lock API /// - [Obsolete] + [Experimental("DAPR_DISTRIBUTEDLOCK", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/distributed-lock/distributed-lock-api-overview/")] public async override Task Lock( string storeName, string resourceId, @@ -2205,7 +2201,7 @@ internal class DaprClientGrpc : DaprClient } /// - [Obsolete] + [Experimental("DAPR_DISTRIBUTEDLOCK", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/distributed-lock/distributed-lock-api-overview/")] public async override Task Unlock( string storeName, string resourceId, diff --git a/src/Dapr.Client/TryLockResponse.cs b/src/Dapr.Client/TryLockResponse.cs index 086eafc1..e341f91e 100644 --- a/src/Dapr.Client/TryLockResponse.cs +++ b/src/Dapr.Client/TryLockResponse.cs @@ -12,6 +12,7 @@ // ------------------------------------------------------------------------ using System; +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; namespace Dapr.Client; @@ -19,7 +20,7 @@ namespace Dapr.Client; /// /// Class representing the response from a Lock API call. /// -[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] +[Experimental("DAPR_DISTRIBUTEDLOCK", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/distributed-lock/distributed-lock-api-overview/")] public sealed class TryLockResponse : IAsyncDisposable { /// diff --git a/src/Dapr.Cryptography/Encryption/DaprEncryptionClient.cs b/src/Dapr.Cryptography/Encryption/DaprEncryptionClient.cs index c99ed571..a62ef39e 100644 --- a/src/Dapr.Cryptography/Encryption/DaprEncryptionClient.cs +++ b/src/Dapr.Cryptography/Encryption/DaprEncryptionClient.cs @@ -11,6 +11,7 @@ // limitations under the License. // ------------------------------------------------------------------------ +using System.Diagnostics.CodeAnalysis; using Dapr.Cryptography.Encryption.Models; using Autogenerated = Dapr.Client.Autogen.Grpc.v1.Dapr; @@ -31,6 +32,7 @@ namespace Dapr.Cryptography.Encryption; /// exhaustion and other problems. /// /// +[Experimental("DAPR_CRYPTOGRAPHY", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/cryptography/cryptography-overview/")] public abstract class DaprEncryptionClient(Autogenerated.DaprClient client, HttpClient httpClient, string? daprApiToken = null) : IDaprEncryptionClient { private bool disposed; @@ -68,7 +70,6 @@ public abstract class DaprEncryptionClient(Autogenerated.DaprClient client, Http /// Options informing how the encryption operation should be configured. /// A that can be used to cancel the operation. /// An array of encrypted bytes. - [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] public abstract Task> EncryptAsync(string vaultResourceName, ReadOnlyMemory plaintextBytes, string keyName, EncryptionOptions encryptionOptions, CancellationToken cancellationToken = default); @@ -82,7 +83,6 @@ public abstract class DaprEncryptionClient(Autogenerated.DaprClient client, Http /// Options informing how the encryption operation should be configured. /// A that can be used to cancel the operation. /// An array of encrypted bytes. - [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] public abstract IAsyncEnumerable> EncryptAsync(string vaultResourceName, Stream plaintextStream, string keyName, EncryptionOptions encryptionOptions, CancellationToken cancellationToken = default); @@ -95,7 +95,6 @@ public abstract class DaprEncryptionClient(Autogenerated.DaprClient client, Http /// Options informing how the decryption operation should be configured. /// A that can be used to cancel the operation. /// An array of decrypted bytes. - [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] public abstract Task> DecryptAsync(string vaultResourceName, ReadOnlyMemory ciphertextBytes, string keyName, DecryptionOptions? options = null, CancellationToken cancellationToken = default); @@ -108,8 +107,6 @@ public abstract class DaprEncryptionClient(Autogenerated.DaprClient client, Http /// Options informing how the decryption operation should be configured. /// A that can be used to cancel the operation. /// An asynchronously enumerable array of decrypted bytes. - [Obsolete( - "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] public abstract IAsyncEnumerable> DecryptAsync(string vaultResourceName, Stream ciphertextStream, string keyName, DecryptionOptions? options = null, CancellationToken cancellationToken = default); diff --git a/src/Dapr.Cryptography/Encryption/DaprEncryptionClientBuilder.cs b/src/Dapr.Cryptography/Encryption/DaprEncryptionClientBuilder.cs index 65ff2391..833a849f 100644 --- a/src/Dapr.Cryptography/Encryption/DaprEncryptionClientBuilder.cs +++ b/src/Dapr.Cryptography/Encryption/DaprEncryptionClientBuilder.cs @@ -11,6 +11,7 @@ // limitations under the License. // ------------------------------------------------------------------------ +using System.Diagnostics.CodeAnalysis; using Dapr.Common; using Microsoft.Extensions.Configuration; using Autogenerated = Dapr.Client.Autogen.Grpc.v1; @@ -21,6 +22,7 @@ namespace Dapr.Cryptography.Encryption; /// Builds a . /// /// An optional instance of . +[Experimental("DAPR_CRYPTOGRAPHY", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/cryptography/cryptography-overview/")] public sealed class DaprEncryptionClientBuilder(IConfiguration? configuration = null) : DaprGenericClientBuilder(configuration) { /// diff --git a/src/Dapr.Cryptography/Encryption/DaprEncryptionGrpcClient.cs b/src/Dapr.Cryptography/Encryption/DaprEncryptionGrpcClient.cs index c2d00fed..271e2fba 100644 --- a/src/Dapr.Cryptography/Encryption/DaprEncryptionGrpcClient.cs +++ b/src/Dapr.Cryptography/Encryption/DaprEncryptionGrpcClient.cs @@ -12,6 +12,7 @@ // ------------------------------------------------------------------------ using System.Buffers; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using Dapr.Common; using Dapr.Cryptography.Extensions; @@ -24,6 +25,7 @@ namespace Dapr.Cryptography.Encryption; /// /// A client for performing cryptography operations with Dapr. /// +[Experimental("DAPR_CRYPTOGRAPHY", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/cryptography/cryptography-overview/")] internal sealed class DaprEncryptionGrpcClient(Autogenerated.Dapr.DaprClient client, HttpClient httpClient, string? daprApiToken = null) : DaprEncryptionClient(client, httpClient, daprApiToken: daprApiToken) { /// @@ -35,7 +37,6 @@ internal sealed class DaprEncryptionGrpcClient(Autogenerated.Dapr.DaprClient cli /// Options informing how the encryption operation should be configured. /// A that can be used to cancel the operation. /// An array of encrypted bytes. - [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, @@ -64,8 +65,6 @@ internal sealed class DaprEncryptionGrpcClient(Autogenerated.Dapr.DaprClient cli /// Options informing how the encryption operation should be configured. /// A that can be used to cancel the operation. /// An array of encrypted bytes. - [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 IAsyncEnumerable> EncryptAsync( string vaultResourceName, Stream plaintextStream, @@ -132,7 +131,6 @@ internal sealed class DaprEncryptionGrpcClient(Autogenerated.Dapr.DaprClient cli /// Options informing how the decryption operation should be configured. /// A that can be used to cancel the operation. /// An array of decrypted bytes. - [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, @@ -161,8 +159,6 @@ internal sealed class DaprEncryptionGrpcClient(Autogenerated.Dapr.DaprClient cli /// Options informing how the decryption operation should be configured. /// A that can be used to cancel the operation. /// An asynchronously enumerable array of decrypted bytes. - [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 IAsyncEnumerable> DecryptAsync( string vaultResourceName, Stream ciphertextStream, diff --git a/src/Dapr.Cryptography/Encryption/Extensions/DaprCryptographyServiceCollectionExtensions.cs b/src/Dapr.Cryptography/Encryption/Extensions/DaprCryptographyServiceCollectionExtensions.cs index 152bbab4..82559110 100644 --- a/src/Dapr.Cryptography/Encryption/Extensions/DaprCryptographyServiceCollectionExtensions.cs +++ b/src/Dapr.Cryptography/Encryption/Extensions/DaprCryptographyServiceCollectionExtensions.cs @@ -11,6 +11,7 @@ // limitations under the License. // ------------------------------------------------------------------------ +using System.Diagnostics.CodeAnalysis; using Dapr.Common.Extensions; using Microsoft.Extensions.DependencyInjection; @@ -19,6 +20,7 @@ namespace Dapr.Cryptography.Encryption.Extensions; /// /// Contains extension methods for using Dapr cryptography with dependency injection. /// +[Experimental("DAPR_CRYPTOGRAPHY", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/cryptography/cryptography-overview/")] public static class DaprCryptographyServiceCollectionExtensions { /// diff --git a/src/Dapr.Cryptography/Encryption/IDaprEncryptionClient.cs b/src/Dapr.Cryptography/Encryption/IDaprEncryptionClient.cs index ef2f2128..e7a10960 100644 --- a/src/Dapr.Cryptography/Encryption/IDaprEncryptionClient.cs +++ b/src/Dapr.Cryptography/Encryption/IDaprEncryptionClient.cs @@ -11,6 +11,7 @@ // limitations under the License. // ------------------------------------------------------------------------ +using System.Diagnostics.CodeAnalysis; using Dapr.Common; using Dapr.Cryptography.Encryption.Models; @@ -19,6 +20,7 @@ namespace Dapr.Cryptography.Encryption; /// /// Provides the implementation shape for the Dapr encryption client. /// +[Experimental("DAPR_CRYPTOGRAPHY", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/cryptography/cryptography-overview/")] public interface IDaprEncryptionClient : IDaprClient { /// @@ -30,7 +32,6 @@ public interface IDaprEncryptionClient : IDaprClient /// Options informing how the encryption operation should be configured. /// A that can be used to cancel the operation. /// An array of encrypted bytes. - [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] public Task> EncryptAsync(string vaultResourceName, ReadOnlyMemory plaintextBytes, string keyName, EncryptionOptions encryptionOptions, CancellationToken cancellationToken = default); @@ -44,7 +45,6 @@ public interface IDaprEncryptionClient : IDaprClient /// Options informing how the encryption operation should be configured. /// A that can be used to cancel the operation. /// An array of encrypted bytes. - [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] public IAsyncEnumerable> EncryptAsync(string vaultResourceName, Stream plaintextStream, string keyName, EncryptionOptions encryptionOptions, CancellationToken cancellationToken = default); @@ -57,7 +57,6 @@ public interface IDaprEncryptionClient : IDaprClient /// Options informing how the decryption operation should be configured. /// A that can be used to cancel the operation. /// An array of decrypted bytes. - [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] public Task> DecryptAsync(string vaultResourceName, ReadOnlyMemory ciphertextBytes, string keyName, DecryptionOptions? options = null, CancellationToken cancellationToken = default); @@ -70,8 +69,6 @@ public interface IDaprEncryptionClient : IDaprClient /// Options informing how the decryption operation should be configured. /// A that can be used to cancel the operation. /// An asynchronously enumerable array of decrypted bytes. - [Obsolete( - "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] public IAsyncEnumerable> DecryptAsync(string vaultResourceName, Stream ciphertextStream, string keyName, DecryptionOptions? options = null, CancellationToken cancellationToken = default); } diff --git a/src/Dapr.Jobs/DaprJobsClient.cs b/src/Dapr.Jobs/DaprJobsClient.cs index c30c34ba..2173a3ba 100644 --- a/src/Dapr.Jobs/DaprJobsClient.cs +++ b/src/Dapr.Jobs/DaprJobsClient.cs @@ -11,6 +11,7 @@ // limitations under the License. // ------------------------------------------------------------------------ +using System.Diagnostics.CodeAnalysis; using Dapr.Common; using Dapr.Jobs.Models; using Dapr.Jobs.Models.Responses; @@ -33,6 +34,7 @@ namespace Dapr.Jobs; /// exhaustion and other problems. /// /// +[Experimental("DAPR_JOBS", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/jobs/jobs-overview/")] public abstract class DaprJobsClient(Autogenerated.DaprClient client, HttpClient httpClient, string? daprApiToken = null) : IDaprClient { private bool disposed; @@ -74,8 +76,6 @@ public abstract class DaprJobsClient(Autogenerated.DaprClient client, HttpClient /// to require that an existing job with the same name be deleted first. /// The characteristics of the policy to apply when a job fails to trigger. /// Cancellation token. - [Obsolete( - "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] public abstract Task ScheduleJobAsync(string jobName, DaprJobSchedule schedule, ReadOnlyMemory? payload = null, DateTimeOffset? startingFrom = null, int? repeats = null, DateTimeOffset? ttl = null, bool overwrite = false, IJobFailurePolicyOptions? failurePolicyOptions = null, @@ -87,7 +87,6 @@ public abstract class DaprJobsClient(Autogenerated.DaprClient client, HttpClient /// The jobName of the job. /// Cancellation token. /// The details comprising the job. - [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] public abstract Task GetJobAsync(string jobName, CancellationToken cancellationToken = default); /// @@ -95,7 +94,6 @@ public abstract class DaprJobsClient(Autogenerated.DaprClient client, HttpClient /// /// The jobName of the job. /// Cancellation token. - [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] public abstract Task DeleteJobAsync(string jobName, CancellationToken cancellationToken = default); internal static KeyValuePair? GetDaprApiTokenHeader(string apiToken) diff --git a/src/Dapr.Jobs/DaprJobsClientBuilder.cs b/src/Dapr.Jobs/DaprJobsClientBuilder.cs index 12a66a89..979f7fb8 100644 --- a/src/Dapr.Jobs/DaprJobsClientBuilder.cs +++ b/src/Dapr.Jobs/DaprJobsClientBuilder.cs @@ -11,6 +11,7 @@ // limitations under the License. // ------------------------------------------------------------------------ +using System.Diagnostics.CodeAnalysis; using Dapr.Common; using Microsoft.Extensions.Configuration; using Autogenerated = Dapr.Client.Autogen.Grpc.v1; @@ -21,6 +22,7 @@ namespace Dapr.Jobs; /// Builds a . /// /// An optional instance of . +[Experimental("DAPR_JOBS", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/jobs/jobs-overview/")] public sealed class DaprJobsClientBuilder(IConfiguration? configuration = null) : DaprGenericClientBuilder(configuration) { /// diff --git a/src/Dapr.Jobs/DaprJobsGrpcClient.cs b/src/Dapr.Jobs/DaprJobsGrpcClient.cs index 31811ee8..245afdaa 100644 --- a/src/Dapr.Jobs/DaprJobsGrpcClient.cs +++ b/src/Dapr.Jobs/DaprJobsGrpcClient.cs @@ -11,6 +11,7 @@ // limitations under the License. // ------------------------------------------------------------------------ +using System.Diagnostics.CodeAnalysis; using Dapr.Common; using Dapr.Jobs.Models; using Dapr.Jobs.Models.Responses; @@ -24,6 +25,7 @@ namespace Dapr.Jobs; /// /// A client for interacting with the Dapr endpoints. /// +[Experimental("DAPR_JOBS", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/jobs/jobs-overview/")] internal sealed class DaprJobsGrpcClient(Autogenerated.Dapr.DaprClient client, HttpClient httpClient, string? daprApiToken = null) : DaprJobsClient(client, httpClient, daprApiToken: daprApiToken) { /// @@ -38,7 +40,6 @@ internal sealed class DaprJobsGrpcClient(Autogenerated.Dapr.DaprClient client, H /// A flag indicating whether the job should be overwritten when submitted (true); otherwise false to require that an existing job with the same name be deleted first. /// The characteristics of the policy to apply when a job fails to trigger. /// Cancellation token. - [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 ScheduleJobAsync(string jobName, DaprJobSchedule schedule, ReadOnlyMemory? payload = null, DateTimeOffset? startingFrom = null, int? repeats = null, DateTimeOffset? ttl = null, bool overwrite = false, IJobFailurePolicyOptions? failurePolicyOptions = null, @@ -157,7 +158,6 @@ internal sealed class DaprJobsGrpcClient(Autogenerated.Dapr.DaprClient client, H /// The name of the job. /// Cancellation token. /// The details comprising the job. - [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 GetJobAsync(string jobName, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(jobName)) @@ -205,7 +205,6 @@ internal sealed class DaprJobsGrpcClient(Autogenerated.Dapr.DaprClient client, H /// The name of the job. /// Cancellation token. /// - [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 DeleteJobAsync(string jobName, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(jobName)) diff --git a/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs b/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs index 7eed80ab..3103e072 100644 --- a/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs +++ b/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs @@ -11,6 +11,7 @@ // limitations under the License. // ------------------------------------------------------------------------ +using System.Diagnostics.CodeAnalysis; using Dapr.Common.Extensions; using Microsoft.Extensions.DependencyInjection; @@ -19,6 +20,7 @@ namespace Dapr.Jobs.Extensions; /// /// Contains extension methods for using Dapr Jobs with dependency injection. /// +[Experimental("DAPR_JOBS", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/jobs/jobs-overview/")] public static class DaprJobsServiceCollectionExtensions { /// diff --git a/src/Dapr.Jobs/Extensions/DaprSerializationExtensions.cs b/src/Dapr.Jobs/Extensions/DaprSerializationExtensions.cs index 2fd27800..fb58ef69 100644 --- a/src/Dapr.Jobs/Extensions/DaprSerializationExtensions.cs +++ b/src/Dapr.Jobs/Extensions/DaprSerializationExtensions.cs @@ -11,6 +11,7 @@ // limitations under the License. // ------------------------------------------------------------------------ +using System.Diagnostics.CodeAnalysis; using System.Text; using System.Text.Json; using Dapr.Jobs.Models; @@ -20,6 +21,7 @@ namespace Dapr.Jobs.Extensions; /// /// Provides helper extensions for performing serialization operations when scheduling one-time Cron jobs for the developer. /// +[Experimental("DAPR_JOBS", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/jobs/jobs-overview/")] public static class DaprJobsSerializationExtensions { /// @@ -41,7 +43,6 @@ public static class DaprJobsSerializationExtensions /// A flag indicating whether the job should be overwritten when submitted (true); otherwise false to require that an existing job with the same name be deleted first. /// The characteristics of the policy to apply when a job fails to trigger. /// Cancellation token. - [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] public static async Task ScheduleJobWithPayloadAsync(this DaprJobsClient client, string jobName, DaprJobSchedule schedule, object payload, DateTime? startingFrom = null, int? repeats = null, JsonSerializerOptions? jsonSerializerOptions = null, DateTimeOffset? ttl = null, @@ -71,7 +72,6 @@ public static class DaprJobsSerializationExtensions /// A flag indicating whether the job should be overwritten when submitted (true); otherwise false to require that an existing job with the same name be deleted first. /// The characteristics of the policy to apply when a job fails to trigger. /// Cancellation token. - [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] public static async Task ScheduleJobWithPayloadAsync(this DaprJobsClient client, string jobName, DaprJobSchedule schedule, string payload, DateTime? startingFrom = null, int? repeats = null, DateTimeOffset? ttl = null, bool overwrite = false, IJobFailurePolicyOptions? failurePolicyOptions = null, diff --git a/test/Dapr.Jobs.Analyzer.Test/MapDaprScheduledJobHandlerTest.cs b/test/Dapr.Jobs.Analyzer.Test/MapDaprScheduledJobHandlerTest.cs index e8d4cb3e..aec9aaff 100644 --- a/test/Dapr.Jobs.Analyzer.Test/MapDaprScheduledJobHandlerTest.cs +++ b/test/Dapr.Jobs.Analyzer.Test/MapDaprScheduledJobHandlerTest.cs @@ -26,6 +26,7 @@ public class DaprJobsAnalyzerAnalyzerTests using Dapr.Jobs.Extensions; using Dapr.Jobs.Models; + #pragma warning disable DAPR_JOBS public static class Program { public static void Main() @@ -41,10 +42,11 @@ public class DaprJobsAnalyzerAnalyzerTests Encoding.UTF8.GetBytes("This is a test"), repeats: 10).GetAwaiter().GetResult(); } } + #pragma warning restore DAPR_JOBS """; var expected = VerifyAnalyzer.Diagnostic(MapDaprScheduledJobHandlerAnalyzer.DaprJobHandlerRule) - .WithSpan(22, 25, 23, 83) + .WithSpan(23, 25, 24, 83) .WithMessage( "Job invocations require the MapDaprScheduledJobHandler be set and configured for job name 'myJob' on IEndpointRouteBuilder"); @@ -66,6 +68,7 @@ public class DaprJobsAnalyzerAnalyzerTests using Dapr.Jobs.Extensions; using Dapr.Jobs.Models; + #pragma warning disable DAPR_JOBS public static class Program { public static void Main() @@ -78,6 +81,7 @@ public class DaprJobsAnalyzerAnalyzerTests var daprJobsClient = scope.ServiceProvider.GetRequiredService(); } } + #pragma warning restore DAPR_JOBS """; var analyzer = new VerifyAnalyzer(Utilities.GetReferences()); @@ -98,6 +102,7 @@ public class DaprJobsAnalyzerAnalyzerTests using Dapr.Jobs.Extensions; using Dapr.Jobs.Models; + #pragma warning disable DAPR_JOBS public static class Program { public static void Main() @@ -115,14 +120,15 @@ public class DaprJobsAnalyzerAnalyzerTests Encoding.UTF8.GetBytes("This is a test"), repeats: 10).GetAwaiter().GetResult(); } } + #pragma warning restore DAPR_JOBS """; var expected1 = VerifyAnalyzer.Diagnostic(MapDaprScheduledJobHandlerAnalyzer.DaprJobHandlerRule) - .WithSpan(22, 25, 23, 83) + .WithSpan(23, 25, 24, 83) .WithMessage( "Job invocations require the MapDaprScheduledJobHandler be set and configured for job name 'myJob' on IEndpointRouteBuilder"); var expected2 = VerifyAnalyzer.Diagnostic(MapDaprScheduledJobHandlerAnalyzer.DaprJobHandlerRule) - .WithSpan(24, 25, 25, 83) + .WithSpan(25, 25, 26, 83) .WithMessage("Job invocations require the MapDaprScheduledJobHandler be set and configured for job name 'myJob2' on IEndpointRouteBuilder"); var analyzer = new VerifyAnalyzer(Utilities.GetReferences()); await analyzer.VerifyAnalyzerAsync(testCode, expected1, expected2); @@ -142,6 +148,7 @@ public class DaprJobsAnalyzerAnalyzerTests using Dapr.Jobs.Extensions; using Dapr.Jobs.Models; + #pragma warning disable DAPR_JOBS public static class Program { public static void Main() @@ -164,6 +171,7 @@ public class DaprJobsAnalyzerAnalyzerTests }, TimeSpan.FromSeconds(5)); } } + #pragma warning restore DAPR_JOBS """; var analyzer = new VerifyAnalyzer(Utilities.GetReferences()); @@ -183,7 +191,8 @@ public class DaprJobsAnalyzerAnalyzerTests using Dapr.Jobs; using Dapr.Jobs.Extensions; using Dapr.Jobs.Models; - + + #pragma warning disable DAPR_JOBS public static class Program { public static async Task Main() @@ -206,6 +215,7 @@ public class DaprJobsAnalyzerAnalyzerTests }, TimeSpan.FromSeconds(5)); } } + #pragma warning restore DAPR_JOBS """; var analyzer = new VerifyAnalyzer(Utilities.GetReferences()); @@ -225,7 +235,8 @@ public class DaprJobsAnalyzerAnalyzerTests using Dapr.Jobs; using Dapr.Jobs.Extensions; using Dapr.Jobs.Models; - + + #pragma warning disable DAPR_JOBS public static class Program { public static async Task Main() @@ -245,6 +256,7 @@ public class DaprJobsAnalyzerAnalyzerTests return Task.CompletedTask; } } + #pragma warning restore DAPR_JOBS """; diff --git a/test/Directory.Build.props b/test/Directory.Build.props index 39b6b97e..65b969f3 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -8,6 +8,7 @@ $(RepoRoot)bin\$(Configuration)\test\$(MSBuildProjectName)\ false + $(NoWarn);DAPR_CONVERSATION;DAPR_JOBS;DAPR_DISTRIBUTEDLOCK;DAPR_CRYPTOGRAPHY From 1bbbc739e6e5bcfc9db57080863893d097c64060 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sat, 21 Jun 2025 05:26:43 -0500 Subject: [PATCH 18/22] Master to release branch (#1566) Merging all the hotfix patches from master into release-1.16 to prepare for release --- .../dotnet-development-dapr-aspire.md | 10 +- .../EncryptDecryptFileStreamExample.cs | 72 +++++++ .../Extensions/DurationExtensions.cs | 5 +- src/Dapr.Actors/Runtime/ActorManager.cs | 76 ++++++- src/Dapr.Actors/Runtime/ConverterUtils.cs | 4 +- .../Crypto/DecryptionStreamProcessor.cs | 153 ++++++++++++++ .../Crypto/EncryptionStreamProcessor.cs | 154 ++++++++++++++ src/Dapr.Client/DaprClient.cs | 6 +- src/Dapr.Client/DaprClientGrpc.cs | 188 +++++------------- src/Dapr.Jobs/DaprJobsGrpcClient.cs | 35 +++- .../Runtime/ActorManagerTests.cs | 158 ++++++++++++++- test/Dapr.Client.Test/CryptographyApiTest.cs | 44 +--- .../Dapr.Jobs.Test/DaprJobsGrpcClientTests.cs | 16 ++ 13 files changed, 706 insertions(+), 215 deletions(-) create mode 100644 examples/Client/Cryptography/Examples/EncryptDecryptFileStreamExample.cs create mode 100644 src/Dapr.Client/Crypto/DecryptionStreamProcessor.cs create mode 100644 src/Dapr.Client/Crypto/EncryptionStreamProcessor.cs 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 index 953af088..2c9366dc 100644 --- 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 @@ -66,12 +66,18 @@ 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. +into the AppHost directory with the following and install the `CommunityToolkit.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. +{{% alert color="primary" %}} + +This package was previously called `Aspire.Hosting.Dapr`, which has been [marked as deprecated](https://www.nuget.org/packages/Aspire.Hosting.Dapr). + +{{% /alert %}} + ```sh cd aspiredemo.AppHost -dotnet add package Aspire.Hosting.Dapr +dotnet add package CommunityToolkit.Aspire.Hosting.Dapr dotnet add reference ../MyApp/ ``` diff --git a/examples/Client/Cryptography/Examples/EncryptDecryptFileStreamExample.cs b/examples/Client/Cryptography/Examples/EncryptDecryptFileStreamExample.cs new file mode 100644 index 00000000..89b55dfb --- /dev/null +++ b/examples/Client/Cryptography/Examples/EncryptDecryptFileStreamExample.cs @@ -0,0 +1,72 @@ +// ------------------------------------------------------------------------ +// Copyright 2023 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System.Buffers; +using Dapr.Client; +#pragma warning disable CS0618 // Type or member is obsolete + +namespace Cryptography.Examples +{ + internal class EncryptDecryptFileStreamExample(string componentName, string keyName) : Example + { + public override string DisplayName => "Use Cryptography to encrypt and decrypt a file"; + public override async Task RunAsync(CancellationToken cancellationToken) + { + using var client = new DaprClientBuilder().Build(); + + // The name of the file we're using as an example + const string fileName = "file.txt"; + + Console.WriteLine("Original file contents:"); + foreach (var line in await File.ReadAllLinesAsync(fileName, cancellationToken)) + { + Console.WriteLine(line); + } + + //Encrypt from a file stream and buffer the resulting bytes to an in-memory buffer + await using var encryptFs = new FileStream(fileName, FileMode.Open); + + var bufferedEncryptedBytes = new ArrayBufferWriter(); + await foreach (var bytes in (client.EncryptAsync(componentName, encryptFs, keyName, + new EncryptionOptions(KeyWrapAlgorithm.Rsa), cancellationToken))) + { + bufferedEncryptedBytes.Write(bytes.Span); + } + + Console.WriteLine("Encrypted bytes:"); + Console.WriteLine(Convert.ToBase64String(bufferedEncryptedBytes.WrittenMemory.ToArray())); + + //We'll write to a temporary file via a FileStream + var tempDecryptedFile = Path.GetTempFileName(); + await using var decryptFs = new FileStream(tempDecryptedFile, FileMode.Create); + + //We'll stream the decrypted bytes from a MemoryStream into the above temporary file + await using var encryptedMs = new MemoryStream(bufferedEncryptedBytes.WrittenMemory.ToArray()); + await foreach (var result in (client.DecryptAsync(componentName, encryptedMs, keyName, + cancellationToken))) + { + decryptFs.Write(result.Span); + } + + decryptFs.Close(); + + //Let's confirm the value as written to the file + var decryptedValue = await File.ReadAllTextAsync(tempDecryptedFile, cancellationToken); + Console.WriteLine("Decrypted value: "); + Console.WriteLine(decryptedValue); + + //And some cleanup to delete our temp file + File.Delete(tempDecryptedFile); + } + } +} diff --git a/src/Dapr.Actors/Extensions/DurationExtensions.cs b/src/Dapr.Actors/Extensions/DurationExtensions.cs index c3f1cafa..dcd38583 100644 --- a/src/Dapr.Actors/Extensions/DurationExtensions.cs +++ b/src/Dapr.Actors/Extensions/DurationExtensions.cs @@ -68,13 +68,12 @@ internal static class DurationExtensions if (period.StartsWith(MonthlyPrefixPeriod)) { - var dateTime = DateTime.UtcNow; - return dateTime.AddMonths(1) - dateTime; + return TimeSpan.FromDays(30); } if (period.StartsWith(MidnightPrefixPeriod)) { - return new TimeSpan(); + return TimeSpan.Zero; } if (period.StartsWith(WeeklyPrefixPeriod)) diff --git a/src/Dapr.Actors/Runtime/ActorManager.cs b/src/Dapr.Actors/Runtime/ActorManager.cs index e60cd455..6c01a4a0 100644 --- a/src/Dapr.Actors/Runtime/ActorManager.cs +++ b/src/Dapr.Actors/Runtime/ActorManager.cs @@ -220,11 +220,11 @@ internal sealed class ActorManager } } - internal async Task FireTimerAsync(ActorId actorId, Stream requestBodyStream, CancellationToken cancellationToken = default) - { -#pragma warning disable 0618 - var timerData = await JsonSerializer.DeserializeAsync(requestBodyStream); -#pragma warning restore 0618 + internal async Task FireTimerAsync(ActorId actorId, Stream requestBodyStream, CancellationToken cancellationToken = default) + { + #pragma warning disable 0618 + var timerData = await DeserializeAsync(requestBodyStream); + #pragma warning restore 0618 // Create a Func to be invoked by common method. async Task RequestFunc(Actor actor, CancellationToken ct) @@ -243,10 +243,66 @@ internal sealed class ActorManager await this.DispatchInternalAsync(actorId, this.timerMethodContext, RequestFunc, cancellationToken); } - internal async Task ActivateActorAsync(ActorId actorId) - { - // An actor is activated by "Dapr" runtime when a call is to be made for an actor. - var state = await this.CreateActorAsync(actorId); +#pragma warning disable 0618 + internal static async Task DeserializeAsync(Stream stream) + { + var json = await JsonSerializer.DeserializeAsync(stream); + if (json.ValueKind == JsonValueKind.Null) + { + return null; + } + + var setAnyProperties = false; // Used to determine if anything was actually deserialized + var dueTime = TimeSpan.Zero; + var callback = ""; + var period = TimeSpan.Zero; + var data = Array.Empty(); + TimeSpan? ttl = null; + if (json.TryGetProperty("callback", out var callbackProperty)) + { + setAnyProperties = true; + callback = callbackProperty.GetString(); + } + if (json.TryGetProperty("dueTime", out var dueTimeProperty)) + { + setAnyProperties = true; + var dueTimeString = dueTimeProperty.GetString(); + dueTime = ConverterUtils.ConvertTimeSpanFromDaprFormat(dueTimeString); + } + + if (json.TryGetProperty("period", out var periodProperty)) + { + setAnyProperties = true; + var periodString = periodProperty.GetString(); + (period, _) = ConverterUtils.ConvertTimeSpanValueFromISO8601Format(periodString); + } + + if (json.TryGetProperty("data", out var dataProperty) && dataProperty.ValueKind != JsonValueKind.Null) + { + setAnyProperties = true; + data = dataProperty.GetBytesFromBase64(); + } + + if (json.TryGetProperty("ttl", out var ttlProperty)) + { + setAnyProperties = true; + var ttlString = ttlProperty.GetString(); + ttl = ConverterUtils.ConvertTimeSpanFromDaprFormat(ttlString); + } + + if (!setAnyProperties) + { + return null; //No properties were ever deserialized, so return null instead of default values + } + + return new TimerInfo(callback, data, dueTime, period, ttl); + } +#pragma warning restore 0618 + + internal async Task ActivateActorAsync(ActorId actorId) + { + // An actor is activated by "Dapr" runtime when a call is to be made for an actor. + var state = await this.CreateActorAsync(actorId); try { @@ -397,4 +453,4 @@ internal sealed class ActorManager return new Tuple(string.Empty, responseMsgBodyBytes); } -} \ No newline at end of file +} diff --git a/src/Dapr.Actors/Runtime/ConverterUtils.cs b/src/Dapr.Actors/Runtime/ConverterUtils.cs index 94cfd3d3..1705c9fe 100644 --- a/src/Dapr.Actors/Runtime/ConverterUtils.cs +++ b/src/Dapr.Actors/Runtime/ConverterUtils.cs @@ -49,7 +49,7 @@ internal static class ConverterUtils int msIndex = spanOfValue.IndexOf("ms"); // handle days from hours. - var hoursSpan = spanOfValue.Slice(0, hIndex); + var hoursSpan = spanOfValue[..hIndex]; var hours = int.Parse(hoursSpan); var days = hours / 24; hours %= 24; @@ -103,7 +103,7 @@ internal static class ConverterUtils builder.Append($"{value.Days}D"); } - builder.Append("T"); + builder.Append('T'); if(value.Hours > 0) { diff --git a/src/Dapr.Client/Crypto/DecryptionStreamProcessor.cs b/src/Dapr.Client/Crypto/DecryptionStreamProcessor.cs new file mode 100644 index 00000000..4a7748b6 --- /dev/null +++ b/src/Dapr.Client/Crypto/DecryptionStreamProcessor.cs @@ -0,0 +1,153 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +#nullable enable +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; +using Google.Protobuf; +using Grpc.Core; +using Autogenerated = Dapr.Client.Autogen.Grpc.v1; +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + +namespace Dapr.Client.Crypto; + +/// +/// Provides the implementation to decrypt a stream of plaintext data with the Dapr runtime. +/// +internal sealed class DecryptionStreamProcessor : IDisposable +{ + private bool disposed; + private readonly Channel> outputChannel = Channel.CreateUnbounded>(); + + /// + /// Surfaces any exceptions encountered while asynchronously processing the inbound and outbound streams. + /// + internal event EventHandler? OnException; + + /// + /// Sends the provided bytes in chunks to the sidecar for the encryption operation. + /// + /// The stream containing the bytes to decrypt. + /// The call to make to the sidecar to process the encryption operation. + /// The size, in bytes, of the streaming blocks. + /// The decryption options. + /// Token used to cancel the ongoing request. + public async Task ProcessStreamAsync( + Stream inputStream, + AsyncDuplexStreamingCall call, + int streamingBlockSizeInBytes, + Autogenerated.DecryptRequestOptions options, + CancellationToken cancellationToken) + { + //Read from the input stream and write to the gRPC call + _ = Task.Run(async () => + { + try + { + await using var bufferedStream = new BufferedStream(inputStream, streamingBlockSizeInBytes); + var buffer = new byte[streamingBlockSizeInBytes]; + int bytesRead; + ulong sequenceNumber = 0; + + while ((bytesRead = await bufferedStream.ReadAsync(buffer, cancellationToken)) > 0) + { + var request = new Autogenerated.DecryptRequest + { + Payload = new Autogenerated.StreamPayload + { + Data = ByteString.CopyFrom(buffer, 0, bytesRead), Seq = sequenceNumber + } + }; + + //Only include the options in the first message + if (sequenceNumber == 0) + { + request.Options = options; + } + + await call.RequestStream.WriteAsync(request, cancellationToken); + + //Increment the sequence number + sequenceNumber++; + } + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + // Expected cancellation exception + } + catch (Exception ex) + { + OnException?.Invoke(this, ex); + } + finally + { + await call.RequestStream.CompleteAsync(); + } + }, cancellationToken); + + //Start reading from the gRPC call and writing to the output channel + _ = Task.Run(async () => + { + try + { + await foreach (var response in call.ResponseStream.ReadAllAsync(cancellationToken)) + { + await outputChannel.Writer.WriteAsync(response.Payload.Data.Memory, cancellationToken); + } + } + catch (Exception ex) + { + OnException?.Invoke(this, ex); + } + finally + { + outputChannel.Writer.Complete(); + } + }, cancellationToken); + } + + /// + /// Retrieves the processed bytes from the operation from the sidecar and + /// returns as an enumerable stream. + /// + public async IAsyncEnumerable> GetProcessedDataAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + await foreach (var data in outputChannel.Reader.ReadAllAsync(cancellationToken)) + { + yield return data; + } + } + + public void Dispose() + { + Dispose(true); + } + + private void Dispose(bool disposing) + { + if (!disposed) + { + if (disposing) + { + outputChannel.Writer.TryComplete(); + } + + disposed = true; + } + } +} diff --git a/src/Dapr.Client/Crypto/EncryptionStreamProcessor.cs b/src/Dapr.Client/Crypto/EncryptionStreamProcessor.cs new file mode 100644 index 00000000..ae37e776 --- /dev/null +++ b/src/Dapr.Client/Crypto/EncryptionStreamProcessor.cs @@ -0,0 +1,154 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +#nullable enable +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; +using Google.Protobuf; +using Grpc.Core; +using Autogenerated = Dapr.Client.Autogen.Grpc.v1; +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + +namespace Dapr.Client.Crypto; + +/// +/// Provides the implementation to encrypt a stream of plaintext data with the Dapr runtime. +/// +internal sealed class EncryptionStreamProcessor : IDisposable +{ + private bool disposed; + private readonly Channel> outputChannel = Channel.CreateUnbounded>(); + + /// + /// Surfaces any exceptions encountered while asynchronously processing the inbound and outbound streams. + /// + internal event EventHandler? OnException; + + /// + /// Sends the provided bytes in chunks to the sidecar for the encryption operation. + /// + /// The stream containing the bytes to encrypt. + /// The call to make to the sidecar to process the encryption operation. + /// The encryption options. + /// The size, in bytes, of the streaming blocks. + /// Token used to cancel the ongoing request. + public async Task ProcessStreamAsync( + Stream inputStream, + AsyncDuplexStreamingCall call, + Autogenerated.EncryptRequestOptions options, + int streamingBlockSizeInBytes, + CancellationToken cancellationToken) + { + //Read from the input stream and write to the gRPC call + _ = Task.Run(async () => + { + try + { + await using var bufferedStream = new BufferedStream(inputStream, streamingBlockSizeInBytes); + var buffer = new byte[streamingBlockSizeInBytes]; + int bytesRead; + ulong sequenceNumber = 0; + + while ((bytesRead = await bufferedStream.ReadAsync(buffer, cancellationToken)) > 0) + { + var request = new Autogenerated.EncryptRequest + { + Payload = new Autogenerated.StreamPayload + { + Data = ByteString.CopyFrom(buffer, 0, bytesRead), Seq = sequenceNumber + } + }; + + //Only include the options in the first message + if (sequenceNumber == 0) + { + request.Options = options; + } + + await call.RequestStream.WriteAsync(request, cancellationToken); + + //Increment the sequence number + sequenceNumber++; + } + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + // Expected cancellation exception + } + catch (Exception ex) + { + OnException?.Invoke(this, ex); + } + finally + { + await call.RequestStream.CompleteAsync(); + } + }, cancellationToken); + + //Start reading from the gRPC call and writing to the output channel + _ = Task.Run(async () => + { + try + { + await foreach (var response in call.ResponseStream.ReadAllAsync(cancellationToken)) + { + await outputChannel.Writer.WriteAsync(response.Payload.Data.Memory, cancellationToken); + } + } + catch (Exception ex) + { + OnException?.Invoke(this, ex); + } + finally + { + outputChannel.Writer.Complete(); + } + }, cancellationToken); + } + + /// + /// Retrieves the processed bytes from the operation from the sidecar and + /// returns as an enumerable stream. + /// + public async IAsyncEnumerable> GetProcessedDataAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + await foreach (var data in outputChannel.Reader.ReadAllAsync(cancellationToken)) + { + yield return data; + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (!disposed) + { + if (disposing) + { + outputChannel.Writer.TryComplete(); + } + + disposed = true; + } + } +} diff --git a/src/Dapr.Client/DaprClient.cs b/src/Dapr.Client/DaprClient.cs index 3e614324..65a7c31d 100644 --- a/src/Dapr.Client/DaprClient.cs +++ b/src/Dapr.Client/DaprClient.cs @@ -1105,7 +1105,7 @@ public abstract class DaprClient : IDisposable /// A that can be used to cancel the operation. /// An array of encrypted bytes. [Experimental("DAPR_CRYPTOGRAPHY", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/cryptography/cryptography-overview/")] - public abstract Task>> EncryptAsync(string vaultResourceName, Stream plaintextStream, string keyName, + public abstract IAsyncEnumerable> EncryptAsync(string vaultResourceName, Stream plaintextStream, string keyName, EncryptionOptions encryptionOptions, CancellationToken cancellationToken = default); /// @@ -1143,7 +1143,7 @@ public abstract class DaprClient : IDisposable /// A that can be used to cancel the operation. /// An asynchronously enumerable array of decrypted bytes. [Experimental("DAPR_CRYPTOGRAPHY", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/cryptography/cryptography-overview/")] - public abstract Task>> DecryptAsync(string vaultResourceName, Stream ciphertextStream, + public abstract IAsyncEnumerable> DecryptAsync(string vaultResourceName, Stream ciphertextStream, string keyName, DecryptionOptions options, CancellationToken cancellationToken = default); /// @@ -1155,7 +1155,7 @@ public abstract class DaprClient : IDisposable /// A that can be used to cancel the operation. /// An asynchronously enumerable array of decrypted bytes. [Experimental("DAPR_CRYPTOGRAPHY", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/cryptography/cryptography-overview/")] - public abstract Task>> DecryptAsync(string vaultResourceName, Stream ciphertextStream, + public abstract IAsyncEnumerable> DecryptAsync(string vaultResourceName, Stream ciphertextStream, string keyName, CancellationToken cancellationToken = default); #endregion diff --git a/src/Dapr.Client/DaprClientGrpc.cs b/src/Dapr.Client/DaprClientGrpc.cs index 50f048ad..67a03fb5 100644 --- a/src/Dapr.Client/DaprClientGrpc.cs +++ b/src/Dapr.Client/DaprClientGrpc.cs @@ -11,10 +11,9 @@ // limitations under the License. // ------------------------------------------------------------------------ -using System.Diagnostics.CodeAnalysis; - namespace Dapr.Client; +using Crypto; using System; using System.Buffers; using System.Collections.Generic; @@ -30,6 +29,7 @@ using Google.Protobuf; using Google.Protobuf.WellKnownTypes; using Grpc.Core; using Grpc.Net.Client; +using System.Diagnostics.CodeAnalysis; using Autogenerated = Dapr.Client.Autogen.Grpc.v1; /// @@ -1675,11 +1675,10 @@ internal class DaprClientGrpc : DaprClient { using var memoryStream = plaintextBytes.CreateMemoryStream(true); - var encryptionResult = - await EncryptAsync(vaultResourceName, memoryStream, keyName, encryptionOptions, cancellationToken); + var encryptionResult = EncryptAsync(vaultResourceName, memoryStream, keyName, encryptionOptions, cancellationToken); var bufferedResult = new ArrayBufferWriter(); - await foreach (var item in encryptionResult.WithCancellation(cancellationToken)) + await foreach (var item in encryptionResult) { bufferedResult.Write(item.Span); } @@ -1689,14 +1688,17 @@ internal class DaprClientGrpc : DaprClient /// [Experimental("DAPR_CRYPTOGRAPHY", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/cryptography/cryptography-overview/")] - public override async Task>> EncryptAsync(string vaultResourceName, + public override async IAsyncEnumerable> EncryptAsync(string vaultResourceName, Stream plaintextStream, - string keyName, EncryptionOptions encryptionOptions, CancellationToken cancellationToken = default) + string keyName, EncryptionOptions encryptionOptions, + [EnumeratorCancellation] CancellationToken cancellationToken = default) { ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); ArgumentVerifier.ThrowIfNull(plaintextStream, nameof(plaintextStream)); ArgumentVerifier.ThrowIfNull(encryptionOptions, nameof(encryptionOptions)); + + EventHandler exceptionHandler = (_, ex) => throw ex; var shouldOmitDecryptionKeyName = string.IsNullOrWhiteSpace(encryptionOptions @@ -1719,169 +1721,76 @@ internal class DaprClientGrpc : DaprClient } var options = CreateCallOptions(headers: null, cancellationToken); - var duplexStream = client.EncryptAlpha1(options); + 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 static 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)) + using var streamProcessor = new EncryptionStreamProcessor(); + try { - var buffer = new byte[streamingBlockSizeInBytes]; - int bytesRead; - ulong sequenceNumber = 0; + streamProcessor.OnException += exceptionHandler; + await streamProcessor.ProcessStreamAsync(plaintextStream, duplexStream, encryptRequestOptions, + encryptionOptions.StreamingBlockSizeInBytes, + cancellationToken); - while ((bytesRead = - await bufferedStream.ReadAsync(buffer.AsMemory(0, streamingBlockSizeInBytes), - cancellationToken)) != - 0) + await foreach (var value in streamProcessor.GetProcessedDataAsync(cancellationToken)) { - await duplexStream.RequestStream.WriteAsync( - new Autogenerated.EncryptRequest - { - Payload = new Autogenerated.StreamPayload - { - Data = ByteString.CopyFrom(buffer, 0, bytesRead), Seq = sequenceNumber - } - }, cancellationToken); - - //Increment the sequence number - sequenceNumber++; + yield return value; } } - - //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 static async IAsyncEnumerable> RetrieveEncryptedStreamAsync( - AsyncDuplexStreamingCall duplexStream, - [EnumeratorCancellation] CancellationToken cancellationToken) - { - await foreach (var encryptResponse in duplexStream.ResponseStream.ReadAllAsync(cancellationToken) - .ConfigureAwait(false)) + finally { - yield return encryptResponse.Payload.Data.Memory; + streamProcessor.OnException -= exceptionHandler; } } /// [Experimental("DAPR_CRYPTOGRAPHY", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/cryptography/cryptography-overview/")] - public override async Task>> DecryptAsync(string vaultResourceName, + public override async IAsyncEnumerable> DecryptAsync(string vaultResourceName, Stream ciphertextStream, string keyName, - DecryptionOptions decryptionOptions, CancellationToken cancellationToken = default) + DecryptionOptions decryptionOptions, + [EnumeratorCancellation] CancellationToken cancellationToken = default) { ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); ArgumentVerifier.ThrowIfNull(ciphertextStream, nameof(ciphertextStream)); - ArgumentVerifier.ThrowIfNull(decryptionOptions, nameof(decryptionOptions)); + decryptionOptions ??= new DecryptionOptions(); + + EventHandler exceptionHandler = (_, ex) => throw ex; var decryptRequestOptions = new Autogenerated.DecryptRequestOptions { - ComponentName = vaultResourceName, KeyName = keyName + 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( - //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 - .ContinueWith(_ => receiveResult.Result, cancellationToken); + using var streamProcessor = new DecryptionStreamProcessor(); + try + { + streamProcessor.OnException += exceptionHandler; + await streamProcessor.ProcessStreamAsync(ciphertextStream, duplexStream, decryptionOptions.StreamingBlockSizeInBytes, + decryptRequestOptions, + cancellationToken); + + await foreach (var value in streamProcessor.GetProcessedDataAsync(cancellationToken)) + { + yield return value; + } + } + finally + { + streamProcessor.OnException -= exceptionHandler; + } } /// [Experimental("DAPR_CRYPTOGRAPHY", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/cryptography/cryptography-overview/")] - public override Task>> DecryptAsync(string vaultResourceName, + public override IAsyncEnumerable> 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 static 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) - { - await duplexStream.RequestStream.WriteAsync( - new Autogenerated.DecryptRequest - { - Payload = new Autogenerated.StreamPayload - { - Data = ByteString.CopyFrom(buffer, 0, bytesRead), Seq = sequenceNumber - } - }, cancellationToken); - - //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 static async IAsyncEnumerable> RetrieveDecryptedStreamAsync( - AsyncDuplexStreamingCall duplexStream, - [EnumeratorCancellation] CancellationToken cancellationToken) - { - await foreach (var decryptResponse in duplexStream.ResponseStream.ReadAllAsync(cancellationToken) - .ConfigureAwait(false)) - { - yield return decryptResponse.Payload.Data.Memory; - } - } - /// [Experimental("DAPR_CRYPTOGRAPHY", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/cryptography/cryptography-overview/")] public override async Task> DecryptAsync(string vaultResourceName, @@ -1890,11 +1799,10 @@ internal class DaprClientGrpc : DaprClient { using var memoryStream = ciphertextBytes.CreateMemoryStream(true); - var decryptionResult = - await DecryptAsync(vaultResourceName, memoryStream, keyName, decryptionOptions, cancellationToken); + var decryptionResult = DecryptAsync(vaultResourceName, memoryStream, keyName, decryptionOptions, cancellationToken); var bufferedResult = new ArrayBufferWriter(); - await foreach (var item in decryptionResult.WithCancellation(cancellationToken)) + await foreach (var item in decryptionResult) { bufferedResult.Write(item.Span); } diff --git a/src/Dapr.Jobs/DaprJobsGrpcClient.cs b/src/Dapr.Jobs/DaprJobsGrpcClient.cs index 245afdaa..7b0df600 100644 --- a/src/Dapr.Jobs/DaprJobsGrpcClient.cs +++ b/src/Dapr.Jobs/DaprJobsGrpcClient.cs @@ -170,17 +170,7 @@ internal sealed class DaprJobsGrpcClient(Autogenerated.Dapr.DaprClient client, H var envelope = new Autogenerated.GetJobRequest { Name = jobName }; var grpcCallOptions = DaprClientUtilities.ConfigureGrpcCallOptions(typeof(DaprJobsClient).Assembly, this.DaprApiToken, cancellationToken); var response = await Client.GetJobAlpha1Async(envelope, grpcCallOptions); - var schedule = DateTime.TryParse(response.Job.DueTime, out var dueTime) - ? DaprJobSchedule.FromDateTime(dueTime) - : new DaprJobSchedule(response.Job.Schedule); - - return new DaprJobDetails(schedule) - { - DueTime = !string.IsNullOrWhiteSpace(response.Job.DueTime) ? DateTime.Parse(response.Job.DueTime) : null, - Ttl = !string.IsNullOrWhiteSpace(response.Job.Ttl) ? DateTime.Parse(response.Job.Ttl) : null, - RepeatCount = (int?)response.Job.Repeats, - Payload = response.Job.Data.ToByteArray() - }; + return DeserializeJobResponse(response); } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { @@ -199,6 +189,29 @@ internal sealed class DaprJobsGrpcClient(Autogenerated.Dapr.DaprClient client, H throw new DaprException("Get job operation failed: the Dapr endpoint did not return the expected value."); } + /// + /// Testable method for performing job response deserialization. + /// + /// + /// This is exposed strictly for testing purposes. + /// + /// The job response to deserialize. + /// The deserialized job response. + internal static DaprJobDetails DeserializeJobResponse(Autogenerated.GetJobResponse response) + { + var schedule = DateTime.TryParse(response.Job.DueTime, out var dueTime) + ? DaprJobSchedule.FromDateTime(dueTime) + : new DaprJobSchedule(response.Job.Schedule); + + return new DaprJobDetails(schedule) + { + DueTime = !string.IsNullOrWhiteSpace(response.Job.DueTime) ? DateTime.Parse(response.Job.DueTime) : null, + Ttl = !string.IsNullOrWhiteSpace(response.Job.Ttl) ? DateTime.Parse(response.Job.Ttl) : null, + RepeatCount = (int?)response.Job.Repeats ?? 0, + Payload = response.Job.Data?.ToByteArray() ?? null + }; + } + /// /// Deletes the specified job. /// diff --git a/test/Dapr.Actors.Test/Runtime/ActorManagerTests.cs b/test/Dapr.Actors.Test/Runtime/ActorManagerTests.cs index 0054a73b..6aa60038 100644 --- a/test/Dapr.Actors.Test/Runtime/ActorManagerTests.cs +++ b/test/Dapr.Actors.Test/Runtime/ActorManagerTests.cs @@ -12,6 +12,8 @@ // ------------------------------------------------------------------------ using System; +using System.IO; +using System.Text; using System.Threading; using System.Threading.Tasks; using Dapr.Actors.Client; @@ -175,6 +177,160 @@ public sealed class ActorManagerTests Assert.Equal(1, activator.DeleteCallCount); } + [Fact] + public async Task DeserializeTimer_Period_Iso8601_Time() + { + const string timerJson = "{\"callback\": \"TimerCallback\", \"period\": \"0h0m7s10ms\"}"; + await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(timerJson)); + var result = await ActorManager.DeserializeAsync(stream); + + Assert.Equal("TimerCallback", result.Callback); + Assert.Equal(Array.Empty(), result.Data); + Assert.Null(result.Ttl); + Assert.Equal(TimeSpan.Zero, result.DueTime); + Assert.Equal(TimeSpan.FromSeconds(7).Add(TimeSpan.FromMilliseconds(10)), result.Period); + } + + [Fact] + public async Task DeserializeTimer_Period_DaprFormat_Every() + { + const string timerJson = "{\"callback\": \"TimerCallback\", \"period\": \"@every 15s\"}"; + await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(timerJson)); + var result = await ActorManager.DeserializeAsync(stream); + + Assert.Equal("TimerCallback", result.Callback); + Assert.Equal(Array.Empty(), result.Data); + Assert.Null(result.Ttl); + Assert.Equal(TimeSpan.Zero, result.DueTime); + Assert.Equal(TimeSpan.FromSeconds(15), result.Period); + } + + [Fact] + public async Task DeserializeTimer_Period_DaprFormat_Every2() + { + const string timerJson = "{\"callback\": \"TimerCallback\", \"period\": \"@every 3h2m15s\"}"; + await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(timerJson)); + var result = await ActorManager.DeserializeAsync(stream); + + Assert.Equal("TimerCallback", result.Callback); + Assert.Equal(Array.Empty(), result.Data); + Assert.Null(result.Ttl); + Assert.Equal(TimeSpan.Zero, result.DueTime); + Assert.Equal(TimeSpan.FromHours(3).Add(TimeSpan.FromMinutes(2)).Add(TimeSpan.FromSeconds(15)), result.Period); + } + + [Fact] + public async Task DeserializeTimer_Period_DaprFormat_Monthly() + { + const string timerJson = "{\"callback\": \"TimerCallback\", \"period\": \"@monthly\"}"; + await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(timerJson)); + var result = await ActorManager.DeserializeAsync(stream); + + Assert.Equal("TimerCallback", result.Callback); + Assert.Equal(Array.Empty(), result.Data); + Assert.Null(result.Ttl); + Assert.Equal(TimeSpan.Zero, result.DueTime); + Assert.Equal(TimeSpan.FromDays(30), result.Period); + } + + [Fact] + public async Task DeserializeTimer_Period_DaprFormat_Weekly() + { + const string timerJson = "{\"callback\": \"TimerCallback\", \"period\": \"@weekly\"}"; + await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(timerJson)); + var result = await ActorManager.DeserializeAsync(stream); + + Assert.Equal("TimerCallback", result.Callback); + Assert.Equal(Array.Empty(), result.Data); + Assert.Null(result.Ttl); + Assert.Equal(TimeSpan.Zero, result.DueTime); + Assert.Equal(TimeSpan.FromDays(7), result.Period); + } + + [Fact] + public async Task DeserializeTimer_Period_DaprFormat_Daily() + { + const string timerJson = "{\"callback\": \"TimerCallback\", \"period\": \"@daily\"}"; + await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(timerJson)); + var result = await ActorManager.DeserializeAsync(stream); + + Assert.Equal("TimerCallback", result.Callback); + Assert.Equal(Array.Empty(), result.Data); + Assert.Null(result.Ttl); + Assert.Equal(TimeSpan.Zero, result.DueTime); + Assert.Equal(TimeSpan.FromDays(1), result.Period); + } + + [Fact] + public async Task DeserializeTimer_Period_DaprFormat_Hourly() + { + const string timerJson = "{\"callback\": \"TimerCallback\", \"period\": \"@hourly\"}"; + await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(timerJson)); + var result = await ActorManager.DeserializeAsync(stream); + + Assert.Equal("TimerCallback", result.Callback); + Assert.Equal(Array.Empty(), result.Data); + Assert.Null(result.Ttl); + Assert.Equal(TimeSpan.Zero, result.DueTime); + Assert.Equal(TimeSpan.FromHours(1), result.Period); + } + + [Fact] + public async Task DeserializeTimer_DueTime_DaprFormat_Hourly() + { + const string timerJson = "{\"callback\": \"TimerCallback\", \"dueTime\": \"@hourly\"}"; + await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(timerJson)); + var result = await ActorManager.DeserializeAsync(stream); + + Assert.Equal("TimerCallback", result.Callback); + Assert.Equal(Array.Empty(), result.Data); + Assert.Null(result.Ttl); + Assert.Equal(TimeSpan.FromHours(1), result.DueTime); + Assert.Equal(TimeSpan.Zero, result.Period); + } + + [Fact] + public async Task DeserializeTimer_DueTime_Iso8601Times() + { + const string timerJson = "{\"callback\": \"TimerCallback\", \"dueTime\": \"0h0m7s10ms\"}"; + await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(timerJson)); + var result = await ActorManager.DeserializeAsync(stream); + + Assert.Equal("TimerCallback", result.Callback); + Assert.Equal(Array.Empty(), result.Data); + Assert.Null(result.Ttl); + Assert.Equal(TimeSpan.Zero, result.Period); + Assert.Equal(TimeSpan.FromSeconds(7).Add(TimeSpan.FromMilliseconds(10)), result.DueTime); + } + + [Fact] + public async Task DeserializeTimer_Ttl_DaprFormat_Hourly() + { + const string timerJson = "{\"callback\": \"TimerCallback\", \"ttl\": \"@hourly\"}"; + await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(timerJson)); + var result = await ActorManager.DeserializeAsync(stream); + + Assert.Equal("TimerCallback", result.Callback); + Assert.Equal(Array.Empty(), result.Data); + Assert.Equal(TimeSpan.Zero, result.DueTime); + Assert.Equal(TimeSpan.Zero, result.Period); + Assert.Equal(TimeSpan.FromHours(1), result.Ttl); + } + + [Fact] + public async Task DeserializeTimer_Ttl_Iso8601Times() + { + const string timerJson = "{\"callback\": \"TimerCallback\", \"ttl\": \"0h0m7s10ms\"}"; + await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(timerJson)); + var result = await ActorManager.DeserializeAsync(stream); + + Assert.Equal("TimerCallback", result.Callback); + Assert.Equal(Array.Empty(), result.Data); + Assert.Equal(TimeSpan.Zero, result.DueTime); + Assert.Equal(TimeSpan.Zero, result.Period); + Assert.Equal(TimeSpan.FromSeconds(7).Add(TimeSpan.FromMilliseconds(10)), result.Ttl); + } + private interface ITestActor : IActor { } private class TestActor : Actor, ITestActor, IDisposable @@ -255,4 +411,4 @@ public sealed class ActorManagerTests return base.DeleteAsync(state); } } -} \ No newline at end of file +} diff --git a/test/Dapr.Client.Test/CryptographyApiTest.cs b/test/Dapr.Client.Test/CryptographyApiTest.cs index 4bc0915e..17dc5ce6 100644 --- a/test/Dapr.Client.Test/CryptographyApiTest.cs +++ b/test/Dapr.Client.Test/CryptographyApiTest.cs @@ -30,28 +30,6 @@ public class CryptographyApiTest (ReadOnlyMemory) Array.Empty(), keyName, new EncryptionOptions(KeyWrapAlgorithm.Rsa), CancellationToken.None)); } - [Fact] - public async Task EncryptAsync_Stream_VaultResourceName_ArgumentVerifierException() - { - var client = new DaprClientBuilder().Build(); - const string vaultResourceName = ""; - //Get response and validate - await Assert.ThrowsAsync(async () => await client.EncryptAsync(vaultResourceName, - new MemoryStream(), "MyKey", new EncryptionOptions(KeyWrapAlgorithm.Rsa), - CancellationToken.None)); - } - - [Fact] - public async Task EncryptAsync_Stream_KeyName_ArgumentVerifierException() - { - var client = new DaprClientBuilder().Build(); - const string keyName = ""; - //Get response and validate - await Assert.ThrowsAsync(async () => await client.EncryptAsync("myVault", - (Stream) new MemoryStream(), keyName, new EncryptionOptions(KeyWrapAlgorithm.Rsa), - CancellationToken.None)); - } - [Fact] public async Task DecryptAsync_ByteArray_VaultResourceName_ArgumentVerifierException() { @@ -71,24 +49,4 @@ public class CryptographyApiTest await Assert.ThrowsAsync(async () => await client.DecryptAsync("myVault", Array.Empty(), keyName, new DecryptionOptions(), CancellationToken.None)); } - - [Fact] - public async Task DecryptAsync_Stream_VaultResourceName_ArgumentVerifierException() - { - var client = new DaprClientBuilder().Build(); - const string vaultResourceName = ""; - //Get response and validate - await Assert.ThrowsAsync(async () => await client.DecryptAsync(vaultResourceName, - new MemoryStream(), "MyKey", new DecryptionOptions(), CancellationToken.None)); - } - - [Fact] - public async Task DecryptAsync_Stream_KeyName_ArgumentVerifierException() - { - var client = new DaprClientBuilder().Build(); - const string keyName = ""; - //Get response and validate - await Assert.ThrowsAsync(async () => await client.DecryptAsync("myVault", - new MemoryStream(), keyName, new DecryptionOptions(), CancellationToken.None)); - } -} \ No newline at end of file +} diff --git a/test/Dapr.Jobs.Test/DaprJobsGrpcClientTests.cs b/test/Dapr.Jobs.Test/DaprJobsGrpcClientTests.cs index 0e04d130..b0192fb7 100644 --- a/test/Dapr.Jobs.Test/DaprJobsGrpcClientTests.cs +++ b/test/Dapr.Jobs.Test/DaprJobsGrpcClientTests.cs @@ -13,6 +13,7 @@ using System; using System.Net.Http; +using Dapr.Client.Autogen.Grpc.v1; using Dapr.Jobs.Models; using Moq; @@ -166,6 +167,21 @@ public sealed class DaprJobsGrpcClientTests }); #pragma warning restore CS0618 // Type or member is obsolete } + + [Fact] + public void ShouldDeserialize_EveryExpression() + { + const string scheduleText = "@every 1m"; + var response = new GetJobResponse { Job = new Job { Name = "test", Schedule = scheduleText } }; + var schedule = DaprJobSchedule.FromExpression(scheduleText); + + var jobDetails = DaprJobsGrpcClient.DeserializeJobResponse(response); + Assert.Null(jobDetails.Payload); + Assert.Equal(0, jobDetails.RepeatCount); + Assert.Null(jobDetails.Ttl); + Assert.Null(jobDetails.DueTime); + Assert.Equal(jobDetails.Schedule.ExpressionValue, schedule.ExpressionValue); + } private sealed record TestPayload(string Name, string Color); } From e1b216cc2e0b200f96ba9cdeb190d662384aff36 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Fri, 4 Jul 2025 05:23:40 -0500 Subject: [PATCH 19/22] Modify build/publish dependencies (#1575) * Today, a publish can happen without actually waiting on integration tests to pass. This changes this behavior. * Added validation requirement to Publish Packages to require that workflow_run dependency Signed-off-by: Whit Waldo --- .github/workflows/itests.yml | 8 ++++++-- .github/workflows/sdk_build.yml | 15 ++++++++++++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/.github/workflows/itests.yml b/.github/workflows/itests.yml index 2ebbd04e..b79edfab 100644 --- a/.github/workflows/itests.yml +++ b/.github/workflows/itests.yml @@ -5,6 +5,8 @@ on: branches: - master - release-* + - dev-* + - feature-* tags: - v* @@ -12,6 +14,8 @@ on: branches: - master - release-* + - dev-* + - feature-* jobs: build: @@ -119,7 +123,7 @@ jobs: - name: Build # disable deterministic builds, just for test run. Deterministic builds break coverage for some reason run: dotnet build --configuration release --no-restore /p:GITHUB_ACTIONS=false - - name: Run General Tests + - name: Run General Integration Tests id: tests continue-on-error: true # proceed if tests fail, the report step will report the failure with more details. run: | @@ -134,7 +138,7 @@ jobs: /p:CollectCoverage=true \ /p:CoverletOutputFormat=opencover \ /p:GITHUB_ACTIONS=false - - name: Run Generators Tests + - name: Run Generators Integration Tests id: generator-tests continue-on-error: true # proceed if tests fail, the report step will report the failure with more details. run: | diff --git a/.github/workflows/sdk_build.yml b/.github/workflows/sdk_build.yml index 7fc5eb67..327bba43 100644 --- a/.github/workflows/sdk_build.yml +++ b/.github/workflows/sdk_build.yml @@ -5,6 +5,8 @@ on: branches: - master - release-* + - dev-* + - feature-* tags: - v* @@ -12,6 +14,13 @@ on: branches: - master - release-* + - dev-* + - feature-* + + workflow_run: + workflows: [ "integration-test" ] + types: + - completed jobs: build: @@ -117,7 +126,11 @@ jobs: name: Publish Packages needs: ['build', 'test'] runs-on: ubuntu-latest - if: startswith(github.ref, 'refs/tags/v') && !(endsWith(github.ref, '-rc') || endsWith(github.ref, '-dev') || endsWith(github.ref, '-prerelease')) + # Only run this job if workflow_run was successful and we're on a tag + if: | + (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success') && + startswith(github.ref, 'refs/tags/v') && + !(endsWith(github.ref, '-rc') || endsWith(github.ref, '-dev') || endsWith(github.ref, '-prerelease')) steps: - name: Download release artifacts uses: actions/download-artifact@v4 From 08c0a3fef1b1af5ca727db1f091256fcbf10d60f Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 8 Jul 2025 06:13:08 -0500 Subject: [PATCH 20/22] Parallel workflow processing (#1580) * Added example to demonstrate parallel workflow concurrency --- Directory.Packages.props | 4 +- all.sln | 7 + .../OrderProcessingWorkflow.cs | 59 +++ .../WorkflowParallelFanOut/OrderRequest.cs | 16 + .../WorkflowParallelFanOut/OrderResult.cs | 21 + .../ProcessOrderActivity.cs | 98 +++++ .../WorkflowParallelFanOut/Program.cs | 82 ++++ .../WorkflowParallelFanOut.csproj | 17 + src/Dapr.Workflow/ParallelExtensions.cs | 148 +++++++ .../ParallelExtensionsTest.cs | 382 ++++++++++++++++++ 10 files changed, 832 insertions(+), 2 deletions(-) create mode 100644 examples/Workflow/WorkflowParallelFanOut/OrderProcessingWorkflow.cs create mode 100644 examples/Workflow/WorkflowParallelFanOut/OrderRequest.cs create mode 100644 examples/Workflow/WorkflowParallelFanOut/OrderResult.cs create mode 100644 examples/Workflow/WorkflowParallelFanOut/ProcessOrderActivity.cs create mode 100644 examples/Workflow/WorkflowParallelFanOut/Program.cs create mode 100644 examples/Workflow/WorkflowParallelFanOut/WorkflowParallelFanOut.csproj create mode 100644 src/Dapr.Workflow/ParallelExtensions.cs create mode 100644 test/Dapr.Workflow.Test/ParallelExtensionsTest.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index f0971244..9d95af64 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -17,8 +17,8 @@ - - + + diff --git a/all.sln b/all.sln index 706cf896..e73e2460 100644 --- a/all.sln +++ b/all.sln @@ -177,6 +177,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cryptography", "examples\Cr EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Messaging", "Messaging", "{442E80E5-8040-4123-B88A-26FD36BA95D9}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkflowParallelFanOut", "examples\Workflow\WorkflowParallelFanOut\WorkflowParallelFanOut.csproj", "{5764B1AA-66B8-43AE-9E0D-0B3B71714B92}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -469,6 +471,10 @@ Global {097D5F6F-D26F-4BFB-9074-FA52577EB442}.Debug|Any CPU.Build.0 = Debug|Any CPU {097D5F6F-D26F-4BFB-9074-FA52577EB442}.Release|Any CPU.ActiveCfg = Release|Any CPU {097D5F6F-D26F-4BFB-9074-FA52577EB442}.Release|Any CPU.Build.0 = Release|Any CPU + {5764B1AA-66B8-43AE-9E0D-0B3B71714B92}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5764B1AA-66B8-43AE-9E0D-0B3B71714B92}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5764B1AA-66B8-43AE-9E0D-0B3B71714B92}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5764B1AA-66B8-43AE-9E0D-0B3B71714B92}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -554,6 +560,7 @@ Global {097D5F6F-D26F-4BFB-9074-FA52577EB442} = {6843B5B3-9E95-4022-B792-8A1DE6BFEFEC} {442E80E5-8040-4123-B88A-26FD36BA95D9} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78} {E070F694-335D-4D96-8951-F41D0A5F2A8B} = {442E80E5-8040-4123-B88A-26FD36BA95D9} + {5764B1AA-66B8-43AE-9E0D-0B3B71714B92} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40} diff --git a/examples/Workflow/WorkflowParallelFanOut/OrderProcessingWorkflow.cs b/examples/Workflow/WorkflowParallelFanOut/OrderProcessingWorkflow.cs new file mode 100644 index 00000000..6efd01d9 --- /dev/null +++ b/examples/Workflow/WorkflowParallelFanOut/OrderProcessingWorkflow.cs @@ -0,0 +1,59 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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.Workflow; +using Microsoft.Extensions.Logging; + +namespace WorkflowParallelFanOut; + +public sealed partial class OrderProcessingWorkflow : Workflow +{ + /// + /// Override to implement workflow logic. + /// + /// The workflow context. + /// The deserialized workflow input. + /// The output of the workflow as a task. + public override async Task RunAsync(WorkflowContext context, OrderRequest[] orders) + { + var logger = context.CreateReplaySafeLogger(); + + if (!context.IsReplaying) + { + LogStartingOrderProcessorWorkflow(logger, orders.Length); + } + + //Process all orders in parallel with controlled concurrency + var orderResults = await context.ProcessInParallelAsync( + orders, + order => context.CallActivityAsync(nameof(ProcessOrderActivity), order), maxConcurrency: 5); + + //Calculate summary statistics + var totalProcessed = orderResults.Count(r => r.IsProcessed); + var totalFailed = orderResults.Length - totalProcessed; + var totalAmount = orderResults.Where(r => r.IsProcessed).Sum(r => r.TotalAmount); + + if (!context.IsReplaying) + { + LogCompletedProcessingWorkflow(logger, orders.Length, totalProcessed, totalFailed, totalAmount); + } + + return orderResults; + } + + [LoggerMessage(LogLevel.Information, "Starting order processing workflow with {OrderCount} orders")] + static partial void LogStartingOrderProcessorWorkflow(ILogger logger, int orderCount); + + [LoggerMessage(LogLevel.Information, "Completed processing {TotalOrders} orders. Processed: {ProcessedCount}, Failed: {FailedCount}, Total Amount: {TotalAmount:c}")] + static partial void LogCompletedProcessingWorkflow(ILogger logger, int totalOrders, int processedCount, int failedCount, decimal totalAmount); +} diff --git a/examples/Workflow/WorkflowParallelFanOut/OrderRequest.cs b/examples/Workflow/WorkflowParallelFanOut/OrderRequest.cs new file mode 100644 index 00000000..a752bd2e --- /dev/null +++ b/examples/Workflow/WorkflowParallelFanOut/OrderRequest.cs @@ -0,0 +1,16 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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 WorkflowParallelFanOut; + +public sealed record OrderRequest(string OrderId, string ProductName, int Quantity, decimal Price); diff --git a/examples/Workflow/WorkflowParallelFanOut/OrderResult.cs b/examples/Workflow/WorkflowParallelFanOut/OrderResult.cs new file mode 100644 index 00000000..b09188dc --- /dev/null +++ b/examples/Workflow/WorkflowParallelFanOut/OrderResult.cs @@ -0,0 +1,21 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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 WorkflowParallelFanOut; + +public sealed record OrderResult( + string OrderId, + bool IsProcessed, + decimal TotalAmount, + string Status, + DateTime ProcessedAt); diff --git a/examples/Workflow/WorkflowParallelFanOut/ProcessOrderActivity.cs b/examples/Workflow/WorkflowParallelFanOut/ProcessOrderActivity.cs new file mode 100644 index 00000000..ba0cb771 --- /dev/null +++ b/examples/Workflow/WorkflowParallelFanOut/ProcessOrderActivity.cs @@ -0,0 +1,98 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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.Workflow; +using Microsoft.Extensions.Logging; +using ILogger = Microsoft.Extensions.Logging.ILogger; + +namespace WorkflowParallelFanOut; + +public sealed partial class ProcessOrderActivity(ILogger logger) : WorkflowActivity +{ + private static readonly Random Random = new(); + + /// + /// Override to implement async (non-blocking) workflow activity logic. + /// + /// Provides access to additional context for the current activity execution. + /// The deserialized activity input. + /// The output of the activity as a task. + public override async Task RunAsync(WorkflowActivityContext context, OrderRequest order) + { + LogProcessingOrder(logger, order.OrderId, order.ProductName); + + //Simulate processing time (between 100 and 2000ms) + var processingTime = Random.Next(100, 2000); + await Task.Delay(processingTime); + + //Simulate occasional failures (10% chance) + var shouldFail = Random.Next(1, 101) <= 10; + + if (shouldFail) + { + LogOrderFailed(logger, order.OrderId); + return new OrderResult( + order.OrderId, + IsProcessed: false, + TotalAmount: 0, + Status: "Failed - System Error", + ProcessedAt: DateTime.UtcNow); + } + + // Simulate inventory check + var hasInventory = Random.Next(1, 101) <= 90; // 90% chance of having inventory + if (!hasInventory) + { + LogInsufficientInventory(logger, order.OrderId); + return new OrderResult( + order.OrderId, + IsProcessed: false, + TotalAmount: 0, + Status: "Failed - Insufficient Inventory", + ProcessedAt: DateTime.UtcNow); + } + + //Calculate total amount (with potential discount) + var totalAmount = order.Quantity * order.Price; + + // Apply bulk discount for large orders + if (order.Quantity >= 10) + { + totalAmount *= 0.9m; // 10% discount + LogDiscountApplied(logger, order.OrderId); + } + + LogSuccessfullyProcessedOrder(logger, order.OrderId, totalAmount); + return new OrderResult( + order.OrderId, + IsProcessed: true, + TotalAmount: totalAmount, + Status: "Processed Successfully", + ProcessedAt: DateTime.UtcNow); + } + + [LoggerMessage(LogLevel.Information, "Processing order {OrderId} for product {ProductName}")] + static partial void LogProcessingOrder(ILogger logger, string orderId, string productName); + + [LoggerMessage(LogLevel.Warning, "Order {OrderId} failed during processing")] + static partial void LogOrderFailed(ILogger logger, string orderId); + + [LoggerMessage(LogLevel.Warning, "Order {OrderId} failed - insufficient inventory")] + static partial void LogInsufficientInventory(ILogger logger, string orderId); + + [LoggerMessage(LogLevel.Information, "Applied bulk discount to order {OrderId}")] + static partial void LogDiscountApplied(ILogger logger, string orderId); + + [LoggerMessage(LogLevel.Information, "Successfully processed order {OrderId} with total amount {TotalAmount:c}")] + static partial void LogSuccessfullyProcessedOrder(ILogger logger, string orderId, decimal totalAmount); +} diff --git a/examples/Workflow/WorkflowParallelFanOut/Program.cs b/examples/Workflow/WorkflowParallelFanOut/Program.cs new file mode 100644 index 00000000..1c7cc08a --- /dev/null +++ b/examples/Workflow/WorkflowParallelFanOut/Program.cs @@ -0,0 +1,82 @@ +using Dapr.Workflow; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using WorkflowParallelFanOut; + +var builder = Host.CreateDefaultBuilder(args).ConfigureServices(services => +{ + services.AddDaprWorkflow(options => + { + options.RegisterWorkflow(); + options.RegisterActivity(); + }); +}); + +var host = builder.Build(); +await host.StartAsync(); + +await using var scope = host.Services.CreateAsyncScope(); +var daprWorkflowClient = scope.ServiceProvider.GetRequiredService(); +var logger = scope.ServiceProvider.GetRequiredService>(); + +// Create sample orders to demonstrate parallel processing +var orders = new[] +{ + new OrderRequest("ORD-001", "Laptop", 2, 999.99m), + new OrderRequest("ORD-002", "Mouse", 5, 29.99m), + new OrderRequest("ORD-003", "Keyboard", 3, 79.99m), + new OrderRequest("ORD-004", "Monitor", 1, 299.99m), + new OrderRequest("ORD-005", "Headphones", 4, 149.99m), + new OrderRequest("ORD-006", "Webcam", 2, 89.99m), + new OrderRequest("ORD-007", "Printer", 1, 199.99m), + new OrderRequest("ORD-008", "Tablet", 6, 249.99m), + new OrderRequest("ORD-009", "Phone", 1, 799.99m), + new OrderRequest("ORD-010", "Charger", 10, 24.99m), // Bulk order for discount + new OrderRequest("ORD-011", "Cable", 20, 9.99m), // Bulk order for discount + new OrderRequest("ORD-012", "Speakers", 3, 119.99m), + new OrderRequest("ORD-013", "Router", 2, 149.99m), + new OrderRequest("ORD-014", "Hard Drive", 4, 129.99m), + new OrderRequest("ORD-015", "Graphics Card", 1, 599.99m) +}; + +logger.LogInformation("Starting workflow with {OrderCount} orders", orders.Length); + +var instanceId = $"orderprocessing-workflow-{Guid.NewGuid().ToString()[..8]}"; +await daprWorkflowClient.ScheduleNewWorkflowAsync(nameof(OrderProcessingWorkflow), instanceId, orders); + +logger.LogInformation("Workflow {InstanceId} started, waiting for completion...", instanceId); + +// Wait for workflow completion +await daprWorkflowClient.WaitForWorkflowCompletionAsync(instanceId); +var state = await daprWorkflowClient.GetWorkflowStateAsync(instanceId); + +logger.LogInformation("Workflow {InstanceId} completed with status: {Status}", instanceId, state.RuntimeStatus); + +if (state.ReadOutputAs() is { } results) +{ + logger.LogInformation("Processing Results:"); + logger.LogInformation("=================="); + + var processedOrders = results.Where(r => r.IsProcessed).ToList(); + var failedOrders = results.Where(r => !r.IsProcessed).ToList(); + + logger.LogInformation("Successfully processed {ProcessedCount} orders:", processedOrders.Count); + foreach (var order in processedOrders) + { + logger.LogInformation(" - {OrderId}: {TotalAmount:C} ({Status})", + order.OrderId, order.TotalAmount, order.Status); + } + + if (failedOrders.Any()) + { + logger.LogWarning("Failed orders ({FailedCount}):", failedOrders.Count); + foreach (var order in failedOrders) + { + logger.LogWarning(" - {OrderId}: {Status}", order.OrderId, order.Status); + } + } + + var totalAmount = processedOrders.Sum(r => r.TotalAmount); + logger.LogInformation("Total processed amount: {TotalAmount:C}", totalAmount); +} diff --git a/examples/Workflow/WorkflowParallelFanOut/WorkflowParallelFanOut.csproj b/examples/Workflow/WorkflowParallelFanOut/WorkflowParallelFanOut.csproj new file mode 100644 index 00000000..b2bed19f --- /dev/null +++ b/examples/Workflow/WorkflowParallelFanOut/WorkflowParallelFanOut.csproj @@ -0,0 +1,17 @@ + + + + Exe + enable + enable + + + + + + + + + + + diff --git a/src/Dapr.Workflow/ParallelExtensions.cs b/src/Dapr.Workflow/ParallelExtensions.cs new file mode 100644 index 00000000..cc15ac74 --- /dev/null +++ b/src/Dapr.Workflow/ParallelExtensions.cs @@ -0,0 +1,148 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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.Threading.Tasks; + +namespace Dapr.Workflow; + +/// +/// Extension methods for that provide high-level parallel processing primitives +/// with controlled concurrency. +/// +public static class ParallelExtensions +{ + /// + /// Processes a collection of inputs in parallel with controlled concurrency using a streaming execution model. + /// + /// The type of input items to process. + /// The type of result items returned by the task factory. + /// The orchestration context. + /// The collection of inputs to process in parallel. + /// + /// A function that creates a task for each input item. This function is called in the orchestration context + /// to ensure all tasks are properly tracked by the durable task framework. + /// + /// + /// The maximum number of tasks to execute concurrently. Defaults to 5 if not specified. + /// Must be greater than 0. + /// + /// + /// A task that completes when all input items have been processed. The result is an array containing + /// the results in the same order as the input collection. + /// + /// + /// Thrown when , , or is null. + /// + /// + /// Thrown when is less than or equal to 0. + /// + /// + /// Thrown when one or more tasks fail during execution. All task exceptions are collected and wrapped. + /// + /// + /// + /// This method uses a streaming execution model that maintains constant memory usage regardless of input size. + /// Only tasks are active at any given time, with new tasks started as + /// existing ones complete. This provides optimal resource utilization and prevents memory issues with large datasets. + /// + /// + /// The method is fully deterministic for durable task orchestrations. All tasks are created in the orchestration + /// context before any coordination logic begins, ensuring proper replay behavior. The framework records history + /// events for each task creation, and during replay, all tasks complete immediately with their recorded results. + /// + /// + /// If any task fails, the method will wait for all currently executing tasks to complete before throwing an + /// containing all failures. + /// + /// + /// Example usage: + /// + /// var orderIds = new[] { "order1", "order2", "order3", "order4", "order5" }; + /// var results = await context.ProcessInParallelAsync( + /// orderIds, + /// orderId => context.CallActivityAsync<OrderResult>("ProcessOrder", orderId), + /// maxConcurrency: 3); + /// + /// + /// + public static async Task ProcessInParallelAsync( + this WorkflowContext context, + IEnumerable inputs, + Func> taskFactory, + int maxConcurrency = 5) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(inputs); + ArgumentNullException.ThrowIfNull(taskFactory); + if (maxConcurrency <= 0) + throw new ArgumentOutOfRangeException(nameof(maxConcurrency), "Max concurrency must be greater than 0."); + + var inputList = inputs.ToList(); + if (inputList.Count == 0) + return []; + + var results = new TResult[inputList.Count]; + var inFlightTasks = new Dictionary, int>(); // Task -> result index + var inputIndex = 0; + var completedCount = 0; + var exceptions = new List(); + + // Start initial batch up to maxConcurrency + while (inputIndex < inputList.Count && inFlightTasks.Count < maxConcurrency) + { + var task = taskFactory(inputList[inputIndex]); + inFlightTasks[task] = inputIndex; + inputIndex++; + } + + // Process remaining items with streaming execution + while (completedCount < inputList.Count) + { + var completedTask = await Task.WhenAny(inFlightTasks.Keys); + var resultIndex = inFlightTasks[completedTask]; + + try + { + results[resultIndex] = await completedTask; + } + catch (Exception ex) + { + exceptions.Add(ex); + } + + inFlightTasks.Remove(completedTask); + completedCount++; + + // Start next task if more work remains + if (inputIndex < inputList.Count) + { + var nextTask = taskFactory(inputList[inputIndex]); + inFlightTasks[nextTask] = inputIndex; + inputIndex++; + } + } + + // If any exceptions occurred, throw them as an aggregate + if (exceptions.Count > 0) + { + throw new AggregateException( + $"One or more tasks failed during parallel processing. {exceptions.Count} out of {inputList.Count} tasks failed.", + exceptions); + } + + return results; + } +} diff --git a/test/Dapr.Workflow.Test/ParallelExtensionsTest.cs b/test/Dapr.Workflow.Test/ParallelExtensionsTest.cs new file mode 100644 index 00000000..a4f38fd8 --- /dev/null +++ b/test/Dapr.Workflow.Test/ParallelExtensionsTest.cs @@ -0,0 +1,382 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 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 Moq; + +namespace Dapr.Workflow.Test; + +/// +/// Contains tests for ParallelExtensions.ProcessInParallelAsync method. +/// +public class ParallelExtensionsTest +{ + private readonly Mock _workflowContextMock = new(); + + [Fact] + public async Task ProcessInParallelAsync_WithValidInputs_ShouldProcessAllItemsSuccessfully() + { + // Arrange + var inputs = new[] { 1, 2, 3, 4, 5 }; + var expectedResults = new[] { 2, 4, 6, 8, 10 }; + + // Act + var results = await _workflowContextMock.Object.ProcessInParallelAsync( + inputs, + async input => await Task.FromResult(input * 2), + maxConcurrency: 2); + + // Assert + Assert.Equal(expectedResults, results); + } + + [Fact] + public async Task ProcessInParallelAsync_WithEmptyInputs_ShouldReturnEmptyArray() + { + // Arrange + var inputs = Array.Empty(); + + // Act + var results = await _workflowContextMock.Object.ProcessInParallelAsync( + inputs, + async input => await Task.FromResult(input * 2)); + + // Assert + Assert.Empty(results); + } + + [Fact] + public async Task ProcessInParallelAsync_WithSingleInput_ShouldProcessCorrectly() + { + // Arrange + var inputs = new[] { 42 }; + + // Act + var results = await _workflowContextMock.Object.ProcessInParallelAsync( + inputs, + async input => await Task.FromResult(input.ToString())); + + // Assert + Assert.Single(results); + Assert.Equal("42", results[0]); + } + + [Fact] + public async Task ProcessInParallelAsync_WithMaxConcurrency1_ShouldProcessSequentially() + { + // Arrange + var inputs = new[] { 1, 2, 3 }; + var processedOrder = new List(); + var processingTasks = new List(); + + // Act + var results = await _workflowContextMock.Object.ProcessInParallelAsync( + inputs, + async input => + { + processedOrder.Add(input); + await Task.Delay(10); // Small delay to ensure order + return input * 2; + }, + maxConcurrency: 1); + + // Assert + Assert.Equal(new[] { 2, 4, 6 }, results); + Assert.Equal(new[] { 1, 2, 3 }, processedOrder); + } + + [Fact] + public async Task ProcessInParallelAsync_WithHighConcurrency_ShouldRespectConcurrencyLimit() + { + // Arrange + var inputs = Enumerable.Range(1, 100).ToArray(); + var concurrentTasks = 0; + var maxConcurrentTasks = 0; + var lockObj = new object(); + + // Act + var results = await _workflowContextMock.Object.ProcessInParallelAsync( + inputs, + async input => + { + lock (lockObj) + { + concurrentTasks++; + maxConcurrentTasks = Math.Max(maxConcurrentTasks, concurrentTasks); + } + + await Task.Delay(10); + + lock (lockObj) + { + concurrentTasks--; + } + + return input * 2; + }, + maxConcurrency: 10); + + // Assert + Assert.Equal(inputs.Length, results.Length); + Assert.True(maxConcurrentTasks <= 10, $"Expected max concurrent tasks <= 10, but was {maxConcurrentTasks}"); + Assert.True(maxConcurrentTasks >= 1, "At least one task should have been executed"); + } + + [Fact] + public async Task ProcessInParallelAsync_WithNullContext_ShouldThrowArgumentNullException() + { + // Arrange + WorkflowContext nullContext = null!; + var inputs = new[] { 1, 2, 3 }; + + // Act & Assert + await Assert.ThrowsAsync( + async () => await nullContext.ProcessInParallelAsync( + inputs, + async input => await Task.FromResult(input * 2))); + } + + [Fact] + public async Task ProcessInParallelAsync_WithNullInputs_ShouldThrowArgumentNullException() + { + // Arrange + IEnumerable nullInputs = null!; + + // Act & Assert + await Assert.ThrowsAsync( + async () => await _workflowContextMock.Object.ProcessInParallelAsync( + nullInputs, + async input => await Task.FromResult(input * 2))); + } + + [Fact] + public async Task ProcessInParallelAsync_WithNullTaskFactory_ShouldThrowArgumentNullException() + { + // Arrange + var inputs = new[] { 1, 2, 3 }; + Func> nullTaskFactory = null!; + + // Act & Assert + await Assert.ThrowsAsync( + async () => await _workflowContextMock.Object.ProcessInParallelAsync( + inputs, + nullTaskFactory)); + } + + [Theory] + [InlineData(0)] + [InlineData(-1)] + [InlineData(-10)] + public async Task ProcessInParallelAsync_WithInvalidMaxConcurrency_ShouldThrowArgumentOutOfRangeException(int maxConcurrency) + { + // Arrange + var inputs = new[] { 1, 2, 3 }; + + // Act & Assert + await Assert.ThrowsAsync( + async () => await _workflowContextMock.Object.ProcessInParallelAsync( + inputs, + async input => await Task.FromResult(input * 2), + maxConcurrency)); + } + + [Fact] + public async Task ProcessInParallelAsync_WithTaskFailure_ShouldThrowAggregateException() + { + // Arrange + var inputs = new[] { 1, 2, 3, 4, 5 }; + //var expectedSuccessfulResults = 3; // Items 1, 3, 5 should succeed + const int expectedFailures = 2; // Items 2, 4 should fail + + // Act & Assert + var aggregateException = await Assert.ThrowsAsync( + async () => await _workflowContextMock.Object.ProcessInParallelAsync( + inputs, + async input => + { + await Task.Delay(10); + if (input % 2 == 0) // Even numbers fail + throw new InvalidOperationException($"Failed processing item {input}"); + return input * 2; + }, + maxConcurrency: 2)); + + // Assert + Assert.Equal(expectedFailures, aggregateException.InnerExceptions.Count); + Assert.All(aggregateException.InnerExceptions, ex => + Assert.IsType(ex)); + Assert.Contains("2 out of 5 tasks failed", aggregateException.Message); + } + + [Fact] + public async Task ProcessInParallelAsync_WithAllTasksFailure_ShouldThrowAggregateExceptionWithAllFailures() + { + // Arrange + var inputs = new[] { 1, 2, 3 }; + var expectedMessage = "Test failure"; + + // Act & Assert + var aggregateException = await Assert.ThrowsAsync( + async () => await _workflowContextMock.Object.ProcessInParallelAsync( + inputs, + async input => + { + await Task.Delay(10); + throw new InvalidOperationException($"{expectedMessage} {input}"); + })); + + // Assert + Assert.Equal(3, aggregateException.InnerExceptions.Count); + Assert.All(aggregateException.InnerExceptions, ex => + { + Assert.IsType(ex); + Assert.Contains(expectedMessage, ex.Message); + }); + } + + [Fact] + public async Task ProcessInParallelAsync_WithMixedSuccessAndFailure_ShouldPreserveOrderInResults() + { + // Arrange + var inputs = new[] { 1, 2, 3, 4, 5 }; + + // Act & Assert + var aggregateException = await Assert.ThrowsAsync( + async () => await _workflowContextMock.Object.ProcessInParallelAsync( + inputs, + async input => + { + await Task.Delay(input * 10); // Different delays to test ordering + if (input == 3) + throw new InvalidOperationException($"Failed on item {input}"); + return input * 2; + }, + maxConcurrency: 2)); + + // Assert that failure occurred + Assert.Single(aggregateException.InnerExceptions); + Assert.Contains("Failed on item 3", aggregateException.InnerExceptions[0].Message); + } + + [Fact] + public async Task ProcessInParallelAsync_WithDefaultMaxConcurrency_ShouldUseDefaultValue() + { + // Arrange + var inputs = Enumerable.Range(1, 20).ToArray(); + var concurrentTasks = 0; + var maxConcurrentTasks = 0; + var lockObj = new object(); + + // Act + var results = await _workflowContextMock.Object.ProcessInParallelAsync( + inputs, + async input => + { + lock (lockObj) + { + concurrentTasks++; + maxConcurrentTasks = Math.Max(maxConcurrentTasks, concurrentTasks); + } + + await Task.Delay(50); // Longer delay to ensure concurrency + + lock (lockObj) + { + concurrentTasks--; + } + + return input * 2; + }); // Using default maxConcurrency (should be 5) + + // Assert + Assert.Equal(inputs.Length, results.Length); + Assert.True(maxConcurrentTasks <= 5, $"Expected max concurrent tasks <= 5, but was {maxConcurrentTasks}"); + Assert.True(maxConcurrentTasks >= 1, "At least one task should have been executed"); + } + + [Fact] + public async Task ProcessInParallelAsync_WithDifferentInputAndOutputTypes_ShouldHandleTypeConversion() + { + // Arrange + var inputs = new[] { "1", "2", "3", "4", "5" }; + var expectedResults = new[] { 1, 2, 3, 4, 5 }; + + // Act + var results = await _workflowContextMock.Object.ProcessInParallelAsync( + inputs, + async input => await Task.FromResult(int.Parse(input)), + maxConcurrency: 3); + + // Assert + Assert.Equal(expectedResults, results); + } + + [Fact] + public async Task ProcessInParallelAsync_WithComplexObjects_ShouldProcessCorrectly() + { + // Arrange + var inputs = new[] + { + new TestInput { Id = 1, Value = "Test1" }, + new TestInput { Id = 2, Value = "Test2" }, + new TestInput { Id = 3, Value = "Test3" } + }; + + // Act + var results = await _workflowContextMock.Object.ProcessInParallelAsync( + inputs, + async input => await Task.FromResult(new TestOutput + { + ProcessedId = input.Id * 10, + ProcessedValue = input.Value.ToUpper() + }), + maxConcurrency: 2); + + // Assert + Assert.Equal(3, results.Length); + Assert.Equal(10, results[0].ProcessedId); + Assert.Equal("TEST1", results[0].ProcessedValue); + Assert.Equal(20, results[1].ProcessedId); + Assert.Equal("TEST2", results[1].ProcessedValue); + Assert.Equal(30, results[2].ProcessedId); + Assert.Equal("TEST3", results[2].ProcessedValue); + } + + [Fact] + public async Task ProcessInParallelAsync_WithLargeDataset_ShouldHandleEfficiently() + { + // Arrange + var inputs = Enumerable.Range(1, 1000).ToArray(); + var expectedResults = inputs.Select(x => x * 2).ToArray(); + + // Act + var results = await _workflowContextMock.Object.ProcessInParallelAsync( + inputs, + async input => await Task.FromResult(input * 2), + maxConcurrency: 10); + + // Assert + Assert.Equal(expectedResults, results); + } + + private class TestInput + { + public int Id { get; set; } + public string Value { get; set; } = string.Empty; + } + + private class TestOutput + { + public int ProcessedId { get; set; } + public string ProcessedValue { get; set; } = string.Empty; + } +} From 704ccdfdc47058a614bd70b3ed0e5537a975d595 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sat, 12 Jul 2025 12:06:52 -0500 Subject: [PATCH 21/22] Fix 1.16 build errors (#1585) * Fixed build errors --- ...InvokeServiceHttpNonDaprEndpointExample.cs | 73 ++++++++------- examples/Client/ServiceInvocation/Program.cs | 22 ++--- properties/dapr_managed_netcore.props | 10 +-- src/Dapr.AspNetCore/BulkSubscribeAttribute.cs | 88 +++++++++---------- src/Dapr.Workflow/WorkflowLoggingService.cs | 84 ++++++++---------- 5 files changed, 134 insertions(+), 143 deletions(-) diff --git a/examples/Client/ServiceInvocation/InvokeServiceHttpNonDaprEndpointExample.cs b/examples/Client/ServiceInvocation/InvokeServiceHttpNonDaprEndpointExample.cs index bc0d7c59..2647ac9d 100644 --- a/examples/Client/ServiceInvocation/InvokeServiceHttpNonDaprEndpointExample.cs +++ b/examples/Client/ServiceInvocation/InvokeServiceHttpNonDaprEndpointExample.cs @@ -17,48 +17,45 @@ using System.Threading; using System.Threading.Tasks; using Dapr.Client; -namespace Samples.Client +namespace Samples.Client; + +public class InvokeServiceHttpNonDaprEndpointExample : Example { - public class InvokeServiceHttpNonDaprEndpointExample : Example + public override string DisplayName => "Invoke a non Dapr endpoint using DaprClient"; + + public override async Task RunAsync(CancellationToken cancellationToken) { - public override string DisplayName => "Invoke a non Dapr endpoint using DaprClient"; - - public override async Task RunAsync(CancellationToken cancellationToken) - { - using var client = new DaprClientBuilder().Build(); + using var client = new DaprClientBuilder().Build(); - // Invoke a POST method named "deposit" that takes input of type "Transaction" as defined in the RoutingSample. - Console.WriteLine("Invoking deposit using non Dapr endpoint."); - var data = new { id = "17", amount = 99m }; - var account = await client.InvokeMethodAsync("http://localhost:5000", "deposit", data, cancellationToken); - Console.WriteLine("Returned: id:{0} | Balance:{1}", account.Id, account.Balance); + // Invoke a POST method named "deposit" that takes input of type "Transaction" as defined in the RoutingSample. + Console.WriteLine("Invoking deposit using non Dapr endpoint."); + var data = new { id = "17", amount = 99m }; + var account = await client.InvokeMethodAsync("http://localhost:5000", "deposit", data, cancellationToken); + Console.WriteLine("Returned: id:{0} | Balance:{1}", account.Id, account.Balance); - // Invokes a POST method named "Withdraw" that takes input of type "Transaction" as defined in the RoutingSample. - Console.WriteLine("Invoking withdraw using non Dapr endpoint."); - data = new { id = "17", amount = 10m, }; - await client.InvokeMethodAsync("http://localhost:5000", "withdraw", data, cancellationToken); - Console.WriteLine("Completed"); + // Invokes a POST method named "Withdraw" that takes input of type "Transaction" as defined in the RoutingSample. + Console.WriteLine("Invoking withdraw using non Dapr endpoint."); + data = new { id = "17", amount = 10m, }; + await client.InvokeMethodAsync("http://localhost:5000", "withdraw", data, cancellationToken); + Console.WriteLine("Completed"); - // Invokes a GET method named "hello" that takes input of type "MyData" and returns a string. - Console.WriteLine("Invoking balance using non Dapr endpoint."); - account = await client.InvokeMethodAsync(HttpMethod.Get, "http://localhost:5000", "17", cancellationToken); - Console.WriteLine($"Received balance {account.Balance}"); - } - - internal class Transaction - { - public string? Id { get; set; } - - public decimal Amount { get; set; } - } - - internal class Account - { - public string? Id { get; set; } - - public decimal Balance { get; set; } - } + // Invokes a GET method named "hello" that takes input of type "MyData" and returns a string. + Console.WriteLine("Invoking balance using non Dapr endpoint."); + account = await client.InvokeMethodAsync(HttpMethod.Get, "http://localhost:5000", "17", cancellationToken); + Console.WriteLine($"Received balance {account.Balance}"); } -} - + internal class Transaction + { + public string? Id { get; set; } + + public decimal Amount { get; set; } + } + + internal class Account + { + public string? Id { get; set; } + + public decimal Balance { get; set; } + } +} \ No newline at end of file diff --git a/examples/Client/ServiceInvocation/Program.cs b/examples/Client/ServiceInvocation/Program.cs index ccf54ddd..829789b1 100644 --- a/examples/Client/ServiceInvocation/Program.cs +++ b/examples/Client/ServiceInvocation/Program.cs @@ -15,24 +15,24 @@ using System; using System.Threading; using System.Threading.Tasks; -namespace Samples.Client +namespace Samples.Client; + +class Program { - class Program - { - private static readonly Example[] Examples = new Example[] - { - new InvokeServiceGrpcExample(), - new InvokeServiceHttpExample(), - new InvokeServiceHttpClientExample(), - new InvokeServiceHttpNonDaprEndpointExample() - }; + private static readonly Example[] Examples = + [ + new InvokeServiceGrpcExample(), + new InvokeServiceHttpExample(), + new InvokeServiceHttpClientExample(), + new InvokeServiceHttpNonDaprEndpointExample() + ]; static async Task Main(string[] args) { if (args.Length > 0 && int.TryParse(args[0], out var index) && index >= 0 && index < Examples.Length) { var cts = new CancellationTokenSource(); - Console.CancelKeyPress += (object? sender, ConsoleCancelEventArgs e) => cts.Cancel(); + Console.CancelKeyPress += (sender, e) => cts.Cancel(); await Examples[index].RunAsync(cts.Token); return 0; diff --git a/properties/dapr_managed_netcore.props b/properties/dapr_managed_netcore.props index 4dab746e..be848b26 100644 --- a/properties/dapr_managed_netcore.props +++ b/properties/dapr_managed_netcore.props @@ -40,10 +40,10 @@ - Microsoft Dapr - Copyright (c) Microsoft Corporation. All rights reserved. - Microsoft - Microsoft + Dapr + Copyright (c) The Dapr Authors. All rights reserved. + The Dapr Authors + The Dapr Authors @@ -62,7 +62,7 @@ v - rc + rc diff --git a/src/Dapr.AspNetCore/BulkSubscribeAttribute.cs b/src/Dapr.AspNetCore/BulkSubscribeAttribute.cs index 4bf63b0e..09b5a953 100644 --- a/src/Dapr.AspNetCore/BulkSubscribeAttribute.cs +++ b/src/Dapr.AspNetCore/BulkSubscribeAttribute.cs @@ -13,58 +13,58 @@ using System; -namespace Dapr.AspNetCore +namespace Dapr.AspNetCore; + +/// +/// BulkSubscribeAttribute describes options for a bulk subscriber with respect to a topic. +/// It needs to be paired with at least one [Topic] depending on the use case. +/// +[AttributeUsage(AttributeTargets.All, AllowMultiple = true)] +public class BulkSubscribeAttribute : Attribute, IBulkSubscribeMetadata { /// - /// BulkSubscribeAttribute describes options for a bulk subscriber with respect to a topic. - /// It needs to be paired with at least one [Topic] depending on the use case. + /// Initializes a new instance of the class. /// - [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] - public class BulkSubscribeAttribute : Attribute, IBulkSubscribeMetadata + /// The name of the topic to be bulk subscribed. + /// Maximum number of messages in a bulk message from the broker. + /// Maximum duration in milliseconds to wait for maxMessagesCount messages by the broker. + public BulkSubscribeAttribute(string topicName, int maxMessagesCount, int maxAwaitDurationMs) { - /// - /// Initializes a new instance of the class. - /// - /// The name of the topic to be bulk subscribed. - /// Maximum number of messages in a bulk message from the broker. - /// Maximum duration in milliseconds to wait for maxMessagesCount messages by the broker. - public BulkSubscribeAttribute(string topicName, int maxMessagesCount, int maxAwaitDurationMs) - { - this.TopicName = topicName; - this.MaxMessagesCount = maxMessagesCount; - this.MaxAwaitDurationMs = maxAwaitDurationMs; - } + this.TopicName = topicName; + this.MaxMessagesCount = maxMessagesCount; + this.MaxAwaitDurationMs = maxAwaitDurationMs; + } - /// - /// Initializes a new instance of the class. - /// - /// The name of the topic to be bulk subscribed. - /// Maximum number of messages in a bulk message from the broker. - public BulkSubscribeAttribute(string topicName, int maxMessagesCount) - { - this.TopicName = topicName; - this.MaxMessagesCount = maxMessagesCount; - } + /// + /// Initializes a new instance of the class. + /// + /// The name of the topic to be bulk subscribed. + /// Maximum number of messages in a bulk message from the broker. + public BulkSubscribeAttribute(string topicName, int maxMessagesCount) + { + this.TopicName = topicName; + this.MaxMessagesCount = maxMessagesCount; + } - /// - /// Initializes a new instance of the class. - /// - /// The name of the topic to be bulk subscribed. - public BulkSubscribeAttribute(string topicName) - { - this.TopicName = topicName; - } + /// + /// Initializes a new instance of the class. + /// + /// The name of the topic to be bulk subscribed. + public BulkSubscribeAttribute(string topicName) + { + this.TopicName = topicName; + } - /// - /// Maximum number of messages in a bulk message from the broker. - /// - public int MaxMessagesCount { get; } = 100; + /// + /// Maximum number of messages in a bulk message from the broker. + /// + public int MaxMessagesCount { get; } = 100; - /// - /// Maximum duration in milliseconds to wait for MaxMessagesCount messages by the broker - /// before sending the messages to Dapr. - /// - public int MaxAwaitDurationMs { get; } = 1000; + /// + /// Maximum duration in milliseconds to wait for MaxMessagesCount messages by the broker + /// before sending the messages to Dapr. + /// + public int MaxAwaitDurationMs { get; } = 1000; /// /// The name of the topic to be bulk subscribed. diff --git a/src/Dapr.Workflow/WorkflowLoggingService.cs b/src/Dapr.Workflow/WorkflowLoggingService.cs index 5a72c8a1..c8930eef 100644 --- a/src/Dapr.Workflow/WorkflowLoggingService.cs +++ b/src/Dapr.Workflow/WorkflowLoggingService.cs @@ -11,61 +11,55 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Workflow +namespace Dapr.Workflow; + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System.Collections.Concurrent; + +/// +/// Defines runtime options for workflows. +/// +internal sealed class WorkflowLoggingService(ILogger logger) : IHostedService { - using System.Threading; - using System.Threading.Tasks; - using Microsoft.Extensions.Hosting; - using Microsoft.Extensions.Logging; - using System.Collections.Concurrent; + private static readonly ConcurrentDictionary registeredWorkflows = new(); + private static readonly ConcurrentDictionary registeredActivities = new(); - /// - /// Defines runtime options for workflows. - /// - internal sealed class WorkflowLoggingService : IHostedService + public Task StartAsync(CancellationToken cancellationToken) { - private readonly ILogger logger; - private static readonly ConcurrentDictionary registeredWorkflows = new(); - private static readonly ConcurrentDictionary registeredActivities = new(); + logger.Log(LogLevel.Information, "WorkflowLoggingService started"); - public WorkflowLoggingService(ILogger logger) + logger.Log(LogLevel.Information, "List of registered workflows"); + foreach (string item in registeredWorkflows.Keys) { - this.logger = logger; - } - public Task StartAsync(CancellationToken cancellationToken) - { - this.logger.Log(LogLevel.Information, "WorkflowLoggingService started"); - - this.logger.Log(LogLevel.Information, "List of registered workflows"); - foreach (string item in registeredWorkflows.Keys) - { - this.logger.Log(LogLevel.Information, item); - } - - this.logger.Log(LogLevel.Information, "List of registered activities:"); - foreach (string item in registeredActivities.Keys) - { - this.logger.Log(LogLevel.Information, item); - } - - return Task.CompletedTask; + logger.Log(LogLevel.Information, item); } - public Task StopAsync(CancellationToken cancellationToken) + logger.Log(LogLevel.Information, "List of registered activities:"); + foreach (string item in registeredActivities.Keys) { - this.logger.Log(LogLevel.Information, "WorkflowLoggingService stopped"); - - return Task.CompletedTask; + logger.Log(LogLevel.Information, item); } - public static void LogWorkflowName(string workflowName) - { - registeredWorkflows.TryAdd(workflowName, 0); - } + return Task.CompletedTask; + } - public static void LogActivityName(string activityName) - { - registeredActivities.TryAdd(activityName, 0); - } + public Task StopAsync(CancellationToken cancellationToken) + { + logger.Log(LogLevel.Information, "WorkflowLoggingService stopped"); + return Task.CompletedTask; + } + + public static void LogWorkflowName(string workflowName) + { + registeredWorkflows.TryAdd(workflowName, 0); + } + + public static void LogActivityName(string activityName) + { + registeredActivities.TryAdd(activityName, 0); + } } From 066c880632579472ffb5434def26d22620681848 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 22 Jul 2025 04:00:53 -0500 Subject: [PATCH 22/22] Adding e2e Aspire hosting demo (#1587) * Added example demonstrating use of Dapr with Aspire across two projects (using service invocation) Signed-off-by: Whit Waldo --- all.sln | 41 ++++++ .../dotnet-sdk-docs/dotnet-client/_index.md | 2 +- .../dotnet-development-dapr-aspire.md | 32 +++-- examples/Hosting/Aspire/Directory.Build.props | 7 + .../BackendApp/BackendApp.csproj | 14 ++ .../BackendApp/Program.cs | 12 ++ .../BackendApp/Properties/launchSettings.json | 23 ++++ .../BackendApp/appsettings.Development.json | 8 ++ .../BackendApp/appsettings.json | 9 ++ .../Common/Common.csproj | 10 ++ .../ServiceInvocationDemo/Common/Fruit.cs | 16 +++ .../FrontendApp/Components/App.razor | 13 ++ .../FrontendApp/Components/Demo.razor | 89 ++++++++++++ .../FrontendApp/Components/Home.razor | 81 +++++++++++ .../FrontendApp/Components/Layout.razor | 5 + .../FrontendApp/Components/Routes.razor | 5 + .../FrontendApp/Components/_Imports.razor | 10 ++ .../FrontendApp/FrontendApp.csproj | 25 ++++ .../FrontendApp/Program.cs | 25 ++++ .../Properties/launchSettings.json | 23 ++++ .../FrontendApp/appsettings.Development.json | 8 ++ .../FrontendApp/appsettings.json | 9 ++ .../Aspire/ServiceInvocationDemo/README.md | 70 ++++++++++ .../ServiceInvocationDemo.AppHost/AppHost.cs | 19 +++ .../Properties/launchSettings.json | 17 +++ .../ServiceInvocationDemo.AppHost.csproj | 25 ++++ .../appsettings.Development.json | 8 ++ .../appsettings.json | 9 ++ .../Extensions.cs | 127 ++++++++++++++++++ ...rviceInvocationDemo.ServiceDefaults.csproj | 23 ++++ 30 files changed, 754 insertions(+), 11 deletions(-) create mode 100644 examples/Hosting/Aspire/Directory.Build.props create mode 100644 examples/Hosting/Aspire/ServiceInvocationDemo/BackendApp/BackendApp.csproj create mode 100644 examples/Hosting/Aspire/ServiceInvocationDemo/BackendApp/Program.cs create mode 100644 examples/Hosting/Aspire/ServiceInvocationDemo/BackendApp/Properties/launchSettings.json create mode 100644 examples/Hosting/Aspire/ServiceInvocationDemo/BackendApp/appsettings.Development.json create mode 100644 examples/Hosting/Aspire/ServiceInvocationDemo/BackendApp/appsettings.json create mode 100644 examples/Hosting/Aspire/ServiceInvocationDemo/Common/Common.csproj create mode 100644 examples/Hosting/Aspire/ServiceInvocationDemo/Common/Fruit.cs create mode 100644 examples/Hosting/Aspire/ServiceInvocationDemo/FrontendApp/Components/App.razor create mode 100644 examples/Hosting/Aspire/ServiceInvocationDemo/FrontendApp/Components/Demo.razor create mode 100644 examples/Hosting/Aspire/ServiceInvocationDemo/FrontendApp/Components/Home.razor create mode 100644 examples/Hosting/Aspire/ServiceInvocationDemo/FrontendApp/Components/Layout.razor create mode 100644 examples/Hosting/Aspire/ServiceInvocationDemo/FrontendApp/Components/Routes.razor create mode 100644 examples/Hosting/Aspire/ServiceInvocationDemo/FrontendApp/Components/_Imports.razor create mode 100644 examples/Hosting/Aspire/ServiceInvocationDemo/FrontendApp/FrontendApp.csproj create mode 100644 examples/Hosting/Aspire/ServiceInvocationDemo/FrontendApp/Program.cs create mode 100644 examples/Hosting/Aspire/ServiceInvocationDemo/FrontendApp/Properties/launchSettings.json create mode 100644 examples/Hosting/Aspire/ServiceInvocationDemo/FrontendApp/appsettings.Development.json create mode 100644 examples/Hosting/Aspire/ServiceInvocationDemo/FrontendApp/appsettings.json create mode 100644 examples/Hosting/Aspire/ServiceInvocationDemo/README.md create mode 100644 examples/Hosting/Aspire/ServiceInvocationDemo/ServiceInvocationDemo.AppHost/AppHost.cs create mode 100644 examples/Hosting/Aspire/ServiceInvocationDemo/ServiceInvocationDemo.AppHost/Properties/launchSettings.json create mode 100644 examples/Hosting/Aspire/ServiceInvocationDemo/ServiceInvocationDemo.AppHost/ServiceInvocationDemo.AppHost.csproj create mode 100644 examples/Hosting/Aspire/ServiceInvocationDemo/ServiceInvocationDemo.AppHost/appsettings.Development.json create mode 100644 examples/Hosting/Aspire/ServiceInvocationDemo/ServiceInvocationDemo.AppHost/appsettings.json create mode 100644 examples/Hosting/Aspire/ServiceInvocationDemo/ServiceInvocationDemo.ServiceDefaults/Extensions.cs create mode 100644 examples/Hosting/Aspire/ServiceInvocationDemo/ServiceInvocationDemo.ServiceDefaults/ServiceInvocationDemo.ServiceDefaults.csproj diff --git a/all.sln b/all.sln index e73e2460..9e19c7cf 100644 --- a/all.sln +++ b/all.sln @@ -179,6 +179,20 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Messaging", "Messaging", "{ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkflowParallelFanOut", "examples\Workflow\WorkflowParallelFanOut\WorkflowParallelFanOut.csproj", "{5764B1AA-66B8-43AE-9E0D-0B3B71714B92}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Hosting", "Hosting", "{953D770B-2DE8-4D1B-B1D4-ED46F4F5F31A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Aspire", "Aspire", "{55E08C7F-81C8-4D0B-AB18-87C89B261477}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BackendApp", "examples\Hosting\Aspire\ServiceInvocationDemo\BackendApp\BackendApp.csproj", "{3553BE3C-C188-460A-AC4C-D3D82DC0922A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FrontendApp", "examples\Hosting\Aspire\ServiceInvocationDemo\FrontendApp\FrontendApp.csproj", "{A6AA3F39-AB3E-4475-B3E2-D53549CBDA49}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServiceInvocationDemo.AppHost", "examples\Hosting\Aspire\ServiceInvocationDemo\ServiceInvocationDemo.AppHost\ServiceInvocationDemo.AppHost.csproj", "{97A47B0B-9D3B-4CF0-A62C-650F2F211A59}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServiceInvocationDemo.ServiceDefaults", "examples\Hosting\Aspire\ServiceInvocationDemo\ServiceInvocationDemo.ServiceDefaults\ServiceInvocationDemo.ServiceDefaults.csproj", "{5BB15C36-BAF7-44F6-BF85-C533B8B47862}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common", "examples\Hosting\Aspire\ServiceInvocationDemo\Common\Common.csproj", "{6CD90C22-0F79-4D61-8DCE-5BE22C1304C4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -475,6 +489,26 @@ Global {5764B1AA-66B8-43AE-9E0D-0B3B71714B92}.Debug|Any CPU.Build.0 = Debug|Any CPU {5764B1AA-66B8-43AE-9E0D-0B3B71714B92}.Release|Any CPU.ActiveCfg = Release|Any CPU {5764B1AA-66B8-43AE-9E0D-0B3B71714B92}.Release|Any CPU.Build.0 = Release|Any CPU + {3553BE3C-C188-460A-AC4C-D3D82DC0922A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3553BE3C-C188-460A-AC4C-D3D82DC0922A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3553BE3C-C188-460A-AC4C-D3D82DC0922A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3553BE3C-C188-460A-AC4C-D3D82DC0922A}.Release|Any CPU.Build.0 = Release|Any CPU + {A6AA3F39-AB3E-4475-B3E2-D53549CBDA49}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A6AA3F39-AB3E-4475-B3E2-D53549CBDA49}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A6AA3F39-AB3E-4475-B3E2-D53549CBDA49}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A6AA3F39-AB3E-4475-B3E2-D53549CBDA49}.Release|Any CPU.Build.0 = Release|Any CPU + {97A47B0B-9D3B-4CF0-A62C-650F2F211A59}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {97A47B0B-9D3B-4CF0-A62C-650F2F211A59}.Debug|Any CPU.Build.0 = Debug|Any CPU + {97A47B0B-9D3B-4CF0-A62C-650F2F211A59}.Release|Any CPU.ActiveCfg = Release|Any CPU + {97A47B0B-9D3B-4CF0-A62C-650F2F211A59}.Release|Any CPU.Build.0 = Release|Any CPU + {5BB15C36-BAF7-44F6-BF85-C533B8B47862}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5BB15C36-BAF7-44F6-BF85-C533B8B47862}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5BB15C36-BAF7-44F6-BF85-C533B8B47862}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5BB15C36-BAF7-44F6-BF85-C533B8B47862}.Release|Any CPU.Build.0 = Release|Any CPU + {6CD90C22-0F79-4D61-8DCE-5BE22C1304C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6CD90C22-0F79-4D61-8DCE-5BE22C1304C4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6CD90C22-0F79-4D61-8DCE-5BE22C1304C4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6CD90C22-0F79-4D61-8DCE-5BE22C1304C4}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -561,6 +595,13 @@ Global {442E80E5-8040-4123-B88A-26FD36BA95D9} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78} {E070F694-335D-4D96-8951-F41D0A5F2A8B} = {442E80E5-8040-4123-B88A-26FD36BA95D9} {5764B1AA-66B8-43AE-9E0D-0B3B71714B92} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9} + {953D770B-2DE8-4D1B-B1D4-ED46F4F5F31A} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78} + {55E08C7F-81C8-4D0B-AB18-87C89B261477} = {953D770B-2DE8-4D1B-B1D4-ED46F4F5F31A} + {3553BE3C-C188-460A-AC4C-D3D82DC0922A} = {55E08C7F-81C8-4D0B-AB18-87C89B261477} + {A6AA3F39-AB3E-4475-B3E2-D53549CBDA49} = {55E08C7F-81C8-4D0B-AB18-87C89B261477} + {97A47B0B-9D3B-4CF0-A62C-650F2F211A59} = {55E08C7F-81C8-4D0B-AB18-87C89B261477} + {5BB15C36-BAF7-44F6-BF85-C533B8B47862} = {55E08C7F-81C8-4D0B-AB18-87C89B261477} + {6CD90C22-0F79-4D61-8DCE-5BE22C1304C4} = {55E08C7F-81C8-4D0B-AB18-87C89B261477} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40} diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-client/_index.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-client/_index.md index b6eb7b07..0f77ae8c 100644 --- a/daprdocs/content/en/dotnet-sdk-docs/dotnet-client/_index.md +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-client/_index.md @@ -40,7 +40,7 @@ using var client = new DaprClientBuilder(). // Invokes a POST method named "deposit" that takes input of type "Transaction" var data = new { id = "17", amount = 99m }; -var account = await client.InvokeMethodAsync("routing", "deposit", data, cancellationToken); +var account = await client.InvokeMethodAsync("routing", "deposit", data, cancellationToken); Console.WriteLine("Returned: id:{0} | Balance:{1}", account.Id, account.Balance); ``` {{% /tab %}} 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 index 2c9366dc..664b8a2c 100644 --- 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 @@ -23,6 +23,9 @@ While Aspire also assists with deployment of your application to various cloud h 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). +An end-to-end demonstration featuring the following and demonstrating service invocation between multiple Dapr-enabled +services can be found [here](https://github.com/dapr/dotnet-sdk/tree/master/examples/Hosting/Aspire/ServiceInvocationDemo). + ## Prerequisites - Both the Dapr .NET SDK and .NET Aspire are compatible with [.NET 8](https://dotnet.microsoft.com/download/dotnet/8.0) or [.NET 9](https://dotnet.microsoft.com/download/dotnet/9.0) @@ -57,17 +60,18 @@ resilience, service discovery and telemetry capabilities offered by Aspire (thes 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`. +We'll next create twp projects that'll serve as our Dapr application and demonstrate Dapr functionality. From the same +directory, use the following to create an empty ASP.NET Core project called `FrontEndApp` and another called +'BackEndApp'. Either one will be created relative to your current directory in +`FrontEndApp\FrontEndApp.csproj` and `BackEndApp\BackEndApp.csproj`, respectively. ```sh -dotnet new web MyApp +dotnet new web --name FrontEndApp ``` 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 `CommunityToolkit.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. +We'll also add a reference to our `FrontEndApp` project so we can reference it during the registration process. {{% alert color="primary" %}} @@ -78,7 +82,8 @@ This package was previously called `Aspire.Hosting.Dapr`, which has been [marked ```sh cd aspiredemo.AppHost dotnet add package CommunityToolkit.Aspire.Hosting.Dapr -dotnet add reference ../MyApp/ +dotnet add reference ../FrontEndApp/ +dotnet add reference ../BackEndApp/ ``` Next, we need to configure Dapr as a resource to be loaded alongside your project. Open the `Program.cs` file in that @@ -97,8 +102,12 @@ Because we've already added a project reference to `MyApp`, we need to start by as well. Add the following before the `builder.Build().Run()` line: ```csharp -var myApp = builder - .AddProject("myapp") +var backEndApp = builder + .AddProject("be") + .WithDaprSidecar(); + +var frontEndApp = builder + .AddProject("fe") .WithDaprSidecar(); ``` @@ -114,7 +123,7 @@ the following example: ```csharp DaprSidecarOptions sidecarOptions = new() { - AppId = "my-other-app", + AppId = "how-dapr-identifies-your-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, @@ -122,7 +131,7 @@ DaprSidecarOptions sidecarOptions = new() }; builder - .AddProject("myotherapp") + .AddProject("be") .WithReference(myApp) .WithDaprSidecar(sidecarOptions); ``` @@ -136,6 +145,9 @@ change in a future release as a fix has been merged and can be tracked [here](ht {{% /alert %}} +Finally, let's add an endpoint to the back-end app that we can invoke using Dapr's service invocation to display to a +page to demonstrate that Dapr is working as expected. + 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/examples/Hosting/Aspire/Directory.Build.props b/examples/Hosting/Aspire/Directory.Build.props new file mode 100644 index 00000000..d260df0f --- /dev/null +++ b/examples/Hosting/Aspire/Directory.Build.props @@ -0,0 +1,7 @@ + + + net9 + false + $(NoWarn);DAPR_CONVERSATION;DAPR_JOBS;DAPR_DISTRIBUTEDLOCK;DAPR_CRYPTOGRAPHY + + \ No newline at end of file diff --git a/examples/Hosting/Aspire/ServiceInvocationDemo/BackendApp/BackendApp.csproj b/examples/Hosting/Aspire/ServiceInvocationDemo/BackendApp/BackendApp.csproj new file mode 100644 index 00000000..96c1ee79 --- /dev/null +++ b/examples/Hosting/Aspire/ServiceInvocationDemo/BackendApp/BackendApp.csproj @@ -0,0 +1,14 @@ + + + + enable + enable + false + false + + + + + + + diff --git a/examples/Hosting/Aspire/ServiceInvocationDemo/BackendApp/Program.cs b/examples/Hosting/Aspire/ServiceInvocationDemo/BackendApp/Program.cs new file mode 100644 index 00000000..75d512d5 --- /dev/null +++ b/examples/Hosting/Aspire/ServiceInvocationDemo/BackendApp/Program.cs @@ -0,0 +1,12 @@ +using Common; +using Microsoft.AspNetCore.Mvc; + +var builder = WebApplication.CreateBuilder(args); +var app = builder.Build(); + +app.MapPost("api/simpleJsonGet", () => new Fruit("Banana", "Yellow")); +app.MapGet("api/simpleStringGet", () => "Hello Dapr world!"); +app.MapGet("api/queryString", + ([FromQuery(Name = "name")] string type, [FromQuery] string color) => Task.FromResult($"Fruit: '{type}', Color: '{color}'")); + +app.Run(); diff --git a/examples/Hosting/Aspire/ServiceInvocationDemo/BackendApp/Properties/launchSettings.json b/examples/Hosting/Aspire/ServiceInvocationDemo/BackendApp/Properties/launchSettings.json new file mode 100644 index 00000000..743349a8 --- /dev/null +++ b/examples/Hosting/Aspire/ServiceInvocationDemo/BackendApp/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5247", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7260;http://localhost:5247", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/examples/Hosting/Aspire/ServiceInvocationDemo/BackendApp/appsettings.Development.json b/examples/Hosting/Aspire/ServiceInvocationDemo/BackendApp/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/examples/Hosting/Aspire/ServiceInvocationDemo/BackendApp/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/examples/Hosting/Aspire/ServiceInvocationDemo/BackendApp/appsettings.json b/examples/Hosting/Aspire/ServiceInvocationDemo/BackendApp/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/examples/Hosting/Aspire/ServiceInvocationDemo/BackendApp/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/examples/Hosting/Aspire/ServiceInvocationDemo/Common/Common.csproj b/examples/Hosting/Aspire/ServiceInvocationDemo/Common/Common.csproj new file mode 100644 index 00000000..0dd4b5d4 --- /dev/null +++ b/examples/Hosting/Aspire/ServiceInvocationDemo/Common/Common.csproj @@ -0,0 +1,10 @@ + + + + net9.0 + enable + enable + false + + + diff --git a/examples/Hosting/Aspire/ServiceInvocationDemo/Common/Fruit.cs b/examples/Hosting/Aspire/ServiceInvocationDemo/Common/Fruit.cs new file mode 100644 index 00000000..238eb9f1 --- /dev/null +++ b/examples/Hosting/Aspire/ServiceInvocationDemo/Common/Fruit.cs @@ -0,0 +1,16 @@ +// // ------------------------------------------------------------------------ +// // Copyright 2025 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 Common; + +public sealed record Fruit(string Name, string Color); diff --git a/examples/Hosting/Aspire/ServiceInvocationDemo/FrontendApp/Components/App.razor b/examples/Hosting/Aspire/ServiceInvocationDemo/FrontendApp/Components/App.razor new file mode 100644 index 00000000..f4d8153b --- /dev/null +++ b/examples/Hosting/Aspire/ServiceInvocationDemo/FrontendApp/Components/App.razor @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/Hosting/Aspire/ServiceInvocationDemo/FrontendApp/Components/Demo.razor b/examples/Hosting/Aspire/ServiceInvocationDemo/FrontendApp/Components/Demo.razor new file mode 100644 index 00000000..e8224077 --- /dev/null +++ b/examples/Hosting/Aspire/ServiceInvocationDemo/FrontendApp/Components/Demo.razor @@ -0,0 +1,89 @@ +
+

@Title

+

@Description

+ +
+ + +
+
+
Response:
+
+ @if (!string.IsNullOrEmpty(Response)) + { + @Response + } + else + { + No response yet... + } +
+
+
+ +@code { + [Parameter] + public string Title { get; set; } = string.Empty; + [Parameter] + public string Description { get; set; } = string.Empty; + [Parameter] + public Func>? ExecuteAsync { get; set; } + + private string Response { get; set; } = string.Empty; + + private bool isLoading = false; + + private void ClearResponse() + { + Response = string.Empty; + } + + private async Task ExecuteDemo() + { + isLoading = true; + StateHasChanged(); + + try + { + var result = await ExecuteAsync!(); + Response = result; + } + catch (Exception ex) + { + Response = $"Error: {ex.Message}"; + } + finally + { + isLoading = false; + StateHasChanged(); + } + } + +} + + \ No newline at end of file diff --git a/examples/Hosting/Aspire/ServiceInvocationDemo/FrontendApp/Components/Home.razor b/examples/Hosting/Aspire/ServiceInvocationDemo/FrontendApp/Components/Home.razor new file mode 100644 index 00000000..61d6f75d --- /dev/null +++ b/examples/Hosting/Aspire/ServiceInvocationDemo/FrontendApp/Components/Home.razor @@ -0,0 +1,81 @@ +@page "/" +@using Common +@using Dapr.Client +@rendermode InteractiveServer +@inject DaprClient _daprClient +@inject IHttpClientFactory _httpFactory + +
+

Service Invocation Demonstrations

+

Various patterns for invoking backend services using Dapr capability hosted in Aspire

+ + + + + + +
+ + +@code { + + private const string BackendAppName = "be"; + + private async Task SimpleGetJsonResponseAsync() + { + var response = await _daprClient.InvokeMethodAsync(BackendAppName, "api/simpleJsonGet"); + return $"{response}"; + } + + private async Task SimpleGetStringResponseAsync() + { + var request = _daprClient.CreateInvokeMethodRequest(HttpMethod.Get, BackendAppName, "api/simpleStringGet"); + var response = await _daprClient.InvokeMethodWithResponseAsync(request); + return await response.Content.ReadAsStringAsync(); + } + + private async Task GetWithQueryParametersAsync() + { + var parameters = new List> + { + new("name", "Orange"), + new("color", "Orange") + }; + var request = _daprClient.CreateInvokeMethodRequest(HttpMethod.Get,BackendAppName, "api/queryString", parameters); + var response = await _daprClient.InvokeMethodWithResponseAsync(request); + return await response.Content.ReadAsStringAsync(); + } + + private async Task ServiceInvocationUsingHttpClientFactoryHeader() + { + // Start with a new HttpClient + var httpClient = _httpFactory.CreateClient(); + + // Specify the name of the app to connect to in a header and send directly to this app's Dapr sidecar (specified in AppHost.cs) + var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost:50000/api/simpleStringGet"); + request.Headers.Add("dapr-app-id", BackendAppName); + + // Send and return the response + var response = await httpClient.SendAsync(request); + return await response.Content.ReadAsStringAsync(); + } + + private async Task ServiceInvocationUsingDaprHttpClient() + { + // Use the HttpClient provided by the Dapr .NET SDK and indicate the app to send the request to + var httpClient = _daprClient.CreateInvokableHttpClient(BackendAppName); + + var response = await httpClient.GetAsync("api/simpleStringGet"); + return await response.Content.ReadAsStringAsync(); + } +} \ No newline at end of file diff --git a/examples/Hosting/Aspire/ServiceInvocationDemo/FrontendApp/Components/Layout.razor b/examples/Hosting/Aspire/ServiceInvocationDemo/FrontendApp/Components/Layout.razor new file mode 100644 index 00000000..afee36a2 --- /dev/null +++ b/examples/Hosting/Aspire/ServiceInvocationDemo/FrontendApp/Components/Layout.razor @@ -0,0 +1,5 @@ +@inherits LayoutComponentBase + +
+ @Body +
\ No newline at end of file diff --git a/examples/Hosting/Aspire/ServiceInvocationDemo/FrontendApp/Components/Routes.razor b/examples/Hosting/Aspire/ServiceInvocationDemo/FrontendApp/Components/Routes.razor new file mode 100644 index 00000000..8c25e6e7 --- /dev/null +++ b/examples/Hosting/Aspire/ServiceInvocationDemo/FrontendApp/Components/Routes.razor @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/examples/Hosting/Aspire/ServiceInvocationDemo/FrontendApp/Components/_Imports.razor b/examples/Hosting/Aspire/ServiceInvocationDemo/FrontendApp/Components/_Imports.razor new file mode 100644 index 00000000..f4ee2aeb --- /dev/null +++ b/examples/Hosting/Aspire/ServiceInvocationDemo/FrontendApp/Components/_Imports.razor @@ -0,0 +1,10 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using static Microsoft.AspNetCore.Components.Web.RenderMode +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@using FrontendApp +@using FrontendApp.Components \ No newline at end of file diff --git a/examples/Hosting/Aspire/ServiceInvocationDemo/FrontendApp/FrontendApp.csproj b/examples/Hosting/Aspire/ServiceInvocationDemo/FrontendApp/FrontendApp.csproj new file mode 100644 index 00000000..f1e3f0c2 --- /dev/null +++ b/examples/Hosting/Aspire/ServiceInvocationDemo/FrontendApp/FrontendApp.csproj @@ -0,0 +1,25 @@ + + + + enable + enable + false + false + + + + + + + + <_ContentIncludedByDefault Remove="Pages\Shared\_Layout.cshtml" /> + <_ContentIncludedByDefault Remove="Pages\_Host.cshtml" /> + <_ContentIncludedByDefault Remove="Pages\Home.razor" /> + <_ContentIncludedByDefault Remove="Pages\Shared\MainLayout.razor" /> + + + + + + + diff --git a/examples/Hosting/Aspire/ServiceInvocationDemo/FrontendApp/Program.cs b/examples/Hosting/Aspire/ServiceInvocationDemo/FrontendApp/Program.cs new file mode 100644 index 00000000..669fd0f3 --- /dev/null +++ b/examples/Hosting/Aspire/ServiceInvocationDemo/FrontendApp/Program.cs @@ -0,0 +1,25 @@ +using Dapr.Client; +using FrontendApp.Components; +using Microsoft.AspNetCore.Mvc; + +var builder = WebApplication.CreateBuilder(args); + +// Services for server-side Blazor +builder.Services.AddRazorComponents() + .AddInteractiveServerComponents(); + +builder.Services.AddHttpClient(); + +// Register the Dapr client +builder.Services.AddDaprClient(); + +var app = builder.Build(); + +app.UseStaticFiles(); +app.UseAntiforgery(); + +app.MapStaticAssets(); +app.MapRazorComponents() + .AddInteractiveServerRenderMode(); + +app.Run(); diff --git a/examples/Hosting/Aspire/ServiceInvocationDemo/FrontendApp/Properties/launchSettings.json b/examples/Hosting/Aspire/ServiceInvocationDemo/FrontendApp/Properties/launchSettings.json new file mode 100644 index 00000000..4e058d3a --- /dev/null +++ b/examples/Hosting/Aspire/ServiceInvocationDemo/FrontendApp/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5054", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7121;http://localhost:5054", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/examples/Hosting/Aspire/ServiceInvocationDemo/FrontendApp/appsettings.Development.json b/examples/Hosting/Aspire/ServiceInvocationDemo/FrontendApp/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/examples/Hosting/Aspire/ServiceInvocationDemo/FrontendApp/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/examples/Hosting/Aspire/ServiceInvocationDemo/FrontendApp/appsettings.json b/examples/Hosting/Aspire/ServiceInvocationDemo/FrontendApp/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/examples/Hosting/Aspire/ServiceInvocationDemo/FrontendApp/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/examples/Hosting/Aspire/ServiceInvocationDemo/README.md b/examples/Hosting/Aspire/ServiceInvocationDemo/README.md new file mode 100644 index 00000000..614c9afe --- /dev/null +++ b/examples/Hosting/Aspire/ServiceInvocationDemo/README.md @@ -0,0 +1,70 @@ +# Aspire with Dapr Service Invocation Demo + +This directory contains demonstration projects showcasing how to use .NET Aspire with Dapr to perform service invocation between frontend and backend applications using different approaches. + +## Overview + +The ServiceInvocationDemo demonstrates various patterns for service-to-service communication in a distributed application architecture using: + +- **.NET Aspire** - For application orchestration, service discovery, and observability +- **Dapr** - For distributed application runtime capabilities including service invocation +- **ASP.NET Core** - For both frontend and backend services + +## Project Structure + +The demo includes: +- **Frontend Application** - Web application that invokes backend services +- **Backend Application** - API services that handle business logic +- **AppHost** - Aspire orchestration project that coordinates all services +- Multiple service invocation approaches and patterns + +## Getting Started + +### Prerequisites + +- .NET 8.0 or later +- Docker Desktop (for Dapr components) +- Dapr CLI installed and initialized + +### Running the Demo + +1. Navigate to the ServiceInvocationDemo.AppHost project directory: + ```bash + cd examples/Hosting/Aspire/ServiceInvocationDemo/ServiceInvocationDemo.AppHost + ``` + +2. Run the application: + ```bash + dotnet run + ``` + +3. The console will display the Aspire dashboard URL. Open the provided link in your browser. + +4. In the Aspire dashboard, click on the `http://localhost:5054/` link to access the demo application. + +5. The main page will present various service invocation demonstration scenarios that you can explore. + +## Key Features Demonstrated + +- Integration between .NET Aspire orchestration and Dapr runtime +- Service-to-service communication patterns +- Development-time productivity with Aspire dashboard +- Production-ready distributed application patterns + +## No Additional Setup Required + +The demo is designed to work out-of-the-box with minimal setup. Simply run `dotnet run` from the AppHost project, and all necessary services will be orchestrated automatically through Aspire's application model. + +## Architecture Benefits + +This demonstration highlights the benefits of combining Aspire and Dapr: + +- **Simplified Local Development** - Aspire handles service orchestration and provides rich debugging experience +- **Production Readiness** - Dapr provides rich distributed application patterns +- **Flexibility** - Multiple approaches to solve service invocation challenges + +Explore the different demo scenarios to understand how these technologies work together to build resilient and maintainable distributed applications. + +## SDK docs + +For more information on the .NET Dapr client visit the [SDK docs](https://docs.dapr.io/developing-applications/sdks/dotnet/dotnet-client/). \ No newline at end of file diff --git a/examples/Hosting/Aspire/ServiceInvocationDemo/ServiceInvocationDemo.AppHost/AppHost.cs b/examples/Hosting/Aspire/ServiceInvocationDemo/ServiceInvocationDemo.AppHost/AppHost.cs new file mode 100644 index 00000000..ce7a6e70 --- /dev/null +++ b/examples/Hosting/Aspire/ServiceInvocationDemo/ServiceInvocationDemo.AppHost/AppHost.cs @@ -0,0 +1,19 @@ +using CommunityToolkit.Aspire.Hosting.Dapr; + +var builder = DistributedApplication.CreateBuilder(args); + +builder.AddProject("be") + .WithDaprSidecar(new DaprSidecarOptions + { + AppPort = 5247, + DaprHttpPort = 50001 + }); + +builder.AddProject("fe") + .WithDaprSidecar(new DaprSidecarOptions + { + AppPort = 5054, + DaprHttpPort = 50000 + }); + +builder.Build().Run(); diff --git a/examples/Hosting/Aspire/ServiceInvocationDemo/ServiceInvocationDemo.AppHost/Properties/launchSettings.json b/examples/Hosting/Aspire/ServiceInvocationDemo/ServiceInvocationDemo.AppHost/Properties/launchSettings.json new file mode 100644 index 00000000..4b662486 --- /dev/null +++ b/examples/Hosting/Aspire/ServiceInvocationDemo/ServiceInvocationDemo.AppHost/Properties/launchSettings.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15024", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19274", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20286" + } + } + } +} diff --git a/examples/Hosting/Aspire/ServiceInvocationDemo/ServiceInvocationDemo.AppHost/ServiceInvocationDemo.AppHost.csproj b/examples/Hosting/Aspire/ServiceInvocationDemo/ServiceInvocationDemo.AppHost/ServiceInvocationDemo.AppHost.csproj new file mode 100644 index 00000000..73fd8f70 --- /dev/null +++ b/examples/Hosting/Aspire/ServiceInvocationDemo/ServiceInvocationDemo.AppHost/ServiceInvocationDemo.AppHost.csproj @@ -0,0 +1,25 @@ + + + + + + Exe + enable + enable + false + 5d18c5e4-5752-4a7d-a461-88e9e4bcbacf + false + $(NoWarn);CS8002 + + + + + + + + + + + + + diff --git a/examples/Hosting/Aspire/ServiceInvocationDemo/ServiceInvocationDemo.AppHost/appsettings.Development.json b/examples/Hosting/Aspire/ServiceInvocationDemo/ServiceInvocationDemo.AppHost/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/examples/Hosting/Aspire/ServiceInvocationDemo/ServiceInvocationDemo.AppHost/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/examples/Hosting/Aspire/ServiceInvocationDemo/ServiceInvocationDemo.AppHost/appsettings.json b/examples/Hosting/Aspire/ServiceInvocationDemo/ServiceInvocationDemo.AppHost/appsettings.json new file mode 100644 index 00000000..31c092aa --- /dev/null +++ b/examples/Hosting/Aspire/ServiceInvocationDemo/ServiceInvocationDemo.AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} diff --git a/examples/Hosting/Aspire/ServiceInvocationDemo/ServiceInvocationDemo.ServiceDefaults/Extensions.cs b/examples/Hosting/Aspire/ServiceInvocationDemo/ServiceInvocationDemo.ServiceDefaults/Extensions.cs new file mode 100644 index 00000000..fd3fb27d --- /dev/null +++ b/examples/Hosting/Aspire/ServiceInvocationDemo/ServiceInvocationDemo.ServiceDefaults/Extensions.cs @@ -0,0 +1,127 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace ServiceInvocationDemo.ServiceDefaults; + +// Adds common .NET Aspire services: service discovery, resilience, health checks, and OpenTelemetry. +// This project should be referenced by each service project in your solution. +// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults +public static class Extensions +{ + private const string HealthEndpointPath = "/health"; + private const string AlivenessEndpointPath = "/alive"; + + public static TBuilder AddServiceDefaults(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.ConfigureOpenTelemetry(); + + builder.AddDefaultHealthChecks(); + + builder.Services.AddServiceDiscovery(); + + builder.Services.ConfigureHttpClientDefaults(http => + { + // Turn on resilience by default + http.AddStandardResilienceHandler(); + + // Turn on service discovery by default + http.AddServiceDiscovery(); + }); + + // Uncomment the following to restrict the allowed schemes for service discovery. + // builder.Services.Configure(options => + // { + // options.AllowedSchemes = ["https"]; + // }); + + return builder; + } + + public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.Logging.AddOpenTelemetry(logging => + { + logging.IncludeFormattedMessage = true; + logging.IncludeScopes = true; + }); + + builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => + { + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); + }) + .WithTracing(tracing => + { + tracing.AddSource(builder.Environment.ApplicationName) + .AddAspNetCoreInstrumentation(tracing => + // Exclude health check requests from tracing + tracing.Filter = context => + !context.Request.Path.StartsWithSegments(HealthEndpointPath) + && !context.Request.Path.StartsWithSegments(AlivenessEndpointPath) + ) + // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) + //.AddGrpcClientInstrumentation() + .AddHttpClientInstrumentation(); + }); + + builder.AddOpenTelemetryExporters(); + + return builder; + } + + private static TBuilder AddOpenTelemetryExporters(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); + + if (useOtlpExporter) + { + builder.Services.AddOpenTelemetry().UseOtlpExporter(); + } + + // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package) + //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"])) + //{ + // builder.Services.AddOpenTelemetry() + // .UseAzureMonitor(); + //} + + return builder; + } + + public static TBuilder AddDefaultHealthChecks(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.Services.AddHealthChecks() + // Add a default liveness check to ensure app is responsive + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + + return builder; + } + + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + // Adding health checks endpoints to applications in non-development environments has security implications. + // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments. + if (app.Environment.IsDevelopment()) + { + // All health checks must pass for app to be considered ready to accept traffic after starting + app.MapHealthChecks(HealthEndpointPath); + + // Only health checks tagged with the "live" tag must pass for app to be considered alive + app.MapHealthChecks(AlivenessEndpointPath, new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); + } + + return app; + } +} diff --git a/examples/Hosting/Aspire/ServiceInvocationDemo/ServiceInvocationDemo.ServiceDefaults/ServiceInvocationDemo.ServiceDefaults.csproj b/examples/Hosting/Aspire/ServiceInvocationDemo/ServiceInvocationDemo.ServiceDefaults/ServiceInvocationDemo.ServiceDefaults.csproj new file mode 100644 index 00000000..98846073 --- /dev/null +++ b/examples/Hosting/Aspire/ServiceInvocationDemo/ServiceInvocationDemo.ServiceDefaults/ServiceInvocationDemo.ServiceDefaults.csproj @@ -0,0 +1,23 @@ + + + + enable + enable + false + false + true + + + + + + + + + + + + + + +