diff --git a/.devcontainer/localinit.sh b/.devcontainer/localinit.sh index 80b27e4f4..69c4c1274 100644 --- a/.devcontainer/localinit.sh +++ b/.devcontainer/localinit.sh @@ -6,4 +6,4 @@ az extension add --name containerapp --yes nvm install v18.12.1 # initialize Dapr -dapr init --runtime-version=1.10.0-rc.2 \ No newline at end of file +dapr init --runtime-version=1.14.0 \ No newline at end of file diff --git a/.editorconfig b/.editorconfig index a4dd20cc2..2e2e40c7a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -22,6 +22,7 @@ charset = utf-8-bom # Organize usings dotnet_sort_system_directives_first = true dotnet_separate_import_directive_groups = false +csharp_using_directive_placement = outside_namespace # this. preferences dotnet_style_qualification_for_field = false:silent diff --git a/.github/holopin.yml b/.github/holopin.yml new file mode 100644 index 000000000..7472cc284 --- /dev/null +++ b/.github/holopin.yml @@ -0,0 +1,6 @@ +organization: dapr +defaultSticker: clrqfdv4x24910fl5n4iwu5oa +stickers: + - + id: clrqfdv4x24910fl5n4iwu5oa + alias: sdk-badge \ No newline at end of file diff --git a/.github/workflows/itests.yml b/.github/workflows/itests.yml index 0044fec93..36741ce7c 100644 --- a/.github/workflows/itests.yml +++ b/.github/workflows/itests.yml @@ -19,37 +19,33 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - dotnet-version: ['3.1', '6.0', '7.0'] + dotnet-version: ['6.0', '7.0', '8.0'] include: - - dotnet-version: '3.1' - install-3: true - display-name: '.NET Core 3.1' - framework: 'netcoreapp3.1' - prefix: 'netcoreapp31' - install-version: '3.1.x' # We always need a new .NET - dotnet-version: '6.0' - install-3: false display-name: '.NET 6.0' framework: 'net6' prefix: 'net6' install-version: '6.0.x' - dotnet-version: '7.0' - install-3: false display-name: '.NET 7.0' framework: 'net7' prefix: 'net7' install-version: '7.0.x' + - dotnet-version: '8.0' + display-name: '.NET 8.0' + framework: 'net8' + prefix: 'net8' + install-version: '8.0.x' env: NUPKG_OUTDIR: bin/Release/nugets GOVER: 1.20.3 GOOS: linux GOARCH: amd64 GOPROXY: https://proxy.golang.org - DAPR_CLI_VER: 1.9.1 - DAPR_RUNTIME_VER: 1.10.5 - DAPR_INSTALL_URL: https://raw.githubusercontent.com/dapr/cli/3dacfb672d55f1436c249057aaebbe597e1066f3/install/install.sh + DAPR_CLI_VER: 1.14.0 + DAPR_RUNTIME_VER: 1.14.0 + DAPR_INSTALL_URL: https://raw.githubusercontent.com/dapr/cli/release-1.14/install/install.sh DAPR_CLI_REF: '' - DAPR_REF: '4181de0edc65fc98a836ae7abc6042c575c8fae5' steps: - name: Set up Dapr CLI run: wget -q ${{ env.DAPR_INSTALL_URL }} -O - | /bin/bash -s ${{ env.DAPR_CLI_VER }} @@ -105,29 +101,19 @@ jobs: - uses: actions/checkout@v1 - name: Parse release version run: python ./.github/scripts/get_release_version.py - - name: Install Local kafka using docker-compose - run: | - docker-compose -f test/Dapr.E2E.Test/deploy/local-test-kafka.yml up -d - docker ps - - name: Install Local Hashicorp Vault using docker-compose - run: | - docker-compose -f test/Dapr.E2E.Test/deploy/local-test-vault.yml up -d - docker ps - - name: Setup Vault's test token - run: echo myroot > /tmp/.hashicorp_vault_token - name: Setup ${{ matrix.display-name }} uses: actions/setup-dotnet@v1 with: dotnet-version: ${{ matrix.install-version }} - - name: Setup .NET 7.0 # net7 is always required. + - name: Setup .NET 8.0 # net8 is always required. uses: actions/setup-dotnet@v1 - if: ${{ matrix.install-version != '7.0.x' }} + if: ${{ matrix.install-version != '8.0.x' }} with: - dotnet-version: 7.0.x + dotnet-version: 8.0.x - 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 - - name: Run Test + - name: Run General Tests id: tests continue-on-error: true # proceed if tests fail, the report step will report the failure with more details. run: | @@ -142,8 +128,24 @@ jobs: /p:CollectCoverage=true \ /p:CoverletOutputFormat=opencover \ /p:GITHUB_ACTIONS=false + - name: Run Generators Tests + id: generator-tests + continue-on-error: true # proceed if tests fail, the report step will report the failure with more details. + run: | + dotnet test ${{ github.workspace }}/test/Dapr.E2E.Test.Actors.Generators/Dapr.E2E.Test.Actors.Generators.csproj \ + --configuration Release \ + --framework ${{ matrix.framework }} \ + --no-build \ + --no-restore \ + --logger "trx;LogFilePrefix=${{ matrix.prefix }}" \ + --logger "GitHubActions;report-warnings=false" \ + --logger "console;verbosity=detailed" \ + --results-directory "${{ github.workspace }}/TestResults" \ + /p:CollectCoverage=true \ + /p:CoverletOutputFormat=opencover \ + /p:GITHUB_ACTIONS=false - name: Check test failure in PR - if: github.event_name == 'pull_request' && steps.tests.outcome != 'success' + if: github.event_name == 'pull_request' && (steps.tests.outcome != 'success' || steps.generator-tests.outcome != 'success') run: exit 1 - name: Upload test coverage uses: codecov/codecov-action@v1 diff --git a/.github/workflows/sdk_build.yml b/.github/workflows/sdk_build.yml index b0b6b042c..fe935bfb8 100644 --- a/.github/workflows/sdk_build.yml +++ b/.github/workflows/sdk_build.yml @@ -26,13 +26,13 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v1 with: - dotnet-version: 7.0.x + dotnet-version: 8.0.x - name: Build run: dotnet build --configuration release - name: Generate Packages run: dotnet pack --configuration release - name: Upload packages - uses: actions/upload-artifact@master + uses: actions/upload-artifact@v4 with: name: packages path: ${{ env.NUPKG_OUTDIR }} @@ -42,14 +42,8 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - dotnet-version: ['3.1', '6.0', '7.0'] + dotnet-version: ['6.0', '7.0', '8.0'] include: - - dotnet-version: '3.1' - install-3: true - display-name: '.NET Core 3.1' - framework: 'netcoreapp3.1' - prefix: 'netcoreapp31' - install-version: '3.1.x' # We always need a new .NET - dotnet-version: '6.0' install-3: false display-name: '.NET 6.0' @@ -62,6 +56,12 @@ jobs: framework: 'net7' prefix: 'net7' install-version: '7.0.x' + - dotnet-version: '8.0' + install-3: false + display-name: '.NET 8.0' + framework: 'net8' + prefix: 'net8' + install-version: '8.0.x' steps: - uses: actions/checkout@v1 - name: Parse release version @@ -70,11 +70,11 @@ jobs: uses: actions/setup-dotnet@v1 with: dotnet-version: ${{ matrix.install-version }} - - name: Setup .NET 7.0 # net7 is always required. + - name: Setup .NET 8.0 # net8 is always required. uses: actions/setup-dotnet@v1 - if: ${{ matrix.install-version != '7.0.x' }} + if: ${{ matrix.install-version != '8.0.x' }} with: - dotnet-version: 7.0.x + dotnet-version: 8.0.x - 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 @@ -116,7 +116,7 @@ jobs: if: 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@v2 + uses: actions/download-artifact@v4 with: name: packages path: packages diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f47877cbd..7712340a5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -123,3 +123,13 @@ A non-exclusive list of code that must be places in `vendor/`: ## Code of Conduct This project has adopted the [Contributor Covenant Code of Conduct](https://github.com/dapr/community/blob/master/CODE-OF-CONDUCT.md) + + + +## GitHub Dapr Bot Commands + +Checkout the [daprbot documentation](https://docs.dapr.io/contributing/daprbot/) for Github commands you can run in this repo for common tasks. For example, you can comment `/assign` on an issue to assign it to yourself. + + + + diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 000000000..d85020770 --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,46 @@ + + + true + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index 54bb606dc..948516fe2 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,6 @@ # Dapr SDK for .NET -[![Build Status](https://github.com/dapr/dotnet-sdk/workflows/build/badge.svg)](https://github.com/dapr/dotnet-sdk/actions?workflow=build) -[![codecov](https://codecov.io/gh/dapr/dotnet-sdk/branch/master/graph/badge.svg)](https://codecov.io/gh/dapr/dotnet-sdk) -[![License: Apache](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0) -[![FOSSA Status](https://app.fossa.com/api/projects/custom%2B162%2Fgithub.com%2Fdapr%2Fcomponents-contrib.svg?type=shield)](https://app.fossa.com/projects/custom%2B162%2Fgithub.com%2Fdapr%2Fcomponents-contrib?ref=badge_shield) +[![NuGet Version](https://img.shields.io/nuget/v/Dapr.Client?logo=nuget&label=Latest%20version&style=flat)](https://www.nuget.org/packages/Dapr.Client) [![NuGet Downloads](https://img.shields.io/nuget/dt/Dapr.Client?style=flat&logo=nuget&label=Downloads)](https://www.nuget.org/packages/Dapr.Client) [![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/dapr/dotnet-sdk/.github%2Fworkflows%2Fsdk_build.yml?branch=master&label=Build&logo=github)](https://github.com/dapr/dotnet-sdk/actions/workflows/sdk_build.yml) [![codecov](https://codecov.io/gh/dapr/dotnet-sdk/branch/master/graph/badge.svg)](https://codecov.io/gh/dapr/dotnet-sdk) [![GitHub License](https://img.shields.io/github/license/dapr/dotnet-sdk?style=flat&label=License&logo=github)](https://github.com/dapr/dotnet-sdk/blob/master/LICENSE) [![GitHub issue custom search in repo](https://img.shields.io/github/issues-search/dapr/dotnet-sdk?query=type%3Aissue%20is%3Aopen%20label%3A%22good%20first%20issue%22&label=Good%20first%20issues&style=flat&logo=github)](https://github.com/dapr/dotnet-sdk/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) [![Discord](https://img.shields.io/discord/778680217417809931?label=Discord&style=flat&logo=discord)](http://bit.ly/dapr-discord) [![YouTube Channel Views](https://img.shields.io/youtube/channel/views/UCtpSQ9BLB_3EXdWAUQYwnRA?style=flat&label=YouTube%20views&logo=youtube)](https://youtube.com/@daprdev) [![X (formerly Twitter) Follow](https://img.shields.io/twitter/follow/daprdev?logo=x&style=flat)](https://twitter.com/daprdev) Dapr SDK for .NET allows you to: diff --git a/all.sln b/all.sln index 47fc9098c..1a5d78efb 100644 --- a/all.sln +++ b/all.sln @@ -1,4 +1,3 @@ - Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.3.32929.385 @@ -33,6 +32,7 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{1BD1276E-D28A-45EA-89B1-6AD48471500D}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig + README.md = README.md EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.Actors.AspNetCore.Test", "test\Dapr.Actors.AspNetCore.Test\Dapr.Actors.AspNetCore.Test.csproj", "{9C1D6ABA-5EDE-4FA0-A8A9-0AB98CB74737}" @@ -71,8 +71,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StateManagement", "examples EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ServiceInvocation", "examples\Client\ServiceInvocation\ServiceInvocation.csproj", "{8B570E70-0E73-4042-A4B6-1CC3CC782A65}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PublishSubscribe", "examples\Client\PublishSubscribe\PublishSubscribe.csproj", "{DE6913E3-E5D9-4D1D-95F9-9FED87BD09BC}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.E2E.Test", "test\Dapr.E2E.Test\Dapr.E2E.Test.csproj", "{4AA9E7B7-36BF-4AAE-BFA3-C9CE8740F4A0}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.E2E.Test.App", "test\Dapr.E2E.Test.App\Dapr.E2E.Test.App.csproj", "{345FC3FB-D1E9-4AE8-9052-17D20AB01FA2}" @@ -104,6 +102,26 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BulkPublishEventExample", " EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowUnitTest", "examples\Workflow\WorkflowUnitTest\WorkflowUnitTest.csproj", "{8CA09061-2BEF-4506-A763-07062D2BD6AC}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GeneratedActor", "GeneratedActor", "{7592AFA4-426B-42F3-AE82-957C86814482}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ActorClient", "examples\GeneratedActor\ActorClient\ActorClient.csproj", "{61C24126-F39D-4BEA-96DC-FC87BA730554}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ActorCommon", "examples\GeneratedActor\ActorCommon\ActorCommon.csproj", "{CB903D21-4869-42EF-BDD6-5B1CFF674337}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.Actors.Generators", "src\Dapr.Actors.Generators\Dapr.Actors.Generators.csproj", "{980B5FD8-0107-41F7-8FAD-E4E8BAE8A625}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ActorService", "examples\GeneratedActor\ActorService\ActorService.csproj", "{7C06FE2D-6C62-48F5-A505-F0D715C554DE}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.Actors.Generators.Test", "test\Dapr.Actors.Generators.Test\Dapr.Actors.Generators.Test.csproj", "{AF89083D-4715-42E6-93E9-38497D12A8A6}" +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}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -248,6 +266,42 @@ Global {DDC41278-FB60-403A-B969-2AEBD7C2D83C}.Release|Any CPU.Build.0 = Release|Any CPU {8CA09061-2BEF-4506-A763-07062D2BD6AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8CA09061-2BEF-4506-A763-07062D2BD6AC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {61C24126-F39D-4BEA-96DC-FC87BA730554}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {61C24126-F39D-4BEA-96DC-FC87BA730554}.Debug|Any CPU.Build.0 = Debug|Any CPU + {61C24126-F39D-4BEA-96DC-FC87BA730554}.Release|Any CPU.ActiveCfg = Release|Any CPU + {61C24126-F39D-4BEA-96DC-FC87BA730554}.Release|Any CPU.Build.0 = Release|Any CPU + {CB903D21-4869-42EF-BDD6-5B1CFF674337}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CB903D21-4869-42EF-BDD6-5B1CFF674337}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CB903D21-4869-42EF-BDD6-5B1CFF674337}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CB903D21-4869-42EF-BDD6-5B1CFF674337}.Release|Any CPU.Build.0 = Release|Any CPU + {980B5FD8-0107-41F7-8FAD-E4E8BAE8A625}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {980B5FD8-0107-41F7-8FAD-E4E8BAE8A625}.Debug|Any CPU.Build.0 = Debug|Any CPU + {980B5FD8-0107-41F7-8FAD-E4E8BAE8A625}.Release|Any CPU.ActiveCfg = Release|Any CPU + {980B5FD8-0107-41F7-8FAD-E4E8BAE8A625}.Release|Any CPU.Build.0 = Release|Any CPU + {7C06FE2D-6C62-48F5-A505-F0D715C554DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7C06FE2D-6C62-48F5-A505-F0D715C554DE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7C06FE2D-6C62-48F5-A505-F0D715C554DE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7C06FE2D-6C62-48F5-A505-F0D715C554DE}.Release|Any CPU.Build.0 = Release|Any CPU + {AF89083D-4715-42E6-93E9-38497D12A8A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AF89083D-4715-42E6-93E9-38497D12A8A6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AF89083D-4715-42E6-93E9-38497D12A8A6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AF89083D-4715-42E6-93E9-38497D12A8A6}.Release|Any CPU.Build.0 = Release|Any CPU + {B5CDB0DC-B26D-48F1-B934-FE5C1C991940}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B5CDB0DC-B26D-48F1-B934-FE5C1C991940}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B5CDB0DC-B26D-48F1-B934-FE5C1C991940}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B5CDB0DC-B26D-48F1-B934-FE5C1C991940}.Release|Any CPU.Build.0 = Release|Any CPU + {C74FBA78-13E8-407F-A173-4555AEE41FF3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C74FBA78-13E8-407F-A173-4555AEE41FF3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C74FBA78-13E8-407F-A173-4555AEE41FF3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C74FBA78-13E8-407F-A173-4555AEE41FF3}.Release|Any CPU.Build.0 = Release|Any CPU + {DFBABB04-50E9-42F6-B470-310E1B545638}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DFBABB04-50E9-42F6-B470-310E1B545638}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DFBABB04-50E9-42F6-B470-310E1B545638}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DFBABB04-50E9-42F6-B470-310E1B545638}.Release|Any CPU.Build.0 = Release|Any CPU + {B445B19C-A925-4873-8CB7-8317898B6970}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B445B19C-A925-4873-8CB7-8317898B6970}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B445B19C-A925-4873-8CB7-8317898B6970}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B445B19C-A925-4873-8CB7-8317898B6970}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -293,6 +347,16 @@ Global {4A175C27-EAFE-47E7-90F6-873B37863656} = {0EF6EA64-D7C3-420D-9890-EAE8D54A57E6} {DDC41278-FB60-403A-B969-2AEBD7C2D83C} = {0EF6EA64-D7C3-420D-9890-EAE8D54A57E6} {8CA09061-2BEF-4506-A763-07062D2BD6AC} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9} + {7592AFA4-426B-42F3-AE82-957C86814482} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78} + {61C24126-F39D-4BEA-96DC-FC87BA730554} = {7592AFA4-426B-42F3-AE82-957C86814482} + {CB903D21-4869-42EF-BDD6-5B1CFF674337} = {7592AFA4-426B-42F3-AE82-957C86814482} + {980B5FD8-0107-41F7-8FAD-E4E8BAE8A625} = {27C5D71D-0721-4221-9286-B94AB07B58CF} + {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} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40} diff --git a/daprdocs/content/en/dotnet-sdk-contributing/dotnet-contributing.md b/daprdocs/content/en/dotnet-sdk-contributing/dotnet-contributing.md index a4e546ffa..6664191d6 100644 --- a/daprdocs/content/en/dotnet-sdk-contributing/dotnet-contributing.md +++ b/daprdocs/content/en/dotnet-sdk-contributing/dotnet-contributing.md @@ -21,3 +21,7 @@ The `daprdocs` directory contains the markdown files that are rendered into the - All rules in the [docs guide]({{< ref contributing-docs.md >}}) should be followed in addition to these. - All files and directories should be prefixed with `dotnet-` to ensure all file/directory names are globally unique across all Dapr documentation. + +## GitHub Dapr Bot Commands + +Checkout the [daprbot documentation](https://docs.dapr.io/contributing/daprbot/) for Github commands you can run in this repo for common tasks. For example, you can comment `/assign` on an issue to assign it to yourself. diff --git a/daprdocs/content/en/dotnet-sdk-docs/_index.md b/daprdocs/content/en/dotnet-sdk-docs/_index.md index 65d818723..121dde310 100644 --- a/daprdocs/content/en/dotnet-sdk-docs/_index.md +++ b/daprdocs/content/en/dotnet-sdk-docs/_index.md @@ -5,6 +5,11 @@ linkTitle: ".NET" weight: 1000 description: .NET SDK packages for developing Dapr applications no_list: true +cascade: + github_repo: https://github.com/dapr/dotnet-sdk + github_subdir: daprdocs/content/en/dotnet-sdk-docs + path_base_for_github_subdir: content/en/developing-applications/sdks/dotnet/ + github_branch: master --- Dapr offers a variety of packages to help with the development of .NET applications. Using them you can create .NET clients, servers, and virtual actors with Dapr. @@ -13,7 +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 Core 3.1 or .NET 5+](https://dotnet.microsoft.com/download) installed +- [.NET 6+](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 986562ca8..eaa13625d 100644 --- a/daprdocs/content/en/dotnet-sdk-docs/dotnet-actors/dotnet-actors-howto.md +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-actors/dotnet-actors-howto.md @@ -45,7 +45,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 Core 3.1 or .NET 6+](https://dotnet.microsoft.com/download) installed. Dapr .NET SDK uses [ASP.NET Core](https://docs.microsoft.com/aspnet/core/introduction-to-aspnet-core?view=aspnetcore-6.0). +- [.NET 6+](https://dotnet.microsoft.com/download) installed. Dapr .NET SDK uses [ASP.NET Core](https://docs.microsoft.com/aspnet/core/introduction-to-aspnet-core?view=aspnetcore-6.0). ## Step 0: Prepare @@ -81,6 +81,7 @@ Define `IMyActor` interface and `MyData` data object. Paste the following code i ```csharp using Dapr.Actors; +using Dapr.Actors.Runtime; using System.Threading.Tasks; namespace MyActor.Interfaces @@ -91,6 +92,7 @@ namespace MyActor.Interfaces Task GetDataAsync(); Task RegisterReminder(); Task UnregisterReminder(); + Task GetReminder(); Task RegisterTimer(); Task UnregisterTimer(); } @@ -219,6 +221,14 @@ namespace MyActorService TimeSpan.FromSeconds(5)); // Time interval between reminder invocations after the first invocation } + /// + /// Get MyReminder reminder details with the actor + /// + public async Task GetReminder() + { + await this.GetReminderAsync("MyReminder"); + } + /// /// Unregister MyReminder reminder with the actor /// @@ -306,22 +316,11 @@ namespace MyActorService { app.UseDeveloperExceptionPage(); } - else - { - // By default, ASP.Net Core uses port 5000 for HTTP. The HTTP - // redirection will interfere with the Dapr runtime. You can - // move this out of the else block if you use port 5001 in this - // example, and developer tooling (such as the VSCode extension). - app.UseHttpsRedirection(); - } app.UseRouting(); - app.UseEndpoints(endpoints => - { - // Register actors handlers that interface with the Dapr runtime. - endpoints.MapActorsHandlers(); - }); + // Register actors handlers that interface with the Dapr runtime. + app.MapActorsHandlers(); } } } @@ -392,7 +391,7 @@ namespace MyActorClient Console.WriteLine($"Calling GetDataAsync on {actorType}:{actorId}..."); var savedData = await proxy.GetDataAsync(); - Console.WriteLine($"Got response: {response}"); + Console.WriteLine($"Got response: {savedData}"); } } } @@ -456,7 +455,7 @@ The projects that you've created can now to test the sample. Calling SetDataAsync on MyActor:1... Got response: Success Calling GetDataAsync on MyActor:1... - Got response: Success + Got response: PropertyA: ValueA, PropertyB: ValueB ``` > 💡 This sample relies on a few assumptions. The default listening port for an ASP.NET Core web project is 5000, which is being passed to `dapr run` as `--app-port 5000`. The default HTTP port for the Dapr sidecar is 3500. We're telling the sidecar for `MyActorService` to use 3500 so that `MyActorClient` can rely on the default value. 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 new file mode 100644 index 000000000..abbeb437d --- /dev/null +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-actors/dotnet-actors-serialization.md @@ -0,0 +1,317 @@ +--- +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 +--- + +The Dapr actor package enables you to use Dapr virtual actors within a .NET application with strongly-typed remoting, but if you intend to send and receive strongly-typed data from your methods, there are a few key ground rules to understand. In this guide, you will learn how to configure your classes and records so they are properly serialized and deserialized at runtime. + +# Data Contract Serialization +When Dapr's virtual actors are invoked via the remoting proxy, your data is serialized using a serialization engine called the [Data Contract Serializer](https://learn.microsoft.com/en-us/dotnet/framework/wcf/feature-details/serializable-types) implemented by the [DataContractSerializer](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.serialization.datacontractserializer) class, which converts your C# types to and from XML documents. When sending or receiving primitives (like strings or ints), this serialization happens transparently and there's no requisite preparation needed on your part. However, when working with complex types such as those you create, there are some important rules to take into consideration so this process works smoothly. + +This serialization framework is not specific to Dapr and is separately maintained by the .NET team within the [.NET Github repository](https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/DataContractSerializer.cs). + +## Serializable Types +There are several important considerations to keep in mind when using the Data Contract Serializer: + +- By default, all types, read/write properties (after construction) and fields marked as publicly visible are serialized +- All types must either expose a public parameterless constructor or be decorated with the DataContractAttribute attribute +- Init-only setters are only supported with the use of the DataContractAttribute attribute +- Read-only fields, properties without a Get and Set method and internal or properties with private Get and Set methods are ignored during serialization +- Serialization is supported for types that use other complex types that are not themselves marked with the DataContractAttribute attribute through the use of the KnownTypesAttribute attribute +- If a type is marked with the DataContractAttribute attribute, all members you wish to serialize and deserialize must be decorated with the DataMemberAttribute attribute as well or they'll be set to their default values + +## How does deserialization work? +The approach used for deserialization depends on whether or not the type is decorated with the [DataContractAttribute](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.serialization.datacontractattribute) attribute. If this attribute isn't present, an instance of the type is created using the parameterless constructor. Each of the properties and fields are then mapped into the type using their respective setters and the instance is returned to the caller. + +If the type _is_ marked with `[DataContract]`, the serializer instead uses reflection to read the metadata of the type and determine which properties or fields should be included based on whether or not they're marked with the DataMemberAttribute attribute as it's performed on an opt-in basis. It then allocates an uninitialized object in memory (avoiding the use of any constructors, parameterless or not) and then sets the value directly on each mapped property or field, even if private or uses init-only setters. Serialization callbacks are invoked as applicable throughout this process and then the object is returned to the caller. + +Use of the serialization attributes is highly recommended as they grant more flexibility to override names and namespaces and generally use more of the modern C# functionality. While the default serializer can be relied on for primitive types, it's not recommended for any of your own types, whether they be classes, structs or records. It's recommended that if you decorate a type with the DataContractAttribute attribute, you also explicitly decorate each of the members you want to serialize or deserialize with the DataMemberAttribute attribute as well. + +### .NET Classes +Classes are fully supported in the Data Contract Serializer provided that that other rules detailed on this page and the [Data Contract Serializer](https://learn.microsoft.com/en-us/dotnet/framework/wcf/feature-details/serializable-types) documentation are also followed. + +The most important thing to remember here is that you must either have a public parameterless constructor or you must decorate it with the appropriate attributes. Let's review some examples to really clarify what will and won't work. + +In the following example, we present a simple class named Doodad. We don't provide an explicit constructor here, so the compiler will provide an default parameterless constructor. Because we're using [supported primitive types](###supported-primitive-types) (Guid, string and int32) and all our members have a public getter and setter, no attributes are required and we'll be able to use this class without issue when sending and receiving it from a Dapr actor method. + +```csharp +public class Doodad +{ + public Guid Id { get; set; } + public string Name { get; set; } + public int Count { get; set; } +} +``` + +By default, this will serialize using the names of the members as used in the type and whatever values it was instantiated with: + +```xml + + a06ced64-4f42-48ad-84dd-46ae6a7e333d + DoodadName + 5 + +``` + +So let's tweak it - let's add our own constructor and only use init-only setters on the members. This will fail to serialize and deserialize not because of the use of the init-only setters, but because there's no parameterless constructors. + +```csharp +// WILL NOT SERIALIZE PROPERLY! +public class Doodad +{ + public Doodad(string name, int count) + { + Id = Guid.NewGuid(); + Name = name; + Count = count; + } + + public Guid Id { get; set; } + public string Name { get; init; } + public int Count { get; init; } +} +``` + +If we add a public parameterless constructor to the type, we're good to go and this will work without further annotations. + +```csharp +public class Doodad +{ + public Doodad() + { + } + + public Doodad(string name, int count) + { + Id = Guid.NewGuid(); + Name = name; + Count = count; + } + + public Guid Id { get; set; } + public string Name { get; set; } + public int Count { get; set; } +} +``` + +But what if we don't want to add this constructor? Perhaps you don't want your developers to accidentally create an instance of this Doodad using an unintended constructor. That's where the more flexible attributes are useful. If you decorate your type with a [DataContractAttribute](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.serialization.datacontractattribute) attribute, you can drop your parameterless constructor and it will work once again. + +```csharp +[DataContract] +public class Doodad +{ + public Doodad(string name, int count) + { + Id = Guid.NewGuid(); + Name = name; + Count = count; + } + + public Guid Id { get; set; } + public string Name { get; set; } + public int Count { get; set; } +} +``` + +In the above example, we don't need to also use the [DataMemberAttribute](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.serialization.datamemberattribute) attributes because again, we're using [built-in primitives](###supported-primitive-types) that the serializer supports. But, we do get more flexibility if we use the attributes. From the DataContractAttribute attribute, we can specify our own XML namespace with the Namespace argument and, via the Name argument, change the name of the type as used when serialized into the XML document. + +It's a recommended practice to append the DataContractAttribute attribute to the type and the DataMemberAttribute attributes to all the members you want to serialize anyway - if they're not necessary and you're not changing the default values, they'll just be ignored, but they give you a mechanism to opt into serializing members that wouldn't otherwise have been included such as those marked as private or that are themselves complex types or collections. + +Note that if you do opt into serializing your private members, their values will be serialized into plain text - they can very well be viewed, intercepted and potentially manipulated based on how you're handing the data once serialized, so it's an important consideration whether you want to mark these members or not in your use case. + +In the following example, we'll look at using the attributes to change the serialized names of some of the members as well as introduce the [IgnoreDataMemberAttribute](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.serialization.ignoredatamemberattribute) attribute. As the name indicates, this tells the serializer to skip this property even though it'd be otherwise eligible to serialize. Further, because I'm decorating the type with the DataContractAttribute attribute, it means that I can use init-only setters on the properties. + +```csharp +[DataContract(Name="Doodad")] +public class Doodad +{ + public Doodad(string name = "MyDoodad", int count = 5) + { + Id = Guid.NewGuid(); + Name = name; + Count = count; + } + + [DataMember(Name = "id")] + public Guid Id { get; init; } + [IgnoreDataMember] + public string Name { get; init; } + [DataMember] + public int Count { get; init; } +} +``` + +When this is serialized, because we're changing the names of the serialized members, we can expect a new instance of Doodad using the default values this to be serialized as: + +```xml + + a06ced64-4f42-48ad-84dd-46ae6a7e333d + 5 + +``` + +#### Classes in C# 12 - Primary Constructors +C# 12 brought us primary constructors on classes. Use of a primary constructor means the compiler will be prevented from creating the default implicit parameterless constructor. While a primary constructor on a class doesn't generate any public properties, it does mean that if you pass this primary constructor any arguments or have non-primitive types in your class, you'll either need to specify your own parameterless constructor or use the serialization attributes. + +Here's an example where we're using the primary constructor to inject an ILogger to a field and add our own parameterless constructor without the need for any attributes. + +```csharp +public class Doodad(ILogger _logger) +{ + public Doodad() {} //Our parameterless constructor + + public Doodad(string name, int count) + { + Id = Guid.NewGuid(); + Name = name; + Count = count; + } + + public Guid Id { get; set; } + public string Name { get; set; } + public int Count { get; set; } +} +``` + +And using our serialization attributes (again, opting for init-only setters since we're using the serialization attributes): + +```csharp +[DataContract] +public class Doodad(ILogger _logger) +{ + public Doodad(string name, int count) + { + Id = Guid.NewGuid(); + Name = name; + Count = count; + } + + [DataMember] + public Guid Id { get; init; } + [DataMember] + public string Name { get; init; } + [DataMember] + public int Count { get; init; } +} +``` + +### .NET Structs +Structs are supported by the Data Contract serializer provided that they are marked with the DataContractAttribute attribute and the members you wish to serialize are marked with the DataMemberAttribute attribute. Further, to support deserialization, the struct will also need to have a parameterless constructor. This works even if you define your own parameterless constructor as enabled in C# 10. + +```csharp +[DataContract] +public struct Doodad +{ + [DataMember] + public int Count { get; set; } +} +``` + +### .NET Records +Records were introduced in C# 9 and follow precisely the same rules as classes when it comes to serialization. We recommend that you should decorate all your records with the DataContractAttribute attribute and members you wish to serialize with DataMemberAttribute attributes so you don't experience any deserialization issues using this or other newer C# functionalities. Because record classes use init-only setters for properties by default and encourage the use of the primary constructor, applying these attributes to your types ensures that the serializer can properly otherwise accommodate your types as-is. + +Typically records are presented as a simple one-line statement using the new primary constructor concept: + +```csharp +public record Doodad(Guid Id, string Name, int Count); +``` + +This will throw an error encouraging the use of the serialization attributes as soon as you use it in a Dapr actor method invocation because there's no parameterless constructor available nor is it decorated with the aforementioned attributes. + +Here we add an explicit parameterless constructor and it won't throw an error, but none of the values will be set during deserialization since they're created with init-only setters. Because this doesn't use the DataContractAttribute attribute or the DataMemberAttribute attribute on any members, the serializer will be unable to map the target members correctly during deserialization. +```csharp +public record Doodad(Guid Id, string Name, int Count) +{ + public Doodad() {} +} +``` + +This approach does without the additional constructor and instead relies on the serialization attributes. Because we mark the type with the DataContractAttribute attribute and decorate each member with its own DataMemberAttribute attribute, the serialization engine will be able to map from the XML document to our type without issue. +```csharp +[DataContract] +public record Doodad( + [property: DataMember] Guid Id, + [property: DataMember] string Name, + [property: DataMember] int Count) +``` + +### Supported Primitive Types +There are several types built into .NET that are considered primitive and eligible for serialization without additional effort on the part of the developer: + +- [Byte](https://learn.microsoft.com/en-us/dotnet/api/system.byte) +- [SByte](https://learn.microsoft.com/en-us/dotnet/api/system.sbyte) +- [Int16](https://learn.microsoft.com/en-us/dotnet/api/system.int16) +- [Int32](https://learn.microsoft.com/en-us/dotnet/api/system.int32) +- [Int64](https://learn.microsoft.com/en-us/dotnet/api/system.int64) +- [UInt16](https://learn.microsoft.com/en-us/dotnet/api/system.uint16) +- [UInt32](https://learn.microsoft.com/en-us/dotnet/api/system.uint32) +- [UInt64](https://learn.microsoft.com/en-us/dotnet/api/system.uint64) +- [Single](https://learn.microsoft.com/en-us/dotnet/api/system.single) +- [Double](https://learn.microsoft.com/en-us/dotnet/api/system.double) +- [Boolean](https://learn.microsoft.com/en-us/dotnet/api/system.boolean) +- [Char](https://learn.microsoft.com/en-us/dotnet/api/system.char) +- [Decimal](https://learn.microsoft.com/en-us/dotnet/api/system.decimal) +- [Object](https://learn.microsoft.com/en-us/dotnet/api/system.object) +- [String](https://learn.microsoft.com/en-us/dotnet/api/system.string) + +There are additional types that aren't actually primitives but have similar built-in support: + +- [DateTime](https://learn.microsoft.com/en-us/dotnet/api/system.datetime) +- [TimeSpan](https://learn.microsoft.com/en-us/dotnet/api/system.timespan) +- [Guid](https://learn.microsoft.com/en-us/dotnet/api/system.guid) +- [Uri](https://learn.microsoft.com/en-us/dotnet/api/system.uri) +- [XmlQualifiedName](https://learn.microsoft.com/en-us/dotnet/api/system.xml.xmlqualifiedname) + +Again, if you want to pass these types around via your actor methods, no additional consideration is necessary as they'll be serialized and deserialized without issue. Further, types that are themselves marked with the (SerializeableAttribute)[https://learn.microsoft.com/en-us/dotnet/api/system.serializableattribute] attribute will be serialized. + +### Enumeration Types +Enumerations, including flag enumerations are serializable if appropriately marked. The enum members you wish to be serialized must be marked with the [EnumMemberAttribute](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.serialization.enummemberattribute) attribute in order to be serialized. Passing a custom value into the optional Value argument on this attribute will allow you to specify the value used for the member in the serialized document instead of having the serializer derive it from the name of the member. + +The enum type does not require that the type be decorated with the `DataContractAttribute` attribute - only that the members you wish to serialize be decorated with the `EnumMemberAttribute` attributes. + +```csharp +public enum Colors +{ + [EnumMember] + Red, + [EnumMember(Value="g")] + Green, + Blue, //Even if used by a type, this value will not be serialized as it's not decorated with the EnumMember attribute +} +``` + +### Collection Types +With regards to the data contact serializer, all collection types that implement the [IEnumerable](https://learn.microsoft.com/en-us/dotnet/api/system.collections.ienumerable) interface including arays and generic collections are considered collections. Those types that implement [IDictionary](https://learn.microsoft.com/en-us/dotnet/api/system.collections.idictionary) or the generic [IDictionary](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.idictionary-2) are considered dictionary collections; all others are list collections. + +Not unlike other complex types, collection types must have a parameterless constructor available. Further, they must also have a method called Add so they can be properly serialized and deserialized. The types used by these collection types must themselves be marked with the `DataContractAttribute` attribute or otherwise be serializable as described throughout this document. + +### Data Contract Versioning +As the data contract serializer is only used in Dapr with respect to serializing the values in the .NET SDK to and from the Dapr actor instances via the proxy methods, there's little need to consider versioning of data contracts as the data isn't being persisted between application versions using the same serializer. For those interested in learning more about data contract versioning visit [here](https://learn.microsoft.com/en-us/dotnet/framework/wcf/feature-details/data-contract-versioning). + +### Known Types +Nesting your own complex types is easily accommodated by marking each of the types with the [DataContractAttribute](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.serialization.datacontractattribute) attribute. This informs the serializer as to how deserialization should be performed. +But what if you're working with polymorphic types and one of your members is a base class or interface with derived classes or other implementations? Here, you'll use the [KnownTypeAttribute](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.serialization.knowntypeattribute) attribute to give a hint to the serializer about how to proceed. + +When you apply the [KnownTypeAttribute](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.serialization.knowntypeattribute) attribute to a type, you are informing the data contract serializer about what subtypes it might encounter allowing it to properly handle the serialization and deserialization of these types, even when the actual type at runtime is different from the declared type. + +```chsarp +[DataContract] +[KnownType(typeof(DerivedClass))] +public class BaseClass +{ + //Members of the base class +} + +[DataContract] +public class DerivedClass : BaseClass +{ + //Additional members of the derived class +} +``` + +In this example, the `BaseClass` is marked with `[KnownType(typeof(DerivedClass))]` which tells the data contract serializer that `DerivedClass` is a possible implementation of `BaseClass` that it may need to serialize or deserialize. Without this attribute, the serialize would not be aware of the `DerivedClass` when it encounters an instance of `BaseClass` that is actually of type `DerivedClass` and this could lead to a serialization exception because the serializer would not know how to handle the derived type. By specifying all possible derived types as known types, you ensure that the serializer can process the type and its members correctly. + +For more information and examples about using `[KnownType]`, please refer to the [official documentation](https://learn.microsoft.com/en-us/dotnet/framework/wcf/feature-details/data-contract-known-types). \ No newline at end of file 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 0404857e8..cab44468b 100644 --- a/daprdocs/content/en/dotnet-sdk-docs/dotnet-client/_index.md +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-client/_index.md @@ -21,13 +21,22 @@ The .NET SDK allows you to interface with all of the [Dapr building blocks]({{< ### Invoke a service +#### HTTP You can either use the `DaprClient` or `System.Net.Http.HttpClient` to invoke your services. +{{% alert title="Note" color="primary" %}} + You can also [invoke a non-Dapr endpoint using either a named `HTTPEndpoint` or an FQDN URL to the non-Dapr environment]({{< ref "howto-invoke-non-dapr-endpoints.md#using-an-httpendpoint-resource-or-fqdn-url-for-non-dapr-endpoints" >}}). + +{{% /alert %}} + + {{< tabs SDK HTTP>}} {{% codetab %}} ```csharp -using var client = new DaprClientBuilder().Build(); +using var client = new DaprClientBuilder(). + UseTimeout(TimeSpan.FromSeconds(2)). // Optionally, set a timeout + Build(); // Invokes a POST method named "deposit" that takes input of type "Transaction" var data = new { id = "17", amount = 99m }; @@ -40,15 +49,31 @@ Console.WriteLine("Returned: id:{0} | Balance:{1}", account.Id, account.Balance) ```csharp var client = DaprClient.CreateInvokeHttpClient(appId: "routing"); +// To set a timeout on the HTTP client: +client.Timeout = TimeSpan.FromSeconds(2); + 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); ``` {{% /codetab %}} - {{< /tabs >}} +#### gRPC +You can use the `DaprClient` to invoke your services over gRPC. + +```csharp +using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20)); +var invoker = DaprClient.CreateInvocationInvoker(appId: myAppId, daprEndpoint: serviceEndpoint); +var client = new MyService.MyServiceClient(invoker); + +var options = new CallOptions(cancellationToken: cts.Token, deadline: DateTime.UtcNow.AddSeconds(1)); +await client.MyMethodAsync(new Empty(), options); + +Assert.Equal(StatusCode.DeadlineExceeded, ex.StatusCode); +``` + - For a full guide on service invocation visit [How-To: Invoke a service]({{< ref howto-invoke-discover-services.md >}}). ### Save & get application state @@ -105,7 +130,7 @@ Console.WriteLine("Published deposit event!"); ``` - For a full list of state operations visit [How-To: Publish & subscribe]({{< ref howto-publish-subscribe.md >}}). -- Visit [.NET SDK examples](https://github.com/dapr/dotnet-sdk/tree/master/examples/client/PublishSubscribe) for code samples and instructions to try out pub/sub +- Visit [.NET SDK examples](https://github.com/dapr/dotnet-sdk/tree/master/examples/Client/PublishSubscribe) for code samples and instructions to try out pub/sub ### Interact with output bindings @@ -141,7 +166,7 @@ var secrets = await client.GetSecretAsync("mysecretstore", "key-value-pair-secre Console.WriteLine($"Got secret keys: {string.Join(", ", secrets.Keys)}"); ``` -{{% / codetab %}} +{{% /codetab %}} {{% codetab %}} @@ -165,7 +190,7 @@ Console.WriteLine("Got a secret value, I'm not going to be print it, it's a secr - For a full guide on secrets visit [How-To: Retrieve secrets]({{< ref howto-secrets.md >}}). -### Get Configuration Keys (Alpha) +### Get Configuration Keys ```csharp var client = new DaprClientBuilder().Build(); @@ -182,7 +207,7 @@ foreach (var item in configItems) } ``` -### Subscribe to Configuration Keys (Alpha) +### Subscribe to Configuration Keys ```csharp var client = new DaprClientBuilder().Build(); @@ -199,6 +224,64 @@ await foreach (var items in subscribeConfigurationResponse.Source.WithCancellati } ``` +### Distributed lock (Alpha) + +#### Acquire a lock + +```csharp +using System; +using Dapr.Client; + +namespace LockService +{ + class Program + { + [Obsolete("Distributed Lock API is in Alpha, this can be removed once it is stable.")] + static async Task Main(string[] args) + { + var daprLockName = "lockstore"; + var fileName = "my_file_name"; + var client = new DaprClientBuilder().Build(); + + // Locking with this approach will also unlock it automatically, as this is a disposable object + await using (var fileLock = await client.Lock(DAPR_LOCK_NAME, fileName, "random_id_abc123", 60)) + { + if (fileLock.Success) + { + Console.WriteLine("Success"); + } + else + { + Console.WriteLine($"Failed to lock {fileName}."); + } + } + } + } +} +``` + +#### Unlock an existing lock + +```csharp +using System; +using Dapr.Client; + +namespace LockService +{ + class Program + { + static async Task Main(string[] args) + { + var daprLockName = "lockstore"; + var client = new DaprClientBuilder().Build(); + + var response = await client.Unlock(DAPR_LOCK_NAME, "my_file_name", "random_id_abc123")); + Console.WriteLine(response.status); + } + } +} +``` + ### Manage workflow instances (Alpha) ```csharp diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-client/dotnet-daprclient-usage.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-client/dotnet-daprclient-usage.md index 00b330693..26328050c 100644 --- a/daprdocs/content/en/dotnet-sdk-docs/dotnet-client/dotnet-daprclient-usage.md +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-client/dotnet-daprclient-usage.md @@ -34,8 +34,10 @@ The `DaprClientBuilder` contains settings for: The SDK will read the following environment variables to configure the default values: -- `DAPR_HTTP_PORT`: used to find the HTTP endpoint of the Dapr sidecar -- `DAPR_GRPC_PORT`: used to find the gRPC endpoint of the Dapr sidecar +- `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 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 d19aee6dd..f6d18bc58 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 @@ -8,13 +8,13 @@ description: Learn how to author and manage Dapr Workflow using the .NET SDK Let's create a Dapr workflow and invoke it using the console. In the [provided order processing workflow example](https://github.com/dapr/dotnet-sdk/tree/master/examples/Workflow), the console prompts provide directions on how to both purchase and restock items. In this guide, you will: -- Create a .NET console application ([WorkflowConsoleApp](./WorkflowConsoleApp)). +- Deploy a .NET console application ([WorkflowConsoleApp](https://github.com/dapr/dotnet-sdk/tree/master/examples/Workflow/WorkflowConsoleApp)). - Utilize the .NET workflow SDK and API calls to start and query workflow instances. In the .NET example project: -- The main `Program.cs` file contains the setup of the app, including the registration of the workflow and workflow activities. -- The workflow definition is found in the `Workflows` directory. -- The workflow activity definitions are found in the `Activities` directory. +- The main [`Program.cs`](https://github.com/dapr/dotnet-sdk/blob/master/examples/Workflow/WorkflowConsoleApp/Program.cs) file contains the setup of the app, including the registration of the workflow and workflow activities. +- The workflow definition is found in the [`Workflows` directory](https://github.com/dapr/dotnet-sdk/tree/master/examples/Workflow/WorkflowConsoleApp/Workflows). +- The workflow activity definitions are found in the [`Activities` directory](https://github.com/dapr/dotnet-sdk/tree/master/examples/Workflow/WorkflowConsoleApp/Activities). ## Prerequisites @@ -72,7 +72,7 @@ This guide focuses on the workflow API option. {{% alert title="Note" color="primary" %}} - You can find the commands below in the `WorkflowConsoleApp`/`demo.http` file. - The body of the curl request is the purchase order information used as the input of the workflow. - - The "1234" in the commands represents the unique identifier for the workflow and can be replaced with any identifier of your choosing. + - The "12345678" in the commands represents the unique identifier for the workflow and can be replaced with any identifier of your choosing. {{% /alert %}} @@ -83,9 +83,9 @@ Run the following command to start a workflow. {{% codetab %}} ```bash -curl -i -X POST http://localhost:3500/v1.0-alpha1/workflows/dapr/OrderProcessingWorkflow/1234/start \ +curl -i -X POST http://localhost:3500/v1.0-beta1/workflows/dapr/OrderProcessingWorkflow/start?instanceID=12345678 \ -H "Content-Type: application/json" \ - -d '{ "input" : {"Name": "Paperclips", "TotalCost": 99.95, "Quantity": 1}}' + -d '{"Name": "Paperclips", "TotalCost": 99.95, "Quantity": 1}' ``` {{% /codetab %}} @@ -93,9 +93,9 @@ curl -i -X POST http://localhost:3500/v1.0-alpha1/workflows/dapr/OrderProcessing {{% codetab %}} ```powershell -curl -i -X POST http://localhost:3500/v1.0-alpha1/workflows/dapr/OrderProcessingWorkflow/1234/start ` +curl -i -X POST http://localhost:3500/v1.0-beta1/workflows/dapr/OrderProcessingWorkflow/start?instanceID=12345678 ` -H "Content-Type: application/json" ` - -d '{ "input" : {"Name": "Paperclips", "TotalCost": 99.95, "Quantity": 1}}' + -d '{"Name": "Paperclips", "TotalCost": 99.95, "Quantity": 1}' ``` {{% /codetab %}} @@ -105,30 +105,27 @@ curl -i -X POST http://localhost:3500/v1.0-alpha1/workflows/dapr/OrderProcessing If successful, you should see a response like the following: ```json -{"instance_id":"1234"} +{"instanceID":"12345678"} ``` Send an HTTP request to get the status of the workflow that was started: ```bash -curl -i -X GET http://localhost:3500/v1.0-alpha1/workflows/dapr/OrderProcessingWorkflow/1234 +curl -i -X GET http://localhost:3500/v1.0-beta1/workflows/dapr/12345678 ``` The workflow is designed to take several seconds to complete. If the workflow hasn't completed when you issue the HTTP request, you'll see the following JSON response (formatted for readability) with workflow status as `RUNNING`: ```json { - "WFInfo": { - "instance_id": "1234" - }, - "start_time": "2023-02-02T23:34:53Z", - "metadata": { + "instanceID": "12345678", + "workflowName": "OrderProcessingWorkflow", + "createdAt": "2023-05-10T00:42:03.911444105Z", + "lastUpdatedAt": "2023-05-10T00:42:06.142214153Z", + "runtimeStatus": "RUNNING", + "properties": { "dapr.workflow.custom_status": "", - "dapr.workflow.input": "{\"Name\":\"Paperclips\",\"Quantity\":1,\"TotalCost\":99.95}", - "dapr.workflow.last_updated": "2023-02-02T23:35:07Z", - "dapr.workflow.name": "OrderProcessingWorkflow", - "dapr.workflow.output": "{\"Processed\":true}", - "dapr.workflow.runtime_status": "RUNNING" + "dapr.workflow.input": "{\"Name\": \"Paperclips\", \"TotalCost\": 99.95, \"Quantity\": 1}" } } ``` @@ -137,17 +134,15 @@ Once the workflow has completed running, you should see the following output, in ```json { - "WFInfo": { - "instance_id": "1234" - }, - "start_time": "2023-02-02T23:34:53Z", - "metadata": { + "instanceID": "12345678", + "workflowName": "OrderProcessingWorkflow", + "createdAt": "2023-05-10T00:42:03.911444105Z", + "lastUpdatedAt": "2023-05-10T00:42:18.527704176Z", + "runtimeStatus": "COMPLETED", + "properties": { "dapr.workflow.custom_status": "", - "dapr.workflow.input": "{\"Name\":\"Paperclips\",\"Quantity\":1,\"TotalCost\":99.95}", - "dapr.workflow.last_updated": "2023-02-02T23:35:07Z", - "dapr.workflow.name": "OrderProcessingWorkflow", - "dapr.workflow.output": "{\"Processed\":true}", - "dapr.workflow.runtime_status": "COMPLETED" + "dapr.workflow.input": "{\"Name\": \"Paperclips\", \"TotalCost\": 99.95, \"Quantity\": 1}", + "dapr.workflow.output": "{\"Processed\":true}" } } ``` @@ -156,17 +151,23 @@ When the workflow has completed, the stdout of the workflow app should look like ```log info: WorkflowConsoleApp.Activities.NotifyActivity[0] - Received order 1234 for Paperclips at $99.95 + Received order 12345678 for Paperclips at $99.95 info: WorkflowConsoleApp.Activities.ReserveInventoryActivity[0] - Reserving inventory: 1234, Paperclips, 1 + Reserving inventory: 12345678, Paperclips, 1 info: WorkflowConsoleApp.Activities.ProcessPaymentActivity[0] - Processing payment: 1234, 99.95, USD + Processing payment: 12345678, 99.95, USD info: WorkflowConsoleApp.Activities.NotifyActivity[0] - Order 1234 processed successfully! + Order 12345678 processed successfully! ``` If you have Zipkin configured for Dapr locally on your machine, then you can view the workflow trace spans in the Zipkin web UI (typically at http://localhost:9411/zipkin/). +## Demo + +Watch this video [demonstrating .NET Workflow](https://youtu.be/BxiKpEmchgQ?t=2557): + + + ## Next steps - [Try the Dapr Workflow quickstart]({{< ref workflow-quickstart.md >}}) diff --git a/examples/Actor/ActorClient/ActorClient.csproj b/examples/Actor/ActorClient/ActorClient.csproj index 48c8318c6..0d1d94f55 100644 --- a/examples/Actor/ActorClient/ActorClient.csproj +++ b/examples/Actor/ActorClient/ActorClient.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp3.1 + net6 diff --git a/examples/Actor/ActorClient/Program.cs b/examples/Actor/ActorClient/Program.cs index 103aed6b9..f6ca26f53 100644 --- a/examples/Actor/ActorClient/Program.cs +++ b/examples/Actor/ActorClient/Program.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -11,6 +11,9 @@ // limitations under the License. // ------------------------------------------------------------------------ +using Dapr.Actors.Communication; +using IDemoActor; + namespace ActorClient { using System; @@ -18,8 +21,6 @@ namespace ActorClient using System.Threading.Tasks; using Dapr.Actors; using Dapr.Actors.Client; - using Dapr.Actors.Communication; - using IDemoActorInterface; /// /// Actor Client class. @@ -44,10 +45,10 @@ public static async Task Main(string[] args) // Make strongly typed Actor calls with Remoting. // DemoActor is the type registered with Dapr runtime in the service. - var proxy = ActorProxy.Create(actorId, "DemoActor"); + var proxy = ActorProxy.Create(actorId, "DemoActor"); Console.WriteLine("Making call using actor proxy to save data."); - await proxy.SaveData(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}."); @@ -69,7 +70,7 @@ public static async Task Main(string[] args) } catch (ActorMethodInvocationException ex) { - if (ex.InnerException is NotImplementedException) + if (ex.InnerException is ActorInvokeException invokeEx && invokeEx.ActualExceptionType is "System.NotImplementedException") { Console.WriteLine($"Got Correct Exception from actor method invocation."); } @@ -96,6 +97,10 @@ public static async Task Main(string[] args) 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."); @@ -105,14 +110,23 @@ public static async Task Main(string[] args) 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)); diff --git a/examples/Actor/DemoActor/BankService.cs b/examples/Actor/DemoActor/BankService.cs index 0a164183f..a24eadedb 100644 --- a/examples/Actor/DemoActor/BankService.cs +++ b/examples/Actor/DemoActor/BankService.cs @@ -11,9 +11,9 @@ // limitations under the License. // ------------------------------------------------------------------------ -using IDemoActorInterface; +using IDemoActor; -namespace DaprDemoActor +namespace DemoActor { public class BankService { diff --git a/examples/Actor/DemoActor/DemoActor.cs b/examples/Actor/DemoActor/DemoActor.cs index 057b7df6d..b5ef53e93 100644 --- a/examples/Actor/DemoActor/DemoActor.cs +++ b/examples/Actor/DemoActor/DemoActor.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -11,14 +11,14 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace DaprDemoActor -{ - using System; - using System.Text.Json; - using System.Threading.Tasks; - using Dapr.Actors.Runtime; - using IDemoActorInterface; +using System; +using System.Text.Json; +using System.Threading.Tasks; +using Dapr.Actors.Runtime; +using IDemoActor; +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. @@ -27,7 +27,7 @@ namespace DaprDemoActor // 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, IBankActor, IRemindable + public class DemoActor : Actor, IDemoActor.IDemoActor, IBankActor, IRemindable { private const string StateName = "my_data"; @@ -41,12 +41,12 @@ public DemoActor(ActorHost host, BankService bank) this.bank = bank; } - public async Task SaveData(MyData data) + public async Task SaveData(MyData data, TimeSpan ttl) { Console.WriteLine($"This is Actor id {this.Id} with data {data}."); // Set State using StateManager, state is saved after the method execution. - await this.StateManager.SetStateAsync(StateName, data); + await this.StateManager.SetStateAsync(StateName, data, ttl); } public Task GetData() @@ -85,6 +85,20 @@ public async Task RegisterReminderWithTtlAndRepetitions(TimeSpan ttl, int repeti await this.RegisterReminderAsync("TestReminder", null, TimeSpan.FromSeconds(0), TimeSpan.FromSeconds(1), repetitions, ttl); } + 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; + } + public Task UnregisterReminder() { return this.UnregisterReminderAsync("TestReminder"); @@ -95,7 +109,7 @@ public async Task ReceiveReminderAsync(string reminderName, byte[] state, TimeSp // 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); + await this.StateManager.SetStateAsync(StateName, actorState, ttl: TimeSpan.FromMinutes(5)); } class TimerParams @@ -159,7 +173,7 @@ 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); + 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); diff --git a/examples/Actor/DemoActor/DemoActor.csproj b/examples/Actor/DemoActor/DemoActor.csproj index 80a3883c3..24a42ee0e 100644 --- a/examples/Actor/DemoActor/DemoActor.csproj +++ b/examples/Actor/DemoActor/DemoActor.csproj @@ -1,13 +1,24 @@  - - netcoreapp3.1 - - - - - - - + + net6 + + + + true + true + demo-actor + + + + + + + + + + + + diff --git a/examples/Actor/DemoActor/Program.cs b/examples/Actor/DemoActor/Program.cs index a56681fdb..1d538b471 100644 --- a/examples/Actor/DemoActor/Program.cs +++ b/examples/Actor/DemoActor/Program.cs @@ -11,11 +11,11 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace DaprDemoActor -{ - using Microsoft.AspNetCore.Hosting; - using Microsoft.Extensions.Hosting; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; +namespace DemoActor +{ public class Program { public static void Main(string[] args) diff --git a/examples/Actor/DemoActor/Startup.cs b/examples/Actor/DemoActor/Startup.cs index c04dfdcba..f1165e3c7 100644 --- a/examples/Actor/DemoActor/Startup.cs +++ b/examples/Actor/DemoActor/Startup.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -11,14 +11,14 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace DaprDemoActor -{ - using Microsoft.AspNetCore.Builder; - using Microsoft.AspNetCore.Hosting; - using Microsoft.Extensions.Configuration; - using Microsoft.Extensions.DependencyInjection; - using Microsoft.Extensions.Hosting; +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) diff --git a/examples/Actor/DemoActor/demo-actor.yaml b/examples/Actor/DemoActor/demo-actor.yaml new file mode 100644 index 000000000..99a8abd34 --- /dev/null +++ b/examples/Actor/DemoActor/demo-actor.yaml @@ -0,0 +1,67 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: statestore +spec: + type: state.in-memory + version: v1 + metadata: + - name: actorStateStore + value: "true" +--- +kind: Service +apiVersion: v1 +metadata: + name: demoactor + labels: + app: demoactor +spec: + selector: + app: demoactor + ports: + - name: app-port + protocol: TCP + port: 5010 + targetPort: app-port + - name: dapr-http + protocol: TCP + port: 3500 + targetPort: 3500 + type: LoadBalancer +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: demoactor + labels: + app: demoactor +spec: + replicas: 1 + selector: + matchLabels: + app: demoactor + template: + metadata: + labels: + app: demoactor + annotations: + dapr.io/enabled: "true" + dapr.io/app-id: "demoactor" + dapr.io/app-port: "5010" + dapr.io/enable-api-logging: "true" + dapr.io/sidecar-listen-addresses: "0.0.0.0" + spec: + containers: + - name: demoactor + # image: /demo-actor:latest + image: demo-actor:latest + # if you are using docker desktop, you can use imagePullPolicy: Never to use local image + imagePullPolicy: Never + env: + - name: APP_PORT + value: "5010" + - name: ASPNETCORE_URLS + value: "http://+:5010" + ports: + - name: app-port + containerPort: 5010 diff --git a/examples/Actor/IDemoActor/IBankActor.cs b/examples/Actor/IDemoActor/IBankActor.cs index 95ac23844..c495f027b 100644 --- a/examples/Actor/IDemoActor/IBankActor.cs +++ b/examples/Actor/IDemoActor/IBankActor.cs @@ -11,12 +11,12 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace IDemoActorInterface -{ - using System; - using System.Threading.Tasks; - using Dapr.Actors; +using System; +using System.Threading.Tasks; +using Dapr.Actors; +namespace IDemoActor +{ public interface IBankActor : IActor { Task GetAccountBalance(); diff --git a/examples/Actor/IDemoActor/IDemoActor.cs b/examples/Actor/IDemoActor/IDemoActor.cs index 3220dfdbd..6f2d32801 100644 --- a/examples/Actor/IDemoActor/IDemoActor.cs +++ b/examples/Actor/IDemoActor/IDemoActor.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -11,12 +11,12 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace IDemoActorInterface -{ - using System; - using System.Threading.Tasks; - using Dapr.Actors; +using System; +using System.Threading.Tasks; +using Dapr.Actors; +namespace IDemoActor +{ /// /// Interface for Actor method. /// @@ -26,8 +26,9 @@ 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); + Task SaveData(MyData data, TimeSpan ttl); /// /// Method to get data. @@ -94,6 +95,13 @@ public interface IDemoActor : IActor /// 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. /// @@ -124,4 +132,18 @@ public override string ToString() 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/Actor/IDemoActor/IDemoActor.csproj b/examples/Actor/IDemoActor/IDemoActor.csproj index 23a96ffc6..9f7744796 100644 --- a/examples/Actor/IDemoActor/IDemoActor.csproj +++ b/examples/Actor/IDemoActor/IDemoActor.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6 diff --git a/examples/Actor/README.md b/examples/Actor/README.md index ddc42cecf..89b6bf0bb 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 Core 3.1 or .NET 5+](https://dotnet.microsoft.com/download) installed +- [.NET 6+](https://dotnet.microsoft.com/download) installed - [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/) - [Initialized Dapr environment](https://docs.dapr.io/getting-started/install-dapr-selfhost/) - [Dapr .NET SDK](https://github.com/dapr/dotnet-sdk/) @@ -80,3 +80,80 @@ On Windows: ```sh curl -X POST http://127.0.0.1:3500/v1.0/actors/DemoActor/abc/method/GetData ``` + +### Build and push Docker image +You can build the docker image of `DemoActor` service by running the following commands in the `DemoActor` project directory: + +``` Bash +dotnet publish --os linux --arch x64 /t:PublishContainer -p ContainerImageTags='"latest"' --self-contained +``` + +The build produce and image with tag `demo-actor:latest` and load it in the local registry. +Now the image can be pushed to your remote Docker registry by running the following commands: + +``` Bash +# Replace with your Docker registry +docker tag demo-actor:latest /demo-actor:latest + +# Push the image to your Docker registry +docker push /demo-actor:latest +``` + +### Deploy the Actor service to Kubernetes +#### Prerequisites +- A Kubernetes cluster with `kubectl` configured to access it. +- Dapr v1.14+ installed on the Kubernetes cluster. Follow the instructions [here](https://docs.dapr.io/getting-started/install-dapr-kubernetes/). +- A Docker registry where you pushed the `DemoActor` image. + +#### Deploy the Actor service +For quick deployment you can install dapr in dev mode using the following command: + +``` Bash +dapr init -k --dev +``` + +To deploy the `DemoActor` service to Kubernetes, you can use the provided Kubernetes manifest file `demo-actor.yaml` in the `DemoActor` project directory. +Before applying the manifest file, replace the image name in the manifest file with the image name you pushed to your Docker registry. + +Part to update in `demo-actor.yaml`: +``` YAML +image: /demoactor:latest +``` + +To install the application in `default` namespace, run the following command: + +``` Bash +kubectl apply -f demo-actor.yaml +``` + +This will deploy the `DemoActor` service to Kubernetes. You can check the status of the deployment by running: + +``` Bash +kubectl get pods -n default --watch +``` + +The manifest create 2 services: + +- `demoactor` service: The service that hosts the `DemoActor` actor. +- `demoactor-dapr` service: The service that hosts the Dapr sidecar for the `DemoActor` actor. + +### Make client calls to the deployed Actor service +To make client calls to the deployed `DemoActor` service, you can use the `ActorClient` project. +Before running the client, update the `DAPR_HTTP_PORT` environment variable in the `ActorClient` project directory to the port on which Dapr is running in the Kubernetes cluster. + +On Linux, MacOS: +``` Bash +export DAPR_HTTP_PORT=3500 +``` + +Than port-forward the `DemoActor` service to your local machine: + +``` Bash +kubectl port-forward svc/demoactor 3500:3500 +``` + +Now you can run the client project from the `ActorClient` directory: + +``` Bash +dotnet run +``` \ No newline at end of file diff --git a/examples/AspNetCore/ControllerSample/ControllerSample.csproj b/examples/AspNetCore/ControllerSample/ControllerSample.csproj index 863290324..6dbe750a6 100644 --- a/examples/AspNetCore/ControllerSample/ControllerSample.csproj +++ b/examples/AspNetCore/ControllerSample/ControllerSample.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6 diff --git a/examples/AspNetCore/ControllerSample/Controllers/SampleController.cs b/examples/AspNetCore/ControllerSample/Controllers/SampleController.cs index 485614150..5b339288c 100644 --- a/examples/AspNetCore/ControllerSample/Controllers/SampleController.cs +++ b/examples/AspNetCore/ControllerSample/Controllers/SampleController.cs @@ -11,6 +11,8 @@ // limitations under the License. // ------------------------------------------------------------------------ +using System.Linq; + namespace ControllerSample.Controllers { using System; @@ -43,6 +45,7 @@ public SampleController(ILogger logger) /// State store name. /// public const string StoreName = "statestore"; + private readonly ILogger logger; /// @@ -72,6 +75,11 @@ public ActionResult Get([FromState(StoreName)] StateEntry acco [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, }; @@ -83,7 +91,7 @@ public async Task> Deposit(Transaction transaction, [FromS } state.Value.Balance += transaction.Amount; - logger.LogInformation("Balance for Id {0} is {1}",state.Value.Id, state.Value.Balance); + logger.LogInformation("Balance for Id {0} is {1}", state.Value.Id, state.Value.Balance); await state.SaveAsync(); return state.Value; } @@ -98,22 +106,23 @@ public async Task> Deposit(Transaction transaction, [FromS [Topic("pubsub", "multideposit", "amountDeadLetterTopic", false)] [BulkSubscribe("multideposit", 500, 2000)] [HttpPost("multideposit")] - public async Task> MultiDeposit([FromBody] BulkSubscribeMessage> - bulkMessage, [FromServices] DaprClient daprClient) + 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}", + logger.LogInformation("Id is {0}, the amount to be deposited is {1}", transaction.Id, transaction.Amount); if (transaction.Amount < 0m) @@ -124,12 +133,16 @@ public async Task> MultiDeposit([FromBody 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) { + 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); } @@ -165,6 +178,7 @@ public async Task> Withdraw(Transaction transaction, [From { return this.NotFound(); } + if (transaction.Amount < 0m) { return BadRequest(new { statusCode = 400, message = "bad request" }); @@ -185,7 +199,8 @@ public async Task> Withdraw(Transaction transaction, [From /// "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) + public async Task> WithdrawV2(TransactionV2 transaction, + [FromServices] DaprClient daprClient) { logger.LogInformation("Enter withdraw.v2"); if (transaction.Channel == "mobile" && transaction.Amount > 10000) @@ -214,12 +229,15 @@ public async Task> WithdrawV2(TransactionV2 transaction, [ /// "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) + 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))}"); + 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 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); @@ -239,7 +257,8 @@ public async Task> RawDeposit([FromBody] JsonDocument rawT /// 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) + public async Task> ThrowException(Transaction transaction, + [FromServices] DaprClient daprClient) { logger.LogInformation("Enter ThrowException"); var task = Task.Delay(10); diff --git a/examples/AspNetCore/ControllerSample/CustomTopicAttribute.cs b/examples/AspNetCore/ControllerSample/CustomTopicAttribute.cs index 96eb918fb..5c9996aea 100644 --- a/examples/AspNetCore/ControllerSample/CustomTopicAttribute.cs +++ b/examples/AspNetCore/ControllerSample/CustomTopicAttribute.cs @@ -11,6 +11,8 @@ // limitations under the License. // ------------------------------------------------------------------------ +using Dapr.AspNetCore; + namespace ControllerSample { using System; diff --git a/examples/AspNetCore/ControllerSample/README.md b/examples/AspNetCore/ControllerSample/README.md index a2700e94d..3b2ca02b9 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 Core 3.1 or .NET 5+](https://dotnet.microsoft.com/download) installed +- [.NET 6+](https://dotnet.microsoft.com/download) installed - [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/) - [Initialized Dapr environment](https://docs.dapr.io/getting-started/install-dapr-selfhost/) - [Dapr .NET SDK](https://docs.dapr.io/developing-applications/sdks/dotnet/) diff --git a/examples/AspNetCore/ControllerSample/Startup.cs b/examples/AspNetCore/ControllerSample/Startup.cs index 11b81d8b3..ddc6d1c5f 100644 --- a/examples/AspNetCore/ControllerSample/Startup.cs +++ b/examples/AspNetCore/ControllerSample/Startup.cs @@ -11,6 +11,11 @@ // limitations under the License. // ------------------------------------------------------------------------ + +using Dapr; +using Dapr.AspNetCore; + + namespace ControllerSample { using Microsoft.AspNetCore.Builder; @@ -61,7 +66,10 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseRouting(); - app.UseCloudEvents(); + app.UseCloudEvents(new CloudEventsMiddlewareOptions + { + ForwardCloudEventPropertiesAsHeaders = true + }); app.UseAuthorization(); diff --git a/examples/AspNetCore/GrpcServiceSample/GrpcServiceSample.csproj b/examples/AspNetCore/GrpcServiceSample/GrpcServiceSample.csproj index 23ead7d4d..2319f6a56 100644 --- a/examples/AspNetCore/GrpcServiceSample/GrpcServiceSample.csproj +++ b/examples/AspNetCore/GrpcServiceSample/GrpcServiceSample.csproj @@ -1,7 +1,7 @@ - netcoreapp3.1 + net6 true @@ -10,15 +10,16 @@ - - - - - + + + + + + diff --git a/examples/AspNetCore/GrpcServiceSample/README.md b/examples/AspNetCore/GrpcServiceSample/README.md index 3f27d280c..d08e96cd9 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 Core 3.1 or .NET 5+](https://dotnet.microsoft.com/download) installed +- [.NET 6+](https://dotnet.microsoft.com/download) installed - [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/) - [Initialized Dapr environment](https://docs.dapr.io/getting-started/install-dapr-selfhost/) - [Dapr .NET SDK](https://docs.dapr.io/developing-applications/sdks/dotnet/) diff --git a/examples/AspNetCore/GrpcServiceSample/Services/BankingService.cs b/examples/AspNetCore/GrpcServiceSample/Services/BankingService.cs index 56b80cad6..9518fd610 100644 --- a/examples/AspNetCore/GrpcServiceSample/Services/BankingService.cs +++ b/examples/AspNetCore/GrpcServiceSample/Services/BankingService.cs @@ -22,7 +22,7 @@ using GrpcServiceSample.Generated; using Microsoft.Extensions.Logging; -namespace GrpcServiceSample +namespace GrpcServiceSample.Services { /// /// BankAccount gRPC service diff --git a/examples/AspNetCore/GrpcServiceSample/Startup.cs b/examples/AspNetCore/GrpcServiceSample/Startup.cs index 752d62448..4aa5ac7d3 100644 --- a/examples/AspNetCore/GrpcServiceSample/Startup.cs +++ b/examples/AspNetCore/GrpcServiceSample/Startup.cs @@ -11,6 +11,8 @@ // limitations under the License. // ------------------------------------------------------------------------ +using Dapr.AspNetCore; +using GrpcServiceSample.Services; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; diff --git a/examples/AspNetCore/RoutingSample/README.md b/examples/AspNetCore/RoutingSample/README.md index cd545b747..901c51c40 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 Core 3.1 or .NET 5+](https://dotnet.microsoft.com/download) installed +- [.NET 6+](https://dotnet.microsoft.com/download) installed - [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/) - [Initialized Dapr environment](https://docs.dapr.io/getting-started/install-dapr-selfhost/) - [Dapr .NET SDK](https://docs.dapr.io/developing-applications/sdks/dotnet/) diff --git a/examples/AspNetCore/SecretStoreConfigurationProviderSample/README.md b/examples/AspNetCore/SecretStoreConfigurationProviderSample/README.md index 5be9eb5e1..09422e474 100644 --- a/examples/AspNetCore/SecretStoreConfigurationProviderSample/README.md +++ b/examples/AspNetCore/SecretStoreConfigurationProviderSample/README.md @@ -2,7 +2,7 @@ ## Prerequisites -- [.NET Core 3.1 or .NET 5+](https://dotnet.microsoft.com/download) installed +- [.NET 6+](https://dotnet.microsoft.com/download) installed - [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/) - [Initialized Dapr environment](https://docs.dapr.io/getting-started/install-dapr-selfhost/) - [Dapr .NET SDK](https://docs.dapr.io/developing-applications/sdks/dotnet/) diff --git a/examples/AspNetCore/SecretStoreConfigurationProviderSample/SecretStoreConfigurationProviderSample.csproj b/examples/AspNetCore/SecretStoreConfigurationProviderSample/SecretStoreConfigurationProviderSample.csproj index 10928470b..01fbc2079 100644 --- a/examples/AspNetCore/SecretStoreConfigurationProviderSample/SecretStoreConfigurationProviderSample.csproj +++ b/examples/AspNetCore/SecretStoreConfigurationProviderSample/SecretStoreConfigurationProviderSample.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp3.1 + net6 diff --git a/examples/Client/ConfigurationApi/ConfigurationApi.csproj b/examples/Client/ConfigurationApi/ConfigurationApi.csproj index f73fd4974..761ebb38f 100644 --- a/examples/Client/ConfigurationApi/ConfigurationApi.csproj +++ b/examples/Client/ConfigurationApi/ConfigurationApi.csproj @@ -1,13 +1,14 @@ - netcoreapp3.1 + net6 + diff --git a/examples/Client/ConfigurationApi/Controllers/ConfigurationController.cs b/examples/Client/ConfigurationApi/Controllers/ConfigurationController.cs index 387c3f94f..55bf6df53 100644 --- a/examples/Client/ConfigurationApi/Controllers/ConfigurationController.cs +++ b/examples/Client/ConfigurationApi/Controllers/ConfigurationController.cs @@ -13,7 +13,6 @@ namespace ConfigurationApi.Controllers { [ApiController] [Route("configuration")] - [Obsolete] public class ConfigurationController : ControllerBase { private ILogger logger; diff --git a/examples/Client/ConfigurationApi/Program.cs b/examples/Client/ConfigurationApi/Program.cs index 12328ebdb..f5218602d 100644 --- a/examples/Client/ConfigurationApi/Program.cs +++ b/examples/Client/ConfigurationApi/Program.cs @@ -1,4 +1,4 @@ -using System; +using System; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; using Dapr.Client; @@ -10,7 +10,6 @@ namespace ConfigurationApi { public class Program { - [Obsolete] public static void Main(string[] args) { Console.WriteLine("Starting application."); diff --git a/examples/Client/ConfigurationApi/README.md b/examples/Client/ConfigurationApi/README.md index 41c1c831f..7425a780a 100644 --- a/examples/Client/ConfigurationApi/README.md +++ b/examples/Client/ConfigurationApi/README.md @@ -9,7 +9,7 @@ It demonstrates the following APIs: ## Prerequisites -- [.NET Core 3.1 or .NET 5+](https://dotnet.microsoft.com/download) installed +- [.NET 6+](https://dotnet.microsoft.com/download) installed - [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/) - [Initialized Dapr environment](https://docs.dapr.io/getting-started/install-dapr-selfhost/) - [Dapr .NET SDK](https://docs.dapr.io/developing-applications/sdks/dotnet/) diff --git a/examples/Client/ConfigurationApi/Startup.cs b/examples/Client/ConfigurationApi/Startup.cs index 62a77ac49..db5b921c9 100644 --- a/examples/Client/ConfigurationApi/Startup.cs +++ b/examples/Client/ConfigurationApi/Startup.cs @@ -1,4 +1,5 @@ using System; +using Dapr.AspNetCore; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; diff --git a/examples/Client/Cryptography/Components/azurekeyvault.yaml b/examples/Client/Cryptography/Components/azurekeyvault.yaml new file mode 100644 index 000000000..5932e0bc8 --- /dev/null +++ b/examples/Client/Cryptography/Components/azurekeyvault.yaml @@ -0,0 +1,25 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: azurekeyvault +spec: + type: crypto.azure.keyvault + metadata: + - name: vaultName + value: "" + - name: azureEnvironment + value: AZUREPUBLICCLOUD + - name: azureTenantId + secretKeyRef: + name: read_azure_tenant_id + key: read_azure_tenant_id + - name: azureClientId + secretKeyRef: + name: read_azure_client_id + key: read_azure_client_id + - name: azureClientSecret + secretKeyRef: + name: read_azure_client_secret + key: read_azure_client_secret +auth: + secureStore: envvar-secret-store \ No newline at end of file diff --git a/examples/Client/Cryptography/Components/env-secretstore.yaml b/examples/Client/Cryptography/Components/env-secretstore.yaml new file mode 100644 index 000000000..fb191414d --- /dev/null +++ b/examples/Client/Cryptography/Components/env-secretstore.yaml @@ -0,0 +1,7 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: envvar-secret-store +spec: + type: secretstores.local.env + version: v1 \ No newline at end of file diff --git a/examples/Client/Cryptography/Cryptography.csproj b/examples/Client/Cryptography/Cryptography.csproj new file mode 100644 index 000000000..525c38562 --- /dev/null +++ b/examples/Client/Cryptography/Cryptography.csproj @@ -0,0 +1,25 @@ + + + + Exe + net6.0 + enable + enable + latest + + + + + + + + + + + + + PreserveNewest + + + + \ No newline at end of file diff --git a/examples/Client/Cryptography/Example.cs b/examples/Client/Cryptography/Example.cs new file mode 100644 index 000000000..2c2d41626 --- /dev/null +++ b/examples/Client/Cryptography/Example.cs @@ -0,0 +1,22 @@ +// ------------------------------------------------------------------------ +// 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. +// ------------------------------------------------------------------------ + +namespace Cryptography +{ + internal abstract class Example + { + public abstract string DisplayName { get; } + + public abstract Task RunAsync(CancellationToken cancellationToken); + } +} diff --git a/examples/Client/Cryptography/Examples/EncryptDecryptFileStreamExample.cs b/examples/Client/Cryptography/Examples/EncryptDecryptFileStreamExample.cs new file mode 100644 index 000000000..aa9c404a7 --- /dev/null +++ b/examples/Client/Cryptography/Examples/EncryptDecryptFileStreamExample.cs @@ -0,0 +1,77 @@ +// ------------------------------------------------------------------------ +// 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 : 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(); + + const string componentName = "azurekeyvault"; // Change this to match the name of the component containing your vault + const string keyName = "myKey"; + + // 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); + } + Console.WriteLine(); + + //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: {Convert.ToBase64String(bufferedEncryptedBytes.GetSpan())}"); + Console.WriteLine(); + + //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); + } + } +} diff --git a/examples/Client/Cryptography/Examples/EncryptDecryptStringExample.cs b/examples/Client/Cryptography/Examples/EncryptDecryptStringExample.cs new file mode 100644 index 000000000..a37ca1b8b --- /dev/null +++ b/examples/Client/Cryptography/Examples/EncryptDecryptStringExample.cs @@ -0,0 +1,47 @@ +// ------------------------------------------------------------------------ +// 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.Text; +using Dapr.Client; +#pragma warning disable CS0618 // Type or member is obsolete + +namespace Cryptography.Examples +{ + internal class EncryptDecryptStringExample : Example + { + 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(); + + const string componentName = "azurekeyvault"; //Change this to match the name of the component containing your vault + const string keyName = "myKey"; //Change this to match the name of the key in your Vault + + + 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); + + Console.WriteLine($"Encrypted bytes: '{Convert.ToBase64String(encryptedBytesResult.Span)}'"); + + //Decrypt the string + var decryptedBytes = await client.DecryptAsync(componentName, encryptedBytesResult, keyName, new DecryptionOptions(), cancellationToken); + Console.WriteLine($"Decrypted string: '{Encoding.UTF8.GetString(decryptedBytes.ToArray())}'"); + } + } +} diff --git a/examples/Client/Cryptography/Program.cs b/examples/Client/Cryptography/Program.cs new file mode 100644 index 000000000..da81bef8f --- /dev/null +++ b/examples/Client/Cryptography/Program.cs @@ -0,0 +1,46 @@ +// ------------------------------------------------------------------------ +// 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 static readonly Example[] Examples = new Example[] + { + new EncryptDecryptStringExample(), + new EncryptDecryptFileStreamExample() + }; + + 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; + } + } +} diff --git a/examples/Client/Cryptography/README.md b/examples/Client/Cryptography/README.md new file mode 100644 index 000000000..c0c884369 --- /dev/null +++ b/examples/Client/Cryptography/README.md @@ -0,0 +1,92 @@ +# Dapr .NET SDK Cryptography example + +## Prerequisites + +- [.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/installation) +- [Dapr .NET SDK](https://docs.dapr.io/developing-applications/sdks/dotnet/) +- [Azure Key Vault instance](https://learn.microsoft.com/en-us/azure/key-vault/general/quick-create-portal) +- [Entra Service Principal](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app) + +### Service Principal/Environment Variables Setup +In your Azure portal, open Microsoft Entra ID and click `App Registrations`. Click the button at the top to create a new registration. Select a name for your service principal +and click register, noting this name for later. + +Once the registration is completed, open it from the list and select Certificates & Secrets from the left navigation. Select "Client secrets" from the page body (middle column) +and click the button to add a new client secret giving it an optional description and changing the expiry date as you desire. Click Add to create the secret. Record the secret +value it shows you - it will not be shown to you again without creating another client secret. + +Click Overview from the left navigation and record the "Application (client) ID" and the "Directory (tenant) ID" values. + +On your computer (assuming Windows), open your start menu and type "Environment Variables". An option should appear named "Edit the system environment variables". Select this +and your System Properties window will open. Click the "Environment Variables" button in the bottom and said window will appear. Click the "New..." button under System variables +to add the requisite service principal values to your environment variables. You can change these names as to want by updating the `./Components/azurekeyvault.yaml` names, but for now +configure as follows: + +| Variable Name | Value | +|--|--| +| read_azure_client_id | Paste the value from your app registration overview for "Application (client) ID" | +| read_azure_client_secret | Paste the value of the client secret you generated for your app registration | +| read_azure_tenant_id | Paste the valeu from your app registration overview for "Directory (tenant) ID" | + +Click OK to save your environment variables and to close your System Properties window. You may need to close restart your command line tool for it to recognize the new values. + +### Azure Key Vault Setup + +This example is implemented using the Azure Key Vault and will not work without it. Assuming you have a Key Vault instance configured, ensure that +you have the `Key Vault Crypto Officer` role assigned to yourself as you'll need to in order to generate a new key in the instance. After selecting Keys +under the Objects header, click the `Generate/Import` button at the top of the instance panel. + +Under options, select `Generate` and name your key. This example is pre-configured to assume a key name of 'myKey', but feel free to change this (but also update the name in the example +you wish to run). The other default options are fine for our purposes, so click Create at the bottom and if you've got the appropriate roles, it will show up in the list of Keys. + +Update your `./Components/azurekeyvault.yaml` file with the name of your Key Vault under `vaultName` where it currently reads "changeMe". This sample assumes authentication +via a service principal, so you might also need to set this up. + +Back in the Azure Portal, assign at least the `Key Vault Crypto User` role to the service principal you previously created in the last step. Do this by clicking +`Access Control (IAM)` from the left navigation, clicking "Add" from the top and clicking "Add Role Assignment". Select `Key Vault Crypto User` from the list and click the Next +button. Ensuring that the "User, group or service principal" option is selected, click the "Select members" link and search for the name of the app registration you created. Click +Add to add this service principal to the list of members for the new role assignment and click Review + Assign twice to assign the role. This will take effect within a few seconds +or minutes. This step ensures that while Dapr can authenticate as your service principal, that it also has permission to access and use the key in your Key Vault. + +## Running the example + +To run the sample locally, run this command in the DaprClient directory: + +```sh +dapr run --resources-path ./Components --app-id DaprClient -- dotnet run +``` + +Running the following command will output a list of the samples included: + +```sh +dapr run --resources-path ./Components --app-id DaprClient -- dotnet run +``` + +Press Ctrl+C to exit, and then run the command again and provide a sample number to run the samples. + +For example, run this command to run the first sample from the list produced earlier (the 0th example): + +```sh +dapr run --resources-path ./Components --app-id DaprClient -- dotnet run 0 +``` + +## Encryption/Decryption with strings +See [EncryptDecryptStringExample.cs](./EncryptDecryptStringExample.cs) for an example of using `DaprClient` for basic +string-based encryption and decryption operations as performed against UTF-8 encoded byte arrays. + +## Encryption/Decryption with streams +See [EncryptDecryptFileStreamExample.cs](./EncryptDecryptFileStreamExample.cs) for an example of using `DaprClient` +to perform an encrypt and decrypt operation against a stream of data. In the example, we stream a local file to the +sidecar to encrypt and write the result (as it's streamed back) to an in-memory buffer. Once the operation fully +completes, we perform the decrypt operation against this in-memory buffer and write the decrypted result back out to a +temporary file. + +In either operation, rather than load the entire stream into memory and send all at once to the +sidecar as we do in the other string-based example (as this might cause you to run out of memory either on the +node the app is running on or do the same to the sidecar itself), this example instead breaks the input stream into +more manageable 4KB chunks (a value you can override via the `EncryptionOptions` or `DecryptionOptions` parameters +respectively up to 64KB. Further, rather than waiting for the entire stream to send to the sidecar before the +encryption operation proceeds, it immediately works to process the sidecar response, continuing to minimize resource +usage. diff --git a/examples/Client/Cryptography/file.txt b/examples/Client/Cryptography/file.txt new file mode 100644 index 000000000..9e8638939 --- /dev/null +++ b/examples/Client/Cryptography/file.txt @@ -0,0 +1,26 @@ +# The Road Not Taken +## By Robert Lee Frost + +Two roads diverged in a yellow wood, +And sorry I could not travel both +And be one traveler, long I stood +And looked down one as far as I could +To where it bent in the undergrowth; + +Then took the other, as just as fair +And having perhaps the better claim, +Because it was grassy and wanted wear; +Though as for that, the passing there +Had worn them really about the same, + +And both that morning equally lay +In leaves no step had trodden black +Oh, I kept the first for another day! +Yet knowing how way leads on to way, +I doubted if I should ever come back. + +I shall be telling this with a sigh +Somewhere ages and ages hence: +Two roads diverged in a wood, and I, +I took the one less traveled by, +And that has made all the difference. \ No newline at end of file diff --git a/examples/Client/DistributedLock/DistributedLock.csproj b/examples/Client/DistributedLock/DistributedLock.csproj index 180c46f28..4c04fb907 100644 --- a/examples/Client/DistributedLock/DistributedLock.csproj +++ b/examples/Client/DistributedLock/DistributedLock.csproj @@ -3,10 +3,11 @@ + - netcoreapp3.1 + net6 diff --git a/examples/Client/DistributedLock/README.md b/examples/Client/DistributedLock/README.md index 788d57e3a..cdac6f91a 100644 --- a/examples/Client/DistributedLock/README.md +++ b/examples/Client/DistributedLock/README.md @@ -2,7 +2,7 @@ ## Prerequisites -- [.NET Core 3.1 or .NET 5+](https://dotnet.microsoft.com/download) installed +- [.NET 6+](https://dotnet.microsoft.com/download) installed - [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/) - [Initialized Dapr environment](https://docs.dapr.io/getting-started/install-dapr-selfhost/) - [Dapr .NET SDK](https://docs.dapr.io/developing-applications/sdks/dotnet/) diff --git a/examples/Client/DistributedLock/Startup.cs b/examples/Client/DistributedLock/Startup.cs index 0309af0f5..9f40e4752 100644 --- a/examples/Client/DistributedLock/Startup.cs +++ b/examples/Client/DistributedLock/Startup.cs @@ -1,4 +1,5 @@ -using DistributedLock.Services; +using Dapr.AspNetCore; +using DistributedLock.Services; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; diff --git a/examples/Client/PublishSubscribe/BulkPublishEventExample/BulkPublishEventExample.csproj b/examples/Client/PublishSubscribe/BulkPublishEventExample/BulkPublishEventExample.csproj index 5c05faf9f..b1e7647c7 100644 --- a/examples/Client/PublishSubscribe/BulkPublishEventExample/BulkPublishEventExample.csproj +++ b/examples/Client/PublishSubscribe/BulkPublishEventExample/BulkPublishEventExample.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp3.1 + net6 Samples.Client enable @@ -16,9 +16,9 @@ - - - + + + diff --git a/examples/Client/PublishSubscribe/BulkPublishEventExample/README.md b/examples/Client/PublishSubscribe/BulkPublishEventExample/README.md index dfcc99ca6..39d206fa2 100644 --- a/examples/Client/PublishSubscribe/BulkPublishEventExample/README.md +++ b/examples/Client/PublishSubscribe/BulkPublishEventExample/README.md @@ -2,7 +2,7 @@ ## Prerequisites -- [.NET Core 3.1 or .NET 5+](https://dotnet.microsoft.com/download) installed +- [.NET 6+](https://dotnet.microsoft.com/download) installed - [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/) - [Initialized Dapr environment](https://docs.dapr.io/getting-started/install-dapr-selfhost/) - [Dapr .NET SDK](https://docs.dapr.io/developing-applications/sdks/dotnet/) diff --git a/examples/Client/PublishSubscribe/PublishEventExample/PublishEventExample.csproj b/examples/Client/PublishSubscribe/PublishEventExample/PublishEventExample.csproj index f7d335879..52b77a3e5 100644 --- a/examples/Client/PublishSubscribe/PublishEventExample/PublishEventExample.csproj +++ b/examples/Client/PublishSubscribe/PublishEventExample/PublishEventExample.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp3.1 + net6 Samples.Client enable @@ -16,9 +16,9 @@ - - - + + + diff --git a/examples/Client/PublishSubscribe/PublishEventExample/README.md b/examples/Client/PublishSubscribe/PublishEventExample/README.md index 455fc2537..9f3af565f 100644 --- a/examples/Client/PublishSubscribe/PublishEventExample/README.md +++ b/examples/Client/PublishSubscribe/PublishEventExample/README.md @@ -2,7 +2,7 @@ ## Prerequisites -- [.NET Core 3.1 or .NET 5+](https://dotnet.microsoft.com/download) installed +- [.NET 6+](https://dotnet.microsoft.com/download) installed - [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/) - [Initialized Dapr environment](https://docs.dapr.io/getting-started/install-dapr-selfhost/) - [Dapr .NET SDK](https://docs.dapr.io/developing-applications/sdks/dotnet/) diff --git a/examples/Client/ServiceInvocation/README.md b/examples/Client/ServiceInvocation/README.md index 171869c15..ede5a506a 100644 --- a/examples/Client/ServiceInvocation/README.md +++ b/examples/Client/ServiceInvocation/README.md @@ -2,7 +2,7 @@ ## Prerequisites -- [.NET Core 3.1 or .NET 5+](https://dotnet.microsoft.com/download) installed +- [.NET 6+](https://dotnet.microsoft.com/download) installed - [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/) - [Initialized Dapr environment](https://docs.dapr.io/getting-started/install-dapr-selfhost/) - [Dapr .NET SDK](https://docs.dapr.io/developing-applications/sdks/dotnet/) diff --git a/examples/Client/ServiceInvocation/ServiceInvocation.csproj b/examples/Client/ServiceInvocation/ServiceInvocation.csproj index 790bfc53a..7b165835e 100644 --- a/examples/Client/ServiceInvocation/ServiceInvocation.csproj +++ b/examples/Client/ServiceInvocation/ServiceInvocation.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp3.1 + net6 Samples.Client enable @@ -16,9 +16,9 @@ - - - + + + diff --git a/examples/Client/StateManagement/README.md b/examples/Client/StateManagement/README.md index 141f16760..fb266a242 100644 --- a/examples/Client/StateManagement/README.md +++ b/examples/Client/StateManagement/README.md @@ -2,7 +2,7 @@ ## Prerequisites -- [.NET Core 3.1 or .NET 5+](https://dotnet.microsoft.com/download) installed +- [.NET 6+](https://dotnet.microsoft.com/download) installed - [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/) - [Initialized Dapr environment](https://docs.dapr.io/getting-started/install-dapr-selfhost/) - [Dapr .NET SDK](https://docs.dapr.io/developing-applications/sdks/dotnet/) diff --git a/examples/Client/StateManagement/StateManagement.csproj b/examples/Client/StateManagement/StateManagement.csproj index 790bfc53a..7b165835e 100644 --- a/examples/Client/StateManagement/StateManagement.csproj +++ b/examples/Client/StateManagement/StateManagement.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp3.1 + net6 Samples.Client enable @@ -16,9 +16,9 @@ - - - + + + diff --git a/examples/GeneratedActor/ActorClient/ActorClient.csproj b/examples/GeneratedActor/ActorClient/ActorClient.csproj new file mode 100644 index 000000000..73b5c2027 --- /dev/null +++ b/examples/GeneratedActor/ActorClient/ActorClient.csproj @@ -0,0 +1,22 @@ + + + + Exe + net6 + 10.0 + enable + enable + + + + + + + + + + + + diff --git a/examples/GeneratedActor/ActorClient/IClientActor.cs b/examples/GeneratedActor/ActorClient/IClientActor.cs new file mode 100644 index 000000000..c5c732cb9 --- /dev/null +++ b/examples/GeneratedActor/ActorClient/IClientActor.cs @@ -0,0 +1,28 @@ +// ------------------------------------------------------------------------ +// Copyright 2023 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.Actors.Generators; + +namespace GeneratedActor; + +internal sealed record ClientState(string Value); + +[GenerateActorClient] +internal interface IClientActor +{ + [ActorMethod(Name = "GetState")] + Task GetStateAsync(CancellationToken cancellationToken = default); + + [ActorMethod(Name = "SetState")] + Task SetStateAsync(ClientState state, CancellationToken cancellationToken = default); +} diff --git a/examples/GeneratedActor/ActorClient/Program.cs b/examples/GeneratedActor/ActorClient/Program.cs new file mode 100644 index 000000000..87f714907 --- /dev/null +++ b/examples/GeneratedActor/ActorClient/Program.cs @@ -0,0 +1,30 @@ +// ------------------------------------------------------------------------ +// Copyright 2023 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.Actors; +using Dapr.Actors.Client; +using GeneratedActor; + +Console.WriteLine("Testing generated client..."); + +var proxy = ActorProxy.Create(ActorId.CreateRandom(), "RemoteActor"); + +using var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(30)); + +var client = new ClientActorClient(proxy); + +var state = await client.GetStateAsync(cancellationTokenSource.Token); + +await client.SetStateAsync(new ClientState("Hello, World!"), cancellationTokenSource.Token); + +Console.WriteLine("Done!"); diff --git a/examples/GeneratedActor/ActorCommon/ActorCommon.csproj b/examples/GeneratedActor/ActorCommon/ActorCommon.csproj new file mode 100644 index 000000000..2cbc61e2c --- /dev/null +++ b/examples/GeneratedActor/ActorCommon/ActorCommon.csproj @@ -0,0 +1,14 @@ + + + + net6 + 10.0 + enable + enable + + + + + + + diff --git a/examples/GeneratedActor/ActorCommon/IRemoteActor.cs b/examples/GeneratedActor/ActorCommon/IRemoteActor.cs new file mode 100644 index 000000000..6d136a704 --- /dev/null +++ b/examples/GeneratedActor/ActorCommon/IRemoteActor.cs @@ -0,0 +1,25 @@ +// ------------------------------------------------------------------------ +// Copyright 2023 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.Actors; + +namespace GeneratedActor; + +public sealed record RemoteState(string Value); + +public interface IRemoteActor : IActor +{ + Task GetState(); + + Task SetState(RemoteState state); +} diff --git a/examples/GeneratedActor/ActorService/ActorService.csproj b/examples/GeneratedActor/ActorService/ActorService.csproj new file mode 100644 index 000000000..a74104363 --- /dev/null +++ b/examples/GeneratedActor/ActorService/ActorService.csproj @@ -0,0 +1,15 @@ + + + + net6 + 10.0 + enable + enable + + + + + + + + diff --git a/examples/GeneratedActor/ActorService/Program.cs b/examples/GeneratedActor/ActorService/Program.cs new file mode 100644 index 000000000..f6e62f720 --- /dev/null +++ b/examples/GeneratedActor/ActorService/Program.cs @@ -0,0 +1,36 @@ +// ------------------------------------------------------------------------ +// 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 GeneratedActor; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddActors( + options => + { + options.UseJsonSerialization = true; + options.Actors.RegisterActor(); + }); + +var app = builder.Build(); + +app.UseRouting(); + +#pragma warning disable ASP0014 +app.UseEndpoints( + endpoints => + { + endpoints.MapActorsHandlers(); + }); + +app.Run(); diff --git a/examples/GeneratedActor/ActorService/Properties/launchSettings.json b/examples/GeneratedActor/ActorService/Properties/launchSettings.json new file mode 100644 index 000000000..8fbb1f581 --- /dev/null +++ b/examples/GeneratedActor/ActorService/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:56372", + "sslPort": 0 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5226", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/examples/GeneratedActor/ActorService/RemoteActor.cs b/examples/GeneratedActor/ActorService/RemoteActor.cs new file mode 100644 index 000000000..f04921f69 --- /dev/null +++ b/examples/GeneratedActor/ActorService/RemoteActor.cs @@ -0,0 +1,45 @@ +// ------------------------------------------------------------------------ +// Copyright 2023 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.Actors.Runtime; + +namespace GeneratedActor; + +internal sealed class RemoteActor : Actor, IRemoteActor +{ + private readonly ILogger logger; + + private RemoteState currentState = new("default"); + + public RemoteActor(ActorHost host, ILogger logger) + : base(host) + { + this.logger = logger; + } + + public Task GetState() + { + this.logger.LogInformation("GetStateAsync called."); + + return Task.FromResult(this.currentState); + } + + public Task SetState(RemoteState state) + { + this.logger.LogInformation("SetStateAsync called."); + + this.currentState = state; + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/examples/GeneratedActor/ActorService/appsettings.Development.json b/examples/GeneratedActor/ActorService/appsettings.Development.json new file mode 100644 index 000000000..ff66ba6b2 --- /dev/null +++ b/examples/GeneratedActor/ActorService/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/examples/GeneratedActor/ActorService/appsettings.json b/examples/GeneratedActor/ActorService/appsettings.json new file mode 100644 index 000000000..4d566948d --- /dev/null +++ b/examples/GeneratedActor/ActorService/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/examples/GeneratedActor/README.md b/examples/GeneratedActor/README.md new file mode 100644 index 000000000..cd595b30e --- /dev/null +++ b/examples/GeneratedActor/README.md @@ -0,0 +1,115 @@ +# Generated Actor Client Example + +An example of generating a strongly-typed actor client. + +## Prerequisites + +- [.NET 6+](https://dotnet.microsoft.com/download) installed +- [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/) +- [Initialized Dapr environment](https://docs.dapr.io/getting-started/install-dapr-selfhost/) +- [Dapr .NET SDK](https://docs.dapr.io/developing-applications/sdks/dotnet/) + +## Overview + +Two options for invoking actor methods exist in the Dapr .NET SDK, a strongly-type (remoting) option and a loosely-typed (non-remoting) option. Each has its own advantages and disadvantages. A "middle" option also exists that combines the two and gains benefits of both without some of the disadvantages of either. Using .NET Source Generators, the Dapr .NET SDK can generate a strongly-typed client implementation that uses loosely-typed method invocation under the covers. + +Strongly-typed clients are generated by: + +1. Referencing the `Dapr.Actors.Generators` NuGet package. + + ```xml + + + + + + ``` + +1. Add the `Dapr.Actors.Generators.GenerateActorClientAttribute` to the actor interface. + + ```csharp + using Dapr.Actors.Generators; + + namespace Sample; + + internal sealed record SampleState(string Value); + + [GenerateActorClient] + internal interface ISampleActor + { + [ActorMethod(Name = "GetState")] + Task GetStateAsync(CancellationToken cancellationToken = default); + + [ActorMethod(Name = "SetState")] + Task SetStateAsync(SampleState state, CancellationToken cancellationToken = default); + } + ``` + + > The `Dapr.Actors.Generators.ActorMethodAttribute` can be used to map interface methods definitions to specific actor methods should the names differ (e.g. the interface uses "Async" suffix common in .NET but the actor methods do not). + +1. A strongly-typed client will be generated that can be used to invoke actor methods. + + ```csharp + using Dapr.Actors; + using Dapr.Actors.Client; + using Sample; + + var proxy = ActorProxy.Create(ActorId.CreateRandom(), "SampleActor"); + + using var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(30)); + + var client = new SampleActorClient(proxy); + + var state = await client.GetStateAsync(cancellationTokenSource.Token); + + await client.SetStateAsync(new SampleState("Hello, World!"), cancellationTokenSource.Token); + + ``` + +## Run the example + +### Start the ActorService + +Change directory to the `ActorService` folder: + +```bash +cd examples/GeneratedActor/ActorService +``` + +To start the `ActorService`, execute the following command: + +```bash +dapr run --app-id generated-service --app-port 5226 -- dotnet run +``` + +### Run the ActorClient + +Change directory to the `ActorClient` folder: + +```bash +cd examples/GeneratedActor/ActorClient +``` + +To run the `ActorClient`, execute the following command: + +```bash +dapr run --app-id generated-client -- dotnet run +``` + +### Expected output + +You should see the following output from the `ActorClient`: + +``` +== APP == Testing generated client... +== APP == Done! +``` + +You should see also see the following output from the `ActorService`: + +``` +== APP == info: GeneratedActor.RemoteActor[0] +== APP == GetStateAsync called. +== APP == info: GeneratedActor.RemoteActor[0] +== APP == SetStateAsync called. +``` \ No newline at end of file diff --git a/examples/Workflow/README.md b/examples/Workflow/README.md index 610077d47..1af855767 100644 --- a/examples/Workflow/README.md +++ b/examples/Workflow/README.md @@ -9,12 +9,24 @@ This Dapr workflow example shows how to create a Dapr workflow (`Workflow`) and - [Initialized Dapr environment](https://docs.dapr.io/getting-started/install-dapr-selfhost/) - [Dapr .NET SDK](https://github.com/dapr/dotnet-sdk/) + +## Optional Setup +Dapr workflow, as well as this example program, now support authentication through the use of API tokens. For more information on this, view the following document: [API Token](https://github.com/dapr/dotnet-sdk/blob/master/docs/api-tokens.md) + ## Projects in sample This sample contains a single [WorkflowConsoleApp](./WorkflowConsoleApp) .NET project. -It utilizes the workflow SDK as well as the workflow management API for starting and querying workflows instances. -The main `Program.cs` file contains the main setup of the app, including the registration of the workflow and workflow activities. -The workflow definition is found in the `Workflows` directory and the workflow activity definitions are found in the `Activities` directory. +It utilizes the workflow SDK as well as the workflow management API for simulating inventory management and sale of goods in a store. +The main `Program.cs` file contains the main setup of the app, the registration of the workflow and its activities, and interaction with the user. The workflow definition is found in the `Workflows` directory and the workflow activity definitions are found in the `Activities` directory. + +There are five activities in the directory that could be called by the workflows: +- `NotifyActivity`: printing logs as notifications +- `ProcessPaymentActivity`: printing logs and delaying for simulating payment processing +- `RequestApprovalActivity`: printing logs to indicate that the order has been approved +- `ReserveInventoryActivity`: checking if there are enough items for purchase +- `UpdateInventoryActivity`: updating the statestore according to purchasing + +The `OrderProcessingWorkflow.cs` in `Workflows` directory implements the running logic of the workflow. Based on the purchase stage and outcome, it calls different activities and waits for the corresponding events to trigger interaction with the user. This sample also contains a [WorkflowUnitTest](./WorkflowUnitTest) .NET project that utilizes [xUnit](https://xunit.net/) and [Moq](https://github.com/moq/moq) to test the workflow logic. It works by creating an instance of the `OrderProcessingWorkflow` (defined in the `WorkflowConsoleApp` project), mocking activity calls, and testing the inputs and outputs. @@ -49,46 +61,43 @@ For the workflow API option, two identical `curl` commands are shown, one for Li Make note of the "1234" in the commands below. This represents the unique identifier for the workflow run and can be replaced with any identifier of your choosing. ```bash -curl -i -X POST http://localhost:3500/v1.0-alpha1/workflows/dapr/OrderProcessingWorkflow/1234/start \ +curl -i -X POST http://localhost:3500/v1.0-beta1/workflows/dapr/OrderProcessingWorkflow/start?instanceID=1234 \ -H "Content-Type: application/json" \ - -d '{ "input" : {"Name": "Paperclips", "TotalCost": 99.95, "Quantity": 1}}' + -d '{"Name": "Paperclips", "TotalCost": 99.95, "Quantity": 1}' ``` On Windows (PowerShell): ```powershell -curl -i -X POST http://localhost:3500/v1.0-alpha1/workflows/dapr/OrderProcessingWorkflow/1234/start ` +curl -i -X POST http://localhost:3500/v1.0-beta1/workflows/dapr/OrderProcessingWorkflow/start?instanceID=1234 ` -H "Content-Type: application/json" ` - -d '{ "input" : {"Name": "Paperclips", "TotalCost": 99.95, "Quantity": 1}}' + -d '{"Name": "Paperclips", "TotalCost": 99.95, "Quantity": 1}' ``` If successful, you should see a response like the following: ```json -{"instance_id":"1234"} +{"instanceID":"1234"} ``` Next, send an HTTP request to get the status of the workflow that was started: ```bash -curl -i -X GET http://localhost:3500/v1.0-alpha1/workflows/dapr/OrderProcessingWorkflow/1234 +curl -i -X GET http://localhost:3500/v1.0-beta1/workflows/dapr/1234 ``` The workflow is designed to take several seconds to complete. If the workflow hasn't completed yet when you issue the previous command, you should see the following JSON response (formatted for readability): ```json { - "WFInfo": { - "instance_id": "1234" - }, - "start_time": "2023-02-02T23:34:53Z", - "metadata": { + "instanceID": "1234", + "workflowName": "OrderProcessingWorkflow", + "createdAt": "2023-05-10T00:42:03.911444105Z", + "lastUpdatedAt": "2023-05-10T00:42:06.142214153Z", + "runtimeStatus": "RUNNING", + "properties": { "dapr.workflow.custom_status": "", - "dapr.workflow.input": "{\"Name\":\"Paperclips\",\"Quantity\":1,\"TotalCost\":99.95}", - "dapr.workflow.last_updated": "2023-02-02T23:35:07Z", - "dapr.workflow.name": "OrderProcessingWorkflow", - "dapr.workflow.output": "{\"Processed\":true}", - "dapr.workflow.runtime_status": "RUNNING" + "dapr.workflow.input": "{\"Name\": \"Paperclips\", \"TotalCost\": 99.95, \"Quantity\": 1}" } } ``` @@ -97,17 +106,15 @@ Once the workflow has completed running, you should see the following output, in ```json { - "WFInfo": { - "instance_id": "1234" - }, - "start_time": "2023-02-02T23:34:53Z", - "metadata": { + "instanceID": "1234", + "workflowName": "OrderProcessingWorkflow", + "createdAt": "2023-05-10T00:42:03.911444105Z", + "lastUpdatedAt": "2023-05-10T00:42:18.527704176Z", + "runtimeStatus": "COMPLETED", + "properties": { "dapr.workflow.custom_status": "", - "dapr.workflow.input": "{\"Name\":\"Paperclips\",\"Quantity\":1,\"TotalCost\":99.95}", - "dapr.workflow.last_updated": "2023-02-02T23:35:07Z", - "dapr.workflow.name": "OrderProcessingWorkflow", - "dapr.workflow.output": "{\"Processed\":true}", - "dapr.workflow.runtime_status": "COMPLETED" + "dapr.workflow.input": "{\"Name\": \"Paperclips\", \"TotalCost\": 99.95, \"Quantity\": 1}", + "dapr.workflow.output": "{\"Processed\":true}" } } ``` @@ -125,4 +132,4 @@ info: WorkflowConsoleApp.Activities.NotifyActivity[0] Order 1234 processed successfully! ``` -If you have Zipkin configured for Dapr locally on your machine, then you can view the workflow trace spans in the Zipkin web UI (typically at http://localhost:9411/zipkin/). +If you have Zipkin configured for Dapr locally on your machine, then you can view the workflow trace spans in the Zipkin web UI (typically at http://localhost:9411/zipkin/). \ No newline at end of file diff --git a/examples/Workflow/WorkflowConsoleApp/Activities/ProcessPaymentActivity.cs b/examples/Workflow/WorkflowConsoleApp/Activities/ProcessPaymentActivity.cs index dc4cc531b..1ddb51bbf 100644 --- a/examples/Workflow/WorkflowConsoleApp/Activities/ProcessPaymentActivity.cs +++ b/examples/Workflow/WorkflowConsoleApp/Activities/ProcessPaymentActivity.cs @@ -1,7 +1,6 @@ using Dapr.Client; using Dapr.Workflow; using Microsoft.Extensions.Logging; -using WorkflowConsoleApp.Models; namespace WorkflowConsoleApp.Activities { diff --git a/examples/Workflow/WorkflowConsoleApp/Activities/RequestApprovalActivity.cs b/examples/Workflow/WorkflowConsoleApp/Activities/RequestApprovalActivity.cs new file mode 100644 index 000000000..d40078fc8 --- /dev/null +++ b/examples/Workflow/WorkflowConsoleApp/Activities/RequestApprovalActivity.cs @@ -0,0 +1,23 @@ +using Dapr.Workflow; +using Microsoft.Extensions.Logging; + +namespace WorkflowConsoleApp.Activities +{ + public class RequestApprovalActivity : WorkflowActivity + { + 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); + } + } +} diff --git a/examples/Workflow/WorkflowConsoleApp/Activities/ReserveInventoryActivity.cs b/examples/Workflow/WorkflowConsoleApp/Activities/ReserveInventoryActivity.cs index 7dce1f46a..cdae1c6ed 100644 --- a/examples/Workflow/WorkflowConsoleApp/Activities/ReserveInventoryActivity.cs +++ b/examples/Workflow/WorkflowConsoleApp/Activities/ReserveInventoryActivity.cs @@ -1,8 +1,6 @@ -using System.Threading.Tasks; -using Dapr.Client; +using Dapr.Client; using Dapr.Workflow; using Microsoft.Extensions.Logging; -using WorkflowConsoleApp.Models; namespace WorkflowConsoleApp.Activities { @@ -27,7 +25,7 @@ public override async Task RunAsync(WorkflowActivityContext con req.ItemName); // Ensure that the store has items - InventoryItem item = await client.GetStateAsync( + InventoryItem item = await this.client.GetStateAsync( storeName, req.ItemName.ToLowerInvariant()); diff --git a/examples/Workflow/WorkflowConsoleApp/Activities/UpdateInventoryActivity.cs b/examples/Workflow/WorkflowConsoleApp/Activities/UpdateInventoryActivity.cs index d136e74cc..c035aadde 100644 --- a/examples/Workflow/WorkflowConsoleApp/Activities/UpdateInventoryActivity.cs +++ b/examples/Workflow/WorkflowConsoleApp/Activities/UpdateInventoryActivity.cs @@ -1,7 +1,5 @@ -using System.Threading.Tasks; -using Dapr.Client; +using Dapr.Client; using Dapr.Workflow; -using WorkflowConsoleApp.Models; using Microsoft.Extensions.Logging; namespace WorkflowConsoleApp.Activities @@ -39,7 +37,7 @@ public override async Task RunAsync(WorkflowActivityContext context, Pay this.logger.LogInformation( "Payment for request ID '{requestId}' could not be processed. Insufficient inventory.", req.RequestId); - throw new InvalidOperationException(); + 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 diff --git a/examples/Workflow/WorkflowConsoleApp/Models.cs b/examples/Workflow/WorkflowConsoleApp/Models.cs index f2f3dcbc8..7892c7525 100644 --- a/examples/Workflow/WorkflowConsoleApp/Models.cs +++ b/examples/Workflow/WorkflowConsoleApp/Models.cs @@ -1,4 +1,4 @@ -namespace WorkflowConsoleApp.Models +namespace WorkflowConsoleApp { public record OrderPayload(string Name, double TotalCost, int Quantity = 1); public record InventoryRequest(string RequestId, string ItemName, int Quantity); @@ -6,4 +6,10 @@ 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, + } } diff --git a/examples/Workflow/WorkflowConsoleApp/Program.cs b/examples/Workflow/WorkflowConsoleApp/Program.cs index 13c159e91..d3daacffa 100644 --- a/examples/Workflow/WorkflowConsoleApp/Program.cs +++ b/examples/Workflow/WorkflowConsoleApp/Program.cs @@ -1,12 +1,12 @@ using Dapr.Client; using Dapr.Workflow; using WorkflowConsoleApp.Activities; -using WorkflowConsoleApp.Models; using WorkflowConsoleApp.Workflows; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.DependencyInjection; +using WorkflowConsoleApp; -const string storeName = "statestore"; +const string StoreName = "statestore"; // The workflow host is a background service that connects to the sidecar over gRPC var builder = Host.CreateDefaultBuilder(args).ConfigureServices(services => @@ -20,6 +20,7 @@ // These are the activities that get invoked by the workflow(s). options.RegisterActivity(); options.RegisterActivity(); + options.RegisterActivity(); options.RegisterActivity(); options.RegisterActivity(); }); @@ -46,7 +47,16 @@ using var host = builder.Build(); host.Start(); -using var daprClient = new DaprClientBuilder().Build(); +DaprClient daprClient; +string apiToken = Environment.GetEnvironmentVariable("DAPR_API_TOKEN"); +if (!string.IsNullOrEmpty(apiToken)) +{ + daprClient = new DaprClientBuilder().UseDaprApiToken(apiToken).Build(); +} +else +{ + daprClient = new DaprClientBuilder().Build(); +} // Wait for the sidecar to become available while (!await daprClient.CheckHealthAsync()) @@ -58,8 +68,6 @@ // This is just to make the log output look a little nicer. Thread.Sleep(TimeSpan.FromSeconds(1)); -DaprWorkflowClient workflowClient = host.Services.GetRequiredService(); - var baseInventory = new List { new InventoryItem(Name: "Computer", PerItemCost: 1500, Quantity: 100), @@ -74,89 +82,148 @@ await RestockInventory(daprClient, baseInventory); // Start the input loop -while (true) +using (daprClient) { - // Get the name of the item to order and make sure we have inventory - string items = string.Join(", ", baseInventory.Select(i => i.Name)); - Console.WriteLine($"Enter the name of one of the following items to order [{items}]."); - Console.WriteLine("To restock items, type 'restock'."); - string itemName = Console.ReadLine()?.Trim(); - if (string.IsNullOrEmpty(itemName)) + bool quit = false; + Console.CancelKeyPress += delegate { - continue; - } - else if (string.Equals("restock", itemName, StringComparison.OrdinalIgnoreCase)) - { - await RestockInventory(daprClient, baseInventory); - continue; - } + quit = true; + Console.WriteLine("Shutting down the example."); + }; - InventoryItem item = baseInventory.FirstOrDefault(item => string.Equals(item.Name, itemName, StringComparison.OrdinalIgnoreCase)); - if (item == null) + while (!quit) { - Console.ForegroundColor = ConsoleColor.Yellow; - Console.WriteLine($"We don't have {itemName}!"); - Console.ResetColor(); - continue; - } + // Get the name of the item to order and make sure we have inventory + string items = string.Join(", ", baseInventory.Select(i => i.Name)); + Console.WriteLine($"Enter the name of one of the following items to order [{items}]."); + Console.WriteLine("To restock items, type 'restock'."); + string itemName = Console.ReadLine()?.Trim(); + if (string.IsNullOrEmpty(itemName)) + { + continue; + } + else if (string.Equals("restock", itemName, StringComparison.OrdinalIgnoreCase)) + { + await RestockInventory(daprClient, baseInventory); + continue; + } - Console.WriteLine($"How many {itemName} would you like to purchase?"); - string amountStr = Console.ReadLine().Trim(); - if (!int.TryParse(amountStr, out int amount) || amount <= 0) - { - Console.ForegroundColor = ConsoleColor.Yellow; - Console.WriteLine($"Invalid input. Assuming you meant to type '1'."); - Console.ResetColor(); - amount = 1; - } + InventoryItem item = baseInventory.FirstOrDefault(item => string.Equals(item.Name, itemName, StringComparison.OrdinalIgnoreCase)); + if (item == null) + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine($"We don't have {itemName}!"); + Console.ResetColor(); + continue; + } - // Construct the order with a unique order ID - string orderId = $"{itemName.ToLowerInvariant()}-{Guid.NewGuid().ToString()[..8]}"; - double totalCost = amount * item.PerItemCost; - var orderInfo = new OrderPayload(itemName.ToLowerInvariant(), totalCost, amount); + Console.WriteLine($"How many {itemName} would you like to purchase?"); + string amountStr = Console.ReadLine().Trim(); + if (!int.TryParse(amountStr, out int amount) || amount <= 0) + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine($"Invalid input. Assuming you meant to type '1'."); + Console.ResetColor(); + amount = 1; + } - // Start the workflow using the order ID as the workflow ID - Console.WriteLine($"Starting order workflow '{orderId}' purchasing {amount} {itemName}"); - await workflowClient.ScheduleNewWorkflowAsync( - name: nameof(OrderProcessingWorkflow), - instanceId: orderId, - input: orderInfo); + var daprWorkflowClient = host.Services.GetRequiredService(); - // Wait for the workflow to complete - WorkflowState state = await workflowClient.WaitForWorkflowCompletionAsync( - instanceId: orderId, - getInputsAndOutputs: true); + // Construct the order with a unique order ID + string orderId = $"{itemName.ToLowerInvariant()}-{Guid.NewGuid().ToString()[..8]}"; + double totalCost = amount * item.PerItemCost; + var orderInfo = new OrderPayload(itemName.ToLowerInvariant(), totalCost, amount); - if (state.RuntimeStatus == WorkflowRuntimeStatus.Completed) - { - OrderResult result = state.ReadOutputAs(); - if (result.Processed) + // Start the workflow using the order ID as the workflow ID + Console.WriteLine($"Starting order workflow '{orderId}' purchasing {amount} {itemName}"); + await daprWorkflowClient.ScheduleNewWorkflowAsync( + name: nameof(OrderProcessingWorkflow), + input: orderInfo, + instanceId: orderId); + + // Wait for the workflow to start and confirm the input + WorkflowState state = await daprWorkflowClient.WaitForWorkflowStartAsync( + instanceId: orderId); + + Console.WriteLine($"{nameof(OrderProcessingWorkflow)} (ID = {orderId}) started successfully with {state.ReadInputAs()}"); + + // Wait for the workflow to complete + while (true) { - Console.ForegroundColor = ConsoleColor.Green; - Console.WriteLine($"Order workflow is {state.RuntimeStatus} and the order was processed successfully."); - Console.ResetColor(); + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + try + { + state = await daprWorkflowClient.WaitForWorkflowCompletionAsync( + instanceId: orderId, + cancellation: cts.Token); + break; + } + catch (OperationCanceledException) + { + // Check to see if the workflow is blocked waiting for an approval + state = await daprWorkflowClient.GetWorkflowStateAsync( + instanceId: orderId); + + if(state.ReadCustomStatusAs()?.Contains("Waiting for approval") == true) + { + Console.WriteLine($"{nameof(OrderProcessingWorkflow)} (ID = {orderId}) requires approval. Approve? [Y/N]"); + string approval = Console.ReadLine(); + ApprovalResult approvalResult = ApprovalResult.Unspecified; + if (string.Equals(approval, "Y", StringComparison.OrdinalIgnoreCase)) + { + Console.WriteLine("Approving order..."); + approvalResult = ApprovalResult.Approved; + } + else if (string.Equals(approval, "N", StringComparison.OrdinalIgnoreCase)) + { + Console.WriteLine("Rejecting order..."); + approvalResult = ApprovalResult.Rejected; + } + + if (approvalResult != ApprovalResult.Unspecified) + { + // Raise the workflow event to the workflow + await daprWorkflowClient.RaiseEventAsync( + instanceId: orderId, + eventName: "ManagerApproval", + eventPayload: approvalResult); + } + + // otherwise, keep waiting + } + } } - else + + if (state.RuntimeStatus == WorkflowRuntimeStatus.Completed) { - Console.WriteLine($"Order workflow is {state.RuntimeStatus} but the order was not processed."); + OrderResult result = state.ReadOutputAs(); + if (result.Processed) + { + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine($"Order workflow is {state.RuntimeStatus} and the order was processed successfully ({result})."); + Console.ResetColor(); + } + else + { + Console.WriteLine($"Order workflow is {state.RuntimeStatus} but the order was not processed."); + } + } + else if (state.RuntimeStatus == WorkflowRuntimeStatus.Failed) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"The workflow failed - {state.FailureDetails}"); + Console.ResetColor(); } - } - else if (state.RuntimeStatus == WorkflowRuntimeStatus.Failed) - { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine($"The workflow failed - {state.FailureDetails}"); - Console.ResetColor(); - } - Console.WriteLine(); + Console.WriteLine(); + } } - static async Task RestockInventory(DaprClient daprClient, List inventory) { Console.WriteLine("*** Restocking inventory..."); foreach (var item in inventory) { Console.WriteLine($"*** \t{item.Name}: {item.Quantity}"); - await daprClient.SaveStateAsync(storeName, item.Name.ToLowerInvariant(), item); + await daprClient.SaveStateAsync(StoreName, item.Name.ToLowerInvariant(), item); } } diff --git a/examples/Workflow/WorkflowConsoleApp/WorkflowConsoleApp.csproj b/examples/Workflow/WorkflowConsoleApp/WorkflowConsoleApp.csproj index 0c40eea0c..25c03a419 100644 --- a/examples/Workflow/WorkflowConsoleApp/WorkflowConsoleApp.csproj +++ b/examples/Workflow/WorkflowConsoleApp/WorkflowConsoleApp.csproj @@ -8,7 +8,6 @@ Exe net6 enable - latest 612,618 diff --git a/examples/Workflow/WorkflowConsoleApp/Workflows/OrderProcessingWorkflow.cs b/examples/Workflow/WorkflowConsoleApp/Workflows/OrderProcessingWorkflow.cs index faed132e3..3b8af5951 100644 --- a/examples/Workflow/WorkflowConsoleApp/Workflows/OrderProcessingWorkflow.cs +++ b/examples/Workflow/WorkflowConsoleApp/Workflows/OrderProcessingWorkflow.cs @@ -1,17 +1,22 @@ using Dapr.Workflow; -using DurableTask.Core.Exceptions; using WorkflowConsoleApp.Activities; -using WorkflowConsoleApp.Models; namespace WorkflowConsoleApp.Workflows { public class OrderProcessingWorkflow : Workflow { + 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)), + }; + public override async Task RunAsync(WorkflowContext context, OrderPayload order) { string orderId = context.InstanceId; - // Notify the user that an order has come through await context.CallActivityAsync( nameof(NotifyActivity), @@ -20,7 +25,8 @@ await context.CallActivityAsync( // 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)); + new InventoryRequest(RequestId: orderId, order.Name, order.Quantity), + this.defaultActivityRetryOptions); // If there is insufficient inventory, fail and let the user know if (!result.Success) @@ -32,24 +38,59 @@ await context.CallActivityAsync( return new OrderResult(Processed: false); } + // Require orders over a certain threshold to be approved + if (order.TotalCost > 50000) + { + // 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)); + context.SetCustomStatus($"Approval result: {approvalResult}"); + if (approvalResult == ApprovalResult.Rejected) + { + // 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) + { + // 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 await context.CallActivityAsync( nameof(ProcessPaymentActivity), - new PaymentRequest(RequestId: orderId, order.Name, order.Quantity, order.TotalCost)); + 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)); + new PaymentRequest(RequestId: orderId, order.Name, order.Quantity, order.TotalCost), + this.defaultActivityRetryOptions); } - catch (TaskFailedException) + catch (WorkflowTaskFailedException e) { - // Let them know their payment was processed + // Let them know their payment processing failed await context.CallActivityAsync( nameof(NotifyActivity), - new Notification($"Order {orderId} Failed! You are now getting a refund")); + new Notification($"Order {orderId} Failed! Details: {e.FailureDetails.ErrorMessage}")); return new OrderResult(Processed: false); } diff --git a/examples/Workflow/WorkflowConsoleApp/demo.http b/examples/Workflow/WorkflowConsoleApp/demo.http index bb91e5045..669cefeb7 100644 --- a/examples/Workflow/WorkflowConsoleApp/demo.http +++ b/examples/Workflow/WorkflowConsoleApp/demo.http @@ -1,17 +1,17 @@ ### Start order processing workflow - replace xxx with any id you like -POST http://localhost:3500/v1.0-alpha1/workflows/dapr/OrderProcessingWorkflow/xxx/start +POST http://localhost:3500/v1.0-beta1/workflows/dapr/OrderProcessingWorkflow/start?instanceID=xxx Content-Type: application/json -{ "input" : {"Name": "Paperclips", "TotalCost": 99.95, "Quantity": 1}} +{"Name": "Paperclips", "TotalCost": 99.95, "Quantity": 1} ### Start order processing workflow - replace xxx with any id you like -POST http://localhost:3500/v1.0-alpha1/workflows/dapr/OrderProcessingWorkflow/xxx/start +POST http://localhost:3500/v1.0-beta1/workflows/dapr/OrderProcessingWorkflow/start?instanceID=xxx Content-Type: application/json -{ "input" : {"Name": "Cars", "TotalCost": 10000, "Quantity": 30}} +{"Name": "Cars", "TotalCost": 10000, "Quantity": 30} ### Query dapr sidecar - replace xxx with id from the workflow you've created above -GET http://localhost:3500/v1.0-alpha1/workflows/dapr/OrderProcessingWorkflow/xxx +GET http://localhost:3500/v1.0-beta1/workflows/dapr/xxx ### Terminate the workflow - replace xxx with id from the workflow you've created above -POST http://localhost:3500/v1.0-alpha1/workflows/dapr/xxx/terminate \ No newline at end of file +POST http://localhost:3500/v1.0-beta1/workflows/dapr/xxx/terminate \ No newline at end of file diff --git a/examples/Workflow/WorkflowUnitTest/OrderProcessingTests.cs b/examples/Workflow/WorkflowUnitTest/OrderProcessingTests.cs index 0cb450fcd..e38a0c940 100644 --- a/examples/Workflow/WorkflowUnitTest/OrderProcessingTests.cs +++ b/examples/Workflow/WorkflowUnitTest/OrderProcessingTests.cs @@ -1,9 +1,8 @@ using System.Threading.Tasks; using Dapr.Workflow; -using Microsoft.DurableTask; using Moq; +using WorkflowConsoleApp; using WorkflowConsoleApp.Activities; -using WorkflowConsoleApp.Models; using WorkflowConsoleApp.Workflows; using Xunit; @@ -24,7 +23,7 @@ public async Task TestSuccessfulOrder() // Mock the call to ReserveInventoryActivity Mock mockContext = new(); mockContext - .Setup(ctx => ctx.CallActivityAsync(nameof(ReserveInventoryActivity), It.IsAny(), It.IsAny())) + .Setup(ctx => ctx.CallActivityAsync(nameof(ReserveInventoryActivity), It.IsAny(), It.IsAny())) .Returns(Task.FromResult(inventoryResult)); // Run the workflow directly @@ -36,17 +35,17 @@ public async Task TestSuccessfulOrder() // Verify that ReserveInventoryActivity was called with a specific input mockContext.Verify( - ctx => ctx.CallActivityAsync(nameof(ReserveInventoryActivity), expectedInventoryRequest, It.IsAny()), + 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()), + 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()), + ctx => ctx.CallActivityAsync(nameof(NotifyActivity), It.IsAny(), It.IsAny()), Times.Exactly(2)); } @@ -61,7 +60,7 @@ public async Task TestInsufficientInventory() // Mock the call to ReserveInventoryActivity Mock mockContext = new(); mockContext - .Setup(ctx => ctx.CallActivityAsync(nameof(ReserveInventoryActivity), It.IsAny(), It.IsAny())) + .Setup(ctx => ctx.CallActivityAsync(nameof(ReserveInventoryActivity), It.IsAny(), It.IsAny())) .Returns(Task.FromResult(inventoryResult)); // Run the workflow directly @@ -69,17 +68,17 @@ public async Task TestInsufficientInventory() // Verify that ReserveInventoryActivity was called with a specific input mockContext.Verify( - ctx => ctx.CallActivityAsync(nameof(ReserveInventoryActivity), expectedInventoryRequest, It.IsAny()), + 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()), + 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()), + ctx => ctx.CallActivityAsync(nameof(NotifyActivity), It.IsAny(), It.IsAny()), Times.Exactly(2)); } } diff --git a/examples/Workflow/WorkflowUnitTest/WorkflowUnitTest.csproj b/examples/Workflow/WorkflowUnitTest/WorkflowUnitTest.csproj index 9c4a74a17..dec14a713 100644 --- a/examples/Workflow/WorkflowUnitTest/WorkflowUnitTest.csproj +++ b/examples/Workflow/WorkflowUnitTest/WorkflowUnitTest.csproj @@ -3,19 +3,18 @@ net6.0 enable - 10 false - - - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/properties/IsExternalInit.cs b/properties/IsExternalInit.cs index 2f68a08f4..34357c39a 100644 --- a/properties/IsExternalInit.cs +++ b/properties/IsExternalInit.cs @@ -13,6 +13,5 @@ namespace System.Runtime.CompilerServices internal static class IsExternalInit { } - - // This is a polyfill for init only properties in netcoreapp3.1 + } diff --git a/properties/dapr_managed_netcore.props b/properties/dapr_managed_netcore.props index 59bb68c9a..6e8c01bfe 100644 --- a/properties/dapr_managed_netcore.props +++ b/properties/dapr_managed_netcore.props @@ -3,7 +3,7 @@ Debug - 9.0 + 10.0 true 4 false @@ -53,7 +53,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/samples/Client/README.md b/samples/Client/README.md index 2501bfadc..2bb738d89 100644 --- a/samples/Client/README.md +++ b/samples/Client/README.md @@ -8,7 +8,7 @@ The following examples will show you how to: ## Prerequisites -* [.Net Core 3.1 or .NET 5+](https://dotnet.microsoft.com/download) +* [.NET 6+](https://dotnet.microsoft.com/download) * [Dapr CLI](https://github.com/dapr/cli) * [Dapr DotNet SDK](https://github.com/dapr/dotnet-sdk) diff --git a/src/Dapr.Actors.AspNetCore/ActorsEndpointRouteBuilderExtensions.cs b/src/Dapr.Actors.AspNetCore/ActorsEndpointRouteBuilderExtensions.cs index e7937bbbf..574a172a8 100644 --- a/src/Dapr.Actors.AspNetCore/ActorsEndpointRouteBuilderExtensions.cs +++ b/src/Dapr.Actors.AspNetCore/ActorsEndpointRouteBuilderExtensions.cs @@ -103,23 +103,23 @@ private static IEndpointConventionBuilder MapActorMethodEndpoint(this IEndpointR try { - var (header, body) = await runtime.DispatchWithRemotingAsync(actorTypeName, actorId, methodName, daprActorheader, context.Request.Body); + 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.Add(Constants.ErrorResponseHeaderName, header); // add error header + context.Response.Headers[Constants.ErrorResponseHeaderName] = header; // add error header } - await context.Response.Body.WriteAsync(body, 0, body.Length); // add response message body + 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.Add(Constants.ErrorResponseHeaderName, header); - await context.Response.Body.WriteAsync(body, 0, body.Length); + context.Response.Headers[Constants.ErrorResponseHeaderName] = header; + await context.Response.Body.WriteAsync(body, 0, body.Length, context.RequestAborted); } finally { @@ -130,7 +130,7 @@ private static IEndpointConventionBuilder MapActorMethodEndpoint(this IEndpointR { try { - await runtime.DispatchWithoutRemotingAsync(actorTypeName, actorId, methodName, context.Request.Body, context.Response.Body); + await runtime.DispatchWithoutRemotingAsync(actorTypeName, actorId, methodName, context.Request.Body, context.Response.Body, context.RequestAborted); } finally { diff --git a/src/Dapr.Actors.AspNetCore/Dapr.Actors.AspNetCore.csproj b/src/Dapr.Actors.AspNetCore/Dapr.Actors.AspNetCore.csproj index a9f6e8818..82c5863db 100644 --- a/src/Dapr.Actors.AspNetCore/Dapr.Actors.AspNetCore.csproj +++ b/src/Dapr.Actors.AspNetCore/Dapr.Actors.AspNetCore.csproj @@ -1,15 +1,6 @@  - - netcoreapp3.1 - - - diff --git a/src/Dapr.Actors.Generators/ActorClientGenerator.cs b/src/Dapr.Actors.Generators/ActorClientGenerator.cs new file mode 100644 index 000000000..349d80188 --- /dev/null +++ b/src/Dapr.Actors.Generators/ActorClientGenerator.cs @@ -0,0 +1,303 @@ +// ------------------------------------------------------------------------ +// 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 Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Dapr.Actors.Generators; + +/// +/// Generates strongly-typed actor clients that use the non-remoting actor proxy. +/// +[Generator] +public sealed class ActorClientGenerator : ISourceGenerator +{ + private const string GeneratorsNamespace = "Dapr.Actors.Generators"; + + private const string ActorMethodAttributeTypeName = "ActorMethodAttribute"; + private const string ActorMethodAttributeFullTypeName = GeneratorsNamespace + "." + ActorMethodAttributeTypeName; + + private const string GenerateActorClientAttribute = "GenerateActorClientAttribute"; + private const string GenerateActorClientAttributeFullTypeName = GeneratorsNamespace + "." + GenerateActorClientAttribute; + + private const string ActorMethodAttributeText = $@" + // + + #nullable enable + + using System; + + namespace {GeneratorsNamespace} + {{ + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] + internal sealed class ActorMethodAttribute : Attribute + {{ + public string? Name {{ get; set; }} + }} + }}"; + + private const string GenerateActorClientAttributeText = $@" + // + + #nullable enable + + using System; + + namespace {GeneratorsNamespace} + {{ + [AttributeUsage(AttributeTargets.Interface, AllowMultiple = false, Inherited = false)] + internal sealed class GenerateActorClientAttribute : Attribute + {{ + public string? Name {{ get; set; }} + + public string? Namespace {{ get; set; }} + }} + }}"; + + private sealed class ActorInterfaceSyntaxReceiver : ISyntaxContextReceiver + { + private readonly List models = new(); + + public IEnumerable Models => this.models; + + #region ISyntaxContextReceiver Members + + public void OnVisitSyntaxNode(GeneratorSyntaxContext context) + { + if (context.Node is not InterfaceDeclarationSyntax interfaceDeclarationSyntax + || interfaceDeclarationSyntax.AttributeLists.Count == 0) + { + return; + } + + var interfaceSymbol = context.SemanticModel.GetDeclaredSymbol(interfaceDeclarationSyntax) as INamedTypeSymbol; + + if (interfaceSymbol is null + || !interfaceSymbol.GetAttributes().Any(a => a.AttributeClass?.ToString() == GenerateActorClientAttributeFullTypeName)) + { + return; + } + + this.models.Add(interfaceSymbol); + } + + #endregion + } + + #region ISourceGenerator Members + + /// + public void Execute(GeneratorExecutionContext context) + { + if (context.SyntaxContextReceiver is not ActorInterfaceSyntaxReceiver actorInterfaceSyntaxReceiver) + { + return; + } + + var actorMethodAttributeSymbol = context.Compilation.GetTypeByMetadataName(ActorMethodAttributeFullTypeName) ?? throw new InvalidOperationException("Could not find ActorMethodAttribute."); + var generateActorClientAttributeSymbol = context.Compilation.GetTypeByMetadataName(GenerateActorClientAttributeFullTypeName) ?? throw new InvalidOperationException("Could not find GenerateActorClientAttribute."); + var cancellationTokenSymbol = context.Compilation.GetTypeByMetadataName("System.Threading.CancellationToken") ?? throw new InvalidOperationException("Could not find CancellationToken."); + + foreach (var interfaceSymbol in actorInterfaceSyntaxReceiver.Models) + { + try + { + var actorInterfaceTypeName = interfaceSymbol.Name; + var fullyQualifiedActorInterfaceTypeName = interfaceSymbol.ToString(); + + var attributeData = interfaceSymbol.GetAttributes().Single(a => a.AttributeClass?.Equals(generateActorClientAttributeSymbol, SymbolEqualityComparer.Default) == true); + + var accessibility = GetClientAccessibility(interfaceSymbol); + var clientTypeName = GetClientName(interfaceSymbol, attributeData); + var namespaceName = attributeData.NamedArguments.SingleOrDefault(kvp => kvp.Key == "Namespace").Value.Value?.ToString() ?? interfaceSymbol.ContainingNamespace.ToDisplayString(); + + var members = interfaceSymbol.GetMembers().OfType().Where(m => m.MethodKind == MethodKind.Ordinary).ToList(); + + var methodImplementations = String.Join("\n", members.Select(member => GenerateMethodImplementation(member, actorMethodAttributeSymbol, cancellationTokenSymbol))); + + var source = $@" +// + +namespace {namespaceName} +{{ + {accessibility} sealed class {clientTypeName} : {fullyQualifiedActorInterfaceTypeName} + {{ + private readonly Dapr.Actors.Client.ActorProxy actorProxy; + + public {clientTypeName}(Dapr.Actors.Client.ActorProxy actorProxy) + {{ + this.actorProxy = actorProxy; + }} + + {methodImplementations} + }} +}} +"; + // Add the source code to the compilation + context.AddSource($"{namespaceName}.{clientTypeName}.g.cs", source); + } + catch (DiagnosticsException e) + { + foreach (var diagnostic in e.Diagnostics) + { + context.ReportDiagnostic(diagnostic); + } + } + } + } + + /// + public void Initialize(GeneratorInitializationContext context) + { + /* + while (!Debugger.IsAttached) + { + System.Threading.Thread.Sleep(500); + } + */ + + context.RegisterForPostInitialization( + i => + { + i.AddSource($"{ActorMethodAttributeFullTypeName}.g.cs", ActorMethodAttributeText); + i.AddSource($"{GenerateActorClientAttributeFullTypeName}.g.cs", GenerateActorClientAttributeText); + }); + + context.RegisterForSyntaxNotifications(() => new ActorInterfaceSyntaxReceiver()); + } + + #endregion + + private static string GetClientAccessibility(INamedTypeSymbol interfaceSymbol) + { + return interfaceSymbol.DeclaredAccessibility switch + { + Accessibility.Public => "public", + Accessibility.Internal => "internal", + Accessibility.Private => "private", + Accessibility.Protected => "protected", + Accessibility.ProtectedAndInternal => "protected internal", + _ => throw new InvalidOperationException("Unexpected accessibility.") + }; + } + + private static string GetClientName(INamedTypeSymbol interfaceSymbol, AttributeData attributeData) + { + string? clientName = attributeData.NamedArguments.SingleOrDefault(kvp => kvp.Key == "Name").Value.Value?.ToString(); + + clientName ??= $"{(interfaceSymbol.Name.StartsWith("I") ? interfaceSymbol.Name.Substring(1) : interfaceSymbol.Name)}Client"; + + return clientName; + } + + private static string GenerateMethodImplementation(IMethodSymbol method, INamedTypeSymbol generateActorClientAttributeSymbol, INamedTypeSymbol cancellationTokenSymbol) + { + int cancellationTokenIndex = method.Parameters.IndexOf(p => p.Type.Equals(cancellationTokenSymbol, SymbolEqualityComparer.Default)); + var cancellationTokenParameter = cancellationTokenIndex != -1 ? method.Parameters[cancellationTokenIndex] : null; + + if (cancellationTokenParameter is not null && cancellationTokenIndex != method.Parameters.Length - 1) + { + throw new DiagnosticsException(new[] + { + Diagnostic.Create( + new DiagnosticDescriptor( + "DAPR0001", + "Invalid method signature.", + "Cancellation tokens must be the last argument.", + "Dapr.Actors.Generators", + DiagnosticSeverity.Error, + true), + cancellationTokenParameter.Locations.First()) + }); + } + + if ((method.Parameters.Length > 1 && cancellationTokenIndex == -1) + || (method.Parameters.Length > 2)) + { + throw new DiagnosticsException(new[] + { + Diagnostic.Create( + new DiagnosticDescriptor( + "DAPR0002", + "Invalid method signature.", + "Only methods with a single argument or a single argument followed by a cancellation token are supported.", + "Dapr.Actors.Generators", + DiagnosticSeverity.Error, + true), + method.Locations.First()) + }); + } + + var attributeData = method.GetAttributes().SingleOrDefault(a => a.AttributeClass?.Equals(generateActorClientAttributeSymbol, SymbolEqualityComparer.Default) == true); + + string? actualMethodName = attributeData?.NamedArguments.SingleOrDefault(kvp => kvp.Key == "Name").Value.Value?.ToString() ?? method.Name; + + var requestParameter = method.Parameters.Length > 0 && cancellationTokenIndex != 0 ? method.Parameters[0] : null; + + var returnTypeArgument = (method.ReturnType as INamedTypeSymbol)?.TypeArguments.FirstOrDefault(); + + string argumentDefinitions = String.Join(", ", method.Parameters.Select(p => $"{p.Type} {p.Name}")); + + if (cancellationTokenParameter is not null + && cancellationTokenParameter.IsOptional + && cancellationTokenParameter.HasExplicitDefaultValue + && cancellationTokenParameter.ExplicitDefaultValue is null) + { + argumentDefinitions = argumentDefinitions + " = default"; + } + + string argumentList = String.Join(", ", new[] { $@"""{actualMethodName}""" }.Concat(method.Parameters.Select(p => p.Name))); + + string templateArgs = + returnTypeArgument is not null + ? $"<{(requestParameter is not null ? $"{requestParameter.Type}, " : "")}{returnTypeArgument}>" + : ""; + + return + $@"public {method.ReturnType} {method.Name}({argumentDefinitions}) + {{ + return this.actorProxy.InvokeMethodAsync{templateArgs}({argumentList}); + }}"; + } +} + +internal static class Extensions +{ + public static int IndexOf(this IEnumerable source, Func predicate) + { + int index = 0; + + foreach (var item in source) + { + if (predicate(item)) + { + return index; + } + + index++; + } + + return -1; + } +} + +internal sealed class DiagnosticsException : Exception +{ + public DiagnosticsException(IEnumerable diagnostics) + : base(String.Join("\n", diagnostics.Select(d => d.ToString()))) + { + this.Diagnostics = diagnostics.ToArray(); + } + + public IEnumerable Diagnostics { get; } +} diff --git a/src/Dapr.Actors.Generators/Dapr.Actors.Generators.csproj b/src/Dapr.Actors.Generators/Dapr.Actors.Generators.csproj new file mode 100644 index 000000000..370d422f1 --- /dev/null +++ b/src/Dapr.Actors.Generators/Dapr.Actors.Generators.csproj @@ -0,0 +1,45 @@ + + + + enable + enable + + + + true + + + + + + + + + + + + netstandard2.0 + + + + false + + + true + + + false + + + This package contains source generators for interacting with Actor services using Dapr. + $(PackageTags);Actors + + + + + + + + diff --git a/src/Dapr.Actors/ActorReference.cs b/src/Dapr.Actors/ActorReference.cs index 0e199c3ae..d72b6676f 100644 --- a/src/Dapr.Actors/ActorReference.cs +++ b/src/Dapr.Actors/ActorReference.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ 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. @@ -69,23 +70,28 @@ public object Bind(Type actorInterfaceType) private static ActorReference GetActorReference(object actor) { - if (actor == null) - { - throw new ArgumentNullException("actor"); - } + ArgumentNullException.ThrowIfNull(actor, nameof(actor)); - // try as IActorProxy for backward compatibility as customers's mock framework may rely on it before V2 remoting stack. - if (actor is IActorProxy actorProxy) + var actorReference = actor switch { - return new ActorReference() + // 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."), + }; - // TODO check for ActorBase - throw new ArgumentOutOfRangeException("actor"); + return actorReference; } } } diff --git a/src/Dapr.Actors/Client/ActorProxyFactory.cs b/src/Dapr.Actors/Client/ActorProxyFactory.cs index 9fd5edddb..4a8fe3a08 100644 --- a/src/Dapr.Actors/Client/ActorProxyFactory.cs +++ b/src/Dapr.Actors/Client/ActorProxyFactory.cs @@ -16,6 +16,7 @@ namespace Dapr.Actors.Client using System; using System.Net.Http; using Dapr.Actors.Builder; + using Dapr.Actors.Communication; using Dapr.Actors.Communication.Client; /// @@ -79,7 +80,15 @@ public object CreateActorProxy(ActorId actorId, Type actorInterfaceType, string options ??= this.DefaultOptions; var daprInteractor = new DaprHttpInteractor(this.handler, options.HttpEndpoint, options.DaprApiToken, options.RequestTimeout); - var remotingClient = new ActorRemotingClient(daprInteractor); + + // 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); diff --git a/src/Dapr.Actors/Client/ActorProxyOptions.cs b/src/Dapr.Actors/Client/ActorProxyOptions.cs index 808605c70..665a1dced 100644 --- a/src/Dapr.Actors/Client/ActorProxyOptions.cs +++ b/src/Dapr.Actors/Client/ActorProxyOptions.cs @@ -62,5 +62,10 @@ public JsonSerializerOptions JsonSerializerOptions /// 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; } } } diff --git a/src/Dapr.Actors/Communication/ActorInvokeException.cs b/src/Dapr.Actors/Communication/ActorInvokeException.cs index ed17a7f55..ac4ea63df 100644 --- a/src/Dapr.Actors/Communication/ActorInvokeException.cs +++ b/src/Dapr.Actors/Communication/ActorInvokeException.cs @@ -93,7 +93,7 @@ internal static bool TryDeserialize(Stream stream, out Exception result, ILogger catch (Exception e) { // swallowing the exception - logger?.LogWarning("RemoteException", "DeSerialization failed : Reason {0}", e); + logger?.LogWarning("RemoteException: DeSerialization failed : Reason {0}", e); } result = null; diff --git a/src/Dapr.Actors/Communication/ActorMessageBodyDataContractSerializationProvider.cs b/src/Dapr.Actors/Communication/ActorMessageBodyDataContractSerializationProvider.cs index e1991df26..cf16ee2d8 100644 --- a/src/Dapr.Actors/Communication/ActorMessageBodyDataContractSerializationProvider.cs +++ b/src/Dapr.Actors/Communication/ActorMessageBodyDataContractSerializationProvider.cs @@ -17,6 +17,7 @@ namespace Dapr.Actors.Communication using System.Collections.Generic; using System.IO; using System.Runtime.Serialization; + using System.Threading.Tasks; using System.Xml; /// @@ -185,21 +186,21 @@ byte[] IActorRequestMessageBodySerializer.Serialize(IActorRequestMessageBody act return stream.ToArray(); } - IActorRequestMessageBody IActorRequestMessageBodySerializer.Deserialize(Stream stream) + ValueTask IActorRequestMessageBodySerializer.DeserializeAsync(Stream stream) { if (stream == null) { - return null; + return default; } if (stream.Length == 0) { - return null; + return default; } stream.Position = 0; using var reader = this.CreateXmlDictionaryReader(stream); - return (TRequest)this.serializer.ReadObject(reader); + return new ValueTask((TRequest)this.serializer.ReadObject(reader)); } byte[] IActorResponseMessageBodySerializer.Serialize(IActorResponseMessageBody actorResponseMessageBody) @@ -217,11 +218,11 @@ byte[] IActorResponseMessageBodySerializer.Serialize(IActorResponseMessageBody a return stream.ToArray(); } - IActorResponseMessageBody IActorResponseMessageBodySerializer.Deserialize(Stream messageBody) + ValueTask IActorResponseMessageBodySerializer.DeserializeAsync(Stream messageBody) { if (messageBody == null) { - return null; + return default; } // TODO check performance @@ -231,11 +232,11 @@ IActorResponseMessageBody IActorResponseMessageBodySerializer.Deserialize(Stream if (stream.Capacity == 0) { - return null; + return default; } using var reader = this.CreateXmlDictionaryReader(stream); - return (TResponse)this.serializer.ReadObject(reader); + return new ValueTask((TResponse)this.serializer.ReadObject(reader)); } /// diff --git a/src/Dapr.Actors/Communication/ActorMessageBodyJsonConverter.cs b/src/Dapr.Actors/Communication/ActorMessageBodyJsonConverter.cs new file mode 100644 index 000000000..0c77adb10 --- /dev/null +++ b/src/Dapr.Actors/Communication/ActorMessageBodyJsonConverter.cs @@ -0,0 +1,95 @@ +// ------------------------------------------------------------------------ +// 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.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Dapr.Actors.Communication +{ + internal class ActorMessageBodyJsonConverter : JsonConverter + { + private readonly List methodRequestParameterTypes; + private readonly List wrappedRequestMessageTypes; + private readonly Type wrapperMessageType; + + public ActorMessageBodyJsonConverter( + List methodRequestParameterTypes, + List wrappedRequestMessageTypes = null + ) + { + 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(); + } + } +} diff --git a/src/Dapr.Actors/Communication/ActorMessageBodyJsonSerializationProvider.cs b/src/Dapr.Actors/Communication/ActorMessageBodyJsonSerializationProvider.cs new file mode 100644 index 000000000..fd35db8e1 --- /dev/null +++ b/src/Dapr.Actors/Communication/ActorMessageBodyJsonSerializationProvider.cs @@ -0,0 +1,175 @@ +// ------------------------------------------------------------------------ +// 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. +// ------------------------------------------------------------------------ + +namespace Dapr.Actors.Communication +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Text.Json; + 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 ActorMessageBodyJsonSerializationProvider : IActorMessageBodySerializationProvider + { + public JsonSerializerOptions Options { get; } + + /// + /// 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 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 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) + { + var _methodRequestParameterTypes = new List(methodRequestParameterTypes); + var _wrappedRequestMessageTypes = new List(wrappedRequestMessageTypes); + + 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)); + } + + byte[] IActorRequestMessageBodySerializer.Serialize(IActorRequestMessageBody actorRequestMessageBody) + { + if (actorRequestMessageBody == null) + { + return null; + } + + return JsonSerializer.SerializeToUtf8Bytes(actorRequestMessageBody, this.serializerOptions); + } + + async ValueTask IActorRequestMessageBodySerializer.DeserializeAsync(Stream stream) + { + if (stream == null) + { + return default; + } + + if (stream.Length == 0) + { + return default; + } + + stream.Position = 0; + return await JsonSerializer.DeserializeAsync(stream, this.serializerOptions); + } + + byte[] IActorResponseMessageBodySerializer.Serialize(IActorResponseMessageBody actorResponseMessageBody) + { + if (actorResponseMessageBody == null) + { + return null; + } + + return JsonSerializer.SerializeToUtf8Bytes(actorResponseMessageBody, this.serializerOptions); + } + + async ValueTask IActorResponseMessageBodySerializer.DeserializeAsync(Stream messageBody) + { + 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); + } + } + } +} diff --git a/src/Dapr.Actors/Communication/ActorStateResponse.cs b/src/Dapr.Actors/Communication/ActorStateResponse.cs new file mode 100644 index 000000000..22b3bf20e --- /dev/null +++ b/src/Dapr.Actors/Communication/ActorStateResponse.cs @@ -0,0 +1,50 @@ +// ------------------------------------------------------------------------ +// 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. +// ------------------------------------------------------------------------ + +namespace Dapr.Actors.Communication +{ + using System; + + /// + /// Represents a response from fetching an actor state key. + /// + 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; + } + + /// + /// 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; } + } +} diff --git a/src/Dapr.Actors/Communication/IActorRequestMessageBodySerializer.cs b/src/Dapr.Actors/Communication/IActorRequestMessageBodySerializer.cs index 66c6abb82..b58257381 100644 --- a/src/Dapr.Actors/Communication/IActorRequestMessageBodySerializer.cs +++ b/src/Dapr.Actors/Communication/IActorRequestMessageBodySerializer.cs @@ -14,6 +14,7 @@ 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. @@ -32,6 +33,6 @@ internal interface IActorRequestMessageBodySerializer /// /// Serialized message body. /// Deserialized remoting request message body object. - IActorRequestMessageBody Deserialize(Stream messageBody); + ValueTask DeserializeAsync(Stream messageBody); } } diff --git a/src/Dapr.Actors/Communication/IActorResponseMessageBodySerializer.cs b/src/Dapr.Actors/Communication/IActorResponseMessageBodySerializer.cs index 2c31019ba..b54191f91 100644 --- a/src/Dapr.Actors/Communication/IActorResponseMessageBodySerializer.cs +++ b/src/Dapr.Actors/Communication/IActorResponseMessageBodySerializer.cs @@ -14,6 +14,7 @@ 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. @@ -32,6 +33,6 @@ internal interface IActorResponseMessageBodySerializer /// /// Serialized message body. /// Deserialized actor response message body object. - IActorResponseMessageBody Deserialize(Stream messageBody); + ValueTask DeserializeAsync(Stream messageBody); } } diff --git a/src/Dapr.Actors/Constants.cs b/src/Dapr.Actors/Constants.cs index be2d8f49f..038caf101 100644 --- a/src/Dapr.Actors/Constants.cs +++ b/src/Dapr.Actors/Constants.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ internal static class Constants 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"; diff --git a/src/Dapr.Actors/Dapr.Actors.csproj b/src/Dapr.Actors/Dapr.Actors.csproj index 1c0e0bdfb..54d3487b8 100644 --- a/src/Dapr.Actors/Dapr.Actors.csproj +++ b/src/Dapr.Actors/Dapr.Actors.csproj @@ -1,26 +1,18 @@  - - netcoreapp3.1 - - This package contains the reference assemblies for developing Actor services using Dapr. $(PackageTags);Actors - - - - - + - + diff --git a/src/Dapr.Actors/DaprHttpInteractor.cs b/src/Dapr.Actors/DaprHttpInteractor.cs index df5207f4e..2565bab62 100644 --- a/src/Dapr.Actors/DaprHttpInteractor.cs +++ b/src/Dapr.Actors/DaprHttpInteractor.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -57,7 +57,7 @@ public DaprHttpInteractor( this.httpClient.Timeout = requestTimeout ?? this.httpClient.Timeout; } - public async Task GetStateAsync(string actorType, string actorId, string keyName, CancellationToken cancellationToken = default) + public async Task> GetStateAsync(string actorType, string actorId, string keyName, CancellationToken cancellationToken = default) { var relativeUrl = string.Format(CultureInfo.InvariantCulture, Constants.ActorStateKeyRelativeUrlFormat, actorType, actorId, keyName); @@ -70,9 +70,20 @@ HttpRequestMessage RequestFunc() return request; } - var response = await this.SendAsync(RequestFunc, relativeUrl, cancellationToken); + using var response = await this.SendAsync(RequestFunc, relativeUrl, cancellationToken); var stringResponse = await response.Content.ReadAsStringAsync(); - return stringResponse; + + 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) @@ -164,11 +175,11 @@ HttpRequestMessage RequestFunc() // actorResponseMessageHeader is not null, it means there is remote exception if (actorResponseMessageHeader != null) { - var isDeserialzied = + var isDeserialized = ActorInvokeException.ToException( responseMessageBody, out var remoteMethodException); - if (isDeserialzied) + if (isDeserialized) { var exceptionDetails = GetExceptionDetails(header.ToString()); throw new ActorMethodInvocationException( @@ -185,7 +196,7 @@ HttpRequestMessage RequestFunc() } } - actorResponseMessageBody = responseBodySerializer.Deserialize(responseMessageBody); + actorResponseMessageBody = await responseBodySerializer.DeserializeAsync(responseMessageBody); } return new ActorResponseMessage(actorResponseMessageHeader, actorResponseMessageBody); @@ -231,8 +242,8 @@ HttpRequestMessage RequestFunc() } var response = await this.SendAsync(RequestFunc, relativeUrl, cancellationToken); - var byteArray = await response.Content.ReadAsStreamAsync(); - return byteArray; + var stream = await response.Content.ReadAsStreamAsync(); + return stream; } public Task RegisterReminderAsync(string actorType, string actorId, string reminderName, string data, CancellationToken cancellationToken = default) @@ -254,6 +265,23 @@ HttpRequestMessage RequestFunc() 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; + } + + var response = await this.SendAsync(RequestFunc, relativeUrl, cancellationToken); + return await response.Content.ReadAsStreamAsync(); + } + public Task UnregisterReminderAsync(string actorType, string actorId, string reminderName, CancellationToken cancellationToken = default) { var relativeUrl = string.Format(CultureInfo.InvariantCulture, Constants.ActorReminderRelativeUrlFormat, actorType, actorId, reminderName); @@ -334,7 +362,7 @@ internal async Task SendAsyncGetResponseAsRawJson( string relativeUri, CancellationToken cancellationToken) { - var response = await this.SendAsyncHandleUnsuccessfulResponse(requestFunc, relativeUri, cancellationToken); + using var response = await this.SendAsyncHandleUnsuccessfulResponse(requestFunc, relativeUri, cancellationToken); var retValue = default(string); if (response != null && response.Content != null) @@ -441,7 +469,7 @@ private async Task SendAsyncHandleSecurityExceptions( HttpResponseMessage response; // Get the request using the Func as same request cannot be resent when retries are implemented. - var request = requestFunc.Invoke(); + using var request = requestFunc.Invoke(); // add token for dapr api token based authentication this.AddDaprApiTokenHeader(request); diff --git a/src/Dapr.Actors/IDaprInteractor.cs b/src/Dapr.Actors/IDaprInteractor.cs index 04eb66de9..5849328a8 100644 --- a/src/Dapr.Actors/IDaprInteractor.cs +++ b/src/Dapr.Actors/IDaprInteractor.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -52,7 +52,7 @@ internal interface IDaprInteractor /// 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); + Task> GetStateAsync(string actorType, string actorId, string keyName, CancellationToken cancellationToken = default); /// /// Invokes Actor method. @@ -74,6 +74,16 @@ internal interface IDaprInteractor /// 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 representing the result of the asynchronous operation. + Task GetReminderAsync(string actorType, string actorId, string reminderName, CancellationToken cancellationToken = default); + /// /// Unregisters a reminder. /// diff --git a/src/Dapr.Actors/Runtime/Actor.cs b/src/Dapr.Actors/Runtime/Actor.cs index 0f74513a1..ddfc266e9 100644 --- a/src/Dapr.Actors/Runtime/Actor.cs +++ b/src/Dapr.Actors/Runtime/Actor.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -101,10 +101,11 @@ internal async Task OnPostActorMethodAsyncInternal(ActorMethodContext actorMetho await this.SaveStateAsync(); } - internal Task OnInvokeFailedAsync() + 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. - return this.ResetStateAsync(); + await this.ResetStateAsync(); } internal Task ResetStateAsync() @@ -190,6 +191,30 @@ protected virtual Task OnPostActorMethodAsync(ActorMethodContext actorMethodCont 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. /// @@ -360,6 +385,18 @@ internal async Task RegisterReminderAsync(ActorReminderOptions o 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 . /// diff --git a/src/Dapr.Actors/Runtime/ActorManager.cs b/src/Dapr.Actors/Runtime/ActorManager.cs index e7f29f5cc..80049d65f 100644 --- a/src/Dapr.Actors/Runtime/ActorManager.cs +++ b/src/Dapr.Actors/Runtime/ActorManager.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -56,7 +56,8 @@ internal sealed class ActorManager internal ActorManager( ActorRegistration registration, ActorActivator activator, - JsonSerializerOptions jsonSerializerOptions, + JsonSerializerOptions jsonSerializerOptions, + bool useJsonSerialization, ILoggerFactory loggerFactory, IActorProxyFactory proxyFactory, IDaprInteractor daprInteractor) @@ -78,7 +79,15 @@ internal ActorManager( this.activeActors = new ConcurrentDictionary(); this.reminderMethodContext = ActorMethodContext.CreateForReminder(ReceiveReminderMethodName); this.timerMethodContext = ActorMethodContext.CreateForTimer(TimerMethodName); - this.serializersManager = IntializeSerializationManager(null); + + // 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()); @@ -103,7 +112,7 @@ internal async Task> DispatchWithRemotingAsync(ActorId act using (var stream = new MemoryStream()) { await data.CopyToAsync(stream); - actorMessageBody = msgBodySerializer.Deserialize(stream); + actorMessageBody = await msgBodySerializer.DeserializeAsync(stream); } // Call the method on the method dispatcher using the Func below. @@ -139,16 +148,16 @@ async Task RequestFunc(Actor actor, CancellationToken ct) var parameters = methodInfo.GetParameters(); dynamic awaitable; - if (parameters.Length == 0) + if (parameters.Length == 0 || (parameters.Length == 1 && parameters[0].ParameterType == typeof(CancellationToken))) { - awaitable = methodInfo.Invoke(actor, null); + awaitable = methodInfo.Invoke(actor, parameters.Length == 0 ? null : new object[] { ct }); } - else if (parameters.Length == 1) + 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, new object[] { deserializedType }); + awaitable = methodInfo.Invoke(actor, parameters.Length == 1 ? new object[] { deserializedType } : new object[] { deserializedType, ct }); } else { @@ -177,7 +186,13 @@ async Task RequestFunc(Actor actor, CancellationToken ct) // Serialize result if it has result (return type was not just Task.) if (methodInfo.ReturnType.Name != typeof(Task).Name) { - await JsonSerializer.SerializeAsync(responseBodyStream, result, result.GetType(), jsonSerializerOptions); +#if NET7_0_OR_GREATER + var resultType = methodInfo.ReturnType.GenericTypeArguments[0]; + await JsonSerializer.SerializeAsync(responseBodyStream, result, resultType, jsonSerializerOptions); +#else + await JsonSerializer.SerializeAsync(responseBodyStream, result, jsonSerializerOptions); +#endif + } } @@ -355,9 +370,9 @@ private async Task DispatchInternalAsync(ActorId actorId, ActorMethodConte // PostActivate will save the state, its not invoked when actorFunc invocation throws. await actor.OnPostActorMethodAsyncInternal(actorMethodContext); } - catch (Exception) + catch (Exception e) { - await actor.OnInvokeFailedAsync(); + await actor.OnActorMethodFailedInternalAsync(actorMethodContext, e); throw; } finally diff --git a/src/Dapr.Actors/Runtime/ActorRuntime.cs b/src/Dapr.Actors/Runtime/ActorRuntime.cs index 8d2ae0cab..7f01fefb7 100644 --- a/src/Dapr.Actors/Runtime/ActorRuntime.cs +++ b/src/Dapr.Actors/Runtime/ActorRuntime.cs @@ -55,6 +55,7 @@ internal ActorRuntime(ActorRuntimeOptions options, ILoggerFactory loggerFactory, actor, actor.Activator ?? this.activatorFactory.CreateActivator(actor.Type), this.options.JsonSerializerOptions, + this.options.UseJsonSerialization, loggerFactory, proxyFactory, daprInteractor); diff --git a/src/Dapr.Actors/Runtime/ActorRuntimeOptions.cs b/src/Dapr.Actors/Runtime/ActorRuntimeOptions.cs index 3f4a6df88..62eaceea6 100644 --- a/src/Dapr.Actors/Runtime/ActorRuntimeOptions.cs +++ b/src/Dapr.Actors/Runtime/ActorRuntimeOptions.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -32,6 +32,7 @@ public sealed class ActorRuntimeOptions { Enabled = false, }; + private bool useJsonSerialization = false; private JsonSerializerOptions jsonSerializerOptions = JsonSerializerDefaults.Web; private string daprApiToken = DaprDefaults.GetDefaultDaprApiToken(); private int? remindersStoragePartitions = null; @@ -151,7 +152,22 @@ public ActorReentrancyConfig ReentrancyConfig } } - + /// + /// 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 /// @@ -220,8 +236,9 @@ public int? RemindersStoragePartitions /// /// /// 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. + /// 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; } = DaprDefaults.GetDefaultHttpEndpoint(); diff --git a/src/Dapr.Actors/Runtime/ActorStateChange.cs b/src/Dapr.Actors/Runtime/ActorStateChange.cs index c09e48df6..34fa68fdf 100644 --- a/src/Dapr.Actors/Runtime/ActorStateChange.cs +++ b/src/Dapr.Actors/Runtime/ActorStateChange.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -27,7 +27,8 @@ public sealed class ActorStateChange /// 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. - public ActorStateChange(string stateName, Type type, object value, StateChangeKind changeKind) + /// The time to live for the state. + public ActorStateChange(string stateName, Type type, object value, StateChangeKind changeKind, DateTimeOffset? ttlExpireTime) { ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName)); @@ -35,6 +36,7 @@ public ActorStateChange(string stateName, Type type, object value, StateChangeKi this.Type = type; this.Value = value; this.ChangeKind = changeKind; + this.TTLExpireTime = ttlExpireTime; } /// @@ -68,5 +70,16 @@ public ActorStateChange(string stateName, Type type, object value, StateChangeKi /// 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; } } } diff --git a/src/Dapr.Actors/Runtime/ActorStateManager.cs b/src/Dapr.Actors/Runtime/ActorStateManager.cs index 9c752f56b..111bb80f4 100644 --- a/src/Dapr.Actors/Runtime/ActorStateManager.cs +++ b/src/Dapr.Actors/Runtime/ActorStateManager.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ using System.Threading; using System.Threading.Tasks; using Dapr.Actors.Resources; +using Dapr.Actors.Communication; namespace Dapr.Actors.Runtime { @@ -38,14 +39,24 @@ internal ActorStateManager(Actor actor) public async Task AddStateAsync(string stateName, T value, CancellationToken cancellationToken) { EnsureStateProviderInitialized(); - + if (!(await this.TryAddStateAsync(stateName, value, cancellationToken))) { throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, SR.ActorStateAlreadyExists, stateName)); } } - public async Task TryAddStateAsync(string stateName, T value, CancellationToken cancellationToken) + 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)); @@ -57,8 +68,8 @@ public async Task TryAddStateAsync(string stateName, T value, Cancellat { var stateMetadata = stateChangeTracker[stateName]; - // Check if the property was marked as remove in the cache - if (stateMetadata.ChangeKind == StateChangeKind.Remove) + // 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; @@ -76,6 +87,37 @@ public async Task TryAddStateAsync(string stateName, T value, Cancellat return true; } + 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)) + { + stateChangeTracker[stateName] = StateMetadata.Create(value, StateChangeKind.Update, ttl: ttl); + return true; + } + + return false; + } + + if (await this.actor.Host.StateProvider.ContainsStateAsync(this.actorTypeName, this.actor.Id.ToString(), stateName, cancellationToken)) + { + return false; + } + + stateChangeTracker[stateName] = StateMetadata.Create(value, StateChangeKind.Add, ttl: ttl); + return true; + } + public async Task GetStateAsync(string stateName, CancellationToken cancellationToken) { EnsureStateProviderInitialized(); @@ -102,8 +144,8 @@ public async Task> TryGetStateAsync(string stateName, Can { var stateMetadata = stateChangeTracker[stateName]; - // Check if the property was marked as remove in the cache - if (stateMetadata.ChangeKind == StateChangeKind.Remove) + // 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); } @@ -114,10 +156,11 @@ public async Task> TryGetStateAsync(string stateName, Can var conditionalResult = await this.TryGetStateFromStateProviderAsync(stateName, cancellationToken); if (conditionalResult.HasValue) { - stateChangeTracker.Add(stateName, StateMetadata.Create(conditionalResult.Value, StateChangeKind.None)); + stateChangeTracker.Add(stateName, StateMetadata.Create(conditionalResult.Value.Value, StateChangeKind.None, ttlExpireTime: conditionalResult.Value.TTLExpireTime)); + return new ConditionalValue(true, conditionalResult.Value.Value); } - return conditionalResult; + return new ConditionalValue(false, default); } public async Task SetStateAsync(string stateName, T value, CancellationToken cancellationToken) @@ -132,6 +175,7 @@ public async Task SetStateAsync(string stateName, T value, CancellationToken { var stateMetadata = stateChangeTracker[stateName]; stateMetadata.Value = value; + stateMetadata.TTLExpireTime = null; if (stateMetadata.ChangeKind == StateChangeKind.None || stateMetadata.ChangeKind == StateChangeKind.Remove) @@ -149,6 +193,36 @@ public async Task SetStateAsync(string stateName, T value, CancellationToken } } + 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(); @@ -171,6 +245,12 @@ public async Task TryRemoveStateAsync(string stateName, CancellationToken { var stateMetadata = stateChangeTracker[stateName]; + if (stateMetadata.TTLExpireTime.HasValue && stateMetadata.TTLExpireTime.Value <= DateTimeOffset.UtcNow) + { + stateChangeTracker.Remove(stateName); + return false; + } + switch (stateMetadata.ChangeKind) { case StateChangeKind.Remove: @@ -235,6 +315,24 @@ public async Task GetOrAddStateAsync(string stateName, T value, Cancellati 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, @@ -272,7 +370,7 @@ public async Task AddOrUpdateStateAsync( var conditionalResult = await this.TryGetStateFromStateProviderAsync(stateName, cancellationToken); if (conditionalResult.HasValue) { - var newValue = updateValueFactory.Invoke(stateName, conditionalResult.Value); + var newValue = updateValueFactory.Invoke(stateName, conditionalResult.Value.Value); stateChangeTracker.Add(stateName, StateMetadata.Create(newValue, StateChangeKind.Update)); return newValue; @@ -282,6 +380,54 @@ public async Task AddOrUpdateStateAsync( 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(); @@ -310,7 +456,7 @@ public async Task SaveStateAsync(CancellationToken cancellationToken = default) if (stateMetadata.ChangeKind != StateChangeKind.None) { stateChangeList.Add( - new ActorStateChange(stateName, stateMetadata.Type, stateMetadata.Value, stateMetadata.ChangeKind)); + new ActorStateChange(stateName, stateMetadata.Type, stateMetadata.Value, stateMetadata.ChangeKind, stateMetadata.TTLExpireTime)); if (stateMetadata.ChangeKind == StateChangeKind.Remove) { @@ -362,7 +508,7 @@ private bool IsStateMarkedForRemove(string stateName) return false; } - private Task> TryGetStateFromStateProviderAsync(string stateName, CancellationToken cancellationToken) + private Task>> TryGetStateFromStateProviderAsync(string stateName, CancellationToken cancellationToken) { EnsureStateProviderInitialized(); return this.actor.Host.StateProvider.TryLoadStateAsync(this.actorTypeName, this.actor.Id.ToString(), stateName, cancellationToken); @@ -392,11 +538,20 @@ private Dictionary GetContextualStateTracker() private sealed class StateMetadata { - private StateMetadata(object value, Type type, StateChangeKind changeKind) + 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; } @@ -405,11 +560,23 @@ private StateMetadata(object value, Type type, StateChangeKind changeKind) 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); diff --git a/src/Dapr.Actors/Runtime/ActorTestOptions.cs b/src/Dapr.Actors/Runtime/ActorTestOptions.cs index 1a1fa6f2e..47e73ce36 100644 --- a/src/Dapr.Actors/Runtime/ActorTestOptions.cs +++ b/src/Dapr.Actors/Runtime/ActorTestOptions.cs @@ -91,6 +91,11 @@ 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); diff --git a/src/Dapr.Actors/Runtime/ActorTimerManager.cs b/src/Dapr.Actors/Runtime/ActorTimerManager.cs index 1dc304fd1..784cf418e 100644 --- a/src/Dapr.Actors/Runtime/ActorTimerManager.cs +++ b/src/Dapr.Actors/Runtime/ActorTimerManager.cs @@ -27,6 +27,13 @@ public abstract class ActorTimerManager /// 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); + /// /// Unregisters the provided reminder with the runtime. /// diff --git a/src/Dapr.Actors/Runtime/ConditionalValue.cs b/src/Dapr.Actors/Runtime/ConditionalValue.cs index 1d2a197eb..ec4f3a5a6 100644 --- a/src/Dapr.Actors/Runtime/ConditionalValue.cs +++ b/src/Dapr.Actors/Runtime/ConditionalValue.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -42,4 +42,4 @@ public ConditionalValue(bool hasValue, TValue 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 ae86fb28b..e81308dbd 100644 --- a/src/Dapr.Actors/Runtime/DaprStateProvider.cs +++ b/src/Dapr.Actors/Runtime/DaprStateProvider.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ namespace Dapr.Actors.Runtime using System.Text.Json; using System.Threading; using System.Threading.Tasks; + using Dapr.Actors.Communication; /// /// State Provider to interact with Dapr runtime. @@ -43,27 +44,27 @@ public DaprStateProvider(IDaprInteractor daprInteractor, JsonSerializerOptions j this.daprInteractor = daprInteractor; } - public async Task> TryLoadStateAsync(string actorType, string actorId, string stateName, CancellationToken cancellationToken = default) + public async Task>> TryLoadStateAsync(string actorType, string actorId, string stateName, CancellationToken cancellationToken = default) { - var result = new ConditionalValue(false, default); - var stringResult = await this.daprInteractor.GetStateAsync(actorType, actorId, stateName, cancellationToken); + var result = new ConditionalValue>(false, default); + var response = await this.daprInteractor.GetStateAsync(actorType, actorId, stateName, cancellationToken); - if (stringResult.Length != 0) + if (response.Value.Length != 0 && (!response.TTLExpireTime.HasValue || response.TTLExpireTime.Value > DateTimeOffset.UtcNow)) { T typedResult; // perform default json de-serialization if custom serializer was not provided. if (this.actorStateSerializer != null) { - var byteResult = Convert.FromBase64String(stringResult.Trim('"')); + var byteResult = Convert.FromBase64String(response.Value.Trim('"')); typedResult = this.actorStateSerializer.Deserialize(byteResult); } else { - typedResult = JsonSerializer.Deserialize(stringResult, jsonSerializerOptions); + typedResult = JsonSerializer.Deserialize(response.Value, jsonSerializerOptions); } - result = new ConditionalValue(true, typedResult); + result = new ConditionalValue>(true, new ActorStateResponse(typedResult, response.TTLExpireTime)); } return result; @@ -71,8 +72,8 @@ public async Task> TryLoadStateAsync(string actorType, st public async Task ContainsStateAsync(string actorType, string actorId, string stateName, CancellationToken cancellationToken = default) { - var byteResult = await this.daprInteractor.GetStateAsync(actorType, actorId, stateName, cancellationToken); - return byteResult.Length != 0; + var result = await this.daprInteractor.GetStateAsync(actorType, actorId, stateName, cancellationToken); + return (result.Value.Length != 0 && (!result.TTLExpireTime.HasValue || result.TTLExpireTime.Value > DateTimeOffset.UtcNow)); } public async Task SaveStateAsync(string actorType, string actorId, IReadOnlyCollection stateChanges, CancellationToken cancellationToken = default) @@ -132,6 +133,15 @@ private async Task DoStateChangesTransactionallyAsync(string actorType, string a 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; diff --git a/src/Dapr.Actors/Runtime/DataContractStateSerializer.cs b/src/Dapr.Actors/Runtime/DataContractStateSerializer.cs deleted file mode 100644 index e30b21d48..000000000 --- a/src/Dapr.Actors/Runtime/DataContractStateSerializer.cs +++ /dev/null @@ -1,82 +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. -// ------------------------------------------------------------------------ - -namespace Dapr.Actors.Runtime -{ - using System; - using System.Collections.Concurrent; - using System.IO; - using System.Runtime.Serialization; - using System.Xml; - - /// - /// DataContract serializer for Actor state serialization/deserialziation. - /// This is the default state serializer used with Service Fabric Reliable Actors. - /// If there is user ask for the compatibility, this can be exposed by adding a compatibility option as an attribute on Actor type so that Service Fabric Reliable Actors state serialization behavior - /// can also be used using Dapr. - /// - internal class DataContractStateSerializer : IActorStateSerializer - { - private readonly ConcurrentDictionary actorStateSerializerCache; - - internal DataContractStateSerializer() - { - this.actorStateSerializerCache = new ConcurrentDictionary(); - } - - public byte[] Serialize(Type stateType, T state) - { - var serializer = this.actorStateSerializerCache.GetOrAdd( - stateType, - CreateDataContractSerializer); - - using var stream = new MemoryStream(); - using var writer = XmlDictionaryWriter.CreateBinaryWriter(stream); - serializer.WriteObject(writer, state); - writer.Flush(); - return stream.ToArray(); - } - - public T Deserialize(byte[] buffer) - { - if ((buffer == null) || (buffer.Length == 0)) - { - return default; - } - - var serializer = this.actorStateSerializerCache.GetOrAdd( - typeof(T), - CreateDataContractSerializer); - - using var stream = new MemoryStream(buffer); - using var reader = XmlDictionaryReader.CreateBinaryReader(stream, XmlDictionaryReaderQuotas.Max); - return (T)serializer.ReadObject(reader); - } - - private static DataContractSerializer CreateDataContractSerializer(Type actorStateType) - { - var dataContractSerializer = new DataContractSerializer( - actorStateType, - new DataContractSerializerSettings - { - MaxItemsInObjectGraph = int.MaxValue, - KnownTypes = new[] - { - typeof(ActorReference), - }, - }); - - return dataContractSerializer; - } - } -} diff --git a/src/Dapr.Actors/Runtime/DefaultActorTimerManager.cs b/src/Dapr.Actors/Runtime/DefaultActorTimerManager.cs index d3378c962..b42b432a1 100644 --- a/src/Dapr.Actors/Runtime/DefaultActorTimerManager.cs +++ b/src/Dapr.Actors/Runtime/DefaultActorTimerManager.cs @@ -14,6 +14,8 @@ using System; using System.Text.Json; using System.Threading.Tasks; +using System.IO; +using System.Text; namespace Dapr.Actors.Runtime { @@ -37,6 +39,18 @@ public override async Task RegisterReminderAsync(ActorReminder 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 responseStream = await this.interactor.GetReminderAsync(token.ActorType, token.ActorId.ToString(), token.Name); + var reminder = await DeserializeReminderAsync(responseStream, token); + return reminder; + } + public override async Task UnregisterReminderAsync(ActorReminderToken reminder) { if (reminder == null) @@ -77,5 +91,21 @@ private async ValueTask SerializeReminderAsync(ActorReminder reminder) reminder.Ttl); return await info.SerializeAsync(); } + + private 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; + } } } diff --git a/src/Dapr.Actors/Runtime/IActorStateManager.cs b/src/Dapr.Actors/Runtime/IActorStateManager.cs index df1eb3356..b85fa2a06 100644 --- a/src/Dapr.Actors/Runtime/IActorStateManager.cs +++ b/src/Dapr.Actors/Runtime/IActorStateManager.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ namespace Dapr.Actors.Runtime using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; + using Dapr.Actors.Communication; /// /// Represents an interface that exposes methods to manage state of an . @@ -45,6 +46,28 @@ public interface IActorStateManager /// Task AddStateAsync(string stateName, T value, 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. /// @@ -85,6 +108,26 @@ public interface IActorStateManager /// 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); + /// /// Removes an actor state with specified state name. /// @@ -121,6 +164,30 @@ public interface IActorStateManager /// 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 get an actor state with specified state name. /// @@ -188,6 +255,29 @@ public interface IActorStateManager /// 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); + /// /// 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. @@ -209,6 +299,28 @@ public interface IActorStateManager /// 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); + /// /// Clears all the cached actor states and any operation(s) performed on /// since last state save operation. diff --git a/src/Dapr.Actors/Runtime/IActorStateSerializer.cs b/src/Dapr.Actors/Runtime/IActorStateSerializer.cs index c6136c057..cff3b7c26 100644 --- a/src/Dapr.Actors/Runtime/IActorStateSerializer.cs +++ b/src/Dapr.Actors/Runtime/IActorStateSerializer.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/src/Dapr.Actors/Runtime/ReminderInfo.cs b/src/Dapr.Actors/Runtime/ReminderInfo.cs index 84e56bbc7..447cf607b 100644 --- a/src/Dapr.Actors/Runtime/ReminderInfo.cs +++ b/src/Dapr.Actors/Runtime/ReminderInfo.cs @@ -20,7 +20,7 @@ namespace Dapr.Actors.Runtime using System.Threading.Tasks; // represents the wire format used by Dapr to store reminder info with the runtime - internal struct ReminderInfo + internal class ReminderInfo { public ReminderInfo( byte[] data, @@ -49,13 +49,16 @@ public ReminderInfo( internal static async Task DeserializeAsync(Stream stream) { var json = await JsonSerializer.DeserializeAsync(stream); + if(json.ValueKind == JsonValueKind.Null) + { + return null; + } var dueTime = default(TimeSpan); var period = default(TimeSpan); var data = default(byte[]); int? repetition = null; TimeSpan? ttl = null; - if (json.TryGetProperty("dueTime", out var dueTimeProperty)) { var dueTimeString = dueTimeProperty.GetString(); diff --git a/src/Dapr.AspNetCore/CloudEventPropertyNames.cs b/src/Dapr.AspNetCore/CloudEventPropertyNames.cs new file mode 100644 index 000000000..87e496004 --- /dev/null +++ b/src/Dapr.AspNetCore/CloudEventPropertyNames.cs @@ -0,0 +1,9 @@ +namespace Dapr +{ + internal static class CloudEventPropertyNames + { + public const string Data = "data"; + public const string DataContentType = "datacontenttype"; + public const string DataBase64 = "data_base64"; + } +} diff --git a/src/Dapr.AspNetCore/CloudEventsMiddleware.cs b/src/Dapr.AspNetCore/CloudEventsMiddleware.cs index 24c89cfed..eac526c26 100644 --- a/src/Dapr.AspNetCore/CloudEventsMiddleware.cs +++ b/src/Dapr.AspNetCore/CloudEventsMiddleware.cs @@ -11,6 +11,9 @@ // limitations under the License. // ------------------------------------------------------------------------ +using System.Collections.Generic; +using System.Linq; + namespace Dapr { using System; @@ -27,6 +30,15 @@ namespace Dapr internal class CloudEventsMiddleware { private const string ContentType = "application/cloudevents+json"; + + // 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 = + { + CloudEventPropertyNames.DataContentType, CloudEventPropertyNames.Data, + CloudEventPropertyNames.DataBase64, "pubsubname", "traceparent" + }; + private readonly RequestDelegate next; private readonly CloudEventsMiddlewareOptions options; @@ -52,7 +64,7 @@ public Task InvokeAsync(HttpContext httpContext) // 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 (!this.MatchesContentType(httpContext, out var charSet)) + if (!MatchesContentType(httpContext, out var charSet)) { return this.next(httpContext); } @@ -69,7 +81,8 @@ private async Task ProcessBodyAsync(HttpContext httpContext, string charSet) } else { - using (var reader = new HttpRequestStreamReader(httpContext.Request.Body, Encoding.GetEncoding(charSet))) + using (var reader = + new HttpRequestStreamReader(httpContext.Request.Body, Encoding.GetEncoding(charSet))) { var text = await reader.ReadToEndAsync(); json = JsonSerializer.Deserialize(text); @@ -83,17 +96,43 @@ private async Task ProcessBodyAsync(HttpContext httpContext, string charSet) 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 - var isDataSet = json.TryGetProperty("data", out var data); - var isBinaryDataSet = json.TryGetProperty("data_base64", out var binaryData); + // 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; } - else if (isDataSet) + + if (isDataSet) { - contentType = this.GetDataContentType(json, out var isJson); + 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. @@ -109,8 +148,8 @@ private async Task ProcessBodyAsync(HttpContext httpContext, string charSet) { // Rehydrate body from contents of the string var text = data.GetString(); - using var writer = new HttpResponseStreamWriter(body, Encoding.UTF8); - writer.Write(text); + await using var writer = new HttpResponseStreamWriter(body, Encoding.UTF8); + await writer.WriteAsync(text); } body.Seek(0L, SeekOrigin.Begin); @@ -120,10 +159,10 @@ private async Task ProcessBodyAsync(HttpContext httpContext, string charSet) // 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 = binaryData.GetBytesFromBase64(); + var decodedBody = data.GetBytesFromBase64(); body = new MemoryStream(decodedBody); body.Seek(0L, SeekOrigin.Begin); - contentType = this.GetDataContentType(json, out _); + contentType = GetDataContentType(json, out _); } else { @@ -131,6 +170,8 @@ private async Task ProcessBodyAsync(HttpContext httpContext, string charSet) contentType = null; } + ForwardCloudEventPropertiesAsHeaders(httpContext, jsonPropNames); + originalBody = httpContext.Request.Body; originalContentType = httpContext.Request.ContentType; @@ -148,16 +189,57 @@ private async Task ProcessBodyAsync(HttpContext httpContext, string charSet) } } - private string GetDataContentType(JsonElement json, out bool isJson) + 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 (json.TryGetProperty("datacontenttype", out var dataContentType) && - dataContentType.ValueKind == JsonValueKind.String && - MediaTypeHeaderValue.TryParse(dataContentType.GetString(), out var parsed)) + + 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) || + 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 @@ -179,7 +261,7 @@ private string GetDataContentType(JsonElement json, out bool isJson) return contentType; } - private bool MatchesContentType(HttpContext httpContext, out string charSet) + private static bool MatchesContentType(HttpContext httpContext, out string charSet) { if (httpContext.Request.ContentType == null) { diff --git a/src/Dapr.AspNetCore/CloudEventsMiddlewareOptions.cs b/src/Dapr.AspNetCore/CloudEventsMiddlewareOptions.cs index 251a939a7..84e68adb5 100644 --- a/src/Dapr.AspNetCore/CloudEventsMiddlewareOptions.cs +++ b/src/Dapr.AspNetCore/CloudEventsMiddlewareOptions.cs @@ -29,9 +29,49 @@ public class CloudEventsMiddlewareOptions /// instead of the expected JSON-decoded value of Hello, "world!". /// /// - /// Setting this property to true restores the previous invalid behavior for compatiblity. + /// 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 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; } } } diff --git a/src/Dapr.AspNetCore/Dapr.AspNetCore.csproj b/src/Dapr.AspNetCore/Dapr.AspNetCore.csproj index c119b2d3e..12b512fbb 100644 --- a/src/Dapr.AspNetCore/Dapr.AspNetCore.csproj +++ b/src/Dapr.AspNetCore/Dapr.AspNetCore.csproj @@ -1,32 +1,17 @@  - - netcoreapp3.1 - - This package contains the reference assemblies for developing services using Dapr and AspNetCore. - - - - - - - - + diff --git a/src/Dapr.AspNetCore/DaprAuthenticationHandler.cs b/src/Dapr.AspNetCore/DaprAuthenticationHandler.cs index 822694c3b..dc21b5926 100644 --- a/src/Dapr.AspNetCore/DaprAuthenticationHandler.cs +++ b/src/Dapr.AspNetCore/DaprAuthenticationHandler.cs @@ -25,6 +25,14 @@ internal class DaprAuthenticationHandler : AuthenticationHandler options, + ILoggerFactory logger, + UrlEncoder encoder) : base(options, logger, encoder) + { + } +#else public DaprAuthenticationHandler( IOptionsMonitor options, ILoggerFactory logger, @@ -32,6 +40,7 @@ public DaprAuthenticationHandler( ISystemClock clock) : base(options, logger, encoder, clock) { } +#endif protected override Task HandleAuthenticateAsync() { diff --git a/src/Dapr.AspNetCore/DaprMvcBuilderExtensions.cs b/src/Dapr.AspNetCore/DaprMvcBuilderExtensions.cs index b775e61ae..6195b9c30 100644 --- a/src/Dapr.AspNetCore/DaprMvcBuilderExtensions.cs +++ b/src/Dapr.AspNetCore/DaprMvcBuilderExtensions.cs @@ -21,6 +21,7 @@ namespace Microsoft.Extensions.DependencyInjection using Dapr.Client; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ApplicationModels; + using Microsoft.Extensions.DependencyInjection.Extensions; /// /// Provides extension methods for . @@ -40,27 +41,19 @@ public static IMvcBuilder AddDapr(this IMvcBuilder builder, Action s.ImplementationType == typeof(DaprMvcMarkerService))) - { - return builder; - } - builder.Services.AddDaprClient(configureClient); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); + builder.Services.TryAddSingleton(); + builder.Services.Configure(options => { - options.ModelBinderProviders.Insert(0, new StateEntryModelBinderProvider()); + if (!options.ModelBinderProviders.Any(p => p is StateEntryModelBinderProvider)) + { + options.ModelBinderProviders.Insert(0, new StateEntryModelBinderProvider()); + } }); return builder; } - - private class DaprMvcMarkerService - { - } } } diff --git a/src/Dapr.AspNetCore/DaprServiceCollectionExtensions.cs b/src/Dapr.AspNetCore/DaprServiceCollectionExtensions.cs index 1da42243d..388015b80 100644 --- a/src/Dapr.AspNetCore/DaprServiceCollectionExtensions.cs +++ b/src/Dapr.AspNetCore/DaprServiceCollectionExtensions.cs @@ -31,34 +31,35 @@ public static class DaprServiceCollectionExtensions /// public static void AddDaprClient(this IServiceCollection services, Action configure = null) { - if (services is null) - { - throw new ArgumentNullException(nameof(services)); - } - - // This pattern prevents registering services multiple times in the case AddDaprClient is called - // by non-user-code. - if (services.Any(s => s.ImplementationType == typeof(DaprClientMarkerService))) - { - return; - } - - services.AddSingleton(); + ArgumentNullException.ThrowIfNull(services, nameof(services)); services.TryAddSingleton(_ => { var builder = new DaprClientBuilder(); - if (configure != null) - { - configure.Invoke(builder); - } + configure?.Invoke(builder); return builder.Build(); }); } - - private class DaprClientMarkerService + + /// + /// Adds Dapr client services to the provided . This does not include integration + /// with ASP.NET Core MVC. Use the AddDapr() extension method on IMvcBuilder to register MVC integration. + /// + /// The . + /// + public static void AddDaprClient(this IServiceCollection services, + Action configure) { + ArgumentNullException.ThrowIfNull(services, nameof(services)); + + services.TryAddSingleton(serviceProvider => + { + var builder = new DaprClientBuilder(); + configure?.Invoke(serviceProvider, builder); + + return builder.Build(); + }); } } } diff --git a/src/Dapr.Client/BulkStateItem.cs b/src/Dapr.Client/BulkStateItem.cs index fb717e1a8..5b30ddf21 100644 --- a/src/Dapr.Client/BulkStateItem.cs +++ b/src/Dapr.Client/BulkStateItem.cs @@ -49,4 +49,42 @@ public BulkStateItem(string key, string value, string etag) /// public string ETag { get; } } + + /// + /// Represents a state object returned from a bulk get state operation where the value has + /// been deserialized to the specified type. + /// + 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; + } + + /// + /// 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; } + } } diff --git a/src/Dapr.Client/CryptographyEnums.cs b/src/Dapr.Client/CryptographyEnums.cs new file mode 100644 index 000000000..f5955b389 --- /dev/null +++ b/src/Dapr.Client/CryptographyEnums.cs @@ -0,0 +1,76 @@ +// ------------------------------------------------------------------------ +// 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.Runtime.Serialization; + +namespace Dapr.Client +{ + /// + /// 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 + }; + + /// + /// 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.Client/CryptographyOptions.cs b/src/Dapr.Client/CryptographyOptions.cs new file mode 100644 index 000000000..ae94a8f2f --- /dev/null +++ b/src/Dapr.Client/CryptographyOptions.cs @@ -0,0 +1,80 @@ +#nullable enable +using System; + +namespace Dapr.Client +{ + /// + /// A collection of options used to configure how encryption cryptographic operations are performed. + /// + public class EncryptionOptions + { + /// + /// Creates a new instance of the . + /// + /// + public EncryptionOptions(KeyWrapAlgorithm keyWrapAlgorithm) + { + KeyWrapAlgorithm = keyWrapAlgorithm; + } + + /// + /// 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; + } + + /// + /// 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 + { + get => streamingBlockSizeInBytes; + set + { + if (value <= 0) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + + streamingBlockSizeInBytes = value; + } + } + } +} diff --git a/src/Dapr.Client/Dapr.Client.csproj b/src/Dapr.Client/Dapr.Client.csproj index 7c9ff8160..7d74a7bb3 100644 --- a/src/Dapr.Client/Dapr.Client.csproj +++ b/src/Dapr.Client/Dapr.Client.csproj @@ -1,34 +1,20 @@  - - netcoreapp3.1 - - - - - - - - - - - - - This package contains the reference assemblies for developing services using Dapr. + - - - - - + + + + - + - + + - + diff --git a/src/Dapr.Client/DaprApiException.cs b/src/Dapr.Client/DaprApiException.cs index e7af8947c..75fc2cf7f 100644 --- a/src/Dapr.Client/DaprApiException.cs +++ b/src/Dapr.Client/DaprApiException.cs @@ -92,6 +92,9 @@ public DaprApiException(string message, Exception inner, string errorCode, bool /// /// 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) { @@ -115,6 +118,9 @@ protected DaprApiException(SerializationInfo info, StreamingContext context) 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); diff --git a/src/Dapr.Client/DaprClient.cs b/src/Dapr.Client/DaprClient.cs index 56b16c459..9f107578f 100644 --- a/src/Dapr.Client/DaprClient.cs +++ b/src/Dapr.Client/DaprClient.cs @@ -3,7 +3,7 @@ // 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 +// 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. @@ -13,6 +13,7 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; @@ -57,12 +58,13 @@ public abstract class DaprClient : IDisposable /// 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 rewitten to follow the format of a Dapr service invocation request. + /// 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. + /// 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. @@ -79,7 +81,8 @@ public static HttpClient CreateInvokeHttpClient(string appId = null, string dapr var handler = new InvocationHandler() { InnerHandler = new HttpClientHandler(), - DaprApiToken = daprApiToken + DaprApiToken = daprApiToken, + DefaultAppId = appId, }; if (daprEndpoint is string) @@ -209,7 +212,7 @@ public abstract Task PublishEventAsync( string topicName, Dictionary metadata, CancellationToken cancellationToken = default); - + /// /// // Bulk Publishes multiple events to the specified topic. /// @@ -292,7 +295,7 @@ public abstract Task InvokeBindingAsync( /// /// Creates an that can be used to perform service invocation for the - /// application idenfied by and invokes the method specified by + /// application identified by and invokes the method specified by /// with the POST HTTP method. /// /// The Dapr application id to invoke the method on. @@ -305,7 +308,21 @@ public HttpRequestMessage CreateInvokeMethodRequest(string appId, string methodN /// /// Creates an that can be used to perform service invocation for the - /// application idenfied by and invokes the method specified by + /// 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. @@ -316,7 +333,20 @@ public HttpRequestMessage CreateInvokeMethodRequest(string appId, string methodN /// /// Creates an that can be used to perform service invocation for the - /// application idenfied by and invokes the method specified by + /// 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. @@ -326,12 +356,12 @@ public HttpRequestMessage CreateInvokeMethodRequest(string appId, string methodN /// An for use with SendInvokeMethodRequestAsync. public HttpRequestMessage CreateInvokeMethodRequest(string appId, string methodName, TRequest data) { - return CreateInvokeMethodRequest(HttpMethod.Post, appId, methodName, data); + return CreateInvokeMethodRequest(HttpMethod.Post, appId, methodName, new List>(), data); } - + /// /// Creates an that can be used to perform service invocation for the - /// application idenfied by and invokes the method specified by + /// application identified by and invokes the method specified by /// with the HTTP method specified by and a JSON serialized request body specified by /// . /// @@ -340,9 +370,10 @@ public HttpRequestMessage CreateInvokeMethodRequest(string appId, stri /// 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, TRequest data); - + 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 @@ -417,6 +448,30 @@ public HttpRequestMessage CreateInvokeMethodRequest(string appId, stri /// A that will return the value when the operation has completed. public abstract Task InvokeMethodWithResponseAsync(HttpRequestMessage request, CancellationToken cancellationToken = default); +#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. @@ -444,7 +499,7 @@ public HttpRequestMessage CreateInvokeMethodRequest(string appId, stri public abstract Task InvokeMethodAsync(HttpRequestMessage request, CancellationToken cancellationToken = default); /// - /// Perform service invocation for the application idenfied by and invokes the method + /// 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. /// @@ -462,7 +517,7 @@ public Task InvokeMethodAsync( } /// - /// Perform service invocation for the application idenfied by and invokes the method + /// 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. /// @@ -482,7 +537,7 @@ public Task InvokeMethodAsync( } /// - /// Perform service invocation for the application idenfied by and invokes the method + /// 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. @@ -504,7 +559,7 @@ public Task InvokeMethodAsync( } /// - /// Perform service invocation for the application idenfied by and invokes the method + /// 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. @@ -523,12 +578,12 @@ public Task InvokeMethodAsync( TRequest data, CancellationToken cancellationToken = default) { - var request = CreateInvokeMethodRequest(httpMethod, appId, methodName, data); + var request = CreateInvokeMethodRequest(httpMethod, appId, methodName, new List>(), data); return InvokeMethodAsync(request, cancellationToken); } /// - /// Perform service invocation for the application idenfied by and invokes the method + /// 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 ; @@ -549,7 +604,7 @@ public Task InvokeMethodAsync( } /// - /// Perform service invocation for the application idenfied by and invokes the method + /// 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 ; @@ -572,7 +627,7 @@ public Task InvokeMethodAsync( } /// - /// Perform service invocation for the application idenfied by and invokes the method + /// 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 ; @@ -596,7 +651,7 @@ public Task InvokeMethodAsync( } /// - /// Perform service invocation for the application idenfied by and invokes the method + /// 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 ; @@ -617,12 +672,12 @@ public Task InvokeMethodAsync( TRequest data, CancellationToken cancellationToken = default) { - var request = CreateInvokeMethodRequest(httpMethod, appId, methodName, data); + var request = CreateInvokeMethodRequest(httpMethod, appId, methodName, new List>(), data); return InvokeMethodAsync(request, cancellationToken); } /// - /// Perform service invocation using gRPC semantics for the application idenfied by and invokes the method + /// 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. /// @@ -636,7 +691,7 @@ public abstract Task InvokeMethodGrpcAsync( CancellationToken cancellationToken = default); /// - /// Perform service invocation using gRPC semantics for the application idenfied by and invokes the method + /// 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. /// @@ -654,7 +709,7 @@ public abstract Task InvokeMethodGrpcAsync( where TRequest : IMessage; /// - /// Perform service invocation using gRPC semantics for the application idenfied by and invokes the method + /// 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. @@ -671,7 +726,7 @@ public abstract Task InvokeMethodGrpcAsync( where TResponse : IMessage, new(); /// - /// Perform service invocation using gRPC semantics for the application idenfied by and invokes the method + /// 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. @@ -714,6 +769,20 @@ public abstract Task InvokeMethodGrpcAsync( /// 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 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); + /// /// Saves a list of to the Dapr state store. /// @@ -907,7 +976,6 @@ public abstract Task>> GetBulkSecr /// Optional metadata that will be sent to the configuration store being queried. /// 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 GetConfiguration( string storeName, IReadOnlyList keys, @@ -922,7 +990,6 @@ public abstract Task GetConfiguration( /// 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. - [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 SubscribeConfiguration( string storeName, IReadOnlyList keys, @@ -936,148 +1003,324 @@ public abstract Task SubscribeConfiguration( /// The Id of the subscription that should no longer be watched. /// A that can be used to cancel the operation. /// - [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 UnsubscribeConfiguration( string storeName, string id, 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); - + #region Cryptography /// - /// Attempt to unlock the given resourceId with response indicating success. + /// Encrypts an array of bytes using the Dapr Cryptography encryption functionality. /// - /// 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, + /// 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); /// - /// Attempt to start the given workflow with response indicating success. + /// Encrypts a stream using the Dapr Cryptography encryption functionality. /// - /// Identifier of the specific run. - /// The component to interface with. - /// Name of the workflow to run. - /// The list of options that are potentially needed to start a workflow. - /// The input input for the given workflow. - /// 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 StartWorkflowAsync( - string instanceId, - string workflowComponent, - string workflowName, - Object input, - IReadOnlyDictionary workflowOptions = default, - CancellationToken cancellationToken = default); + /// 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); /// - /// Attempt to get information about the given workflow. + /// Decrypts the specified ciphertext bytes using the Dapr Cryptography encryption functionality. /// - /// The unique ID of the target workflow instance. - /// The component to interface with. - /// 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 GetWorkflowAsync( - string instanceId, - string workflowComponent, + /// 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); /// - /// Attempt to get terminate the given workflow. + /// Decrypts the specified ciphertext bytes using the Dapr Cryptography encryption functionality. /// - /// The unique ID of the target workflow instance. - /// The component to interface with. - /// A that can be used to cancel the operation. - /// A that will complete when the terminate operation has been scheduled. If the wrapped value is true the operation suceeded. - [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 TerminateWorkflowAsync( - string instanceId, - string workflowComponent, - CancellationToken cancellationToken = default); + /// 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); /// - /// Attempt to raise an event the given workflow with response indicating success. + /// Decrypts the specified stream of ciphertext using the Dapr Cryptography encryption functionality. /// - /// Identifier of the specific run. - /// The component to interface with. - /// Name of the event to raise. - /// The JSON-serializable event payload to include in the raised event. - /// A that can be used to cancel the operation. - /// A that will complete when the raise event operation has been scheduled. If the wrapped value is true the operation suceeded. - [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 RaiseWorkflowEventAsync( - string instanceId, - string workflowComponent, - string eventName, - object eventData = null, - CancellationToken cancellationToken = default); - + /// 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); /// - /// Pauses the specified workflow instance. + /// Decrypts the specified stream of ciphertext using the Dapr Cryptography encryption functionality. /// - /// The unique ID of the target workflow instance. - /// The component to interface with. - /// A that can be used to cancel the operation. - /// A that will complete when the pause operation has been scheduled. If the wrapped value is true the operation suceeded. - [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 PauseWorkflowAsync( - string instanceId, - string workflowComponent, - CancellationToken cancellationToken = default); - + /// 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 + + #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); + + ///// + ///// 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); + + ///// + ///// 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); + + ///// + ///// 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); + + ///// + ///// 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. + ///// 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); + + ///// + ///// 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 /// - /// Resumes a paused workflow instance. + /// Attempt to lock the given resourceId with response indicating success. /// - /// The unique ID of the target workflow instance. - /// The component to interface with. + /// 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 that will complete when the resume operation has been scheduled. If the wrapped value is true the operation suceeded. + /// 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 ResumeWorkflowAsync( - string instanceId, - string workflowComponent, + public abstract Task Lock( + string storeName, + string resourceId, + string lockOwner, + Int32 expiryInSeconds, CancellationToken cancellationToken = default); + /// - /// Delete all state associated with the specified workflow instance. The workflow must be in a non-running state to be purged. + /// Attempt to unlock the given resourceId with response indicating success. /// - /// The unique ID of the target workflow instance. - /// The component to interface with. + /// 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 that will complete when the purge operation has been scheduled. If the wrapped value is true the operation suceeded. + /// 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 PurgeWorkflowAsync( - string instanceId, - string workflowComponent, + public abstract Task Unlock( + string storeName, + string resourceId, + string lockOwner, CancellationToken cancellationToken = default); - + /// public void Dispose() { diff --git a/src/Dapr.Client/DaprClientBuilder.cs b/src/Dapr.Client/DaprClientBuilder.cs index 5b484e208..50a4979d1 100644 --- a/src/Dapr.Client/DaprClientBuilder.cs +++ b/src/Dapr.Client/DaprClientBuilder.cs @@ -57,14 +57,16 @@ public DaprClientBuilder() // 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 - /// http://127.0.0.1:DAPR_HTTP_PORT where DAPR_HTTP_PORT represents the value of the - /// DAPR_HTTP_PORT environment variable. + /// 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) @@ -135,6 +137,17 @@ public DaprClientBuilder UseDaprApiToken(string 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. /// @@ -161,9 +174,16 @@ public DaprClient Build() 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); } } diff --git a/src/Dapr.Client/DaprClientGrpc.cs b/src/Dapr.Client/DaprClientGrpc.cs index 61a73d920..c70aef77b 100644 --- a/src/Dapr.Client/DaprClientGrpc.cs +++ b/src/Dapr.Client/DaprClientGrpc.cs @@ -3,7 +3,7 @@ // 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 +// 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. @@ -14,10 +14,14 @@ namespace Dapr.Client { using System; + using System.Buffers; using System.Collections.Generic; + using System.IO; using System.Linq; using System.Net.Http; using System.Net.Http.Json; + using System.Runtime.CompilerServices; + using System.Runtime.InteropServices; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -341,7 +345,32 @@ public override async Task InvokeBindingAsync(BindingRequest re #region InvokeMethod Apis + /// + /// Creates an that can be used to perform service invocation for the + /// application identified by and invokes the method specified by + /// with the HTTP method specified by . + /// + /// The to use for the invocation request. + /// The Dapr application id to invoke the method on. + /// The name of the method to invoke. + /// An for use with SendInvokeMethodRequestAsync. public override HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMethod, string appId, string methodName) + { + return CreateInvokeMethodRequest(httpMethod, appId, methodName, new List>()); + } + + /// + /// Creates an that can be used to perform service invocation for the + /// application identified by and invokes the method specified by + /// with the HTTP method specified by . + /// + /// The to use for the invocation request. + /// The Dapr application id to invoke the method on. + /// The name of the method to invoke. + /// A collection of key/value pairs to populate the query string from. + /// An for use with SendInvokeMethodRequestAsync. + public override HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMethod, string appId, string methodName, + IReadOnlyCollection> queryStringParameters) { ArgumentVerifier.ThrowIfNull(httpMethod, nameof(httpMethod)); ArgumentVerifier.ThrowIfNullOrEmpty(appId, nameof(appId)); @@ -352,14 +381,11 @@ public override HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMeth // // This approach avoids some common pitfalls that could lead to undesired encoding. var path = $"/v1.0/invoke/{appId}/method/{methodName.TrimStart('/')}"; - var request = new HttpRequestMessage(httpMethod, new Uri(this.httpEndpoint, path)) - { - Properties = - { - { AppIdKey, appId }, - { MethodNameKey, methodName }, - } - }; + var requestUri = new Uri(this.httpEndpoint, path).AddQueryParameters(queryStringParameters); + var request = new HttpRequestMessage(httpMethod, requestUri); + + request.Options.Set(new HttpRequestOptionsKey(AppIdKey), appId); + request.Options.Set(new HttpRequestOptionsKey(MethodNameKey), methodName); if (this.apiTokenHeader is not null) { @@ -369,13 +395,27 @@ public override HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMeth return request; } - public override HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMethod, string appId, string methodName, 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 override HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMethod, string appId, string methodName, + IReadOnlyCollection> queryStringParameters, TRequest data) { ArgumentVerifier.ThrowIfNull(httpMethod, nameof(httpMethod)); ArgumentVerifier.ThrowIfNullOrEmpty(appId, nameof(appId)); ArgumentVerifier.ThrowIfNull(methodName, nameof(methodName)); - var request = CreateInvokeMethodRequest(httpMethod, appId, methodName); + var request = CreateInvokeMethodRequest(httpMethod, appId, methodName, queryStringParameters); request.Content = JsonContent.Create(data, options: this.JsonSerializerOptions); return request; } @@ -399,8 +439,8 @@ public override async Task InvokeMethodWithResponseAsync(Ht { // Our code path for creating requests places these keys in the request properties. We don't want to fail // if they are not present. - request.Properties.TryGetValue(AppIdKey, out var appId); - request.Properties.TryGetValue(MethodNameKey, out var methodName); + request.Options.TryGetValue(new HttpRequestOptionsKey(AppIdKey), out var appId); + request.Options.TryGetValue(new HttpRequestOptionsKey(MethodNameKey), out var methodName); throw new InvocationException( appId: appId as string, @@ -410,6 +450,31 @@ public override async Task InvokeMethodWithResponseAsync(Ht } } + /// + /// + /// Creates an that can be used to perform Dapr service invocation using + /// objects. + /// + /// + /// The client will read the property, and + /// interpret the hostname as the destination app-id. The + /// property will be replaced with a new URI with the authority section replaced by the instance's value + /// and the path portion of the URI rewritten to follow the format of a Dapr service invocation request. + /// + /// + /// + /// An optional app-id. If specified, the app-id will be configured as the value of + /// so that relative URIs can be used. It is mandatory to set this parameter if your app-id contains at least one upper letter. + /// If some requests use absolute URL with an app-id which contains at least one upper letter, it will not work, the workaround is to create one HttpClient for each app-id with the app-ip parameter set. + /// + /// An that can be used to perform service invocation requests. + /// + /// +#nullable enable + public override HttpClient CreateInvokableHttpClient(string? appId = null) => + DaprClient.CreateInvokeHttpClient(appId, this.httpEndpoint?.AbsoluteUri, this.apiTokenHeader?.Value); + #nullable disable + public async override Task InvokeMethodAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) { ArgumentVerifier.ThrowIfNull(request, nameof(request)); @@ -423,8 +488,8 @@ public async override Task InvokeMethodAsync(HttpRequestMessage request, Cancell { // Our code path for creating requests places these keys in the request properties. We don't want to fail // if they are not present. - request.Properties.TryGetValue(AppIdKey, out var appId); - request.Properties.TryGetValue(MethodNameKey, out var methodName); + request.Options.TryGetValue(new HttpRequestOptionsKey(AppIdKey), out var appId); + request.Options.TryGetValue(new HttpRequestOptionsKey(MethodNameKey), out var methodName); throw new InvocationException( appId: appId as string, @@ -447,8 +512,8 @@ public async override Task InvokeMethodAsync(HttpRequestMe { // Our code path for creating requests places these keys in the request properties. We don't want to fail // if they are not present. - request.Properties.TryGetValue(AppIdKey, out var appId); - request.Properties.TryGetValue(MethodNameKey, out var methodName); + request.Options.TryGetValue(new HttpRequestOptionsKey(AppIdKey), out var appId); + request.Options.TryGetValue(new HttpRequestOptionsKey(MethodNameKey), out var methodName); throw new InvocationException( appId: appId as string, @@ -465,8 +530,8 @@ public async override Task InvokeMethodAsync(HttpRequestMe { // Our code path for creating requests places these keys in the request properties. We don't want to fail // if they are not present. - request.Properties.TryGetValue(AppIdKey, out var appId); - request.Properties.TryGetValue(MethodNameKey, out var methodName); + request.Options.TryGetValue(new HttpRequestOptionsKey(AppIdKey), out var appId); + request.Options.TryGetValue(new HttpRequestOptionsKey(MethodNameKey), out var methodName); throw new InvocationException( appId: appId as string, @@ -476,8 +541,8 @@ public async override Task InvokeMethodAsync(HttpRequestMe } catch (JsonException ex) { - request.Properties.TryGetValue(AppIdKey, out var appId); - request.Properties.TryGetValue(MethodNameKey, out var methodName); + request.Options.TryGetValue(new HttpRequestOptionsKey(AppIdKey), out var appId); + request.Options.TryGetValue(new HttpRequestOptionsKey(MethodNameKey), out var methodName); throw new InvocationException( appId: appId as string, @@ -601,7 +666,50 @@ public override async Task InvokeMethodGrpcAsync #region State Apis + /// public override async Task> GetBulkStateAsync(string storeName, IReadOnlyList keys, int? parallelism, IReadOnlyDictionary metadata = default, CancellationToken cancellationToken = default) + { + var rawBulkState = await GetBulkStateRawAsync(storeName, keys, parallelism, metadata, cancellationToken); + + var bulkResponse = new List(); + foreach (var item in rawBulkState) + { + bulkResponse.Add(new BulkStateItem(item.Key, item.Value.ToStringUtf8(), item.Etag)); + } + + return bulkResponse; + } + + /// + public override async Task>> GetBulkStateAsync( + string storeName, + IReadOnlyList keys, + int? parallelism, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + var rawBulkState = await GetBulkStateRawAsync(storeName, keys, parallelism, metadata, cancellationToken); + + var bulkResponse = new List>(); + foreach (var item in rawBulkState) + { + var deserializedValue = TypeConverters.FromJsonByteString(item.Value, this.JsonSerializerOptions); + bulkResponse.Add(new BulkStateItem(item.Key, deserializedValue, item.Etag)); + } + + return bulkResponse; + } + + /// + /// Retrieves the bulk state data, but rather than deserializing the values, leaves the specific handling + /// to the public callers of this method to avoid duplicate deserialization. + /// + private async Task> GetBulkStateRawAsync( + string storeName, + IReadOnlyList keys, + int? parallelism, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) { ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); if (keys.Count == 0) @@ -609,7 +717,7 @@ public override async Task> GetBulkStateAsync(strin var envelope = new Autogenerated.GetBulkStateRequest() { - StoreName = storeName, + StoreName = storeName, Parallelism = parallelism ?? default }; @@ -632,18 +740,20 @@ public override async Task> GetBulkStateAsync(strin } catch (RpcException ex) { - throw new DaprException("State operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + throw new DaprException( + "State operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", + ex); } - var bulkResponse = new List(); + var bulkResponse = new List<(string Key, string Etag, ByteString Value)>(); foreach (var item in response.Items) { - bulkResponse.Add(new BulkStateItem(item.Key, item.Data.ToStringUtf8(), item.Etag)); + bulkResponse.Add((item.Key, item.Etag, item.Data)); } return bulkResponse; } - + /// public override async Task GetStateAsync( string storeName, @@ -1286,7 +1396,6 @@ public async override Task>> GetBu #region Configuration API /// - [Obsolete] public async override Task GetConfiguration( string storeName, IReadOnlyList keys, @@ -1330,7 +1439,6 @@ public async override Task GetConfiguration( } /// - [Obsolete] public override Task SubscribeConfiguration( string storeName, IReadOnlyList keys, @@ -1361,7 +1469,6 @@ public override Task SubscribeConfiguration( return Task.FromResult(new SubscribeConfigurationResponse(new DaprSubscribeConfigurationSource(client.SubscribeConfiguration(request, options)))); } - [Obsolete] public override async Task UnsubscribeConfiguration( string storeName, string id, @@ -1380,342 +1487,579 @@ public override async Task UnsubscribeConfigur var resp = await client.UnsubscribeConfigurationAsync(request, options); return new UnsubscribeConfigurationResponse(resp.Ok, resp.Message); } + #endregion - #region Distributed Lock API - /// - [Obsolete] - public async override Task Lock( - string storeName, - string resourceId, - string lockOwner, - Int32 expiryInSeconds, + #region Cryptography + + /// + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public override async Task> EncryptAsync(string vaultResourceName, + ReadOnlyMemory plaintextBytes, string keyName, EncryptionOptions encryptionOptions, CancellationToken cancellationToken = default) { - ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); - ArgumentVerifier.ThrowIfNullOrEmpty(resourceId, nameof(resourceId)); - ArgumentVerifier.ThrowIfNullOrEmpty(lockOwner, nameof(lockOwner)); - - if (expiryInSeconds == 0 || expiryInSeconds < 0) + if (MemoryMarshal.TryGetArray(plaintextBytes, out var plaintextSegment) && plaintextSegment.Array != null) { - throw new ArgumentException("The value cannot be zero or less than zero: " + expiryInSeconds); - } - - var request = new Autogenerated.TryLockRequest() - { - StoreName = storeName, - ResourceId = resourceId, - LockOwner = lockOwner, - ExpiryInSeconds = expiryInSeconds - }; + var encryptionResult = await EncryptAsync(vaultResourceName, new MemoryStream(plaintextSegment.Array), keyName, encryptionOptions, + cancellationToken); + + var bufferedResult = new ArrayBufferWriter(); - try - { - var options = CreateCallOptions(headers: null, cancellationToken); - var response = await client.TryLockAlpha1Async(request, options); - return new TryLockResponse() + await foreach (var item in encryptionResult.WithCancellation(cancellationToken)) { - StoreName = storeName, - ResourceId = resourceId, - LockOwner = lockOwner, - Success = response.Success - }; - } - catch (RpcException ex) - { - throw new DaprException("Lock operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + bufferedResult.Write(item.Span); + } + + return bufferedResult.WrittenMemory; } + + throw new ArgumentException("The input instance doesn't have a valid underlying data store.", nameof(plaintextBytes)); } - /// - [Obsolete] - public async override Task Unlock( - string storeName, - string resourceId, - string lockOwner, - CancellationToken cancellationToken = default) + /// + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public override async Task>> EncryptAsync(string vaultResourceName, Stream plaintextStream, + string keyName, EncryptionOptions encryptionOptions, CancellationToken cancellationToken = default) { - ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); - ArgumentVerifier.ThrowIfNullOrEmpty(resourceId, nameof(resourceId)); - ArgumentVerifier.ThrowIfNullOrEmpty(lockOwner, nameof(lockOwner)); + ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); + ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); + ArgumentVerifier.ThrowIfNull(plaintextStream, nameof(plaintextStream)); + ArgumentVerifier.ThrowIfNull(encryptionOptions, nameof(encryptionOptions)); - var request = new Autogenerated.UnlockRequest() + var shouldOmitDecryptionKeyName = string.IsNullOrWhiteSpace(encryptionOptions.DecryptionKeyName); //Whitespace isn't likely a valid key name either + + var encryptRequestOptions = new Autogenerated.EncryptRequestOptions { - StoreName = storeName, - ResourceId = resourceId, - LockOwner = lockOwner + ComponentName = vaultResourceName, + DataEncryptionCipher = encryptionOptions.EncryptionCipher.GetValueFromEnumMember(), + KeyName = keyName, + KeyWrapAlgorithm = encryptionOptions.KeyWrapAlgorithm.GetValueFromEnumMember(), + OmitDecryptionKeyName = shouldOmitDecryptionKeyName }; - var options = CreateCallOptions(headers: null, cancellationToken); - Autogenerated.UnlockResponse response = new Autogenerated.UnlockResponse(); - try + if (!shouldOmitDecryptionKeyName) { - response = await client.UnlockAlpha1Async(request, options); - } - catch (RpcException ex) - { - throw new DaprException("Lock operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + ArgumentVerifier.ThrowIfNullOrEmpty(encryptionOptions.DecryptionKeyName, nameof(encryptionOptions.DecryptionKeyName)); + encryptRequestOptions.DecryptionKeyName = encryptRequestOptions.DecryptionKeyName; } - return new UnlockResponse(GetUnLockStatus(response.Status)); - } - - #endregion + var options = CreateCallOptions(headers: null, cancellationToken); + var duplexStream = client.EncryptAlpha1(options); + //Run both operations at the same time, but return the output of the streaming values coming from the operation + var receiveResult = Task.FromResult(RetrieveEncryptedStreamAsync(duplexStream, cancellationToken)); + return await Task.WhenAll( + //Stream the plaintext data to the sidecar in chunks + SendPlaintextStreamAsync(plaintextStream, encryptionOptions.StreamingBlockSizeInBytes, + duplexStream, encryptRequestOptions, cancellationToken), + //At the same time, retrieve the encrypted response from the sidecar + receiveResult).ContinueWith(_ => receiveResult.Result, cancellationToken); + } - #region Workflow API - /// - [Obsolete] - public async override Task StartWorkflowAsync( - string instanceId, - string workflowComponent, - string workflowName, - Object input, - IReadOnlyDictionary workflowOptions = default, - CancellationToken cancellationToken = default) + /// + /// Sends the plaintext bytes in chunks to the sidecar to be encrypted. + /// + private async Task SendPlaintextStreamAsync(Stream plaintextStream, + int streamingBlockSizeInBytes, + AsyncDuplexStreamingCall duplexStream, + Autogenerated.EncryptRequestOptions encryptRequestOptions, + CancellationToken cancellationToken) { - ArgumentVerifier.ThrowIfNullOrEmpty(instanceId, nameof(instanceId)); - ArgumentVerifier.ThrowIfNullOrEmpty(workflowComponent, nameof(workflowComponent)); - ArgumentVerifier.ThrowIfNullOrEmpty(workflowName, nameof(workflowName)); - ArgumentVerifier.ThrowIfNull(input, nameof(input)); + //Start with passing the metadata about the encryption request itself in the first message + await duplexStream.RequestStream.WriteAsync( + new Autogenerated.EncryptRequest {Options = encryptRequestOptions}, cancellationToken); - // Serialize json data. Converts input object to bytes and then bytestring inside the request. - var jsonUtf8Bytes = JsonSerializer.SerializeToUtf8Bytes(input); - - var request = new Autogenerated.StartWorkflowRequest() + //Send the plaintext bytes in blocks in subsequent messages + await using (var bufferedStream = new BufferedStream(plaintextStream, streamingBlockSizeInBytes)) { - InstanceId = instanceId, - WorkflowComponent = workflowComponent, - WorkflowName = workflowName, - Input = ByteString.CopyFrom(jsonUtf8Bytes), - }; + var buffer = new byte[streamingBlockSizeInBytes]; + int bytesRead; + ulong sequenceNumber = 0; - if (workflowOptions?.Count > 0) - { - foreach (var item in workflowOptions) + while ((bytesRead = + await bufferedStream.ReadAsync(buffer.AsMemory(0, streamingBlockSizeInBytes), cancellationToken)) != + 0) { - request.Options[item.Key] = item.Value; + 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++; } } - try - { - var options = CreateCallOptions(headers: null, cancellationToken); - var response = await client.StartWorkflowAlpha1Async(request, options); - return new StartWorkflowResponse(response.InstanceId); - - } - catch (RpcException ex) - { - throw new DaprException("Start Workflow operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); - } + //Send the completion message + await duplexStream.RequestStream.CompleteAsync(); } - /// - [Obsolete] - public async override Task GetWorkflowAsync( - string instanceId, - string workflowComponent, - CancellationToken cancellationToken = default) + /// + /// Retrieves the encrypted bytes from the encryption operation on the sidecar and returns as an enumerable stream. + /// + private async IAsyncEnumerable> RetrieveEncryptedStreamAsync(AsyncDuplexStreamingCall duplexStream, [EnumeratorCancellation] CancellationToken cancellationToken) { - ArgumentVerifier.ThrowIfNullOrEmpty(instanceId, nameof(instanceId)); - ArgumentVerifier.ThrowIfNullOrEmpty(workflowComponent, nameof(workflowComponent)); - - var request = new Autogenerated.GetWorkflowRequest() - { - InstanceId = instanceId, - WorkflowComponent = workflowComponent - }; - - try + await foreach (var encryptResponse in duplexStream.ResponseStream.ReadAllAsync(cancellationToken) + .ConfigureAwait(false)) { - var options = CreateCallOptions(headers: null, cancellationToken); - var response = await client.GetWorkflowAlpha1Async(request, options); - if (response == null) - { - throw new DaprException("Get workflow operation failed: the object response is null"); - } - if (response.CreatedAt == null) - { - response.CreatedAt = new Timestamp(); - throw new DaprException("Get workflow operation failed: CreatedAt object response is null"); - } - if (response.LastUpdatedAt == null) - { - response.LastUpdatedAt = response.CreatedAt; - } - return new GetWorkflowResponse(response.InstanceId, - response.WorkflowName, - response.CreatedAt.ToDateTime(), - response.LastUpdatedAt.ToDateTime(), - response.RuntimeStatus, - response.Properties); - } - catch (RpcException ex) - { - throw new DaprException("Get workflow operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + yield return encryptResponse.Payload.Data.Memory; } - } - - - /// - [Obsolete] - public async override Task TerminateWorkflowAsync( - string instanceId, - string workflowComponent, - CancellationToken cancellationToken = default) + + /// + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public override async Task>> DecryptAsync(string vaultResourceName, Stream ciphertextStream, string keyName, + DecryptionOptions decryptionOptions, CancellationToken cancellationToken = default) { - ArgumentVerifier.ThrowIfNullOrEmpty(instanceId, nameof(instanceId)); - ArgumentVerifier.ThrowIfNullOrEmpty(workflowComponent, nameof(workflowComponent)); + ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); + ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); + ArgumentVerifier.ThrowIfNull(ciphertextStream, nameof(ciphertextStream)); + ArgumentVerifier.ThrowIfNull(decryptionOptions, nameof(decryptionOptions)); - var request = new Autogenerated.TerminateWorkflowRequest() + var decryptRequestOptions = new Autogenerated.DecryptRequestOptions { - InstanceId = instanceId, - WorkflowComponent = workflowComponent + ComponentName = vaultResourceName, + KeyName = keyName }; var options = CreateCallOptions(headers: null, cancellationToken); + var duplexStream = client.DecryptAlpha1(options); - try + //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(t => receiveResult.Result, cancellationToken); + } + + /// + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public override Task>> DecryptAsync(string vaultResourceName, + Stream ciphertextStream, string keyName, CancellationToken cancellationToken = default) => + DecryptAsync(vaultResourceName, ciphertextStream, keyName, new DecryptionOptions(), + cancellationToken); + + /// + /// Sends the ciphertext bytes in chunks to the sidecar to be decrypted. + /// + private async Task SendCiphertextStreamAsync(Stream ciphertextStream, + int streamingBlockSizeInBytes, + AsyncDuplexStreamingCall duplexStream, + Autogenerated.DecryptRequestOptions decryptRequestOptions, + CancellationToken cancellationToken) + { + //Start with passing the metadata about the decryption request itself in the first message + await duplexStream.RequestStream.WriteAsync( + new Autogenerated.DecryptRequest { Options = decryptRequestOptions }, cancellationToken); + + //Send the ciphertext bytes in blocks in subsequent messages + await using (var bufferedStream = new BufferedStream(ciphertextStream, streamingBlockSizeInBytes)) { - await client.TerminateWorkflowAlpha1Async(request, options); + 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++; + } } - catch (RpcException ex) + + //Send the completion message + await duplexStream.RequestStream.CompleteAsync(); + } + + /// + /// Retrieves the decrypted bytes from the decryption operation on the sidecar and returns as an enumerable stream. + /// + private async IAsyncEnumerable> RetrieveDecryptedStreamAsync( + AsyncDuplexStreamingCall duplexStream, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + await foreach (var decryptResponse in duplexStream.ResponseStream.ReadAllAsync(cancellationToken) + .ConfigureAwait(false)) { - throw new DaprException("Terminate workflow operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + yield return decryptResponse.Payload.Data.Memory; } - } - - /// - [Obsolete] - public async override Task RaiseWorkflowEventAsync( - string instanceId, - string workflowComponent, - string eventName, - Object eventData, + + /// + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public override async Task> DecryptAsync(string vaultResourceName, + ReadOnlyMemory ciphertextBytes, string keyName, DecryptionOptions decryptionOptions, CancellationToken cancellationToken = default) { - ArgumentVerifier.ThrowIfNullOrEmpty(instanceId, nameof(instanceId)); - ArgumentVerifier.ThrowIfNullOrEmpty(workflowComponent, nameof(workflowComponent)); - ArgumentVerifier.ThrowIfNullOrEmpty(eventName, nameof(eventName)); - - byte[] jsonUtf8Bytes = new byte[0]; - // Serialize json data. Converts eventData object to bytes and then bytestring inside the request. - if (eventData != null) + if (MemoryMarshal.TryGetArray(ciphertextBytes, out var ciphertextSegment) && ciphertextSegment.Array != null) { - jsonUtf8Bytes = JsonSerializer.SerializeToUtf8Bytes(eventData); + var decryptionResult = await DecryptAsync(vaultResourceName, new MemoryStream(ciphertextSegment.Array), + keyName, decryptionOptions, cancellationToken); + + var bufferedResult = new ArrayBufferWriter(); + await foreach (var item in decryptionResult.WithCancellation(cancellationToken)) + { + bufferedResult.Write(item.Span); + } + + return bufferedResult.WrittenMemory; } - var request = new Autogenerated.RaiseEventWorkflowRequest() - { - InstanceId = instanceId, - WorkflowComponent = workflowComponent, - EventName = eventName, - EventData = ByteString.CopyFrom(jsonUtf8Bytes), - }; + throw new ArgumentException("The input instance doesn't have a valid underlying data store", nameof(ciphertextBytes)); + } - var options = CreateCallOptions(headers: null, cancellationToken); + /// + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public override async Task> DecryptAsync(string vaultResourceName, + ReadOnlyMemory ciphertextBytes, string keyName, CancellationToken cancellationToken = default) => + await DecryptAsync(vaultResourceName, ciphertextBytes, keyName, + new DecryptionOptions(), cancellationToken); + + #region Subtle Crypto Implementation + + ///// + //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public override async Task<(string Name, string PublicKey)> GetKeyAsync(string vaultResourceName, string keyName, Autogenerated.SubtleGetKeyRequest.Types.KeyFormat keyFormat, + // CancellationToken cancellationToken = default) + //{ + // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); + // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); + + // var envelope = new Autogenerated.SubtleGetKeyRequest() + // { + // ComponentName = vaultResourceName, Format = keyFormat, Name = keyName + // }; + + // var options = CreateCallOptions(headers: null, cancellationToken); + // Autogenerated.SubtleGetKeyResponse response; + + // try + // { + // response = await client.SubtleGetKeyAlpha1Async(envelope, options); + // } + // catch (RpcException ex) + // { + // throw new DaprException( + // "Cryptography operation failed: the Dapr endpoint indicated a failure. See InnerException for details", ex); + // } + + // return (response.Name, response.PublicKey); + //} + + ///// + //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public override async Task<(byte[] CipherTextBytes, byte[] AuthenticationTag)> EncryptAsync(string vaultResourceName, byte[] plainTextBytes, string algorithm, + // string keyName, byte[] nonce, byte[] associatedData, CancellationToken cancellationToken = default) + //{ + // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); + // ArgumentVerifier.ThrowIfNullOrEmpty(algorithm, nameof(algorithm)); + // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); + + // var envelope = new Autogenerated.SubtleEncryptRequest + // { + // ComponentName = vaultResourceName, + // Algorithm = algorithm, + // KeyName = keyName, + // Nonce = ByteString.CopyFrom(nonce), + // Plaintext = ByteString.CopyFrom(plainTextBytes), + // AssociatedData = ByteString.CopyFrom(associatedData) + // }; + + // var options = CreateCallOptions(headers: null, cancellationToken); + // Autogenerated.SubtleEncryptResponse response; + + // try + // { + // response = await client.SubtleEncryptAlpha1Async(envelope, options); + // } + // catch (RpcException ex) + // { + // throw new DaprException( + // "Cryptography operation failed: the Dapr endpoint indicated a failure. See InnerException for details", + // ex); + // } + + // return (response.Ciphertext.ToByteArray(), response.Tag.ToByteArray() ?? Array.Empty()); + //} + + ///// + //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public override async Task DecryptAsync(string vaultResourceName, byte[] cipherTextBytes, string algorithm, string keyName, byte[] nonce, byte[] tag, + // byte[] associatedData, CancellationToken cancellationToken = default) + //{ + // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); + // ArgumentVerifier.ThrowIfNullOrEmpty(algorithm, nameof(algorithm)); + // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); + + // var envelope = new Autogenerated.SubtleDecryptRequest + // { + // ComponentName = vaultResourceName, + // Algorithm = algorithm, + // KeyName = keyName, + // Nonce = ByteString.CopyFrom(nonce), + // Ciphertext = ByteString.CopyFrom(cipherTextBytes), + // AssociatedData = ByteString.CopyFrom(associatedData), + // Tag = ByteString.CopyFrom(tag) + // }; + + // var options = CreateCallOptions(headers: null, cancellationToken); + // Autogenerated.SubtleDecryptResponse response; + + // try + // { + // response = await client.SubtleDecryptAlpha1Async(envelope, options); + // } + // catch (RpcException ex) + // { + // throw new DaprException( + // "Cryptography operation failed: the Dapr endpoint included a failure. See InnerException for details", ex); + // } + + // return response.Plaintext.ToByteArray(); + //} + + ///// + //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public override async Task<(byte[] WrappedKey, byte[] AuthenticationTag)> WrapKeyAsync(string vaultResourceName, byte[] plainTextKey, string keyName, + // string algorithm, byte[] nonce, byte[] associatedData, CancellationToken cancellationToken = default) + //{ + // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); + // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); + // ArgumentVerifier.ThrowIfNullOrEmpty(algorithm, nameof(algorithm)); + + // var envelope = new Autogenerated.SubtleWrapKeyRequest + // { + // ComponentName = vaultResourceName, + // Algorithm = algorithm, + // KeyName = keyName, + // Nonce = ByteString.CopyFrom(nonce), + // PlaintextKey = ByteString.CopyFrom(plainTextKey), + // AssociatedData = ByteString.CopyFrom(associatedData) + // }; + + // var options = CreateCallOptions(headers: null, cancellationToken); + // Autogenerated.SubtleWrapKeyResponse response; + + // try + // { + // response = await client.SubtleWrapKeyAlpha1Async(envelope, options); + // } + // catch (RpcException ex) + // { + // throw new DaprException( + // "Cryptography operation failed: the Dapr endpoint included a failure. See InnerException for details", + // ex); + // } + + // return (response.WrappedKey.ToByteArray(), response.Tag.ToByteArray() ?? Array.Empty()); + //} + + ///// + //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public override async Task UnwrapKeyAsync(string vaultResourceName, byte[] wrappedKey, string algorithm, + // string keyName, byte[] nonce, byte[] tag, byte[] associatedData, CancellationToken cancellationToken = default) + //{ + // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); + // ArgumentVerifier.ThrowIfNullOrEmpty(algorithm, nameof(algorithm)); + // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); + + // var envelope = new Autogenerated.SubtleUnwrapKeyRequest + // { + // ComponentName = vaultResourceName, + // WrappedKey = ByteString.CopyFrom(wrappedKey), + // AssociatedData = ByteString.CopyFrom(associatedData), + // Algorithm = algorithm, + // KeyName = keyName, + // Nonce = ByteString.CopyFrom(nonce), + // Tag = ByteString.CopyFrom(tag) + // }; + + // var options = CreateCallOptions(headers: null, cancellationToken); + // Autogenerated.SubtleUnwrapKeyResponse response; + + // try + // { + // response = await client.SubtleUnwrapKeyAlpha1Async(envelope, options); + // } + // catch (RpcException ex) + // { + // throw new DaprException( + // "Cryptography operation failed: the Dapr endpoint included a failure. See InnerException for details", + // ex); + // } + + // return response.PlaintextKey.ToByteArray(); + //} + + ///// + //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public override async Task SignAsync(string vaultResourceName, byte[] digest, string algorithm, string keyName, CancellationToken cancellationToken = default) + //{ + // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); + // ArgumentVerifier.ThrowIfNullOrEmpty(algorithm, nameof(algorithm)); + // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); + + // var envelope = new Autogenerated.SubtleSignRequest + // { + // ComponentName = vaultResourceName, + // Digest = ByteString.CopyFrom(digest), + // Algorithm = algorithm, + // KeyName = keyName + // }; + + // var options = CreateCallOptions(headers: null, cancellationToken); + // Autogenerated.SubtleSignResponse response; + + // try + // { + // response = await client.SubtleSignAlpha1Async(envelope, options); + // } + // catch (RpcException ex) + // { + // throw new DaprException( + // "Cryptography operation failed: the Dapr endpoint included a failure. See InnerException for details", + // ex); + // } + + // return response.Signature.ToByteArray(); + //} + + ///// + //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public override async Task VerifyAsync(string vaultResourceName, byte[] digest, byte[] signature, + // string algorithm, string keyName, CancellationToken cancellationToken = default) + //{ + // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); + // ArgumentVerifier.ThrowIfNullOrEmpty(algorithm, nameof(algorithm)); + // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); + + // var envelope = new Autogenerated.SubtleVerifyRequest + // { + // ComponentName = vaultResourceName, + // Algorithm = algorithm, + // KeyName = keyName, + // Signature = ByteString.CopyFrom(signature), + // Digest = ByteString.CopyFrom(digest) + // }; + + // var options = CreateCallOptions(headers: null, cancellationToken); + // Autogenerated.SubtleVerifyResponse response; + + // try + // { + // response = await client.SubtleVerifyAlpha1Async(envelope, options); + // } + // catch (RpcException ex) + // { + // throw new DaprException( + // "Cryptography operation failed: the Dapr endpoint included a failure. See InnerException for details", + // ex); + // } + + // return response.Valid; + //} - try - { - await client.RaiseEventWorkflowAlpha1Async(request, options); - } - catch (RpcException ex) - { - throw new DaprException("Start Workflow operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); - } - } + #endregion + #endregion + + #region Distributed Lock API /// [Obsolete] - public async override Task PauseWorkflowAsync( - string instanceId, - string workflowComponent, + public async override Task Lock( + string storeName, + string resourceId, + string lockOwner, + Int32 expiryInSeconds, CancellationToken cancellationToken = default) { - ArgumentVerifier.ThrowIfNullOrEmpty(instanceId, nameof(instanceId)); - ArgumentVerifier.ThrowIfNullOrEmpty(workflowComponent, nameof(workflowComponent)); - - var request = new Autogenerated.PauseWorkflowRequest() - { - InstanceId = instanceId, - WorkflowComponent = workflowComponent - }; - - var options = CreateCallOptions(headers: null, cancellationToken); + ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); + ArgumentVerifier.ThrowIfNullOrEmpty(resourceId, nameof(resourceId)); + ArgumentVerifier.ThrowIfNullOrEmpty(lockOwner, nameof(lockOwner)); - try - { - await client.PauseWorkflowAlpha1Async(request, options); - } - catch (RpcException ex) + if (expiryInSeconds == 0 || expiryInSeconds < 0) { - throw new DaprException("Pause workflow operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + throw new ArgumentException("The value cannot be zero or less than zero: " + expiryInSeconds); } - } - /// - [Obsolete] - public async override Task ResumeWorkflowAsync( - string instanceId, - string workflowComponent, - CancellationToken cancellationToken = default) - { - ArgumentVerifier.ThrowIfNullOrEmpty(instanceId, nameof(instanceId)); - ArgumentVerifier.ThrowIfNullOrEmpty(workflowComponent, nameof(workflowComponent)); - - var request = new Autogenerated.ResumeWorkflowRequest() + var request = new Autogenerated.TryLockRequest() { - InstanceId = instanceId, - WorkflowComponent = workflowComponent + StoreName = storeName, + ResourceId = resourceId, + LockOwner = lockOwner, + ExpiryInSeconds = expiryInSeconds }; - var options = CreateCallOptions(headers: null, cancellationToken); - try { - await client.ResumeWorkflowAlpha1Async(request, options); + var options = CreateCallOptions(headers: null, cancellationToken); + var response = await client.TryLockAlpha1Async(request, options); + return new TryLockResponse() + { + StoreName = storeName, + ResourceId = resourceId, + LockOwner = lockOwner, + Success = response.Success + }; } catch (RpcException ex) { - throw new DaprException("Resume workflow operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + throw new DaprException("Lock operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); } - } /// [Obsolete] - public async override Task PurgeWorkflowAsync( - string instanceId, - string workflowComponent, + public async override Task Unlock( + string storeName, + string resourceId, + string lockOwner, CancellationToken cancellationToken = default) { - ArgumentVerifier.ThrowIfNullOrEmpty(instanceId, nameof(instanceId)); - ArgumentVerifier.ThrowIfNullOrEmpty(workflowComponent, nameof(workflowComponent)); + ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); + ArgumentVerifier.ThrowIfNullOrEmpty(resourceId, nameof(resourceId)); + ArgumentVerifier.ThrowIfNullOrEmpty(lockOwner, nameof(lockOwner)); - var request = new Autogenerated.PurgeWorkflowRequest() + var request = new Autogenerated.UnlockRequest() { - InstanceId = instanceId, - WorkflowComponent = workflowComponent + StoreName = storeName, + ResourceId = resourceId, + LockOwner = lockOwner }; var options = CreateCallOptions(headers: null, cancellationToken); - + Autogenerated.UnlockResponse response = new Autogenerated.UnlockResponse(); try { - await client.PurgeWorkflowAlpha1Async(request, options); + response = await client.UnlockAlpha1Async(request, options); } catch (RpcException ex) { - throw new DaprException("Purge workflow operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + throw new DaprException("Lock operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); } + return new UnlockResponse(GetUnLockStatus(response.Status)); } - #endregion + #endregion #region Dapr Sidecar Methods @@ -1724,6 +2068,12 @@ public override async Task CheckHealthAsync(CancellationToken cancellation { var path = "/v1.0/healthz"; var request = new HttpRequestMessage(HttpMethod.Get, new Uri(this.httpEndpoint, path)); + + if (this.apiTokenHeader is not null) + { + request.Headers.Add(this.apiTokenHeader.Value.Key, this.apiTokenHeader.Value.Value); + } + try { using var response = await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); @@ -1740,6 +2090,12 @@ public override async Task CheckOutboundHealthAsync(CancellationToken canc { var path = "/v1.0/healthz/outbound"; var request = new HttpRequestMessage(HttpMethod.Get, new Uri(this.httpEndpoint, path)); + + if (this.apiTokenHeader is not null) + { + request.Headers.Add(this.apiTokenHeader.Value.Key, this.apiTokenHeader.Value.Value); + } + try { using var response = await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); @@ -1768,7 +2124,7 @@ public override async Task WaitForSidecarAsync(CancellationToken cancellationTok /// public async override Task ShutdownSidecarAsync(CancellationToken cancellationToken = default) { - await client.ShutdownAsync(new Empty(), CreateCallOptions(null, cancellationToken)); + await client.ShutdownAsync(new Autogenerated.ShutdownRequest(), CreateCallOptions(null, cancellationToken)); } /// @@ -1777,11 +2133,15 @@ public override async Task GetMetadataAsync(CancellationToken canc var options = CreateCallOptions(headers: null, cancellationToken); try { - var response = await client.GetMetadataAsync(new Empty(), options); - return new DaprMetadata(response.Id, - response.ActiveActorsCount.Select(c => new DaprActorMetadata(c.Type, c.Count)).ToList(), - response.ExtendedMetadata.ToDictionary(c => c.Key, c => c.Value), - response.RegisteredComponents.Select(c => new DaprComponentsMetadata(c.Name, c.Type, c.Version, c.Capabilities.ToArray())).ToList()); + var response = await client.GetMetadataAsync(new Autogenerated.GetMetadataRequest(), options); + return new DaprMetadata(response.Id ?? "", + response.ActorRuntime?.ActiveActors?.Select(c => new DaprActorMetadata(c.Type, c.Count)).ToList() ?? + new List(), + response.ExtendedMetadata?.ToDictionary(c => c.Key, c => c.Value) ?? + new Dictionary(), + response.RegisteredComponents?.Select(c => + new DaprComponentsMetadata(c.Name, c.Type, c.Version, c.Capabilities.ToArray())).ToList() ?? + new List()); } catch (RpcException ex) { diff --git a/src/Dapr.Client/Extensions/EnumExtensions.cs b/src/Dapr.Client/Extensions/EnumExtensions.cs new file mode 100644 index 000000000..df9c9ad33 --- /dev/null +++ b/src/Dapr.Client/Extensions/EnumExtensions.cs @@ -0,0 +1,41 @@ +// ------------------------------------------------------------------------ +// Copyright 2023 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +#nullable enable +using System; +using System.Reflection; +using System.Runtime.Serialization; + +namespace Dapr.Client +{ + internal static class EnumExtensions + { + /// + /// Reads the value of an enum out of the attached attribute. + /// + /// The enum. + /// The value of the enum to pull the value for. + /// + public static string GetValueFromEnumMember(this T value) where T : Enum + { + ArgumentNullException.ThrowIfNull(value, nameof(value)); + + var memberInfo = typeof(T).GetMember(value.ToString(), BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly); + if (memberInfo.Length <= 0) + return value.ToString(); + + var attributes = memberInfo[0].GetCustomAttributes(typeof(EnumMemberAttribute), false); + return (attributes.Length > 0 ? ((EnumMemberAttribute)attributes[0]).Value : value.ToString()) ?? value.ToString(); + } + } +} diff --git a/src/Dapr.Client/Extensions/HttpExtensions.cs b/src/Dapr.Client/Extensions/HttpExtensions.cs new file mode 100644 index 000000000..259d2747d --- /dev/null +++ b/src/Dapr.Client/Extensions/HttpExtensions.cs @@ -0,0 +1,51 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +#nullable enable +using System; +using System.Collections.Generic; +using System.Text; + +namespace Dapr.Client +{ + /// + /// Provides extensions specific to HTTP types. + /// + internal static class HttpExtensions + { + /// + /// 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) + { + if (qsBuilder.Length > 0) + qsBuilder.Append('&'); + qsBuilder.Append($"{Uri.EscapeDataString(kvParam.Key)}={Uri.EscapeDataString(kvParam.Value)}"); + } + + uriBuilder.Query = qsBuilder.ToString(); + return uriBuilder.Uri; + } + } +} diff --git a/src/Dapr.Client/GetWorkflowResponse.cs b/src/Dapr.Client/GetWorkflowResponse.cs deleted file mode 100644 index eb24b9429..000000000 --- a/src/Dapr.Client/GetWorkflowResponse.cs +++ /dev/null @@ -1,35 +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.Collections.Generic; - -namespace Dapr.Client -{ - /// - /// Initializes a new . - /// - /// The instance ID associated with this response. - /// The name of the workflow associated with this response. - /// The time at which the workflow started executing. - /// The time at which the workflow started executing. - /// The current runtime status of the workflow. - /// The response properties. - public record GetWorkflowResponse( - string InstanceId, - string WorkflowName, - DateTime CreatedAt, - DateTime LastUpdatedAt, - string RuntimeStatus, - IReadOnlyDictionary Properties); -} diff --git a/src/Dapr.Client/InvocationHandler.cs b/src/Dapr.Client/InvocationHandler.cs index b2f74c7f9..1b55436aa 100644 --- a/src/Dapr.Client/InvocationHandler.cs +++ b/src/Dapr.Client/InvocationHandler.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -82,6 +82,12 @@ public string DaprEndpoint } } + /// + /// 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 { @@ -117,7 +123,7 @@ protected override async Task SendAsync(HttpRequestMessage } // Internal for testing - internal bool TryRewriteUri(Uri uri, [NotNullWhen(true)] out Uri? rewritten) + 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). @@ -128,13 +134,23 @@ internal bool TryRewriteUri(Uri uri, [NotNullWhen(true)] out Uri? rewritten) 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/{uri.Host}/method" + uri.AbsolutePath, + Path = $"/v1.0/invoke/{host}/method" + uri.AbsolutePath, }; rewritten = builder.Uri; diff --git a/src/Dapr.Client/StateQueryResponse.cs b/src/Dapr.Client/StateQueryResponse.cs index 8ef4a65f0..c1cc87386 100644 --- a/src/Dapr.Client/StateQueryResponse.cs +++ b/src/Dapr.Client/StateQueryResponse.cs @@ -11,6 +11,7 @@ You may obtain a copy of the License at limitations under the License. */ +#nullable enable using System.Collections.Generic; namespace Dapr.Client @@ -75,9 +76,9 @@ public StateQueryItem(string key, TValue data, string etag, string error) public string Key { get; } /// - /// The data of the the key from the matched query. + /// The data of the key from the matched query. /// - public TValue Data { get; } + public TValue? Data { get; } /// /// The ETag for the key from the matched query. diff --git a/src/Dapr.Client/SubscribeConfigurationResponse.cs b/src/Dapr.Client/SubscribeConfigurationResponse.cs index 1a3179dee..b371bd21c 100644 --- a/src/Dapr.Client/SubscribeConfigurationResponse.cs +++ b/src/Dapr.Client/SubscribeConfigurationResponse.cs @@ -6,7 +6,6 @@ namespace Dapr.Client /// /// Response for a Subscribe Configuration request. /// - [Obsolete("This response utilizes an alpha API which is subject to change. This attribute will be removed when the API is no longer Alpha.")] public class SubscribeConfigurationResponse { private ConfigurationSource source; diff --git a/src/Shared/ArgumentVerifier.cs b/src/Dapr.Common/ArgumentVerifier.cs similarity index 96% rename from src/Shared/ArgumentVerifier.cs rename to src/Dapr.Common/ArgumentVerifier.cs index 907543f01..62ae98b54 100644 --- a/src/Shared/ArgumentVerifier.cs +++ b/src/Dapr.Common/ArgumentVerifier.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/src/Dapr.Common/AssemblyInfo.cs b/src/Dapr.Common/AssemblyInfo.cs new file mode 100644 index 000000000..a18d03bbc --- /dev/null +++ b/src/Dapr.Common/AssemblyInfo.cs @@ -0,0 +1,40 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Dapr.Actors, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.Actors.Generators, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.Actors.AspNetCore, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.Client, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.AspNetCore, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.Extensions.Configuration, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.Workflow, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] + +[assembly: InternalsVisibleTo("Dapr.Actors.AspNetCore.IntegrationTest, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.Actors.AspNetCore.IntegrationTest.App, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.Actors.AspNetCore.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.Actors.Generators.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.Actors.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.AspNetCore.IntegrationTest, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[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.Common.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.E2E.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.E2E.Test.Actors, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.E2E.Test.Actors.Generators, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.E2E.Test.App, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.E2E.Test.App.Grpc, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.E2E.Test.App.ReentrantActors, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.Extensions.Configuration.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] diff --git a/src/Dapr.Common/Dapr.Common.csproj b/src/Dapr.Common/Dapr.Common.csproj new file mode 100644 index 000000000..ea3e8ae84 --- /dev/null +++ b/src/Dapr.Common/Dapr.Common.csproj @@ -0,0 +1,14 @@ + + + + net6;net7;net8 + enable + enable + + + + + + + + diff --git a/src/Shared/DaprDefaults.cs b/src/Dapr.Common/DaprDefaults.cs similarity index 65% rename from src/Shared/DaprDefaults.cs rename to src/Dapr.Common/DaprDefaults.cs index 28c02c148..575a3c148 100644 --- a/src/Shared/DaprDefaults.cs +++ b/src/Dapr.Common/DaprDefaults.cs @@ -11,16 +11,14 @@ // limitations under the License. // ------------------------------------------------------------------------ -using System; - namespace Dapr { internal static class DaprDefaults { - private static string httpEndpoint; - private static string grpcEndpoint; - private static string daprApiToken; - private static string appApiToken; + private static string httpEndpoint = string.Empty; + private static string grpcEndpoint = string.Empty; + private static string daprApiToken = string.Empty; + private static string appApiToken = string.Empty; /// /// Get the value of environment variable DAPR_API_TOKEN @@ -31,11 +29,11 @@ public static string GetDefaultDaprApiToken() // Lazy-init is safe because this is just populating the default // We don't plan to support the case where the user changes environment variables // for a running process. - if (daprApiToken == null) + if (string.IsNullOrEmpty(daprApiToken)) { // Treat empty the same as null since it's an environment variable var value = Environment.GetEnvironmentVariable("DAPR_API_TOKEN"); - daprApiToken = (value == string.Empty) ? null : value; + daprApiToken = string.IsNullOrEmpty(value) ? string.Empty : value; } return daprApiToken; @@ -47,23 +45,29 @@ public static string GetDefaultDaprApiToken() /// The value of environment variable APP_API_TOKEN public static string GetDefaultAppApiToken() { - if (appApiToken == null) + if (string.IsNullOrEmpty(appApiToken)) { var value = Environment.GetEnvironmentVariable("APP_API_TOKEN"); - appApiToken = (value == string.Empty) ? null : value; + appApiToken = string.IsNullOrEmpty(value) ? string.Empty : value; } return appApiToken; } /// - /// Get the value of environment variable DAPR_HTTP_PORT + /// Get the value of HTTP endpoint based off environment variables /// - /// The value of environment variable DAPR_HTTP_PORT + /// The value of HTTP endpoint based off environment variables public static string GetDefaultHttpEndpoint() { - if (httpEndpoint == null) + if (string.IsNullOrEmpty(httpEndpoint)) { + var endpoint = Environment.GetEnvironmentVariable("DAPR_HTTP_ENDPOINT"); + if (!string.IsNullOrEmpty(endpoint)) { + httpEndpoint = endpoint; + return httpEndpoint; + } + var port = Environment.GetEnvironmentVariable("DAPR_HTTP_PORT"); port = string.IsNullOrEmpty(port) ? "3500" : port; httpEndpoint = $"http://127.0.0.1:{port}"; @@ -73,13 +77,19 @@ public static string GetDefaultHttpEndpoint() } /// - /// Get the value of environment variable DAPR_GRPC_PORT + /// Get the value of gRPC endpoint based off environment variables /// - /// The value of environment variable DAPR_GRPC_PORT + /// The value of gRPC endpoint based off environment variables public static string GetDefaultGrpcEndpoint() { - if (grpcEndpoint == null) + if (string.IsNullOrEmpty(grpcEndpoint)) { + var endpoint = Environment.GetEnvironmentVariable("DAPR_GRPC_ENDPOINT"); + if (!string.IsNullOrEmpty(endpoint)) { + grpcEndpoint = endpoint; + return grpcEndpoint; + } + var port = Environment.GetEnvironmentVariable("DAPR_GRPC_PORT"); port = string.IsNullOrEmpty(port) ? "50001" : port; grpcEndpoint = $"http://127.0.0.1:{port}"; diff --git a/src/Dapr.Client/DaprException.cs b/src/Dapr.Common/DaprException.cs similarity index 92% rename from src/Dapr.Client/DaprException.cs rename to src/Dapr.Common/DaprException.cs index 8c94a452d..2b600ef3a 100644 --- a/src/Dapr.Client/DaprException.cs +++ b/src/Dapr.Common/DaprException.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -11,7 +11,6 @@ // limitations under the License. // ------------------------------------------------------------------------ -using System; using System.Runtime.Serialization; namespace Dapr @@ -47,6 +46,9 @@ public DaprException(string message, Exception innerException) /// /// 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) { diff --git a/src/Dapr.Extensions.Configuration/Dapr.Extensions.Configuration.csproj b/src/Dapr.Extensions.Configuration/Dapr.Extensions.Configuration.csproj index 7eec7f4f7..5cc1043d3 100644 --- a/src/Dapr.Extensions.Configuration/Dapr.Extensions.Configuration.csproj +++ b/src/Dapr.Extensions.Configuration/Dapr.Extensions.Configuration.csproj @@ -1,14 +1,9 @@ - netcoreapp3.1 enable - - - - Dapr Secret Store configuration provider implementation for Microsoft.Extensions.Configuration. @@ -16,10 +11,11 @@ + - + \ No newline at end of file diff --git a/src/Dapr.Extensions.Configuration/DaprConfigurationStoreExtension.cs b/src/Dapr.Extensions.Configuration/DaprConfigurationStoreExtension.cs index f2b132bfe..190acacd1 100644 --- a/src/Dapr.Extensions.Configuration/DaprConfigurationStoreExtension.cs +++ b/src/Dapr.Extensions.Configuration/DaprConfigurationStoreExtension.cs @@ -22,7 +22,6 @@ namespace Dapr.Extensions.Configuration /// /// Extension used to call the Dapr Configuration API and store the values in a . /// - [Obsolete] public static class DaprConfigurationStoreExtension { /// diff --git a/src/Dapr.Extensions.Configuration/DaprConfigurationStoreProvider.cs b/src/Dapr.Extensions.Configuration/DaprConfigurationStoreProvider.cs index a48e6175a..ea00f6c43 100644 --- a/src/Dapr.Extensions.Configuration/DaprConfigurationStoreProvider.cs +++ b/src/Dapr.Extensions.Configuration/DaprConfigurationStoreProvider.cs @@ -24,7 +24,6 @@ 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. /// - [Obsolete] internal class DaprConfigurationStoreProvider : ConfigurationProvider, IDisposable { private string store; diff --git a/src/Dapr.Extensions.Configuration/DaprConfigurationStoreSource.cs b/src/Dapr.Extensions.Configuration/DaprConfigurationStoreSource.cs index 2107c9101..2007ebc5b 100644 --- a/src/Dapr.Extensions.Configuration/DaprConfigurationStoreSource.cs +++ b/src/Dapr.Extensions.Configuration/DaprConfigurationStoreSource.cs @@ -22,7 +22,6 @@ namespace Dapr.Extensions.Configuration /// /// Configuration source that provides a . /// - [Obsolete] public class DaprConfigurationStoreSource : IConfigurationSource { /// diff --git a/src/Dapr.Extensions.Configuration/DaprSecretDescriptor.cs b/src/Dapr.Extensions.Configuration/DaprSecretDescriptor.cs index e708ad712..6d86dc046 100644 --- a/src/Dapr.Extensions.Configuration/DaprSecretDescriptor.cs +++ b/src/Dapr.Extensions.Configuration/DaprSecretDescriptor.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -21,8 +21,11 @@ namespace Dapr.Extensions.Configuration public class DaprSecretDescriptor { /// - /// Gets or sets the secret name. + /// 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; } /// @@ -31,20 +34,39 @@ public class DaprSecretDescriptor public IReadOnlyDictionary Metadata { get; } /// - /// Secret Descriptor Construcutor + /// 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) : this(secretName, new Dictionary()) + public DaprSecretDescriptor(string secretName, bool isRequired = true, string secretKey = "") + : this(secretName, new Dictionary(), isRequired, secretKey) { } /// - /// Secret Descriptor Construcutor + /// Secret Descriptor Constructor /// - public DaprSecretDescriptor(string secretName, IReadOnlyDictionary metadata) + 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/DaprSecretStoreConfigurationProvider.cs b/src/Dapr.Extensions.Configuration/DaprSecretStoreConfigurationProvider.cs index da5349c30..ecd0ac91b 100644 --- a/src/Dapr.Extensions.Configuration/DaprSecretStoreConfigurationProvider.cs +++ b/src/Dapr.Extensions.Configuration/DaprSecretStoreConfigurationProvider.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -54,7 +54,7 @@ public DaprSecretStoreConfigurationProvider( bool normalizeKey, IEnumerable secretDescriptors, DaprClient client) : this(store, normalizeKey, null, secretDescriptors, client, DefaultSidecarWaitTimeout) - { + { } /// @@ -181,6 +181,10 @@ private string NormalizeKey(string key) 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() @@ -197,16 +201,41 @@ private async Task LoadAsync() { foreach (var secretDescriptor in secretDescriptors) { - var result = await client.GetSecretAsync(store, secretDescriptor.SecretName, secretDescriptor.Metadata).ConfigureAwait(false); + + Dictionary result; + + try + { + 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."); + throw new InvalidOperationException( + $"A duplicate key '{key}' was found in the secret store '{store}'. Please remove any duplicates from your secret store."); } - data.Add(normalizeKey ? NormalizeKey(key) : key, result[key]); + // 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]); } } diff --git a/src/Dapr.Protos/Dapr.Protos.csproj b/src/Dapr.Protos/Dapr.Protos.csproj new file mode 100644 index 000000000..8a8804b22 --- /dev/null +++ b/src/Dapr.Protos/Dapr.Protos.csproj @@ -0,0 +1,22 @@ + + + + enable + enable + This package contains the reference protos used by develop services using Dapr. + + + + + + + + + + + + + + + + diff --git a/src/Dapr.Client/Protos/dapr/proto/common/v1/common.proto b/src/Dapr.Protos/Protos/dapr/proto/common/v1/common.proto similarity index 96% rename from src/Dapr.Client/Protos/dapr/proto/common/v1/common.proto rename to src/Dapr.Protos/Protos/dapr/proto/common/v1/common.proto index 3faea3016..0eb882b89 100644 --- a/src/Dapr.Client/Protos/dapr/proto/common/v1/common.proto +++ b/src/Dapr.Protos/Protos/dapr/proto/common/v1/common.proto @@ -1,4 +1,4 @@ -/* +/* Copyright 2021 The Dapr Authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -26,7 +26,7 @@ option go_package = "github.com/dapr/dapr/pkg/proto/common/v1;common"; // 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 +// `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 { @@ -77,7 +77,7 @@ message InvokeRequest { HTTPExtension http_extension = 4; } -// InvokeResponse is the response message inclduing data and its content type +// InvokeResponse is the response message including data and its content type // from app callback. // This message is used in InvokeService of Dapr gRPC Service and OnInvoke // of AppCallback gRPC service. @@ -157,4 +157,4 @@ message ConfigurationItem { // the metadata which will be passed to/from configuration store component. map metadata = 3; -} +} \ No newline at end of file diff --git a/src/Dapr.Client/Protos/dapr/proto/dapr/v1/appcallback.proto b/src/Dapr.Protos/Protos/dapr/proto/runtime/v1/appcallback.proto similarity index 90% rename from src/Dapr.Client/Protos/dapr/proto/dapr/v1/appcallback.proto rename to src/Dapr.Protos/Protos/dapr/proto/runtime/v1/appcallback.proto index 823c0aae4..51dee5539 100644 --- a/src/Dapr.Client/Protos/dapr/proto/dapr/v1/appcallback.proto +++ b/src/Dapr.Protos/Protos/dapr/proto/runtime/v1/appcallback.proto @@ -1,4 +1,4 @@ -/* +/* Copyright 2021 The Dapr Authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,6 +15,7 @@ syntax = "proto3"; package dapr.proto.runtime.v1; +import "google/protobuf/any.proto"; import "google/protobuf/empty.proto"; import "dapr/proto/common/v1/common.proto"; import "google/protobuf/struct.proto"; @@ -57,10 +58,39 @@ service AppCallbackHealthCheck { // AppCallbackAlpha V1 is an optional extension to AppCallback V1 to opt // for Alpha RPCs. service AppCallbackAlpha { - // Subscribes bulk events from Pubsub + // Subscribes bulk events from Pubsub rpc OnBulkTopicEventAlpha1(TopicEventBulkRequest) returns (TopicEventBulkResponse) {} + + // Sends job back to the app's endpoint at trigger time. + rpc OnJobEventAlpha1 (JobEventRequest) returns (JobEventResponse); } +message JobEventRequest { + // Job name. + string name = 1; + + // Job data to be sent back to app. + google.protobuf.Any data = 2; + + // Required. method is a method name which will be invoked by caller. + string method = 3; + + // The type of data content. + // + // This field is required if data delivers http request body + // Otherwise, this is optional. + string content_type = 4; + + // HTTP specific fields if request conveys http-compatible request. + // + // This field is required for http-compatible request. Otherwise, + // this field is optional. + common.v1.HTTPExtension http_extension = 5; +} + +// JobEventResponse is the response from the app when a job is triggered. +message JobEventResponse {} + // TopicEventRequest message is compatible with CloudEvent spec v1.0 // https://github.com/cloudevents/spec/blob/v1.0/spec.md message TopicEventRequest { @@ -155,14 +185,14 @@ message TopicEventBulkRequestEntry { // content type of the event contained. string content_type = 4; - + // The metadata associated with the event. map metadata = 5; } // TopicEventBulkRequest represents request for bulk message message TopicEventBulkRequest { - // Unique identifier for the bulk request. + // Unique identifier for the bulk request. string id = 1; // The list of items inside this bulk request. @@ -173,10 +203,10 @@ message TopicEventBulkRequest { // The pubsub topic which publisher sent to. string topic = 4; - + // The name of the pubsub the publisher sent to. string pubsub_name = 5; - + // The type of event related to the originating occurrence. string type = 6; @@ -280,8 +310,8 @@ message TopicRoutes { message TopicRule { // The optional CEL expression used to match the event. - // If the match is not specified, then the route is considered - // the default. + // If the match is not specified, then the route is considered + // the default. string match = 1; // The path used to identify matches for this subscription. @@ -310,4 +340,4 @@ message ListInputBindingsResponse { // HealthCheckResponse is the message with the response to the health check. // This message is currently empty as used as placeholder. -message HealthCheckResponse {} +message HealthCheckResponse {} \ No newline at end of file diff --git a/src/Dapr.Client/Protos/dapr/proto/dapr/v1/dapr.proto b/src/Dapr.Protos/Protos/dapr/proto/runtime/v1/dapr.proto similarity index 74% rename from src/Dapr.Client/Protos/dapr/proto/dapr/v1/dapr.proto rename to src/Dapr.Protos/Protos/dapr/proto/runtime/v1/dapr.proto index 883527adb..ecf0f76f7 100644 --- a/src/Dapr.Client/Protos/dapr/proto/dapr/v1/dapr.proto +++ b/src/Dapr.Protos/Protos/dapr/proto/runtime/v1/dapr.proto @@ -1,4 +1,4 @@ -/* +/* Copyright 2021 The Dapr Authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import "google/protobuf/any.proto"; import "google/protobuf/empty.proto"; import "google/protobuf/timestamp.proto"; import "dapr/proto/common/v1/common.proto"; +import "dapr/proto/runtime/v1/appcallback.proto"; option csharp_namespace = "Dapr.Client.Autogen.Grpc.v1"; option java_outer_classname = "DaprProtos"; @@ -58,6 +59,10 @@ service Dapr { // Bulk Publishes multiple events to the specified topic. rpc BulkPublishEventAlpha1(BulkPublishRequest) returns (BulkPublishResponse) {} + // SubscribeTopicEventsAlpha1 subscribes to a PubSub topic and receives topic + // events from it. + rpc SubscribeTopicEventsAlpha1(stream SubscribeTopicEventsRequestAlpha1) returns (stream SubscribeTopicEventsResponseAlpha1) {} + // Invokes binding data to specific output bindings rpc InvokeBinding(InvokeBindingRequest) returns (InvokeBindingResponse) {} @@ -79,9 +84,6 @@ service Dapr { // Unregister an actor reminder. rpc UnregisterActorReminder(UnregisterActorReminderRequest) returns (google.protobuf.Empty) {} - // Rename an actor reminder. - rpc RenameActorReminder(RenameActorReminderRequest) returns (google.protobuf.Empty) {} - // Gets the state for a specific actor. rpc GetActorState(GetActorStateRequest) returns (GetActorStateResponse) {} @@ -122,7 +124,7 @@ service Dapr { rpc DecryptAlpha1(stream DecryptRequest) returns (stream DecryptResponse); // Gets metadata of the sidecar - rpc GetMetadata (google.protobuf.Empty) returns (GetMetadataResponse) {} + rpc GetMetadata (GetMetadataRequest) returns (GetMetadataResponse) {} // Sets value in extended metadata of the sidecar rpc SetMetadata (SetMetadataRequest) returns (google.protobuf.Empty) {} @@ -169,8 +171,37 @@ service Dapr { // Raise an event to a running workflow instance rpc RaiseEventWorkflowAlpha1 (RaiseEventWorkflowRequest) returns (google.protobuf.Empty) {} + // Starts a new instance of a workflow + rpc StartWorkflowBeta1 (StartWorkflowRequest) returns (StartWorkflowResponse) {} + + // Gets details about a started workflow instance + rpc GetWorkflowBeta1 (GetWorkflowRequest) returns (GetWorkflowResponse) {} + + // Purge Workflow + rpc PurgeWorkflowBeta1 (PurgeWorkflowRequest) returns (google.protobuf.Empty) {} + + // Terminates a running workflow instance + rpc TerminateWorkflowBeta1 (TerminateWorkflowRequest) returns (google.protobuf.Empty) {} + + // Pauses a running workflow instance + rpc PauseWorkflowBeta1 (PauseWorkflowRequest) returns (google.protobuf.Empty) {} + + // Resumes a paused workflow instance + rpc ResumeWorkflowBeta1 (ResumeWorkflowRequest) returns (google.protobuf.Empty) {} + + // Raise an event to a running workflow instance + rpc RaiseEventWorkflowBeta1 (RaiseEventWorkflowRequest) returns (google.protobuf.Empty) {} // Shutdown the sidecar - rpc Shutdown (google.protobuf.Empty) returns (google.protobuf.Empty) {} + rpc Shutdown (ShutdownRequest) returns (google.protobuf.Empty) {} + + // Create and schedule a job + rpc ScheduleJobAlpha1(ScheduleJobRequest) returns (ScheduleJobResponse) {} + + // Gets a scheduled job + rpc GetJobAlpha1(GetJobRequest) returns (GetJobResponse) {} + + // Delete a job + rpc DeleteJobAlpha1(DeleteJobRequest) returns (DeleteJobResponse) {} } // InvokeServiceRequest represents the request message for Service invocation. @@ -387,7 +418,6 @@ message BulkPublishResponse { // BulkPublishResponseFailedEntry is the message containing the entryID and error of a failed event in BulkPublishEvent call message BulkPublishResponseFailedEntry { - // The response scoped unique ID referring to this message string entry_id = 1; @@ -395,6 +425,61 @@ message BulkPublishResponseFailedEntry { string error = 2; } +// SubscribeTopicEventsRequestAlpha1 is a message containing the details for +// subscribing to a topic via streaming. +// The first message must always be the initial request. All subsequent +// messages must be event processed responses. +message SubscribeTopicEventsRequestAlpha1 { + oneof subscribe_topic_events_request_type { + SubscribeTopicEventsRequestInitialAlpha1 initial_request = 1; + SubscribeTopicEventsRequestProcessedAlpha1 event_processed = 2; + } +} + +// SubscribeTopicEventsRequestInitialAlpha1 is the initial message containing +// the details for subscribing to a topic via streaming. +message SubscribeTopicEventsRequestInitialAlpha1 { + // The name of the pubsub component + string pubsub_name = 1; + + // The pubsub topic + string topic = 2; + + // The metadata passing to pub components + // + // metadata property: + // - key : the key of the message. + map metadata = 3; + + // dead_letter_topic is the topic to which messages that fail to be processed + // are sent. + optional string dead_letter_topic = 4; +} + +// SubscribeTopicEventsRequestProcessedAlpha1 is the message containing the +// subscription to a topic. +message SubscribeTopicEventsRequestProcessedAlpha1 { + // id is the unique identifier for the subscription request. + string id = 1; + + // status is the result of the subscription request. + TopicEventResponse status = 2; +} + + +// SubscribeTopicEventsResponseAlpha1 is a message returned from daprd +// when subscribing to a topic via streaming. +message SubscribeTopicEventsResponseAlpha1 { + oneof subscribe_topic_events_response_type { + SubscribeTopicEventsResponseInitialAlpha1 initial_response = 1; + TopicEventRequest event_message = 2; + } +} + +// SubscribeTopicEventsResponseInitialAlpha1 is the initial response from daprd +// when subscribing to a topic. +message SubscribeTopicEventsResponseInitialAlpha1 {} + // InvokeBindingRequest is the message to send data to output bindings message InvokeBindingRequest { @@ -408,6 +493,7 @@ message InvokeBindingRequest { // // Common metadata property: // - ttlInSeconds : the time to live in seconds for the message. + // // If set in the binding definition will cause all messages to // have a default time to live. The message ttl overrides any value // in the binding definition. @@ -489,65 +575,60 @@ message ExecuteStateTransactionRequest { // RegisterActorTimerRequest is the message to register a timer for an actor of a given type and id. message RegisterActorTimerRequest { - string actor_type = 1; - string actor_id = 2; + string actor_type = 1 [json_name = "actorType"]; + string actor_id = 2 [json_name = "actorId"]; string name = 3; - string due_time = 4; + string due_time = 4 [json_name = "dueTime"]; string period = 5; string callback = 6; - bytes data = 7; + bytes data = 7; string ttl = 8; } // UnregisterActorTimerRequest is the message to unregister an actor timer message UnregisterActorTimerRequest { - string actor_type = 1; - string actor_id = 2; + string actor_type = 1 [json_name = "actorType"]; + string actor_id = 2 [json_name = "actorId"]; string name = 3; } // RegisterActorReminderRequest is the message to register a reminder for an actor of a given type and id. message RegisterActorReminderRequest { - string actor_type = 1; - string actor_id = 2; + string actor_type = 1 [json_name = "actorType"]; + string actor_id = 2 [json_name = "actorId"]; string name = 3; - string due_time = 4; + string due_time = 4 [json_name = "dueTime"]; string period = 5; - bytes data = 6; + bytes data = 6; string ttl = 7; } // UnregisterActorReminderRequest is the message to unregister an actor reminder. message UnregisterActorReminderRequest { - string actor_type = 1; - string actor_id = 2; + string actor_type = 1 [json_name = "actorType"]; + string actor_id = 2 [json_name = "actorId"]; string name = 3; } -// RenameActorReminderRequest is the message to rename an actor reminder. -message RenameActorReminderRequest { - string actor_type = 1; - string actor_id = 2; - string old_name = 3; - string new_name = 4; -} - // GetActorStateRequest is the message to get key-value states from specific actor. message GetActorStateRequest { - string actor_type = 1; - string actor_id = 2; + string actor_type = 1 [json_name = "actorType"]; + string actor_id = 2 [json_name = "actorId"]; string key = 3; } // GetActorStateResponse is the response conveying the actor's state value. message GetActorStateResponse { bytes data = 1; + + // The metadata which will be sent to app. + map metadata = 2; } // ExecuteActorStateTransactionRequest is the message to execute multiple operations on a specified actor. message ExecuteActorStateTransactionRequest { - string actor_type = 1; - string actor_id = 2; + string actor_type = 1 [json_name = "actorType"]; + string actor_id = 2 [json_name = "actorId"]; repeated TransactionalActorStateOperation operations = 3; } @@ -565,8 +646,8 @@ message TransactionalActorStateOperation { // InvokeActorRequest is the message to call an actor. message InvokeActorRequest { - string actor_type = 1; - string actor_id = 2; + string actor_type = 1 [json_name = "actorType"]; + string actor_id = 2 [json_name = "actorId"]; string method = 3; bytes data = 4; map metadata = 5; @@ -577,13 +658,46 @@ message InvokeActorResponse { bytes data = 1; } -// GetMetadataResponse is a message that is returned on GetMetadata rpc call +// GetMetadataRequest is the message for the GetMetadata request. +message GetMetadataRequest { + // Empty +} + +// GetMetadataResponse is a message that is returned on GetMetadata rpc call. message GetMetadataResponse { string id = 1; - repeated ActiveActorsCount active_actors_count = 2; - repeated RegisteredComponents registered_components = 3; - map extended_metadata = 4; - repeated PubsubSubscription subscriptions = 5; + // Deprecated alias for actor_runtime.active_actors. + repeated ActiveActorsCount active_actors_count = 2 [json_name = "actors", deprecated = true]; + repeated RegisteredComponents registered_components = 3 [json_name = "components"]; + map extended_metadata = 4 [json_name = "extended"]; + repeated PubsubSubscription subscriptions = 5 [json_name = "subscriptions"]; + repeated MetadataHTTPEndpoint http_endpoints = 6 [json_name = "httpEndpoints"]; + AppConnectionProperties app_connection_properties = 7 [json_name = "appConnectionProperties"]; + 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 +} + +message ActorRuntime { + enum ActorRuntimeStatus { + // Indicates that the actor runtime is still being initialized. + INITIALIZING = 0; + // Indicates that the actor runtime is disabled. + // This normally happens when Dapr is started without "placement-host-address" + DISABLED = 1; + // Indicates the actor runtime is running, either as an actor host or client. + RUNNING = 2; + } + + // Contains an enum indicating whether the actor runtime has been initialized. + ActorRuntimeStatus runtime_status = 1 [json_name = "runtimeStatus"]; + // Count of active actors per type. + repeated ActiveActorsCount active_actors = 2 [json_name = "activeActors"]; + // Indicates whether the actor runtime is ready to host actors. + bool host_ready = 3 [json_name = "hostReady"]; + // Custom message from the placement provider. + string placement = 4 [json_name = "placement"]; } message ActiveActorsCount { @@ -598,12 +712,44 @@ message RegisteredComponents { repeated string capabilities = 4; } +message MetadataHTTPEndpoint { + string name = 1 [json_name = "name"]; +} + +message AppConnectionProperties { + int32 port = 1; + string protocol = 2; + string channel_address = 3 [json_name = "channelAddress"]; + int32 max_concurrency = 4 [json_name = "maxConcurrency"]; + AppConnectionHealthProperties health = 5; +} + +message AppConnectionHealthProperties { + string health_check_path = 1 [json_name = "healthCheckPath"]; + string health_probe_interval = 2 [json_name = "healthProbeInterval"]; + string health_probe_timeout = 3 [json_name = "healthProbeTimeout"]; + int32 health_threshold = 4 [json_name = "healthThreshold"]; +} + message PubsubSubscription { - string pubsub_name = 1; - string topic = 2; - map metadata = 3; - PubsubSubscriptionRules rules = 4; - string dead_letter_topic = 5; + string pubsub_name = 1 [json_name = "pubsubname"]; + string topic = 2 [json_name = "topic"]; + map metadata = 3 [json_name = "metadata"]; + PubsubSubscriptionRules rules = 4 [json_name = "rules"]; + string dead_letter_topic = 5 [json_name = "deadLetterTopic"]; + PubsubSubscriptionType type = 6 [json_name = "type"]; +} + +// PubsubSubscriptionType indicates the type of subscription +enum PubsubSubscriptionType { + // UNKNOWN is the default value for the subscription type. + UNKNOWN = 0; + // Declarative subscription (k8s CRD) + DECLARATIVE = 1; + // Programmatically created subscription + PROGRAMMATIC = 2; + // Bidirectional Streaming subscription + STREAMING = 3; } message PubsubSubscriptionRules { @@ -694,11 +840,11 @@ message TryLockRequest { // // The reason why we don't make it automatically generated is: // 1. If it is automatically generated,there must be a 'my_lock_owner_id' field in the response. - // This name is so weird that we think it is inappropriate to put it into the api spec + // This name is so weird that we think it is inappropriate to put it into the api spec // 2. If we change the field 'my_lock_owner_id' in the response to 'lock_owner',which means the current lock owner of this lock, - // we find that in some lock services users can't get the current lock owner.Actually users don't need it at all. + // we find that in some lock services users can't get the current lock owner.Actually users don't need it at all. // 3. When reentrant lock is needed,the existing lock_owner is required to identify client and check "whether this client can reenter this lock". - // So this field in the request shouldn't be removed. + // So this field in the request shouldn't be removed. string lock_owner = 3 [json_name = "lockOwner"]; // Required. The time before expiry.The time unit is second. @@ -735,7 +881,7 @@ message SubtleGetKeyRequest { // JSON (JSON Web Key) as string JSON = 1; } - + // Name of the component string component_name = 1 [json_name="componentName"]; // Name (or name/version) of the key to use in the key vault @@ -900,7 +1046,7 @@ message EncryptRequest { // Request details. Must be present in the first message only. EncryptRequestOptions options = 1; // Chunk of data of arbitrary size. - // common.v1.StreamPayload payload = 2; // TODO: Commented out since it was causing an issue + common.v1.StreamPayload payload = 2; } // EncryptRequestOptions contains options for the first message in the EncryptAlpha1 request. @@ -917,7 +1063,7 @@ message EncryptRequestOptions { // If true, the encrypted document does not contain a key reference. // In that case, calls to the Decrypt method must provide a key reference (name or name/version). // Defaults to false. - bool omit_decryption_key_name = 11 [json_name="omitDecryptionKeyName"]; + bool omit_decryption_key_name = 11 [json_name="omitDecryptionKeyName"]; // Key reference to embed in the encrypted document (name or name/version). // This is helpful if the reference of the key used to decrypt the document is different from the one used to encrypt it. // If unset, uses the reference of the key used to encrypt the document (this is the default behavior). @@ -928,7 +1074,7 @@ message EncryptRequestOptions { // EncryptResponse is the response for EncryptAlpha1. message EncryptResponse { // Chunk of data. - // common.v1.StreamPayload payload = 1; // TODO: Commented out since it was causing an issue + common.v1.StreamPayload payload = 1; } // DecryptRequest is the request for DecryptAlpha1. @@ -936,7 +1082,7 @@ message DecryptRequest { // Request details. Must be present in the first message only. DecryptRequestOptions options = 1; // Chunk of data of arbitrary size. - // common.v1.StreamPayload payload = 2; // TODO: Commented out since it was causing an issue + common.v1.StreamPayload payload = 2; } // DecryptRequestOptions contains options for the first message in the DecryptAlpha1 request. @@ -952,10 +1098,10 @@ message DecryptRequestOptions { // DecryptResponse is the response for DecryptAlpha1. message DecryptResponse { // Chunk of data. - // common.v1.StreamPayload payload = 1; // TODO: Commented out since it was causing an issue + common.v1.StreamPayload payload = 1; } -// GetWorkflowRequest is the request for GetWorkflowAlpha1. +// GetWorkflowRequest is the request for GetWorkflowBeta1. message GetWorkflowRequest { // ID of the workflow instance to query. string instance_id = 1 [json_name = "instanceID"]; @@ -963,7 +1109,7 @@ message GetWorkflowRequest { string workflow_component = 2 [json_name = "workflowComponent"]; } -// GetWorkflowResponse is the response for GetWorkflowAlpha1. +// GetWorkflowResponse is the response for GetWorkflowBeta1. message GetWorkflowResponse { // ID of the workflow instance. string instance_id = 1 [json_name = "instanceID"]; @@ -979,7 +1125,7 @@ message GetWorkflowResponse { map properties = 6; } -// StartWorkflowRequest is the request for StartWorkflowAlpha1. +// StartWorkflowRequest is the request for StartWorkflowBeta1. message StartWorkflowRequest { // The ID to assign to the started workflow instance. If empty, a random ID is generated. string instance_id = 1 [json_name = "instanceID"]; @@ -993,13 +1139,13 @@ message StartWorkflowRequest { bytes input = 5; } -// StartWorkflowResponse is the response for StartWorkflowAlpha1. +// StartWorkflowResponse is the response for StartWorkflowBeta1. message StartWorkflowResponse { // ID of the started workflow instance. string instance_id = 1 [json_name = "instanceID"]; } -// TerminateWorkflowRequest is the request for TerminateWorkflowAlpha1. +// TerminateWorkflowRequest is the request for TerminateWorkflowBeta1. message TerminateWorkflowRequest { // ID of the workflow instance to terminate. string instance_id = 1 [json_name = "instanceID"]; @@ -1007,7 +1153,7 @@ message TerminateWorkflowRequest { string workflow_component = 2 [json_name = "workflowComponent"]; } -// PauseWorkflowRequest is the request for PauseWorkflowAlpha1. +// PauseWorkflowRequest is the request for PauseWorkflowBeta1. message PauseWorkflowRequest { // ID of the workflow instance to pause. string instance_id = 1 [json_name = "instanceID"]; @@ -1015,7 +1161,7 @@ message PauseWorkflowRequest { string workflow_component = 2 [json_name = "workflowComponent"]; } -// ResumeWorkflowRequest is the request for ResumeWorkflowAlpha1. +// ResumeWorkflowRequest is the request for ResumeWorkflowBeta1. message ResumeWorkflowRequest { // ID of the workflow instance to resume. string instance_id = 1 [json_name = "instanceID"]; @@ -1023,7 +1169,7 @@ message ResumeWorkflowRequest { string workflow_component = 2 [json_name = "workflowComponent"]; } -// RaiseEventWorkflowRequest is the request for RaiseEventWorkflowAlpha1. +// RaiseEventWorkflowRequest is the request for RaiseEventWorkflowBeta1. message RaiseEventWorkflowRequest { // ID of the workflow instance to raise an event for. string instance_id = 1 [json_name = "instanceID"]; @@ -1035,10 +1181,97 @@ message RaiseEventWorkflowRequest { bytes event_data = 4; } -// PurgeWorkflowRequest is the request for PurgeWorkflowAlpha1. +// PurgeWorkflowRequest is the request for PurgeWorkflowBeta1. message PurgeWorkflowRequest { // ID of the workflow instance to purge. string instance_id = 1 [json_name = "instanceID"]; // Name of the workflow component. string workflow_component = 2 [json_name = "workflowComponent"]; +} + +// ShutdownRequest is the request for Shutdown. +message ShutdownRequest { + // Empty +} + +// Job is the definition of a job. At least one of schedule or due_time must be +// provided but can also be provided together. +message Job { + // The unique name for the job. + string name = 1 [json_name = "name"]; + + // schedule is an optional schedule at which the job is to be run. + // Accepts both systemd timer style cron expressions, as well as human + // readable '@' prefixed period strings as defined below. + // + // Systemd timer style cron accepts 6 fields: + // seconds | minutes | hours | day of month | month | day of week + // 0-59 | 0-59 | 0-23 | 1-31 | 1-12/jan-dec | 0-7/sun-sat + // + // "0 30 * * * *" - every hour on the half hour + // "0 15 3 * * *" - every day at 03:15 + // + // Period string expressions: + // Entry | Description | Equivalent To + // ----- | ----------- | ------------- + // @every `` | Run every `` (e.g. '@every 1h30m') | N/A + // @yearly (or @annually) | Run once a year, midnight, Jan. 1st | 0 0 0 1 1 * + // @monthly | Run once a month, midnight, first of month | 0 0 0 1 * * + // @weekly | Run once a week, midnight on Sunday | 0 0 0 * * 0 + // @daily (or @midnight) | Run once a day, midnight | 0 0 0 * * * + // @hourly | Run once an hour, beginning of hour | 0 0 * * * * + optional string schedule = 2 [json_name = "schedule"]; + + // repeats is the optional number of times in which the job should be + // triggered. If not set, the job will run indefinitely or until expiration. + optional uint32 repeats = 3 [json_name = "repeats"]; + + // due_time is the optional time at which the job should be active, or the + // "one shot" time if other scheduling type fields are not provided. Accepts + // a "point in time" string in the format of RFC3339, Go duration string + // (calculated from job creation time), or non-repeating ISO8601. + optional string due_time = 4 [json_name = "dueTime"]; + + // ttl is the optional time to live or expiration of the job. Accepts a + // "point in time" string in the format of RFC3339, Go duration string + // (calculated from job creation time), or non-repeating ISO8601. + optional string ttl = 5 [json_name = "ttl"]; + + // 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"]; +} + +// ScheduleJobRequest is the message to create/schedule the job. +message ScheduleJobRequest { + // The job details. + Job job = 1; +} + +// ScheduleJobResponse is the message response to create/schedule the job. +message ScheduleJobResponse { + // Empty +} + +// GetJobRequest is the message to retrieve a job. +message GetJobRequest { + // The name of the job. + string name = 1; +} + +// GetJobResponse is the message's response for a job retrieved. +message GetJobResponse { + // The job details. + Job job = 1; +} + +// DeleteJobRequest is the message to delete the job by name. +message DeleteJobRequest { + // The name of the job. + string name = 1; +} + +// DeleteJobResponse is the message response to delete the job by name. +message DeleteJobResponse { + // Empty } \ No newline at end of file diff --git a/src/Dapr.Workflow/Dapr.Workflow.csproj b/src/Dapr.Workflow/Dapr.Workflow.csproj index a951350e8..992baee73 100644 --- a/src/Dapr.Workflow/Dapr.Workflow.csproj +++ b/src/Dapr.Workflow/Dapr.Workflow.csproj @@ -2,24 +2,26 @@ - netcoreapp3.1;net6;net7 + + net6;net7;net8 enable Dapr.Workflow Dapr Workflow Authoring SDK Dapr Workflow SDK for building workflows as code with Dapr - 0.2.0 + 0.3.0 alpha - 10.0 - - + + + + \ No newline at end of file diff --git a/src/Dapr.Workflow/DaprWorkflowClient.cs b/src/Dapr.Workflow/DaprWorkflowClient.cs index e50632be5..e4c88f0ef 100644 --- a/src/Dapr.Workflow/DaprWorkflowClient.cs +++ b/src/Dapr.Workflow/DaprWorkflowClient.cs @@ -10,15 +10,15 @@ // See the License for the specific language governing permissions and // limitations under the License. // ------------------------------------------------------------------------ +using System; +using System.Threading; +using System.Threading.Tasks; +using Dapr.Client; +using Microsoft.DurableTask; +using Microsoft.DurableTask.Client; namespace Dapr.Workflow { - using System; - using System.Threading; - using System.Threading.Tasks; - using Microsoft.DurableTask; - using Microsoft.DurableTask.Client; - /// /// Defines client operations for managing Dapr Workflow instances. /// @@ -70,9 +70,9 @@ public Task ScheduleNewWorkflowAsync( /// 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 false. + /// omit them. Defaults to true. /// - public async Task GetWorkflowStateAsync(string instanceId, bool getInputsAndOutputs = false) + public async Task GetWorkflowStateAsync(string instanceId, bool getInputsAndOutputs = true) { OrchestrationMetadata? metadata = await this.innerClient.GetInstancesAsync( instanceId, @@ -94,7 +94,7 @@ public async Task GetWorkflowStateAsync(string instanceId, bool g /// 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. The default value is false to minimize the network bandwidth, serialization, and memory costs + /// 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. @@ -104,7 +104,7 @@ public async Task GetWorkflowStateAsync(string instanceId, bool g /// public async Task WaitForWorkflowStartAsync( string instanceId, - bool getInputsAndOutputs = false, + bool getInputsAndOutputs = true, CancellationToken cancellation = default) { OrchestrationMetadata metadata = await this.innerClient.WaitForInstanceStartAsync( @@ -135,7 +135,7 @@ public async Task WaitForWorkflowStartAsync( /// public async Task WaitForWorkflowCompletionAsync( string instanceId, - bool getInputsAndOutputs = false, + bool getInputsAndOutputs = true, CancellationToken cancellation = default) { OrchestrationMetadata metadata = await this.innerClient.WaitForInstanceCompletionAsync( @@ -158,10 +158,10 @@ public async Task WaitForWorkflowCompletionAsync( /// the terminated state. /// /// - /// Terminating a workflow instance has no effect on any in-flight activity function executions - /// or child workflows that were started by the terminated instance. Those actions will continue to run - /// without interruption. However, their results will be discarded. If you want to terminate child-workflows, - /// you must issue separate terminate commands for each child workflow instance individually. + /// 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. /// @@ -178,7 +178,11 @@ public Task TerminateWorkflowAsync( string? output = null, CancellationToken cancellation = default) { - return this.innerClient.TerminateInstanceAsync(instanceId, output, cancellation); + TerminateInstanceOptions options = new TerminateInstanceOptions { + Output = output, + Recursive = true, + }; + return this.innerClient.TerminateInstanceAsync(instanceId, options, cancellation); } /// @@ -218,7 +222,7 @@ public Task RaiseEventAsync( object? eventPayload = null, CancellationToken cancellation = default) { - return this.innerClient.RaiseEventAsync(instanceId, eventName, cancellation); + return this.innerClient.RaiseEventAsync(instanceId, eventName, eventPayload, cancellation); } /// @@ -269,6 +273,9 @@ public Task ResumeWorkflowAsync( /// , , 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. /// @@ -280,7 +287,8 @@ public Task ResumeWorkflowAsync( /// public async Task PurgeInstanceAsync(string instanceId, CancellationToken cancellation = default) { - PurgeResult result = await this.innerClient.PurgeInstanceAsync(instanceId, cancellation); + PurgeInstanceOptions options = new PurgeInstanceOptions {Recursive = true}; + PurgeResult result = await this.innerClient.PurgeInstanceAsync(instanceId, options, cancellation); return result.PurgedInstanceCount > 0; } diff --git a/src/Dapr.Workflow/DaprWorkflowContext.cs b/src/Dapr.Workflow/DaprWorkflowContext.cs index d78b0e2df..be08ef421 100644 --- a/src/Dapr.Workflow/DaprWorkflowContext.cs +++ b/src/Dapr.Workflow/DaprWorkflowContext.cs @@ -35,14 +35,14 @@ internal DaprWorkflowContext(TaskOrchestrationContext innerContext) public override bool IsReplaying => this.innerContext.IsReplaying; - public override Task CallActivityAsync(string name, object? input = null, TaskOptions? options = null) + public override Task CallActivityAsync(string name, object? input = null, WorkflowTaskOptions? options = null) { - return this.innerContext.CallActivityAsync(name, input, options); + return WrapExceptions(this.innerContext.CallActivityAsync(name, input, options?.ToDurableTaskOptions())); } - public override Task CallActivityAsync(string name, object? input = null, TaskOptions? options = null) + public override Task CallActivityAsync(string name, object? input = null, WorkflowTaskOptions? options = null) { - return this.innerContext.CallActivityAsync(name, input, options); + return WrapExceptions(this.innerContext.CallActivityAsync(name, input, options?.ToDurableTaskOptions())); } public override Task CreateTimer(TimeSpan delay, CancellationToken cancellationToken = default) @@ -75,14 +75,14 @@ public override void SetCustomStatus(object? customStatus) this.innerContext.SetCustomStatus(customStatus); } - public override Task CallChildWorkflowAsync(string workflowName, object? input = null, TaskOptions? options = null) + public override Task CallChildWorkflowAsync(string workflowName, object? input = null, ChildWorkflowTaskOptions? options = null) { - return this.innerContext.CallSubOrchestratorAsync(workflowName, input, options); + return WrapExceptions(this.innerContext.CallSubOrchestratorAsync(workflowName, input, options?.ToDurableTaskOptions())); } - public override Task CallChildWorkflowAsync(string workflowName, object? input = null, TaskOptions? options = null) + public override Task CallChildWorkflowAsync(string workflowName, object? input = null, ChildWorkflowTaskOptions? options = null) { - return this.innerContext.CallSubOrchestratorAsync(workflowName, input, options); + return WrapExceptions(this.innerContext.CallSubOrchestratorAsync(workflowName, input, options?.ToDurableTaskOptions())); } public override void ContinueAsNew(object? newInput = null, bool preserveUnprocessedEvents = true) @@ -94,5 +94,31 @@ public override Guid NewGuid() { return this.innerContext.NewGuid(); } + + 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); + } + } } } diff --git a/src/Dapr.Workflow/WorkflowContext.cs b/src/Dapr.Workflow/WorkflowContext.cs index f6b7836b8..98b8be96b 100644 --- a/src/Dapr.Workflow/WorkflowContext.cs +++ b/src/Dapr.Workflow/WorkflowContext.cs @@ -16,7 +16,6 @@ namespace Dapr.Workflow using System; using System.Threading; using System.Threading.Tasks; - using Microsoft.DurableTask; /// /// Context object used by workflow implementations to perform actions such as scheduling activities, durable timers, waiting for @@ -97,11 +96,11 @@ public abstract class WorkflowContext /// /// 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. + /// property. /// - public virtual Task CallActivityAsync(string name, object? input = null, TaskOptions? options = null) + public virtual Task CallActivityAsync(string name, object? input = null, WorkflowTaskOptions? options = null) { return this.CallActivityAsync(name, input, options); } @@ -110,7 +109,7 @@ public virtual Task CallActivityAsync(string name, object? input = null, TaskOpt /// 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, TaskOptions? options = null); + public abstract Task CallActivityAsync(string name, object? input = null, WorkflowTaskOptions? options = null); /// /// Creates a durable timer that expires after the specified delay. @@ -160,6 +159,9 @@ public virtual Task CreateTimer(TimeSpan delay, CancellationToken cancellationTo /// /// 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. /// @@ -173,6 +175,9 @@ public virtual Task CreateTimer(TimeSpan delay, CancellationToken cancellationTo /// 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); @@ -212,8 +217,11 @@ public virtual Task CreateTimer(TimeSpan delay, CancellationToken cancellationTo /// /// The type into which to deserialize the child workflow's output. /// - /// - public abstract Task CallChildWorkflowAsync(string workflowName, object? input = null, TaskOptions? options = null); + /// + public abstract Task CallChildWorkflowAsync( + string workflowName, + object? input = null, + ChildWorkflowTaskOptions? options = null); /// /// Executes the specified workflow as a child workflow. @@ -222,7 +230,8 @@ public virtual Task CreateTimer(TimeSpan delay, CancellationToken cancellationTo /// /// 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. + /// 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: /// @@ -236,27 +245,29 @@ public virtual Task CreateTimer(TimeSpan delay, CancellationToken cancellationTo /// 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. /// - /// Because child workflows are independent of their parents, terminating a parent workflow does not affect - /// any child workflows. You must terminate each child workflow independently using its instance ID, which is - /// specified by supplying in place of . + /// 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. Callers can choose to - /// supply the derived type . + /// 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. + /// property. /// - public virtual Task CallChildWorkflowAsync(string workflowName, object? input = null, TaskOptions? options = null) + public virtual Task CallChildWorkflowAsync( + string workflowName, + object? input = null, + ChildWorkflowTaskOptions? options = null) { return this.CallChildWorkflowAsync(workflowName, input, options); } diff --git a/src/Dapr.Workflow/WorkflowLoggingService.cs b/src/Dapr.Workflow/WorkflowLoggingService.cs new file mode 100644 index 000000000..482d95b97 --- /dev/null +++ b/src/Dapr.Workflow/WorkflowLoggingService.cs @@ -0,0 +1,75 @@ +// ------------------------------------------------------------------------ +// Copyright 2022 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.Workflow +{ + using System; + 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 : IHostedService + { + private readonly ILogger logger; + private static readonly HashSet registeredWorkflows = new(); + private static readonly HashSet registeredActivities = new(); + + public WorkflowLoggingService(ILogger logger, IConfiguration configuration) + { + 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; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + 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); + } + + } +} diff --git a/src/Dapr.Workflow/WorkflowRetryPolicy.cs b/src/Dapr.Workflow/WorkflowRetryPolicy.cs new file mode 100644 index 000000000..35f0f9108 --- /dev/null +++ b/src/Dapr.Workflow/WorkflowRetryPolicy.cs @@ -0,0 +1,104 @@ +// ------------------------------------------------------------------------ +// 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; +using System.Threading; +using Microsoft.DurableTask; + +namespace Dapr.Workflow +{ + /// + /// A declarative retry policy that can be configured for activity or child workflow calls. + /// + public class WorkflowRetryPolicy + { + readonly RetryPolicy durableRetryPolicy; + + /// + /// 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 max number of attempts for executing a given task. + /// + public int MaxNumberOfAttempts => this.durableRetryPolicy.MaxNumberOfAttempts; + + /// + /// Gets the amount of time to delay between the first and second attempt. + /// + public TimeSpan FirstRetryInterval => this.durableRetryPolicy.FirstRetryInterval; + + /// + /// 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 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; + } +} diff --git a/src/Dapr.Workflow/WorkflowRuntimeOptions.cs b/src/Dapr.Workflow/WorkflowRuntimeOptions.cs index 4dd202b1a..adc925777 100644 --- a/src/Dapr.Workflow/WorkflowRuntimeOptions.cs +++ b/src/Dapr.Workflow/WorkflowRuntimeOptions.cs @@ -54,6 +54,7 @@ public void RegisterWorkflow(string name, Func(string name, Func(); return new OrchestratorWrapper(workflow); }); + WorkflowLoggingService.LogWorkflowName(name); }); } @@ -91,6 +93,7 @@ public void RegisterActivity(string name, Func() where TActivity : class, IWorkflowActi TActivity activity = ActivatorUtilities.CreateInstance(serviceProvider); return new ActivityWrapper(activity); }); + WorkflowLoggingService.LogActivityName(name); }); } diff --git a/src/Dapr.Workflow/WorkflowRuntimeStatus.cs b/src/Dapr.Workflow/WorkflowRuntimeStatus.cs index 01198d327..24024cd63 100644 --- a/src/Dapr.Workflow/WorkflowRuntimeStatus.cs +++ b/src/Dapr.Workflow/WorkflowRuntimeStatus.cs @@ -14,7 +14,7 @@ namespace Dapr.Workflow { /// - /// Enum describing the runtime status of the workflow. + /// Enum describing the runtime status of a workflow. /// public enum WorkflowRuntimeStatus { diff --git a/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs b/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs index 161c29175..ca514f221 100644 --- a/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs +++ b/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs @@ -14,16 +14,20 @@ namespace Dapr.Workflow { using System; + using Grpc.Net.Client; using Microsoft.DurableTask.Client; using Microsoft.DurableTask.Worker; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; + using System.Net.Http; + using Dapr; /// /// Contains extension methods for using Dapr Workflow with dependency injection. /// public static class WorkflowServiceCollectionExtensions { + /// /// Adds Dapr Workflow support to the service collection. /// @@ -43,7 +47,7 @@ public static IServiceCollection AddDaprWorkflow( #pragma warning disable CS0618 // Type or member is obsolete - keeping around temporarily - replaced by DaprWorkflowClient serviceCollection.TryAddSingleton(); #pragma warning restore CS0618 // Type or member is obsolete - + serviceCollection.AddHostedService(); serviceCollection.TryAddSingleton(); serviceCollection.AddDaprClient(); serviceCollection.AddDaprWorkflowClient(); @@ -57,7 +61,18 @@ public static IServiceCollection AddDaprWorkflow( if (TryGetGrpcAddress(out string address)) { - builder.UseGrpc(address); + var daprApiToken = DaprDefaults.GetDefaultDaprApiToken(); + if (!string.IsNullOrEmpty(daprApiToken)) + { + var client = new HttpClient(); + client.DefaultRequestHeaders.Add("Dapr-Api-Token", daprApiToken); + builder.UseGrpc(CreateChannel(address, client)); + } + else + { + builder.UseGrpc(address); + } + } else { @@ -85,7 +100,18 @@ public static IServiceCollection AddDaprWorkflowClient(this IServiceCollection s { if (TryGetGrpcAddress(out string address)) { - builder.UseGrpc(address); + var daprApiToken = DaprDefaults.GetDefaultDaprApiToken(); + if (!string.IsNullOrEmpty(daprApiToken)) + { + var client = new HttpClient(); + client.DefaultRequestHeaders.Add("Dapr-Api-Token", daprApiToken); + builder.UseGrpc(CreateChannel(address, client)); + } + else + { + builder.UseGrpc(address); + } + } else { @@ -104,7 +130,13 @@ static bool TryGetGrpcAddress(out string address) // 1. DaprDefaults.cs uses 127.0.0.1 instead of localhost, which prevents testing with Dapr on WSL2 and the app on Windows // 2. DaprDefaults.cs doesn't compile when the project has C# nullable reference types enabled. // If the above issues are fixed (ensuring we don't regress anything) we should switch to using the logic in DaprDefaults.cs. - string? daprPortStr = Environment.GetEnvironmentVariable("DAPR_GRPC_PORT"); + var daprEndpoint = DaprDefaults.GetDefaultGrpcEndpoint(); + if (!String.IsNullOrEmpty(daprEndpoint)) { + address = daprEndpoint; + return true; + } + + var daprPortStr = Environment.GetEnvironmentVariable("DAPR_GRPC_PORT"); if (int.TryParse(daprPortStr, out int daprGrpcPort)) { // There is a bug in the Durable Task SDK that requires us to change the format of the address @@ -120,6 +152,33 @@ static bool TryGetGrpcAddress(out string address) address = string.Empty; return false; } + + static GrpcChannel CreateChannel(string address, HttpClient client) + { + + GrpcChannelOptions options = new() { HttpClient = client}; + var daprEndpoint = DaprDefaults.GetDefaultGrpcEndpoint(); + if (!String.IsNullOrEmpty(daprEndpoint)) { + return GrpcChannel.ForAddress(daprEndpoint, options); + } + + var daprPortStr = Environment.GetEnvironmentVariable("DAPR_GRPC_PORT"); + if (int.TryParse(daprPortStr, out int daprGrpcPort)) + { + // If there is no address passed in, we default to localhost + if (String.IsNullOrEmpty(address)) + { + // There is a bug in the Durable Task SDK that requires us to change the format of the address + // depending on the version of .NET that we're targeting. For now, we work around this manually. + #if NET6_0_OR_GREATER + address = $"http://localhost:{daprGrpcPort}"; + #else + address = $"localhost:{daprGrpcPort}"; + #endif + } + + } + return GrpcChannel.ForAddress(address, options); + } } } - diff --git a/src/Dapr.Workflow/WorkflowState.cs b/src/Dapr.Workflow/WorkflowState.cs index 7be914133..ea1ffae22 100644 --- a/src/Dapr.Workflow/WorkflowState.cs +++ b/src/Dapr.Workflow/WorkflowState.cs @@ -14,6 +14,7 @@ namespace Dapr.Workflow { using System; + using Dapr.Client; using Microsoft.DurableTask.Client; /// @@ -83,6 +84,8 @@ public WorkflowRuntimeStatus RuntimeStatus return WorkflowRuntimeStatus.Terminated; case OrchestrationRuntimeStatus.Pending: return WorkflowRuntimeStatus.Pending; + case OrchestrationRuntimeStatus.Suspended: + return WorkflowRuntimeStatus.Suspended; default: return WorkflowRuntimeStatus.Unknown; } diff --git a/src/Dapr.Workflow/WorkflowTaskFailedException.cs b/src/Dapr.Workflow/WorkflowTaskFailedException.cs new file mode 100644 index 000000000..575947a3a --- /dev/null +++ b/src/Dapr.Workflow/WorkflowTaskFailedException.cs @@ -0,0 +1,39 @@ +// ------------------------------------------------------------------------ +// 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. +// ------------------------------------------------------------------------ + +namespace Dapr.Workflow +{ + using System; + + /// + /// Exception type for Dapr Workflow task failures. + /// + 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; } + } +} diff --git a/src/Dapr.Workflow/WorkflowTaskOptions.cs b/src/Dapr.Workflow/WorkflowTaskOptions.cs new file mode 100644 index 000000000..d93dbc551 --- /dev/null +++ b/src/Dapr.Workflow/WorkflowTaskOptions.cs @@ -0,0 +1,68 @@ +// ------------------------------------------------------------------------ +// 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 Microsoft.DurableTask; + +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) + { + internal TaskOptions ToDurableTaskOptions() + { + 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; + } + + /// + /// 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); + } + } +} diff --git a/src/Directory.Build.props b/src/Directory.Build.props index cdfef31a6..35f0fbf7c 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -3,6 +3,7 @@ + net6;net8 $(RepoRoot)bin\$(Configuration)\prod\$(MSBuildProjectName)\ $(OutputPath)$(MSBuildProjectName).xml @@ -14,7 +15,7 @@ true - + diff --git a/test/Dapr.Actors.AspNetCore.IntegrationTest.App/Dapr.Actors.AspNetCore.IntegrationTest.App.csproj b/test/Dapr.Actors.AspNetCore.IntegrationTest.App/Dapr.Actors.AspNetCore.IntegrationTest.App.csproj index a518db3f5..c06d651f4 100644 --- a/test/Dapr.Actors.AspNetCore.IntegrationTest.App/Dapr.Actors.AspNetCore.IntegrationTest.App.csproj +++ b/test/Dapr.Actors.AspNetCore.IntegrationTest.App/Dapr.Actors.AspNetCore.IntegrationTest.App.csproj @@ -1,9 +1,5 @@ - - netcoreapp3.1;net6;net7 - - diff --git a/test/Dapr.Actors.AspNetCore.IntegrationTest/Dapr.Actors.AspNetCore.IntegrationTest.csproj b/test/Dapr.Actors.AspNetCore.IntegrationTest/Dapr.Actors.AspNetCore.IntegrationTest.csproj index 0f2d6b580..921e2dda4 100644 --- a/test/Dapr.Actors.AspNetCore.IntegrationTest/Dapr.Actors.AspNetCore.IntegrationTest.csproj +++ b/test/Dapr.Actors.AspNetCore.IntegrationTest/Dapr.Actors.AspNetCore.IntegrationTest.csproj @@ -1,18 +1,14 @@ - - netcoreapp3.1;net6;net7 - - - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - - - + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/test/Dapr.Actors.AspNetCore.Test/Dapr.Actors.AspNetCore.Test.csproj b/test/Dapr.Actors.AspNetCore.Test/Dapr.Actors.AspNetCore.Test.csproj index 071b72746..c448e915c 100644 --- a/test/Dapr.Actors.AspNetCore.Test/Dapr.Actors.AspNetCore.Test.csproj +++ b/test/Dapr.Actors.AspNetCore.Test/Dapr.Actors.AspNetCore.Test.csproj @@ -1,22 +1,20 @@ - netcoreapp3.1;net6;net7 Dapr.Actors.AspNetCore - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - - - - - + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/test/Dapr.Actors.Generators.Test/ActorClientGeneratorTests.cs b/test/Dapr.Actors.Generators.Test/ActorClientGeneratorTests.cs new file mode 100644 index 000000000..ce4c0accd --- /dev/null +++ b/test/Dapr.Actors.Generators.Test/ActorClientGeneratorTests.cs @@ -0,0 +1,696 @@ +// ------------------------------------------------------------------------ +// 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. +// ------------------------------------------------------------------------ + +namespace Dapr.Actors.Generators; + +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.CodeAnalysis.Text; +using VerifyCS = CSharpSourceGeneratorVerifier; + +public sealed class ActorClientGeneratorTests +{ + private const string ActorMethodAttributeText = $@" + // + + #nullable enable + + using System; + + namespace Dapr.Actors.Generators + {{ + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] + internal sealed class ActorMethodAttribute : Attribute + {{ + public string? Name {{ get; set; }} + }} + }}"; + + private static readonly (string, SourceText) ActorMethodAttributeSource = ("Dapr.Actors.Generators/Dapr.Actors.Generators.ActorClientGenerator/Dapr.Actors.Generators.ActorMethodAttribute.g.cs", SourceText.From(ActorMethodAttributeText, Encoding.UTF8)); + + private const string GenerateActorClientAttributeText = $@" + // + + #nullable enable + + using System; + + namespace Dapr.Actors.Generators + {{ + [AttributeUsage(AttributeTargets.Interface, AllowMultiple = false, Inherited = false)] + internal sealed class GenerateActorClientAttribute : Attribute + {{ + public string? Name {{ get; set; }} + + public string? Namespace {{ get; set; }} + }} + }}"; + + private static readonly (string, SourceText) GenerateActorClientAttributeSource = ("Dapr.Actors.Generators/Dapr.Actors.Generators.ActorClientGenerator/Dapr.Actors.Generators.GenerateActorClientAttribute.g.cs", SourceText.From(GenerateActorClientAttributeText, Encoding.UTF8)); + + private static VerifyCS.Test CreateTest(string originalSource, string? generatedName = null, string? generatedSource = null) + { + var test = new VerifyCS.Test + { + TestState = + { + AdditionalReferences = { AdditionalMetadataReferences.Actors }, + Sources = { originalSource }, + GeneratedSources = + { + ActorMethodAttributeSource, + GenerateActorClientAttributeSource + }, + } + }; + + if (generatedName is not null && generatedSource is not null) + { + test.TestState.GeneratedSources.Add(($"Dapr.Actors.Generators/Dapr.Actors.Generators.ActorClientGenerator/{generatedName}", SourceText.From(generatedSource, Encoding.UTF8))); + } + + return test; + } + + [Fact] + public async Task TestMethodWithNoArgumentsOrReturnValue() + { + var originalSource = @" +using Dapr.Actors.Generators; +using System.Threading.Tasks; + +namespace Test +{ + [GenerateActorClient] + public interface ITestActor + { + Task TestMethod(); + } +} +"; + + var generatedSource = @" +// + +namespace Test +{ + public sealed class TestActorClient : Test.ITestActor + { + private readonly Dapr.Actors.Client.ActorProxy actorProxy; + + public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) + { + this.actorProxy = actorProxy; + } + + public System.Threading.Tasks.Task TestMethod() + { + return this.actorProxy.InvokeMethodAsync(""TestMethod""); + } + } +} +"; + + await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync(); + } + + [Fact] + public async Task TestInternalInterface() + { + var originalSource = @" +using Dapr.Actors.Generators; +using System.Threading.Tasks; + +namespace Test +{ + [GenerateActorClient] + internal interface ITestActor + { + Task TestMethod(); + } +} +"; + + var generatedSource = @" +// + +namespace Test +{ + internal sealed class TestActorClient : Test.ITestActor + { + private readonly Dapr.Actors.Client.ActorProxy actorProxy; + + public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) + { + this.actorProxy = actorProxy; + } + + public System.Threading.Tasks.Task TestMethod() + { + return this.actorProxy.InvokeMethodAsync(""TestMethod""); + } + } +} +"; + + await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync(); + } + + [Fact] + public async Task TestRenamedClient() + { + var originalSource = @" +using Dapr.Actors.Generators; +using System.Threading.Tasks; + +namespace Test +{ + [GenerateActorClient(Name = ""MyTestActorClient"")] + internal interface ITestActor + { + Task TestMethod(); + } +} +"; + + var generatedSource = @" +// + +namespace Test +{ + internal sealed class MyTestActorClient : Test.ITestActor + { + private readonly Dapr.Actors.Client.ActorProxy actorProxy; + + public MyTestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) + { + this.actorProxy = actorProxy; + } + + public System.Threading.Tasks.Task TestMethod() + { + return this.actorProxy.InvokeMethodAsync(""TestMethod""); + } + } +} +"; + + await CreateTest(originalSource, "Test.MyTestActorClient.g.cs", generatedSource).RunAsync(); + } + + [Fact] + public async Task TestCustomNamespace() + { + var originalSource = @" +using Dapr.Actors.Generators; +using System.Threading.Tasks; + +namespace Test +{ + [GenerateActorClient(Namespace = ""MyTest"")] + internal interface ITestActor + { + Task TestMethod(); + } +} +"; + + var generatedSource = @" +// + +namespace MyTest +{ + internal sealed class TestActorClient : Test.ITestActor + { + private readonly Dapr.Actors.Client.ActorProxy actorProxy; + + public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) + { + this.actorProxy = actorProxy; + } + + public System.Threading.Tasks.Task TestMethod() + { + return this.actorProxy.InvokeMethodAsync(""TestMethod""); + } + } +} +"; + + await CreateTest(originalSource, "MyTest.TestActorClient.g.cs", generatedSource).RunAsync(); + } + + [Fact] + public async Task TestRenamedMethod() + { + var originalSource = @" +using Dapr.Actors.Generators; +using System.Threading.Tasks; + +namespace Test +{ + [GenerateActorClient] + public interface ITestActor + { + [ActorMethod(Name = ""MyTestMethod"")] + Task TestMethod(); + } +} +"; + + var generatedSource = @" +// + +namespace Test +{ + public sealed class TestActorClient : Test.ITestActor + { + private readonly Dapr.Actors.Client.ActorProxy actorProxy; + + public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) + { + this.actorProxy = actorProxy; + } + + public System.Threading.Tasks.Task TestMethod() + { + return this.actorProxy.InvokeMethodAsync(""MyTestMethod""); + } + } +} +"; + + await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync(); + } + + [Fact] + public async Task TestMethodWithArgumentsButNoReturnValue() + { + var originalSource = @" +using Dapr.Actors.Generators; +using System.Threading.Tasks; + +namespace Test +{ + public record TestValue(int Value); + + [GenerateActorClient] + public interface ITestActor + { + Task TestMethod(TestValue value); + } +} +"; + + var generatedSource = @" +// + +namespace Test +{ + public sealed class TestActorClient : Test.ITestActor + { + private readonly Dapr.Actors.Client.ActorProxy actorProxy; + + public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) + { + this.actorProxy = actorProxy; + } + + public System.Threading.Tasks.Task TestMethod(Test.TestValue value) + { + return this.actorProxy.InvokeMethodAsync(""TestMethod"", value); + } + } +} +"; + + await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync(); + } + + [Fact] + public async Task TestMethodWithNoArgumentsButReturnValue() + { + var originalSource = @" +using Dapr.Actors.Generators; +using System.Threading.Tasks; + +namespace Test +{ + public record TestValue(int Value); + + [GenerateActorClient] + public interface ITestActor + { + Task TestMethodAsync(); + } +} +"; + + var generatedSource = @" +// + +namespace Test +{ + public sealed class TestActorClient : Test.ITestActor + { + private readonly Dapr.Actors.Client.ActorProxy actorProxy; + + public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) + { + this.actorProxy = actorProxy; + } + + public System.Threading.Tasks.Task TestMethodAsync() + { + return this.actorProxy.InvokeMethodAsync(""TestMethodAsync""); + } + } +} +"; + + await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync(); + } + + [Fact] + public async Task TestMethodWithArgumentsAndReturnValue() + { + var originalSource = @" +using Dapr.Actors.Generators; +using System.Threading.Tasks; + +namespace Test +{ + public record TestRequestValue(int Value); + + public record TestReturnValue(int Value); + + [GenerateActorClient] + public interface ITestActor + { + Task TestMethodAsync(TestRequestValue value); + } +} +"; + + var generatedSource = @" +// + +namespace Test +{ + public sealed class TestActorClient : Test.ITestActor + { + private readonly Dapr.Actors.Client.ActorProxy actorProxy; + + public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) + { + this.actorProxy = actorProxy; + } + + public System.Threading.Tasks.Task TestMethodAsync(Test.TestRequestValue value) + { + return this.actorProxy.InvokeMethodAsync(""TestMethodAsync"", value); + } + } +} +"; + + await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync(); + } + + [Fact] + public async Task TestMethodWithCancellationTokenArgument() + { + var originalSource = @" +using Dapr.Actors.Generators; +using System.Threading; +using System.Threading.Tasks; + +namespace Test +{ + [GenerateActorClient] + public interface ITestActor + { + Task TestMethodAsync(CancellationToken cancellationToken); + } +} +"; + + var generatedSource = @" +// + +namespace Test +{ + public sealed class TestActorClient : Test.ITestActor + { + private readonly Dapr.Actors.Client.ActorProxy actorProxy; + + public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) + { + this.actorProxy = actorProxy; + } + + public System.Threading.Tasks.Task TestMethodAsync(System.Threading.CancellationToken cancellationToken) + { + return this.actorProxy.InvokeMethodAsync(""TestMethodAsync"", cancellationToken); + } + } +} +"; + + await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync(); + } + + [Fact] + public async Task TestMethodWithDefaultCancellationTokenArgument() + { + var originalSource = @" +using Dapr.Actors.Generators; +using System.Threading; +using System.Threading.Tasks; + +namespace Test +{ + [GenerateActorClient] + public interface ITestActor + { + Task TestMethodAsync(CancellationToken cancellationToken = default); + } +} +"; + + var generatedSource = @" +// + +namespace Test +{ + public sealed class TestActorClient : Test.ITestActor + { + private readonly Dapr.Actors.Client.ActorProxy actorProxy; + + public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) + { + this.actorProxy = actorProxy; + } + + public System.Threading.Tasks.Task TestMethodAsync(System.Threading.CancellationToken cancellationToken = default) + { + return this.actorProxy.InvokeMethodAsync(""TestMethodAsync"", cancellationToken); + } + } +} +"; + + await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync(); + } + + [Fact] + public async Task TestMethodWithValueAndCancellationTokenArguments() + { + var originalSource = @" +using Dapr.Actors.Generators; +using System.Threading; +using System.Threading.Tasks; + +namespace Test +{ + public record TestValue(int Value); + + [GenerateActorClient] + public interface ITestActor + { + Task TestMethodAsync(TestValue value, CancellationToken cancellationToken); + } +} +"; + + var generatedSource = @" +// + +namespace Test +{ + public sealed class TestActorClient : Test.ITestActor + { + private readonly Dapr.Actors.Client.ActorProxy actorProxy; + + public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) + { + this.actorProxy = actorProxy; + } + + public System.Threading.Tasks.Task TestMethodAsync(Test.TestValue value, System.Threading.CancellationToken cancellationToken) + { + return this.actorProxy.InvokeMethodAsync(""TestMethodAsync"", value, cancellationToken); + } + } +} +"; + + await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync(); + } + + [Fact] + public async Task TestMethodWithValueAndDefaultCancellationTokenArguments() + { + var originalSource = @" +using Dapr.Actors.Generators; +using System.Threading; +using System.Threading.Tasks; + +namespace Test +{ + public record TestValue(int Value); + + [GenerateActorClient] + public interface ITestActor + { + Task TestMethodAsync(TestValue value, CancellationToken cancellationToken = default); + } +} +"; + + var generatedSource = @" +// + +namespace Test +{ + public sealed class TestActorClient : Test.ITestActor + { + private readonly Dapr.Actors.Client.ActorProxy actorProxy; + + public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) + { + this.actorProxy = actorProxy; + } + + public System.Threading.Tasks.Task TestMethodAsync(Test.TestValue value, System.Threading.CancellationToken cancellationToken = default) + { + return this.actorProxy.InvokeMethodAsync(""TestMethodAsync"", value, cancellationToken); + } + } +} +"; + + await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync(); + } + + [Fact] + public async Task TestMethodWithReversedArguments() + { + var originalSource = @" +using Dapr.Actors.Generators; +using System.Threading; +using System.Threading.Tasks; + +namespace Test +{ + public record TestValue(int Value); + + [GenerateActorClient] + public interface ITestActor + { + Task TestMethodAsync(CancellationToken cancellationToken, int value); + } +} +"; + + var test = CreateTest(originalSource); + + test.TestState.ExpectedDiagnostics.Add( + new DiagnosticResult("DAPR0001", DiagnosticSeverity.Error) + .WithSpan(13, 48, 13, 65) + .WithMessage("Cancellation tokens must be the last argument.")); + + await test.RunAsync(); + } + + [Fact] + public async Task TestMethodWithTooManyArguments() + { + var originalSource = @" +using Dapr.Actors.Generators; +using System.Threading; +using System.Threading.Tasks; + +namespace Test +{ + public record TestValue(int Value); + + [GenerateActorClient] + public interface ITestActor + { + Task TestMethodAsync(int value1, int value2); + } +} +"; + + var test = CreateTest(originalSource); + + test.TestState.ExpectedDiagnostics.Add( + new DiagnosticResult("DAPR0002", DiagnosticSeverity.Error) + .WithSpan(13, 14, 13, 29) + .WithMessage("Only methods with a single argument or a single argument followed by a cancellation token are supported.")); + + await test.RunAsync(); + } + + [Fact] + public async Task TestMethodWithFarTooManyArguments() + { + var originalSource = @" +using Dapr.Actors.Generators; +using System.Threading; +using System.Threading.Tasks; + +namespace Test +{ + public record TestValue(int Value); + + [GenerateActorClient] + public interface ITestActor + { + Task TestMethodAsync(int value1, int value2, CancellationToken cancellationToken); + } +} +"; + + var test = CreateTest(originalSource); + + test.TestState.ExpectedDiagnostics.Add( + new DiagnosticResult("DAPR0002", DiagnosticSeverity.Error) + .WithSpan(13, 14, 13, 29) + .WithMessage("Only methods with a single argument or a single argument followed by a cancellation token are supported.")); + + await test.RunAsync(); + } +} \ No newline at end of file diff --git a/test/Dapr.Actors.Generators.Test/AdditionalMetadataReferences.cs b/test/Dapr.Actors.Generators.Test/AdditionalMetadataReferences.cs new file mode 100644 index 000000000..afa557026 --- /dev/null +++ b/test/Dapr.Actors.Generators.Test/AdditionalMetadataReferences.cs @@ -0,0 +1,21 @@ +// ------------------------------------------------------------------------ +// 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 Microsoft.CodeAnalysis; + +namespace Dapr.Actors.Generators; + +internal static class AdditionalMetadataReferences +{ + public static readonly MetadataReference Actors = MetadataReference.CreateFromFile(typeof(Dapr.Actors.Client.ActorProxy).Assembly.Location); +} \ No newline at end of file diff --git a/test/Dapr.Actors.Generators.Test/CSharpSourceGeneratorVerifier.cs b/test/Dapr.Actors.Generators.Test/CSharpSourceGeneratorVerifier.cs new file mode 100644 index 000000000..2b1046e1a --- /dev/null +++ b/test/Dapr.Actors.Generators.Test/CSharpSourceGeneratorVerifier.cs @@ -0,0 +1,83 @@ +// ------------------------------------------------------------------------ +// 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.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.CodeAnalysis.Testing.Verifiers; + +/// +/// From Roslyn Source Generators Cookbook: https://github.com/dotnet/roslyn/blob/main/docs/features/source-generators.cookbook.md#unit-testing-of-generators +/// +internal static class CSharpSourceGeneratorVerifier + where TSourceGenerator : ISourceGenerator, new() +{ +#pragma warning disable CS0618 // Type or member is obsolete + public class Test : CSharpSourceGeneratorTest +#pragma warning restore CS0618 // Type or member is obsolete + { + public Test() + { + int frameworkVersion = + #if NET6_0 + 6; + #elif NET7_0 + 7; + #elif NET8_0 + 8; + #endif + + // + // NOTE: Ordinarily we'd use the following: + // + // this.ReferenceAssemblies = Microsoft.CodeAnalysis.Testing.ReferenceAssemblies.Net.Net60; + // + // However, Net70 and Net80 are not yet available in the current version of the Roslyn SDK. + // + + this.ReferenceAssemblies = + new ReferenceAssemblies( + $"net{frameworkVersion}.0", + new PackageIdentity( + "Microsoft.NETCore.App.Ref", + $"{frameworkVersion}.0.0"), + Path.Combine("ref", $"net{frameworkVersion}.0")); + } + + protected override CompilationOptions CreateCompilationOptions() + { + var compilationOptions = base.CreateCompilationOptions(); + + return compilationOptions + .WithSpecificDiagnosticOptions(compilationOptions.SpecificDiagnosticOptions.SetItems(GetNullableWarningsFromCompiler())); + } + + public LanguageVersion LanguageVersion { get; set; } = LanguageVersion.Default; + + private static ImmutableDictionary GetNullableWarningsFromCompiler() + { + string[] args = { "/warnaserror:nullable" }; + var commandLineArguments = CSharpCommandLineParser.Default.Parse(args, baseDirectory: Environment.CurrentDirectory, sdkDirectory: Environment.CurrentDirectory); + var nullableWarnings = commandLineArguments.CompilationOptions.SpecificDiagnosticOptions; + + return nullableWarnings; + } + + protected override ParseOptions CreateParseOptions() + { + return ((CSharpParseOptions)base.CreateParseOptions()).WithLanguageVersion(LanguageVersion); + } + } +} diff --git a/test/Dapr.Actors.Generators.Test/Dapr.Actors.Generators.Test.csproj b/test/Dapr.Actors.Generators.Test/Dapr.Actors.Generators.Test.csproj new file mode 100644 index 000000000..91c7e8b42 --- /dev/null +++ b/test/Dapr.Actors.Generators.Test/Dapr.Actors.Generators.Test.csproj @@ -0,0 +1,32 @@ + + + + enable + enable + + true + + + + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + diff --git a/test/Dapr.Actors.Generators.Test/GlobalUsings.cs b/test/Dapr.Actors.Generators.Test/GlobalUsings.cs new file mode 100644 index 000000000..48f0c59b2 --- /dev/null +++ b/test/Dapr.Actors.Generators.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.Actors.Test/ActorCodeBuilderTests.cs b/test/Dapr.Actors.Test/ActorCodeBuilderTests.cs index 93f6ba92f..6bb3c827d 100644 --- a/test/Dapr.Actors.Test/ActorCodeBuilderTests.cs +++ b/test/Dapr.Actors.Test/ActorCodeBuilderTests.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/test/Dapr.Actors.Test/ActorReferenceTests.cs b/test/Dapr.Actors.Test/ActorReferenceTests.cs new file mode 100644 index 000000000..7450f616c --- /dev/null +++ b/test/Dapr.Actors.Test/ActorReferenceTests.cs @@ -0,0 +1,93 @@ +using System; +using System.Threading.Tasks; +using Dapr.Actors.Client; +using Dapr.Actors.Runtime; +using Dapr.Actors.Test; +using Xunit; + +namespace Dapr.Actors +{ + public class ActorReferenceTests + { + [Fact] + public void Get_WhenActorIsNull_ReturnsNull() + { + // Arrange + object actor = null; + + // Act + var result = ActorReference.Get(actor); + + // Assert + Assert.Null(result); + } + + [Fact] + public void Get_FromActorProxy_ReturnsActorReference() + { + // Arrange + var expectedActorId = new ActorId("abc"); + var expectedActorType = "TestActor"; + var proxy = ActorProxy.Create(expectedActorId, typeof(ITestActor), expectedActorType); + + // Act + var actorReference = ActorReference.Get(proxy); + + // Assert + Assert.NotNull(actorReference); + Assert.Equal(expectedActorId, actorReference.ActorId); + Assert.Equal(expectedActorType, actorReference.ActorType); + } + + [Fact] + public async Task Get_FromActorImplementation_ReturnsActorReference() + { + // Arrange + var expectedActorId = new ActorId("abc"); + var expectedActorType = nameof(ActorReferenceTestActor); + var host = ActorHost.CreateForTest(new ActorTestOptions() { ActorId = expectedActorId }); + var actor = new ActorReferenceTestActor(host); + + // Act + var actorReference = await actor.GetActorReference(); + + // Assert + Assert.NotNull(actorReference); + Assert.Equal(expectedActorId, actorReference.ActorId); + Assert.Equal(expectedActorType, actorReference.ActorType); + } + + [Fact] + public void Get_WithInvalidObjectType_ThrowArgumentOutOfRangeException() + { + // Arrange + var actor = new object(); + + // Act + var act = () => ActorReference.Get(actor); + + // Assert + var exception = Assert.Throws(act); + Assert.Equal("actor", exception.ParamName); + Assert.Equal("Invalid actor object type. (Parameter 'actor')", exception.Message); + } + } + + public interface IActorReferenceTestActor : IActor + { + Task GetActorReference(); + } + + public class ActorReferenceTestActor : Actor, IActorReferenceTestActor + { + public ActorReferenceTestActor(ActorHost host) + : base(host) + { + } + + public Task GetActorReference() + { + return Task.FromResult(ActorReference.Get(this)); + } + } +} diff --git a/test/Dapr.Actors.Test/ActorStateManagerTest.cs b/test/Dapr.Actors.Test/ActorStateManagerTest.cs new file mode 100644 index 000000000..a6517a6b4 --- /dev/null +++ b/test/Dapr.Actors.Test/ActorStateManagerTest.cs @@ -0,0 +1,199 @@ +// ------------------------------------------------------------------------ +// 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. +// ------------------------------------------------------------------------ + +namespace Dapr.Actors.Test +{ + using System; + using System.Globalization; + using System.Linq; + using System.Net; + using System.Net.Http; + using System.Security; + using System.Security.Authentication; + using System.Text.Json; + using System.Threading; + using System.Threading.Tasks; + using System.Collections.Generic; + using FluentAssertions; + using Xunit; + using Dapr.Actors.Communication; + using Dapr.Actors.Runtime; + using Moq; + + /// + /// Contains tests for ActorStateManager. + /// + public class ActorStateManagerTest + { + [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(); + + 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 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)); + } + } +} diff --git a/test/Dapr.Actors.Test/ActorUnitTestTests.cs b/test/Dapr.Actors.Test/ActorUnitTestTests.cs index 318ede9fc..baa52c568 100644 --- a/test/Dapr.Actors.Test/ActorUnitTestTests.cs +++ b/test/Dapr.Actors.Test/ActorUnitTestTests.cs @@ -74,6 +74,7 @@ public async Task CanTestStartingAndStoppingTimer() public async Task CanTestStartingAndStoppinReminder() { var reminders = new List(); + IActorReminder getReminder = null; var timerManager = new Mock(MockBehavior.Strict); timerManager @@ -84,6 +85,9 @@ public async Task CanTestStartingAndStoppinReminder() .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); @@ -109,6 +113,10 @@ public async Task CanTestStartingAndStoppinReminder() 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); @@ -148,6 +156,11 @@ public async Task StartReminderAsync(Message message) await this.RegisterReminderAsync("record", bytes, dueTime: TimeSpan.Zero, period: TimeSpan.FromSeconds(5)); } + public async Task GetReminderAsync() + { + return await this.GetReminderAsync("record"); + } + public async Task StopReminderAsync() { await this.UnregisterReminderAsync("record"); diff --git a/test/Dapr.Actors.Test/Dapr.Actors.Test.csproj b/test/Dapr.Actors.Test/Dapr.Actors.Test.csproj index 0ac27e077..9ef26cd13 100644 --- a/test/Dapr.Actors.Test/Dapr.Actors.Test.csproj +++ b/test/Dapr.Actors.Test/Dapr.Actors.Test.csproj @@ -1,21 +1,20 @@  - netcoreapp3.1;net6;net7 Dapr.Actors $(DefineConstants);ACTORS - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - - - - + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/test/Dapr.Actors.Test/DaprHttpInteractorTest.cs b/test/Dapr.Actors.Test/DaprHttpInteractorTest.cs index 80dae342f..21c142267 100644 --- a/test/Dapr.Actors.Test/DaprHttpInteractorTest.cs +++ b/test/Dapr.Actors.Test/DaprHttpInteractorTest.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -13,6 +13,7 @@ namespace Dapr.Actors.Test { + using System; using System.Globalization; using System.Linq; using System.Net; @@ -23,6 +24,7 @@ namespace Dapr.Actors.Test using System.Threading.Tasks; using FluentAssertions; using Xunit; + using Dapr.Actors.Communication; /// /// Contains tests for DaprHttpInteractor. @@ -350,5 +352,58 @@ public async Task InvokeActorMethodOmitsReentrancyIdIfNotSet_ValidateHeaders() request.Dismiss(); Assert.False(request.Request.Headers.Contains(Constants.ReentrancyRequestHeaderName)); } + + [Fact] + public async Task GetState_TTLExpireTimeExists() + { + await using var client = TestClient.CreateForDaprHttpInterator(); + + var actorType = "ActorType_Test"; + var actorId = "ActorId_Test"; + var 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(); + + var actorType = "ActorType_Test"; + var actorId = "ActorId_Test"; + var 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); + } } } diff --git a/test/Dapr.Actors.Test/DaprStateProviderTest.cs b/test/Dapr.Actors.Test/DaprStateProviderTest.cs new file mode 100644 index 000000000..63be89e95 --- /dev/null +++ b/test/Dapr.Actors.Test/DaprStateProviderTest.cs @@ -0,0 +1,137 @@ +// ------------------------------------------------------------------------ +// 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. +// ------------------------------------------------------------------------ + +namespace Dapr.Actors.Test +{ + using System; + using System.Globalization; + using System.Linq; + using System.Net; + using System.Net.Http; + using System.Security; + using System.Security.Authentication; + using System.Text.Json; + using System.Threading; + using System.Threading.Tasks; + using System.Collections.Generic; + using FluentAssertions; + using Xunit; + using Dapr.Actors.Communication; + using Dapr.Actors.Runtime; + using Moq; + + /// + /// Contains tests for DaprStateProvider. + /// + public class DaprStateProviderTest + { + [Fact] + public async Task SaveStateAsync() + { + 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)); + + 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)); + + 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); + } + } +} diff --git a/test/Dapr.Actors.Test/Runtime/ActorManagerTests.cs b/test/Dapr.Actors.Test/Runtime/ActorManagerTests.cs index dd3683124..6b92c7e18 100644 --- a/test/Dapr.Actors.Test/Runtime/ActorManagerTests.cs +++ b/test/Dapr.Actors.Test/Runtime/ActorManagerTests.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -26,7 +26,7 @@ private ActorManager CreateActorManager(Type type, ActorActivator activator = nu { 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, NullLoggerFactory.Instance, ActorProxy.DefaultProxyFactory, interactor); + return new ActorManager(registration, activator ?? new DefaultActorActivator(), JsonSerializerDefaults.Web, false, NullLoggerFactory.Instance, ActorProxy.DefaultProxyFactory, interactor); } [Fact] diff --git a/test/Dapr.Actors.Test/Runtime/ActorRuntimeTests.cs b/test/Dapr.Actors.Test/Runtime/ActorRuntimeTests.cs index 52ae4aa7b..c74d0b754 100644 --- a/test/Dapr.Actors.Test/Runtime/ActorRuntimeTests.cs +++ b/test/Dapr.Actors.Test/Runtime/ActorRuntimeTests.cs @@ -27,6 +27,7 @@ namespace Dapr.Actors.Test using Xunit; using Dapr.Actors.Client; using System.Reflection; + using System.Threading; public sealed class ActorRuntimeTests { @@ -109,6 +110,111 @@ public async Task NoActivateMessageFromRuntime() 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() { diff --git a/test/Dapr.Actors.Test/Runtime/ActorTests.cs b/test/Dapr.Actors.Test/Runtime/ActorTests.cs index b18800f0e..f88b4e03f 100644 --- a/test/Dapr.Actors.Test/Runtime/ActorTests.cs +++ b/test/Dapr.Actors.Test/Runtime/ActorTests.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/test/Dapr.Actors.Test/TestDaprInteractor.cs b/test/Dapr.Actors.Test/TestDaprInteractor.cs index 1b382208d..11f88e684 100644 --- a/test/Dapr.Actors.Test/TestDaprInteractor.cs +++ b/test/Dapr.Actors.Test/TestDaprInteractor.cs @@ -67,10 +67,10 @@ public Task InvokeActorMethodWithoutRemotingAsync(string actorType, stri /// JSON data with state changes as per the Dapr spec for transaction state update. /// Cancels the operation. /// A task that represents the asynchronous operation. - public Task SaveStateTransactionallyAsync(string actorType, string actorId, string data, + public virtual async Task SaveStateTransactionallyAsync(string actorType, string actorId, string data, CancellationToken cancellationToken = default) { - throw new System.NotImplementedException(); + await _testDaprInteractor.SaveStateTransactionallyAsync(actorType, actorId, data); } /// @@ -81,9 +81,9 @@ public Task SaveStateTransactionallyAsync(string actorType, string actorId, stri /// Name of key to get value for. /// Cancels the operation. /// A task that represents the asynchronous operation. - public Task GetStateAsync(string actorType, string actorId, string keyName, CancellationToken cancellationToken = default) + public virtual async Task> GetStateAsync(string actorType, string actorId, string keyName, CancellationToken cancellationToken = default) { - throw new System.NotImplementedException(); + return await _testDaprInteractor.GetStateAsync(actorType, actorId, keyName); } /// @@ -99,6 +99,20 @@ Task IDaprInteractor.InvokeActorMethodWithRemotingAsync(A throw new System.NotImplementedException(); } + /// + /// Gets a reminder. + /// + /// Type of actor. + /// ActorId. + /// Name of reminder to unregister. + /// Cancels the operation. + /// A representing the result of the asynchronous operation. + public Task GetReminderAsync(string actorType, string actorId, string reminderName, + CancellationToken cancellationToken = default) + { + throw new System.NotImplementedException(); + } + /// /// Unregisters a reminder. /// diff --git a/test/Dapr.AspNetCore.IntegrationTest.App/Dapr.AspNetCore.IntegrationTest.App.csproj b/test/Dapr.AspNetCore.IntegrationTest.App/Dapr.AspNetCore.IntegrationTest.App.csproj index 86a561f59..f415639be 100644 --- a/test/Dapr.AspNetCore.IntegrationTest.App/Dapr.AspNetCore.IntegrationTest.App.csproj +++ b/test/Dapr.AspNetCore.IntegrationTest.App/Dapr.AspNetCore.IntegrationTest.App.csproj @@ -1,9 +1,5 @@  - - netcoreapp3.1;net6;net7 - - diff --git a/test/Dapr.AspNetCore.IntegrationTest/Dapr.AspNetCore.IntegrationTest.csproj b/test/Dapr.AspNetCore.IntegrationTest/Dapr.AspNetCore.IntegrationTest.csproj index f3c08b1b7..d51dc70e8 100644 --- a/test/Dapr.AspNetCore.IntegrationTest/Dapr.AspNetCore.IntegrationTest.csproj +++ b/test/Dapr.AspNetCore.IntegrationTest/Dapr.AspNetCore.IntegrationTest.csproj @@ -1,25 +1,24 @@  - - netcoreapp3.1;net6;net7 - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/test/Dapr.AspNetCore.Test/CloudEventsMiddlewareTest.cs b/test/Dapr.AspNetCore.Test/CloudEventsMiddlewareTest.cs index 904f6648f..c8a5ff402 100644 --- a/test/Dapr.AspNetCore.Test/CloudEventsMiddlewareTest.cs +++ b/test/Dapr.AspNetCore.Test/CloudEventsMiddlewareTest.cs @@ -84,7 +84,151 @@ public async Task InvokeAsync_ReplacesBodyJson(string dataContentType, string ch 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 app = new ApplicationBuilder(null); + app.UseCloudEvents(); + // Do verification in the scope of the middleware + app.Run(httpContext => + { + httpContext.Request.ContentType.Should().Be(dataContentType ?? "application/json"); + ReadBody(httpContext.Request.Body).Should().Be("{\"name\":\"jimmy\"}"); + return Task.CompletedTask; + }); + + var pipeline = app.Build(); + + var context = new DefaultHttpContext(); + context.Request.ContentType = charSet == null ? "application/cloudevents+json" : $"application/cloudevents+json;charset={charSet}"; + context.Request.Body = dataContentType == null ? + MakeBody("{ \"Data\": { \"name\":\"jimmy\" } }", encoding) : + MakeBody($"{{ \"DataContentType\": \"{dataContentType}\", \"Data\": {{ \"name\":\"jimmy\" }} }}", encoding); + + 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 app = new ApplicationBuilder(null); + app.UseCloudEvents(new CloudEventsMiddlewareOptions + { + ForwardCloudEventPropertiesAsHeaders = true + }); + + // Do verification in the scope of the middleware + app.Run(httpContext => + { + httpContext.Request.ContentType.Should().Be(dataContentType ?? "application/json"); + ReadBody(httpContext.Request.Body).Should().Be("{\"name\":\"jimmy\"}"); + + httpContext.Request.Headers.Should().ContainKey("Cloudevent.type").WhichValue.Should().BeEquivalentTo("Test.Type"); + httpContext.Request.Headers.Should().ContainKey("Cloudevent.subject").WhichValue.Should().BeEquivalentTo("Test.Subject"); + return Task.CompletedTask; + }); + + var pipeline = app.Build(); + + var context = new DefaultHttpContext(); + context.Request.ContentType = charSet == null ? "application/cloudevents+json" : $"application/cloudevents+json;charset={charSet}"; + context.Request.Body = dataContentType == null ? + MakeBody("{ \"type\": \"Test.Type\", \"subject\": \"Test.Subject\", \"data\": { \"name\":\"jimmy\" } }", encoding) : + MakeBody($"{{ \"datacontenttype\": \"{dataContentType}\", \"type\":\"Test.Type\", \"subject\": \"Test.Subject\", \"data\": {{ \"name\":\"jimmy\" }} }}", encoding); + + 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 app = new ApplicationBuilder(null); + app.UseCloudEvents(new CloudEventsMiddlewareOptions + { + ForwardCloudEventPropertiesAsHeaders = true, + IncludedCloudEventPropertiesAsHeaders = new []{"type"} + }); + + // Do verification in the scope of the middleware + app.Run(httpContext => + { + httpContext.Request.ContentType.Should().Be(dataContentType ?? "application/json"); + ReadBody(httpContext.Request.Body).Should().Be("{\"name\":\"jimmy\"}"); + + httpContext.Request.Headers.Should().ContainKey("Cloudevent.type").WhichValue.Should().BeEquivalentTo("Test.Type"); + httpContext.Request.Headers.Should().NotContainKey("Cloudevent.subject"); + return Task.CompletedTask; + }); + + var pipeline = app.Build(); + + var context = new DefaultHttpContext(); + context.Request.ContentType = charSet == null ? "application/cloudevents+json" : $"application/cloudevents+json;charset={charSet}"; + context.Request.Body = dataContentType == null ? + MakeBody("{ \"type\": \"Test.Type\", \"subject\": \"Test.Subject\", \"data\": { \"name\":\"jimmy\" } }", encoding) : + MakeBody($"{{ \"datacontenttype\": \"{dataContentType}\", \"type\":\"Test.Type\", \"subject\": \"Test.Subject\", \"data\": {{ \"name\":\"jimmy\" }} }}", encoding); + + 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 app = new ApplicationBuilder(null); + app.UseCloudEvents(new CloudEventsMiddlewareOptions + { + ForwardCloudEventPropertiesAsHeaders = true, + ExcludedCloudEventPropertiesFromHeaders = new []{"type"} + }); + + // Do verification in the scope of the middleware + app.Run(httpContext => + { + httpContext.Request.ContentType.Should().Be(dataContentType ?? "application/json"); + ReadBody(httpContext.Request.Body).Should().Be("{\"name\":\"jimmy\"}"); + + httpContext.Request.Headers.Should().NotContainKey("Cloudevent.type"); + httpContext.Request.Headers.Should().ContainKey("Cloudevent.subject").WhichValue.Should().BeEquivalentTo("Test.Subject"); + return Task.CompletedTask; + }); + + var pipeline = app.Build(); + + var context = new DefaultHttpContext(); + context.Request.ContentType = charSet == null ? "application/cloudevents+json" : $"application/cloudevents+json;charset={charSet}"; + context.Request.Body = dataContentType == null ? + MakeBody("{ \"type\": \"Test.Type\", \"subject\": \"Test.Subject\", \"data\": { \"name\":\"jimmy\" } }", encoding) : + MakeBody($"{{ \"datacontenttype\": \"{dataContentType}\", \"type\":\"Test.Type\", \"subject\": \"Test.Subject\", \"data\": {{ \"name\":\"jimmy\" }} }}", encoding); + + await pipeline.Invoke(context); + } + [Fact] public async Task InvokeAsync_ReplacesBodyNonJsonData() { diff --git a/test/Dapr.AspNetCore.Test/Dapr.AspNetCore.Test.csproj b/test/Dapr.AspNetCore.Test/Dapr.AspNetCore.Test.csproj index b153287b2..9135e63d4 100644 --- a/test/Dapr.AspNetCore.Test/Dapr.AspNetCore.Test.csproj +++ b/test/Dapr.AspNetCore.Test/Dapr.AspNetCore.Test.csproj @@ -1,17 +1,14 @@  - - netcoreapp3.1;net6;net7 - - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -24,6 +21,7 @@ + \ No newline at end of file diff --git a/test/Dapr.AspNetCore.Test/DaprClientBuilderTest.cs b/test/Dapr.AspNetCore.Test/DaprClientBuilderTest.cs index 2da50922a..52d0b7000 100644 --- a/test/Dapr.AspNetCore.Test/DaprClientBuilderTest.cs +++ b/test/Dapr.AspNetCore.Test/DaprClientBuilderTest.cs @@ -14,6 +14,7 @@ using System; using System.Text.Json; using Dapr.Client; +using Grpc.Core; using Grpc.Net.Client; using Xunit; @@ -110,5 +111,15 @@ public void DaprClientBuilder_ApiTokenNotSet_EmptyApiTokenHeader() 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); + } } + } diff --git a/test/Dapr.AspNetCore.Test/DaprMvcBuilderExtensionsTest.cs b/test/Dapr.AspNetCore.Test/DaprMvcBuilderExtensionsTest.cs index 693afee4c..a1df1fe1e 100644 --- a/test/Dapr.AspNetCore.Test/DaprMvcBuilderExtensionsTest.cs +++ b/test/Dapr.AspNetCore.Test/DaprMvcBuilderExtensionsTest.cs @@ -84,5 +84,23 @@ public void AddDapr_RegistersDaprOnlyOnce() Assert.False(daprClient.JsonSerializerOptions.PropertyNameCaseInsensitive); } + +#if NET8_0_OR_GREATER + [Fact] + public void AddDapr_WithKeyedServices() + { + var services = new ServiceCollection(); + + services.AddKeyedSingleton("key1", new Object()); + + services.AddControllers().AddDapr(); + + var serviceProvider = services.BuildServiceProvider(); + + var daprClient = serviceProvider.GetService(); + + Assert.NotNull(daprClient); + } +#endif } } diff --git a/test/Dapr.AspNetCore.Test/DaprServiceCollectionExtensionsTest.cs b/test/Dapr.AspNetCore.Test/DaprServiceCollectionExtensionsTest.cs index 6a581d228..a82948cf3 100644 --- a/test/Dapr.AspNetCore.Test/DaprServiceCollectionExtensionsTest.cs +++ b/test/Dapr.AspNetCore.Test/DaprServiceCollectionExtensionsTest.cs @@ -47,5 +47,53 @@ public void AddDaprClient_RegistersDaprClientOnlyOnce() Assert.True(daprClient.JsonSerializerOptions.PropertyNameCaseInsensitive); } + + [Fact] + public void AddDaprClient_RegistersUsingDependencyFromIServiceProvider() + { + + var services = new ServiceCollection(); + services.AddSingleton(); + services.AddDaprClient((provider, builder) => + { + var configProvider = provider.GetRequiredService(); + var caseSensitivity = configProvider.GetCaseSensitivity(); + + builder.UseJsonSerializationOptions(new JsonSerializerOptions + { + PropertyNameCaseInsensitive = caseSensitivity + }); + }); + + var serviceProvider = services.BuildServiceProvider(); + + DaprClientGrpc client = serviceProvider.GetRequiredService() as DaprClientGrpc; + + //Registers with case-insensitive as true by default, but we set as false above + Assert.False(client.JsonSerializerOptions.PropertyNameCaseInsensitive); + } + +#if NET8_0_OR_GREATER + [Fact] + public void AddDaprClient_WithKeyedServices() + { + var services = new ServiceCollection(); + + services.AddKeyedSingleton("key1", new Object()); + + services.AddDaprClient(); + + var serviceProvider = services.BuildServiceProvider(); + + var daprClient = serviceProvider.GetService(); + + Assert.NotNull(daprClient); + } +#endif + + private class TestConfigurationProvider + { + public bool GetCaseSensitivity() => false; + } } } diff --git a/test/Dapr.Client.Test/ConfigurationApiTest.cs b/test/Dapr.Client.Test/ConfigurationApiTest.cs index cf81da95a..2e96582bd 100644 --- a/test/Dapr.Client.Test/ConfigurationApiTest.cs +++ b/test/Dapr.Client.Test/ConfigurationApiTest.cs @@ -20,7 +20,6 @@ namespace Dapr.Client.Test { - [System.Obsolete] public class ConfigurationApiTest { [Fact] diff --git a/test/Dapr.Client.Test/ConfigurationSourceTest.cs b/test/Dapr.Client.Test/ConfigurationSourceTest.cs index af5d05a45..c381d41de 100644 --- a/test/Dapr.Client.Test/ConfigurationSourceTest.cs +++ b/test/Dapr.Client.Test/ConfigurationSourceTest.cs @@ -9,7 +9,6 @@ namespace Dapr.Client.Test { - [Obsolete] public class ConfigurationSourceTest { private readonly string StoreName = "testStore"; diff --git a/test/Dapr.Client.Test/CryptographyApiTest.cs b/test/Dapr.Client.Test/CryptographyApiTest.cs new file mode 100644 index 000000000..a7d57a096 --- /dev/null +++ b/test/Dapr.Client.Test/CryptographyApiTest.cs @@ -0,0 +1,95 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Xunit; +#pragma warning disable CS0618 // Type or member is obsolete + +namespace Dapr.Client.Test +{ + public class CryptographyApiTest + { + [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)); + } + } +} diff --git a/test/Dapr.Client.Test/Dapr.Client.Test.csproj b/test/Dapr.Client.Test/Dapr.Client.Test.csproj index c94f031e9..f0bea601f 100644 --- a/test/Dapr.Client.Test/Dapr.Client.Test.csproj +++ b/test/Dapr.Client.Test/Dapr.Client.Test.csproj @@ -1,24 +1,21 @@  - - netcoreapp3.1;net6;net7 - - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - - - - - - - - - + + + + + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -36,6 +33,8 @@ + + diff --git a/test/Dapr.Client.Test/DaprClientTest.CreateInvokableHttpClientTest.cs b/test/Dapr.Client.Test/DaprClientTest.CreateInvokableHttpClientTest.cs new file mode 100644 index 000000000..99fbd4972 --- /dev/null +++ b/test/Dapr.Client.Test/DaprClientTest.CreateInvokableHttpClientTest.cs @@ -0,0 +1,52 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System; +using Xunit; + +namespace Dapr.Client +{ + public partial class DaprClientTest + { + [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); + } + } +} diff --git a/test/Dapr.Client.Test/DaprClientTest.InvokeMethodAsync.cs b/test/Dapr.Client.Test/DaprClientTest.InvokeMethodAsync.cs index 5d46000a1..3359c3b48 100644 --- a/test/Dapr.Client.Test/DaprClientTest.InvokeMethodAsync.cs +++ b/test/Dapr.Client.Test/DaprClientTest.InvokeMethodAsync.cs @@ -11,6 +11,9 @@ // limitations under the License. // ------------------------------------------------------------------------ +using System.Linq; +using System.Net.Http.Headers; + namespace Dapr.Client.Test { using System; @@ -518,6 +521,18 @@ public async Task CreateInvokeMethodRequest_TransformsUrlCorrectly(string 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() { @@ -617,6 +632,60 @@ public async Task CreateInvokeMethodRequest_WithData_CreatesJsonContent() 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() { diff --git a/test/Dapr.Client.Test/DaprClientTest.InvokeMethodGrpcAsync.cs b/test/Dapr.Client.Test/DaprClientTest.InvokeMethodGrpcAsync.cs index 5412c4063..65b9b1e7d 100644 --- a/test/Dapr.Client.Test/DaprClientTest.InvokeMethodGrpcAsync.cs +++ b/test/Dapr.Client.Test/DaprClientTest.InvokeMethodGrpcAsync.cs @@ -197,7 +197,7 @@ public void InvokeMethodGrpcAsync_CanInvokeMethodWithNoReturnTypeAndData() .Setup(m => m.InvokeServiceAsync(It.IsAny(), It.IsAny())) .Returns(response); - FluentActions.Awaiting(async () => await client.DaprClient.InvokeMethodGrpcAsync("test", "test", request)).Should().NotThrow(); + FluentActions.Awaiting(async () => await client.DaprClient.InvokeMethodGrpcAsync("test", "test", request)).Should().NotThrowAsync(); } [Fact] @@ -368,7 +368,7 @@ public async Task GetMetadataAsync_WrapsRpcException() var rpcException = new RpcException(rpcStatus, new Metadata(), rpcExceptionMessage); client.Mock - .Setup(m => m.GetMetadataAsync(It.IsAny(), It.IsAny())) + .Setup(m => m.GetMetadataAsync(It.IsAny(), It.IsAny())) .Throws(rpcException); var ex = await Assert.ThrowsAsync(async () => @@ -395,9 +395,10 @@ public async Task GetMetadataAsync_WithReturnTypeAndData() // Create Response & Respond var response = new Autogen.Grpc.v1.GetMetadataResponse() { + ActorRuntime = new(), Id = "testId", }; - response.ActiveActorsCount.Add(new ActiveActorsCount { Type = "testType", Count = 1 }); + 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"); diff --git a/test/Dapr.Client.Test/DaprClientTest.cs b/test/Dapr.Client.Test/DaprClientTest.cs index a822bdf89..01d22edcf 100644 --- a/test/Dapr.Client.Test/DaprClientTest.cs +++ b/test/Dapr.Client.Test/DaprClientTest.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/test/Dapr.Client.Test/Extensions/EnumExtensionTest.cs b/test/Dapr.Client.Test/Extensions/EnumExtensionTest.cs new file mode 100644 index 000000000..83c4354f9 --- /dev/null +++ b/test/Dapr.Client.Test/Extensions/EnumExtensionTest.cs @@ -0,0 +1,38 @@ +using System.Runtime.Serialization; +using Xunit; + +namespace Dapr.Client.Test.Extensions +{ + public class EnumExtensionTest + { + [Fact] + public void GetValueFromEnumMember_RedResolvesAsExpected() + { + var value = TestEnum.Red.GetValueFromEnumMember(); + Assert.Equal("red", value); + } + + [Fact] + public void GetValueFromEnumMember_YellowResolvesAsExpected() + { + var value = TestEnum.Yellow.GetValueFromEnumMember(); + Assert.Equal("YELLOW", value); + } + + [Fact] + public void GetValueFromEnumMember_BlueResolvesAsExpected() + { + var value = TestEnum.Blue.GetValueFromEnumMember(); + Assert.Equal("Blue", value); + } + } + + public enum TestEnum + { + [EnumMember(Value = "red")] + Red, + [EnumMember(Value = "YELLOW")] + Yellow, + Blue + } +} diff --git a/test/Dapr.Client.Test/Extensions/HttpExtensionTest.cs b/test/Dapr.Client.Test/Extensions/HttpExtensionTest.cs new file mode 100644 index 000000000..7b93c1c91 --- /dev/null +++ b/test/Dapr.Client.Test/Extensions/HttpExtensionTest.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; +using System.Net.Http; +using Xunit; + +namespace Dapr.Client.Test.Extensions +{ + public class HttpExtensionTest + { + [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); + } + } +} diff --git a/test/Dapr.Client.Test/InvocationHandlerTests.cs b/test/Dapr.Client.Test/InvocationHandlerTests.cs index c6adb93df..3dac84113 100644 --- a/test/Dapr.Client.Test/InvocationHandlerTests.cs +++ b/test/Dapr.Client.Test/InvocationHandlerTests.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -30,8 +30,8 @@ public class InvocationHandlerTests public void DaprEndpoint_InvalidScheme() { var handler = new InvocationHandler(); - var ex = Assert.Throws(() => - { + var ex = Assert.Throws(() => + { handler.DaprEndpoint = "ftp://localhost:3500"; }); @@ -43,7 +43,7 @@ public void DaprEndpoint_InvalidUri() { var handler = new InvocationHandler(); Assert.Throws(() => - { + { handler.DaprEndpoint = ""; }); @@ -79,17 +79,50 @@ public void TryRewriteUri_FailsForRelativeUris() } [Theory] - [InlineData("http://bank", "https://some.host:3499/v1.0/invoke/bank/method/")] - [InlineData("http://bank:3939", "https://some.host:3499/v1.0/invoke/bank/method/")] - [InlineData("http://app-id.with.dots", "https://some.host:3499/v1.0/invoke/app-id.with.dots/method/")] - [InlineData("http://bank:3939/", "https://some.host:3499/v1.0/invoke/bank/method/")] - [InlineData("http://bank:3939/some/path", "https://some.host:3499/v1.0/invoke/bank/method/some/path")] - [InlineData("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_RewritesUriToDaprInvoke(string uri, string expected) + [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)); @@ -97,12 +130,12 @@ public void TryRewriteUri_RewritesUriToDaprInvoke(string uri, string expected) } [Fact] - public async Task SendAsync_InvalidUri_ThrowsException() + public async Task SendAsync_InvalidNotSetUri_ThrowsException() { var handler = new InvocationHandler(); var ex = await Assert.ThrowsAsync(async () => { - await CallSendAsync(handler, new HttpRequestMessage(){ }); // No URI set + await CallSendAsync(handler, new HttpRequestMessage() { }); // No URI set }); Assert.Contains("The request URI '' is not a valid Dapr service invocation destination.", ex.Message); @@ -132,6 +165,31 @@ public async Task SendAsync_RewritesUri() 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); + var response = 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() { @@ -164,7 +222,7 @@ private async Task CallSendAsync(InvocationHandler handler, try { - return await (Task)method!.Invoke(handler, new object[]{ message, cancellationToken, })!; + return await (Task)method!.Invoke(handler, new object[] { message, cancellationToken, })!; } catch (TargetInvocationException tie) // reflection always adds an extra layer of exceptions. { diff --git a/test/Dapr.Client.Test/PublishEventApiTest.cs b/test/Dapr.Client.Test/PublishEventApiTest.cs index d8caf63d1..77d6ee905 100644 --- a/test/Dapr.Client.Test/PublishEventApiTest.cs +++ b/test/Dapr.Client.Test/PublishEventApiTest.cs @@ -11,6 +11,12 @@ // limitations under the License. // ------------------------------------------------------------------------ +using System.Collections.Immutable; +using System.Linq; +using System.Net.Http; +using System.Text.Json.Serialization; +using Grpc.Net.Client; + namespace Dapr.Client.Test { using System; @@ -51,6 +57,44 @@ public async Task PublishEventAsync_CanPublishTopicWithData() envelope.Metadata.Count.Should().Be(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.Should().Be(1); + client.InnerClient.JsonSerializerOptions.Converters.First().GetType().Name.Should() + .Match(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.Should() + .Be(JsonSerializer.Serialize(publishData, client.InnerClient.JsonSerializerOptions)); + jsonFromRequest.Should().Match("{\"Size\":\"Large\",\"Color\":\"Red\"}"); + } + [Fact] public async Task PublishEventAsync_CanPublishTopicWithData_WithMetadata() { @@ -259,5 +303,18 @@ 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 + } } } diff --git a/test/Dapr.Client.Test/SecretApiTest.cs b/test/Dapr.Client.Test/SecretApiTest.cs index c94c82844..26048e2a4 100644 --- a/test/Dapr.Client.Test/SecretApiTest.cs +++ b/test/Dapr.Client.Test/SecretApiTest.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -93,6 +93,32 @@ public async Task GetSecretAsync_ReturnSingleSecret() secretsResponse["redis_secret"].Should().Be("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.Should().Be("testStore"); + envelope.Key.Should().Be("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.Should().Be(1); + secretsResponse.ContainsKey("us-west-1/org/xpto/secretabc").Should().BeTrue(); + secretsResponse["us-west-1/org/xpto/secretabc"].Should().Be("abc123"); + } + [Fact] public async Task GetSecretAsync_ReturnMultipleSecrets() { diff --git a/test/Dapr.Client.Test/StateApiTest.cs b/test/Dapr.Client.Test/StateApiTest.cs index 90c06e6b1..0684a8db0 100644 --- a/test/Dapr.Client.Test/StateApiTest.cs +++ b/test/Dapr.Client.Test/StateApiTest.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -75,6 +75,30 @@ public async Task GetBulkStateAsync_CanReadState() state.Should().HaveCount(1); } + [Fact] + public async Task GetBulkStateAsync_CanReadDeserializedState() + { + await using var client = TestClient.CreateForDaprClient(); + + var key = "test"; + var request = await client.CaptureGrpcRequestAsync(async daprClient => + { + return await daprClient.GetBulkStateAsync("testStore", new List() {key}, null); + }); + + // 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.Should().HaveCount(1); + state[0].Value.Size.Should().Match(size); + state[0].Value.Color.Should().Match(color); + } + [Fact] public async Task GetBulkStateAsync_WrapsRpcException() { @@ -481,7 +505,7 @@ public async Task ExecuteStateTransactionAsync_CanSaveState() req1.Request.Etag.Value.Should().Be("testEtag"); req1.Request.Metadata.Count.Should().Be(1); req1.Request.Metadata["a"].Should().Be("b"); - req1.Request.Options.Concurrency.Should().Be(2); + req1.Request.Options.Concurrency.Should().Be(StateConcurrency.ConcurrencyLastWrite); var req2 = envelope.Operations[1]; req2.Request.Key.Should().Be("stateKey2"); diff --git a/test/Dapr.E2E.Test.Actors.Generators/ActorState.cs b/test/Dapr.E2E.Test.Actors.Generators/ActorState.cs new file mode 100644 index 000000000..6965c751c --- /dev/null +++ b/test/Dapr.E2E.Test.Actors.Generators/ActorState.cs @@ -0,0 +1,40 @@ +// ------------------------------------------------------------------------ +// Copyright 2023 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.Actors; +using Dapr.Actors.Client; + +namespace Dapr.E2E.Test.Actors.Generators; + +internal static class ActorState +{ + public static async Task EnsureReadyAsync(ActorId actorId, string actorType, ActorProxyOptions? options = null, CancellationToken cancellationToken = default) + where TActor : IPingActor + { + var pingProxy = ActorProxy.Create(actorId, actorType, options); + + while (true) + { + try + { + await pingProxy.Ping(); + + break; + } + catch (DaprApiException) + { + await Task.Delay(TimeSpan.FromMilliseconds(250), cancellationToken); + } + } + } +} diff --git a/test/Dapr.E2E.Test.Actors.Generators/ActorWebApplicationFactory.cs b/test/Dapr.E2E.Test.Actors.Generators/ActorWebApplicationFactory.cs new file mode 100644 index 000000000..b5e81b8aa --- /dev/null +++ b/test/Dapr.E2E.Test.Actors.Generators/ActorWebApplicationFactory.cs @@ -0,0 +1,57 @@ +// ------------------------------------------------------------------------ +// Copyright 2023 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.Actors.Runtime; + +namespace Dapr.E2E.Test.Actors.Generators; + +internal sealed record ActorWebApplicationOptions(Action ConfigureActors) +{ + public ILoggerProvider? LoggerProvider { get; init; } + + public string? Url { get; init; } +} + +internal sealed class ActorWebApplicationFactory +{ + public static WebApplication Create(ActorWebApplicationOptions options) + { + var builder = WebApplication.CreateBuilder(); + + if (options.LoggerProvider is not null) + { + builder.Logging.ClearProviders(); + builder.Logging.AddProvider(options.LoggerProvider); + } + + builder.Services.AddActors(options.ConfigureActors); + + var app = builder.Build(); + + if (options.Url is not null) + { + app.Urls.Add(options.Url); + } + + app.UseRouting(); + + #pragma warning disable ASP0014 + app.UseEndpoints( + endpoints => + { + endpoints.MapActorsHandlers(); + }); + + return app; + } +} diff --git a/test/Dapr.E2E.Test.Actors.Generators/Clients/GeneratedClientTests.cs b/test/Dapr.E2E.Test.Actors.Generators/Clients/GeneratedClientTests.cs new file mode 100644 index 000000000..6079b5df7 --- /dev/null +++ b/test/Dapr.E2E.Test.Actors.Generators/Clients/GeneratedClientTests.cs @@ -0,0 +1,107 @@ +// ------------------------------------------------------------------------ +// Copyright 2023 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.Actors; +using Dapr.Actors.Client; +using Xunit.Abstractions; + +namespace Dapr.E2E.Test.Actors.Generators.Clients; + +public class GeneratedClientTests +{ + private readonly ILoggerProvider testLoggerProvider; + + public GeneratedClientTests(ITestOutputHelper testOutputHelper) + { + this.testLoggerProvider = new XUnitLoggingProvider(testOutputHelper); + } + + [Fact] + public async Task TestGeneratedClientAsync() + { + var portManager = new PortManager(); + + (int appPort, int clientAppHttpPort) = portManager.ReservePorts(); + + var templateSidecarOptions = new DaprSidecarOptions("template-app") + { + LoggerFactory = new LoggerFactory(new[] { this.testLoggerProvider }), + LogLevel = "debug" + }; + + var serviceAppSidecarOptions = templateSidecarOptions with + { + AppId = "service-app", + AppPort = appPort + }; + + var clientAppSidecarOptions = templateSidecarOptions with + { + AppId = "client-app", + DaprHttpPort = clientAppHttpPort + }; + + await using var app = ActorWebApplicationFactory.Create( + new ActorWebApplicationOptions(options => + { + options.UseJsonSerialization = true; + options.Actors.RegisterActor(); + }) + { + LoggerProvider = this.testLoggerProvider, + Url = $"http://localhost:{appPort}" + }); + + using var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromMinutes(1)); + + // + // Start application... + // + + await app.StartAsync(cancellationTokenSource.Token); + + // + // Start sidecars... + // + + await using var serviceAppSidecar = DaprSidecarFactory.Create(serviceAppSidecarOptions); + + await serviceAppSidecar.StartAsync(cancellationTokenSource.Token); + + await using var clientAppSidecar = DaprSidecarFactory.Create(clientAppSidecarOptions); + + await clientAppSidecar.StartAsync(cancellationTokenSource.Token); + + // + // Ensure actor is ready... + // + + var actorId = ActorId.CreateRandom(); + var actorType = "RemoteActor"; + var actorOptions = new ActorProxyOptions { HttpEndpoint = $"http://localhost:{clientAppHttpPort}" }; + + await ActorState.EnsureReadyAsync(actorId, actorType, actorOptions, cancellationTokenSource.Token); + + // + // Start test... + // + + var actorProxy = ActorProxy.Create(actorId, actorType, actorOptions); + + var client = new ClientActorClient(actorProxy); + + var result = await client.GetStateAsync(cancellationTokenSource.Token); + + await client.SetStateAsync(new ClientState("updated state"), cancellationTokenSource.Token); + } +} diff --git a/test/Dapr.E2E.Test.Actors.Generators/Clients/IClientActor.cs b/test/Dapr.E2E.Test.Actors.Generators/Clients/IClientActor.cs new file mode 100644 index 000000000..a6cf30a76 --- /dev/null +++ b/test/Dapr.E2E.Test.Actors.Generators/Clients/IClientActor.cs @@ -0,0 +1,28 @@ +// ------------------------------------------------------------------------ +// Copyright 2023 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.Actors.Generators; + +namespace Dapr.E2E.Test.Actors.Generators.Clients; + +internal record ClientState(string Value); + +[GenerateActorClient] +internal interface IClientActor +{ + [ActorMethod(Name = "GetState")] + Task GetStateAsync(CancellationToken cancellationToken = default); + + [ActorMethod(Name = "SetState")] + Task SetStateAsync(ClientState state, CancellationToken cancellationToken = default); +} diff --git a/test/Dapr.E2E.Test.Actors.Generators/Clients/IRemoteActor.cs b/test/Dapr.E2E.Test.Actors.Generators/Clients/IRemoteActor.cs new file mode 100644 index 000000000..77ad6e75b --- /dev/null +++ b/test/Dapr.E2E.Test.Actors.Generators/Clients/IRemoteActor.cs @@ -0,0 +1,23 @@ +// ------------------------------------------------------------------------ +// 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. +// ------------------------------------------------------------------------ + +namespace Dapr.E2E.Test.Actors.Generators.Clients; + +public record RemoteState(string Value); + +public interface IRemoteActor : IPingActor +{ + Task GetState(); + + Task SetState(RemoteState state); +} \ No newline at end of file diff --git a/test/Dapr.E2E.Test.Actors.Generators/Clients/RemoteActor.cs b/test/Dapr.E2E.Test.Actors.Generators/Clients/RemoteActor.cs new file mode 100644 index 000000000..9c049019d --- /dev/null +++ b/test/Dapr.E2E.Test.Actors.Generators/Clients/RemoteActor.cs @@ -0,0 +1,50 @@ +// ------------------------------------------------------------------------ +// Copyright 2023 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.Actors.Runtime; + +namespace Dapr.E2E.Test.Actors.Generators.Clients; + +internal sealed class RemoteActor : Actor, IRemoteActor +{ + private readonly ILogger logger; + + private RemoteState currentState = new("default"); + + public RemoteActor(ActorHost host, ILogger logger) + : base(host) + { + this.logger = logger; + } + + public Task GetState() + { + this.logger.LogInformation("GetStateAsync called."); + + return Task.FromResult(this.currentState); + } + + public Task SetState(RemoteState state) + { + this.logger.LogInformation("SetStateAsync called."); + + this.currentState = state; + + return Task.CompletedTask; + } + + public Task Ping() + { + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/test/Dapr.E2E.Test.Actors.Generators/Dapr.E2E.Test.Actors.Generators.csproj b/test/Dapr.E2E.Test.Actors.Generators/Dapr.E2E.Test.Actors.Generators.csproj new file mode 100644 index 000000000..cb375af01 --- /dev/null +++ b/test/Dapr.E2E.Test.Actors.Generators/Dapr.E2E.Test.Actors.Generators.csproj @@ -0,0 +1,30 @@ + + + + enable + enable + + false + true + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/test/Dapr.E2E.Test.Actors.Generators/DaprSidecarFactory.cs b/test/Dapr.E2E.Test.Actors.Generators/DaprSidecarFactory.cs new file mode 100644 index 000000000..56d1954d4 --- /dev/null +++ b/test/Dapr.E2E.Test.Actors.Generators/DaprSidecarFactory.cs @@ -0,0 +1,151 @@ +// ------------------------------------------------------------------------ +// 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.Diagnostics; + +namespace Dapr.E2E.Test.Actors.Generators; + +internal sealed record DaprSidecarOptions(string AppId) +{ + public int? AppPort { get; init; } + + public int? DaprGrpcPort { get; init;} + + public int? DaprHttpPort { get; init; } + + public ILoggerFactory? LoggerFactory { get; init; } + + public string? LogLevel { get; init; } +} + +internal sealed class DaprSidecar : IAsyncDisposable +{ + private const string StartupOutputString = "You're up and running! Dapr logs will appear here."; + + private readonly string appId; + private readonly Process process; + private readonly ILogger? logger; + private readonly TaskCompletionSource tcs = new(); + + public DaprSidecar(DaprSidecarOptions options) + { + string arguments = $"run --app-id {options.AppId}"; + + if (options.AppPort is not null) + { + arguments += $" --app-port {options.AppPort}"; + } + + if (options.DaprGrpcPort is not null) + { + arguments += $" --dapr-grpc-port {options.DaprGrpcPort}"; + } + + if (options.DaprHttpPort is not null) + { + arguments += $" --dapr-http-port {options.DaprHttpPort}"; + } + + if (options.LogLevel is not null) + { + arguments += $" --log-level {options.LogLevel}"; + } + + this.process = new Process + { + EnableRaisingEvents = false, // ? + StartInfo = + { + Arguments = arguments, + CreateNoWindow = true, + FileName = "dapr", + RedirectStandardError = true, + RedirectStandardOutput = true, + UseShellExecute = false, + WindowStyle = ProcessWindowStyle.Hidden + } + }; + + if (options.LoggerFactory is not null) + { + this.logger = options.LoggerFactory.CreateLogger(options.AppId); + } + + this.process.OutputDataReceived += (_, args) => + { + if (args.Data is not null) + { + if (args.Data.Contains(StartupOutputString)) + { + this.tcs.SetResult(true); + } + + this.logger?.LogInformation(args.Data); + } + }; + + this.process.ErrorDataReceived += (_, args) => + { + if (args.Data is not null) + { + this.logger?.LogError(args.Data); + } + }; + + this.appId = options.AppId; + } + + public Task StartAsync(CancellationToken cancellationToken = default) + { + process.Start(); + + process.BeginErrorReadLine(); + process.BeginOutputReadLine(); + + return this.tcs.Task; + } + + public async Task StopAsync(CancellationToken cancellationToken = default) + { + var stopProcess = new Process + { + StartInfo = + { + Arguments = $"stop --app-id {this.appId}", + CreateNoWindow = true, + FileName = "dapr", + UseShellExecute = false, + WindowStyle = ProcessWindowStyle.Hidden + } + }; + + stopProcess.Start(); + + await stopProcess.WaitForExitAsync(cancellationToken); + + await process.WaitForExitAsync(cancellationToken); + } + + public async ValueTask DisposeAsync() + { + await StopAsync(CancellationToken.None); + } +} + +internal sealed class DaprSidecarFactory +{ + public static DaprSidecar Create(DaprSidecarOptions options) + { + return new(options); + } +} \ No newline at end of file diff --git a/test/Dapr.E2E.Test.Actors.Generators/GlobalUsings.cs b/test/Dapr.E2E.Test.Actors.Generators/GlobalUsings.cs new file mode 100644 index 000000000..48f0c59b2 --- /dev/null +++ b/test/Dapr.E2E.Test.Actors.Generators/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.E2E.Test.Actors.Generators/IPingActor.cs b/test/Dapr.E2E.Test.Actors.Generators/IPingActor.cs new file mode 100644 index 000000000..484c4d150 --- /dev/null +++ b/test/Dapr.E2E.Test.Actors.Generators/IPingActor.cs @@ -0,0 +1,21 @@ +// ------------------------------------------------------------------------ +// Copyright 2023 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.Actors; + +namespace Dapr.E2E.Test.Actors.Generators; + +public interface IPingActor : IActor +{ + Task Ping(); +} diff --git a/test/Dapr.E2E.Test.Actors.Generators/PortManager.cs b/test/Dapr.E2E.Test.Actors.Generators/PortManager.cs new file mode 100644 index 000000000..fcd296977 --- /dev/null +++ b/test/Dapr.E2E.Test.Actors.Generators/PortManager.cs @@ -0,0 +1,63 @@ +// ------------------------------------------------------------------------ +// 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.Net.NetworkInformation; + +namespace Dapr.E2E.Test.Actors.Generators; + +internal sealed class PortManager +{ + private readonly ISet reservedPorts = new HashSet(); + + private readonly object reservationLock = new(); + + public int ReservePort(int rangeStart = 55000) + { + var ports = this.ReservePorts(1, rangeStart); + + return ports.First(); + } + + public (int, int) ReservePorts(int rangeStart = 55000) + { + var ports = this.ReservePorts(2, rangeStart).ToArray(); + + return (ports[0], ports[1]); + } + + public ISet ReservePorts(int count, int rangeStart = 55000) + { + lock (this.reservationLock) + { + var globalProperties = IPGlobalProperties.GetIPGlobalProperties(); + + var activePorts = + globalProperties + .GetActiveTcpListeners() + .Select(endPoint => endPoint.Port) + .ToHashSet(); + + var availablePorts = + Enumerable + .Range(rangeStart, Int32.MaxValue - rangeStart + 1) + .Where(port => !activePorts.Contains(port)) + .Where(port => !this.reservedPorts.Contains(port)); + + var newReservedPorts = availablePorts.Take(count).ToHashSet(); + + this.reservedPorts.UnionWith(newReservedPorts); + + return newReservedPorts; + } + } +} \ No newline at end of file diff --git a/test/Dapr.E2E.Test.Actors.Generators/XUnitLoggingProvider.cs b/test/Dapr.E2E.Test.Actors.Generators/XUnitLoggingProvider.cs new file mode 100644 index 000000000..641d66d80 --- /dev/null +++ b/test/Dapr.E2E.Test.Actors.Generators/XUnitLoggingProvider.cs @@ -0,0 +1,76 @@ +// ------------------------------------------------------------------------ +// 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 Xunit.Abstractions; + +namespace Dapr.E2E.Test.Actors.Generators; + +internal sealed class XUnitLoggingProvider : ILoggerProvider +{ + private readonly ITestOutputHelper output; + + public XUnitLoggingProvider(ITestOutputHelper output) + { + this.output = output; + } + + public ILogger CreateLogger(string categoryName) + { + return new XUnitLogger(categoryName, this.output); + } + + public void Dispose() + { + } + + private sealed class XUnitLogger : ILogger + { + private readonly string categoryName; + private readonly ITestOutputHelper output; + + public XUnitLogger(string categoryName, ITestOutputHelper output) + { + this.categoryName = categoryName; + this.output = output; + } + +#nullable disable + public IDisposable BeginScope(TState state) + { + return new XUnitLoggerScope(); + } +#nullable enable + + public bool IsEnabled(LogLevel logLevel) + { + return true; + } + + public void Log( + LogLevel logLevel, + EventId eventId, + TState state, + Exception? exception, + Func formatter) + { + this.output.WriteLine($"{this.categoryName}: {formatter(state, exception).TrimEnd(Environment.NewLine.ToCharArray())}"); + } + } + + private sealed class XUnitLoggerScope : IDisposable + { + public void Dispose() + { + } + } +} \ No newline at end of file diff --git a/test/Dapr.E2E.Test.Actors/Dapr.E2E.Test.Actors.csproj b/test/Dapr.E2E.Test.Actors/Dapr.E2E.Test.Actors.csproj index a0f13978d..56ab3d222 100644 --- a/test/Dapr.E2E.Test.Actors/Dapr.E2E.Test.Actors.csproj +++ b/test/Dapr.E2E.Test.Actors/Dapr.E2E.Test.Actors.csproj @@ -1,9 +1,5 @@ - - netcoreapp3.1;net6;net7 - - diff --git a/test/Dapr.E2E.Test.Actors/ISerializationActor.cs b/test/Dapr.E2E.Test.Actors/ISerializationActor.cs new file mode 100644 index 000000000..28190a0d7 --- /dev/null +++ b/test/Dapr.E2E.Test.Actors/ISerializationActor.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; +using Dapr.Actors; + +namespace Dapr.E2E.Test.Actors +{ + public interface ISerializationActor : IActor, IPingActor + { + Task SendAsync(string name, SerializationPayload payload, CancellationToken cancellationToken = default); + } + + public record SerializationPayload(string Message) + { + public JsonElement Value { get; set; } + + [JsonExtensionData] + public Dictionary ExtensionData { get; set; } + } +} diff --git a/test/Dapr.E2E.Test.Actors/Reminders/IReminderActor.cs b/test/Dapr.E2E.Test.Actors/Reminders/IReminderActor.cs index 33a5e0d05..c0e3f86a2 100644 --- a/test/Dapr.E2E.Test.Actors/Reminders/IReminderActor.cs +++ b/test/Dapr.E2E.Test.Actors/Reminders/IReminderActor.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -28,5 +28,7 @@ public interface IReminderActor : IPingActor, IActor Task StartReminderWithTtlAndRepetitions(TimeSpan ttl, int repetitions); Task GetState(); + + Task GetReminder(); } } diff --git a/test/Dapr.E2E.Test.Actors/State/IStateActor.cs b/test/Dapr.E2E.Test.Actors/State/IStateActor.cs new file mode 100644 index 000000000..f19122102 --- /dev/null +++ b/test/Dapr.E2E.Test.Actors/State/IStateActor.cs @@ -0,0 +1,26 @@ +// ------------------------------------------------------------------------ +// 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.State +{ + public interface IStateActor : IPingActor, IActor + { + Task GetState(string key); + + Task SetState(string key, string value, TimeSpan? ttl); + } +} diff --git a/test/Dapr.E2E.Test.Actors/WeaklyTypedTesting/DerivedResponse.cs b/test/Dapr.E2E.Test.Actors/WeaklyTypedTesting/DerivedResponse.cs new file mode 100644 index 000000000..c11f547e7 --- /dev/null +++ b/test/Dapr.E2E.Test.Actors/WeaklyTypedTesting/DerivedResponse.cs @@ -0,0 +1,20 @@ +// ------------------------------------------------------------------------ +// 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. +// ------------------------------------------------------------------------ + +namespace Dapr.E2E.Test.Actors.WeaklyTypedTesting +{ + public class DerivedResponse : ResponseBase + { + public string DerivedProperty { get; set; } + } +} diff --git a/test/Dapr.E2E.Test.Actors/WeaklyTypedTesting/IWeaklyTypedTestingActor.cs b/test/Dapr.E2E.Test.Actors/WeaklyTypedTesting/IWeaklyTypedTestingActor.cs new file mode 100644 index 000000000..2c5d1e82d --- /dev/null +++ b/test/Dapr.E2E.Test.Actors/WeaklyTypedTesting/IWeaklyTypedTestingActor.cs @@ -0,0 +1,25 @@ +// ------------------------------------------------------------------------ +// 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.Threading.Tasks; +using Dapr.Actors; + +namespace Dapr.E2E.Test.Actors.WeaklyTypedTesting +{ + public interface IWeaklyTypedTestingActor : IPingActor, IActor + { + Task GetPolymorphicResponse(); + + Task GetNullResponse(); + } +} diff --git a/test/Dapr.E2E.Test.Actors/WeaklyTypedTesting/ResponseBase.cs b/test/Dapr.E2E.Test.Actors/WeaklyTypedTesting/ResponseBase.cs new file mode 100644 index 000000000..45736d808 --- /dev/null +++ b/test/Dapr.E2E.Test.Actors/WeaklyTypedTesting/ResponseBase.cs @@ -0,0 +1,25 @@ +// ------------------------------------------------------------------------ +// 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.Text.Json.Serialization; + +namespace Dapr.E2E.Test.Actors.WeaklyTypedTesting +{ +#if NET7_0_OR_GREATER + [JsonDerivedType(typeof(DerivedResponse), typeDiscriminator: nameof(DerivedResponse))] +#endif + public class ResponseBase + { + public string BasePropeprty { get; set; } + } +} diff --git a/test/Dapr.E2E.Test.App.Grpc/Dapr.E2E.Test.App.Grpc.csproj b/test/Dapr.E2E.Test.App.Grpc/Dapr.E2E.Test.App.Grpc.csproj index abd821d39..9505df276 100644 --- a/test/Dapr.E2E.Test.App.Grpc/Dapr.E2E.Test.App.Grpc.csproj +++ b/test/Dapr.E2E.Test.App.Grpc/Dapr.E2E.Test.App.Grpc.csproj @@ -1,9 +1,6 @@ - - netcoreapp3.1;net6;net7 - - + diff --git a/test/Dapr.E2E.Test.App.Grpc/Proto/message.proto b/test/Dapr.E2E.Test.App.Grpc/Proto/message.proto index a8f67f578..3b73b73a9 100644 --- a/test/Dapr.E2E.Test.App.Grpc/Proto/message.proto +++ b/test/Dapr.E2E.Test.App.Grpc/Proto/message.proto @@ -25,6 +25,7 @@ service Messager { rpc GetMessage(GetMessageRequest) returns (MessageResponse); // Send a series of broadcast messages. rpc StreamBroadcast(stream Broadcast) returns (stream MessageResponse); + rpc DelayedResponse(google.protobuf.Empty) returns (google.protobuf.Empty); } message SendMessageRequest { diff --git a/test/Dapr.E2E.Test.App.Grpc/Services/MessagerService.cs b/test/Dapr.E2E.Test.App.Grpc/Services/MessagerService.cs index 0e22f3eec..950668932 100644 --- a/test/Dapr.E2E.Test.App.Grpc/Services/MessagerService.cs +++ b/test/Dapr.E2E.Test.App.Grpc/Services/MessagerService.cs @@ -11,6 +11,7 @@ // limitations under the License. // ------------------------------------------------------------------------ +using System; using System.Threading.Tasks; using Google.Protobuf.WellKnownTypes; using Grpc.Core; @@ -44,5 +45,11 @@ public override async Task StreamBroadcast(IAsyncStreamReader request 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.ReentrantActor/Actors/ReentrantActor.cs b/test/Dapr.E2E.Test.App.ReentrantActor/Actors/ReentrantActor.cs index 5f2d5db86..58776fe28 100644 --- a/test/Dapr.E2E.Test.App.ReentrantActor/Actors/ReentrantActor.cs +++ b/test/Dapr.E2E.Test.App.ReentrantActor/Actors/ReentrantActor.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -67,4 +67,4 @@ private async Task UpdateState(bool isEnter, int callNumber) } } } -} \ No newline at end of file +} diff --git a/test/Dapr.E2E.Test.App.ReentrantActor/Dapr.E2E.Test.App.ReentrantActors.csproj b/test/Dapr.E2E.Test.App.ReentrantActor/Dapr.E2E.Test.App.ReentrantActors.csproj index b533b52f3..2fda3becc 100644 --- a/test/Dapr.E2E.Test.App.ReentrantActor/Dapr.E2E.Test.App.ReentrantActors.csproj +++ b/test/Dapr.E2E.Test.App.ReentrantActor/Dapr.E2E.Test.App.ReentrantActors.csproj @@ -1,9 +1,5 @@ - - netcoreapp3.1;net6;net7 - - diff --git a/test/Dapr.E2E.Test.App/Actors/ReminderActor.cs b/test/Dapr.E2E.Test.App/Actors/ReminderActor.cs index f9b9d7573..57536377d 100644 --- a/test/Dapr.E2E.Test.App/Actors/ReminderActor.cs +++ b/test/Dapr.E2E.Test.App/Actors/ReminderActor.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -44,6 +44,12 @@ public async Task StartReminder(StartReminderOptions options) 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() diff --git a/test/Dapr.E2E.Test.App/Actors/SerializationActor.cs b/test/Dapr.E2E.Test.App/Actors/SerializationActor.cs new file mode 100644 index 000000000..e8da59826 --- /dev/null +++ b/test/Dapr.E2E.Test.App/Actors/SerializationActor.cs @@ -0,0 +1,26 @@ + +using System.Threading; +using System.Threading.Tasks; +using Dapr.Actors.Runtime; + +namespace Dapr.E2E.Test.Actors.Serialization +{ + public class SerializationActor : Actor, ISerializationActor + { + 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); + } + } +} diff --git a/test/Dapr.E2E.Test.App/Actors/StateActor.cs b/test/Dapr.E2E.Test.App/Actors/StateActor.cs new file mode 100644 index 000000000..71a952e0f --- /dev/null +++ b/test/Dapr.E2E.Test.App/Actors/StateActor.cs @@ -0,0 +1,47 @@ +// ------------------------------------------------------------------------ +// 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.Text.Json; +using System.Threading.Tasks; +using Dapr.Actors.Runtime; + +namespace Dapr.E2E.Test.Actors.State +{ + public class StateActor : Actor, IStateActor + { + 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); + } + } +} diff --git a/test/Dapr.E2E.Test.App/Actors/TimerActor.cs b/test/Dapr.E2E.Test.App/Actors/TimerActor.cs index bbe6cf7ae..14b9fef4e 100644 --- a/test/Dapr.E2E.Test.App/Actors/TimerActor.cs +++ b/test/Dapr.E2E.Test.App/Actors/TimerActor.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -39,7 +39,7 @@ public Task GetState() 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(50)); + 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, }); } diff --git a/test/Dapr.E2E.Test.App/Actors/WeaklyTypedTestingActor.cs b/test/Dapr.E2E.Test.App/Actors/WeaklyTypedTestingActor.cs new file mode 100644 index 000000000..3cbe20bad --- /dev/null +++ b/test/Dapr.E2E.Test.App/Actors/WeaklyTypedTestingActor.cs @@ -0,0 +1,47 @@ +// ------------------------------------------------------------------------ +// 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.Threading.Tasks; +using Dapr.Actors.Runtime; + +namespace Dapr.E2E.Test.Actors.WeaklyTypedTesting +{ + public class WeaklyTypedTestingActor : Actor, IWeaklyTypedTestingActor + { + 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; + } + } +} diff --git a/test/Dapr.E2E.Test.App/Controllers/TestController.cs b/test/Dapr.E2E.Test.App/Controllers/TestController.cs index 1f26ce809..4e475c197 100644 --- a/test/Dapr.E2E.Test.App/Controllers/TestController.cs +++ b/test/Dapr.E2E.Test.App/Controllers/TestController.cs @@ -65,5 +65,14 @@ public ActionResult AccountDetailsRequiresApiToken(Transaction transact }; return account; } + + [Authorize("Dapr")] + [HttpGet("DelayedResponse")] + public async Task DelayedResponse() + { + await Task.Delay(TimeSpan.FromSeconds(2)); + return Ok(); + } + } } diff --git a/test/Dapr.E2E.Test.App/Dapr.E2E.Test.App.csproj b/test/Dapr.E2E.Test.App/Dapr.E2E.Test.App.csproj index 7e114e8df..3454ac25d 100644 --- a/test/Dapr.E2E.Test.App/Dapr.E2E.Test.App.csproj +++ b/test/Dapr.E2E.Test.App/Dapr.E2E.Test.App.csproj @@ -1,13 +1,16 @@ - - netcoreapp3.1;net6;net7 - - + + + + + + + diff --git a/test/Dapr.E2E.Test.App/Program.cs b/test/Dapr.E2E.Test.App/Program.cs index e617558bf..5713129d5 100644 --- a/test/Dapr.E2E.Test.App/Program.cs +++ b/test/Dapr.E2E.Test.App/Program.cs @@ -1,23 +1,25 @@ -// ------------------------------------------------------------------------ -// 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. +// ------------------------------------------------------------------------ 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(); } @@ -26,6 +28,6 @@ public static IHostBuilder CreateHostBuilder(string[] args) => .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); - }); + }).UseSerilog(); } } diff --git a/test/Dapr.E2E.Test.App/Startup.cs b/test/Dapr.E2E.Test.App/Startup.cs index b5aebbba2..bfca60f91 100644 --- a/test/Dapr.E2E.Test.App/Startup.cs +++ b/test/Dapr.E2E.Test.App/Startup.cs @@ -16,7 +16,10 @@ namespace Dapr.E2E.Test using Dapr.E2E.Test.Actors.Reentrancy; 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; @@ -28,12 +31,17 @@ namespace Dapr.E2E.Test using Microsoft.Extensions.Hosting; using System.Threading.Tasks; using System; + using Microsoft.Extensions.Logging; + using Serilog; /// /// Startup class. /// public class Startup { + bool JsonSerializationEnabled => + System.Linq.Enumerable.Contains(System.Environment.GetCommandLineArgs(), "--json-serialization"); + /// /// Initializes a new instance of the class. /// @@ -57,6 +65,10 @@ 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 => { @@ -66,6 +78,11 @@ public void ConfigureServices(IServiceCollection services) 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 @@ -83,10 +100,14 @@ public void ConfigureServices(IServiceCollection services) }); 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(); }); } @@ -97,11 +118,19 @@ public void ConfigureServices(IServiceCollection services) /// Webhost environment. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { + var logger = new LoggerConfiguration().WriteTo.File("log.txt").CreateLogger(); + + var loggerFactory = LoggerFactory.Create(builder => + { + builder.AddSerilog(logger); + }); if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } + app.UseSerilogRequestLogging(); + app.UseRouting(); app.UseAuthentication(); diff --git a/test/Dapr.E2E.Test/Actors/E2ETests.CustomSerializerTests.cs b/test/Dapr.E2E.Test/Actors/E2ETests.CustomSerializerTests.cs new file mode 100644 index 000000000..c393f2ef1 --- /dev/null +++ b/test/Dapr.E2E.Test/Actors/E2ETests.CustomSerializerTests.cs @@ -0,0 +1,88 @@ +// ------------------------------------------------------------------------ +// 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. +// ------------------------------------------------------------------------ +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 + { + 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 + { + 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() + { + 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() + { + { "baz", "qux" }, + { "count", 42 }, + } + }; + + var result = await proxy.SendAsync("test", payload, CancellationToken.None); + + 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)); + } + } + } +} diff --git a/test/Dapr.E2E.Test/Actors/E2ETests.ReminderTests.cs b/test/Dapr.E2E.Test/Actors/E2ETests.ReminderTests.cs index 50cd87219..ff39cce8a 100644 --- a/test/Dapr.E2E.Test/Actors/E2ETests.ReminderTests.cs +++ b/test/Dapr.E2E.Test/Actors/E2ETests.ReminderTests.cs @@ -13,6 +13,7 @@ namespace Dapr.E2E.Test { using System; + using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Dapr.Actors; @@ -49,6 +50,58 @@ public async Task ActorCanStartAndStopReminder() 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() { diff --git a/test/Dapr.E2E.Test/Actors/E2ETests.StateTests.cs b/test/Dapr.E2E.Test/Actors/E2ETests.StateTests.cs new file mode 100644 index 000000000..184a40448 --- /dev/null +++ b/test/Dapr.E2E.Test/Actors/E2ETests.StateTests.cs @@ -0,0 +1,123 @@ +// ------------------------------------------------------------------------ +// 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. +// ------------------------------------------------------------------------ +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.State; + using Xunit; + + public partial class E2ETests : IAsyncLifetime + { + [Fact] + public async Task ActorCanSaveStateWithTTL() + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + var proxy = this.ProxyFactory.CreateActorProxy(ActorId.CreateRandom(), "StateActor"); + + await WaitForActorRuntimeAsync(proxy, cts.Token); + + await proxy.SetState("key", "value", TimeSpan.FromSeconds(2)); + + var resp = await proxy.GetState("key"); + Assert.Equal("value", resp); + + await Task.Delay(TimeSpan.FromSeconds(2.5)); + + // 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")); + } + } +} diff --git a/test/Dapr.E2E.Test/Actors/E2ETests.WeaklyTypedTests.cs b/test/Dapr.E2E.Test/Actors/E2ETests.WeaklyTypedTests.cs new file mode 100644 index 000000000..b250d731e --- /dev/null +++ b/test/Dapr.E2E.Test/Actors/E2ETests.WeaklyTypedTests.cs @@ -0,0 +1,68 @@ +// ------------------------------------------------------------------------ +// 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. +// ------------------------------------------------------------------------ +namespace Dapr.E2E.Test +{ + using System; + using System.Threading; + using System.Threading.Tasks; + using Dapr.Actors; + using Dapr.E2E.Test.Actors.WeaklyTypedTesting; + using FluentAssertions; + 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"); + + await WaitForActorRuntimeAsync(pingProxy, cts.Token); + + var result = await proxy.InvokeMethodAsync(nameof(IWeaklyTypedTestingActor.GetPolymorphicResponse)); + + result.Should().BeOfType().Which.DerivedProperty.Should().NotBeNullOrWhiteSpace(); + } +#else + [Fact] + public async Task WeaklyTypedActorCanReturnDerivedResponse() + { + 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); + + var result = await proxy.InvokeMethodAsync(nameof(IWeaklyTypedTestingActor.GetPolymorphicResponse)); + + result.Should().BeOfType().Which.DerivedProperty.Should().NotBeNullOrWhiteSpace(); + } +#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"); + + await WaitForActorRuntimeAsync(pingProxy, cts.Token); + + var result = await proxy.InvokeMethodAsync(nameof(IWeaklyTypedTestingActor.GetNullResponse)); + + result.Should().BeNull(); + } + } +} diff --git a/test/Dapr.E2E.Test/Dapr.E2E.Test.csproj b/test/Dapr.E2E.Test/Dapr.E2E.Test.csproj index 10ae69b38..fc92396a6 100644 --- a/test/Dapr.E2E.Test/Dapr.E2E.Test.csproj +++ b/test/Dapr.E2E.Test/Dapr.E2E.Test.csproj @@ -1,15 +1,12 @@  - - netcoreapp3.1;net6;net7 - - - - - - - - + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/test/Dapr.E2E.Test/DaprRunConfiguration.cs b/test/Dapr.E2E.Test/DaprRunConfiguration.cs index 9e423205d..fccfbcdd4 100644 --- a/test/Dapr.E2E.Test/DaprRunConfiguration.cs +++ b/test/Dapr.E2E.Test/DaprRunConfiguration.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -20,9 +20,11 @@ public class DaprRunConfiguration public string AppId { get; set; } public string AppProtocol { get; set; } + + public bool AppJsonSerialization { get; set; } public string ConfigurationPath { 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 ee842f27b..83f9948ac 100644 --- a/test/Dapr.E2E.Test/DaprTestApp.cs +++ b/test/Dapr.E2E.Test/DaprTestApp.cs @@ -89,6 +89,11 @@ public DaprTestApp(ITestOutputHelper output, string appId) 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) { @@ -127,24 +132,14 @@ public void Stop() private static string GetTargetFrameworkName() { var targetFrameworkName = ((TargetFrameworkAttribute)Assembly.GetExecutingAssembly().GetCustomAttributes(typeof(TargetFrameworkAttribute), false).FirstOrDefault()).FrameworkName; - string frameworkMoniker; - if (targetFrameworkName == ".NETCoreApp,Version=v3.1") - { - frameworkMoniker = "netcoreapp3.1"; - } - else if (targetFrameworkName == ".NETCoreApp,Version=v5.0") - { - frameworkMoniker = "net5"; - } - else if (targetFrameworkName == ".NETCoreApp,Version=v6.0") - { - frameworkMoniker = "net6"; - } - else + + return targetFrameworkName switch { - frameworkMoniker = "net7"; - } - return frameworkMoniker; + ".NETCoreApp,Version=v6.0" => "net6", + ".NETCoreApp,Version=v7.0" => "net7", + ".NETCoreApp,Version=v8.0" => "net8", + _ => throw new InvalidOperationException($"Unsupported target framework: {targetFrameworkName}") + }; } private static (int, int, int, int) GetFreePorts() diff --git a/test/Dapr.E2E.Test/E2ETests.cs b/test/Dapr.E2E.Test/E2ETests.cs index 94ebbf3df..bc469f715 100644 --- a/test/Dapr.E2E.Test/E2ETests.cs +++ b/test/Dapr.E2E.Test/E2ETests.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -20,6 +20,8 @@ using Xunit; using Xunit.Abstractions; +[assembly: CollectionBehavior(DisableTestParallelization = true)] + namespace Dapr.E2E.Test { // We're using IClassFixture to manage the state we need across tests. diff --git a/test/Dapr.E2E.Test/ServiceInvocation/E2ETests.GrpcProxyInvocationTests.cs b/test/Dapr.E2E.Test/ServiceInvocation/E2ETests.GrpcProxyInvocationTests.cs index 0f991a9c7..3b6f31e84 100644 --- a/test/Dapr.E2E.Test/ServiceInvocation/E2ETests.GrpcProxyInvocationTests.cs +++ b/test/Dapr.E2E.Test/ServiceInvocation/E2ETests.GrpcProxyInvocationTests.cs @@ -15,6 +15,7 @@ using System.Threading; using System.Threading.Tasks; using Dapr.Client; +using Google.Protobuf.WellKnownTypes; using Grpc.Core; using Xunit; using Xunit.Abstractions; @@ -77,5 +78,21 @@ public async Task TestGrpcProxyStreamingBroadcast() 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 cdc6170ce..da49e6721 100644 --- a/test/Dapr.E2E.Test/ServiceInvocation/E2ETests.ServiceInvocationTests.cs +++ b/test/Dapr.E2E.Test/ServiceInvocation/E2ETests.ServiceInvocationTests.cs @@ -13,10 +13,15 @@ namespace Dapr.E2E.Test { using System; + using System.Net; + using System.Net.Http; using System.Net.Http.Json; + using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; using Dapr.Client; + using Google.Protobuf.WellKnownTypes; + using Grpc.Core; using Xunit; public partial class E2ETests @@ -58,6 +63,25 @@ public async Task TestServiceInvocationRequiresApiToken() 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); + }); + } } internal class Transaction diff --git a/test/Dapr.E2E.Test/Workflows/WorkflowTest.cs b/test/Dapr.E2E.Test/Workflows/WorkflowTest.cs deleted file mode 100644 index 5ba9f283e..000000000 --- a/test/Dapr.E2E.Test/Workflows/WorkflowTest.cs +++ /dev/null @@ -1,112 +0,0 @@ -// ------------------------------------------------------------------------ -// Copyright 2022 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.E2E.Test -{ - - using System.Threading; - using System.Threading.Tasks; - using Xunit; - using FluentAssertions; - using System; - using System.Collections.Generic; - using Google.Protobuf; - using Dapr.Client; - - [System.Obsolete] - public partial class E2ETests - { - [Fact] - public async Task TestWorkflows() - { - string instanceId = "testInstanceId"; - string instanceId2 = "EventRaiseId"; - string workflowComponent = "dapr"; - string workflowName = "PlaceOrder"; - object input = "paperclips"; - Dictionary workflowOptions = new Dictionary(); - workflowOptions.Add("task_queue", "testQueue"); - CancellationToken cts = new CancellationToken(); - - using var daprClient = new DaprClientBuilder().UseGrpcEndpoint(this.GrpcEndpoint).UseHttpEndpoint(this.HttpEndpoint).Build(); - var health = await daprClient.CheckHealthAsync(); - health.Should().Be(true, "DaprClient is not healthy"); - - // START WORKFLOW TEST - var startResponse = await daprClient.StartWorkflowAsync( - instanceId: instanceId, - workflowComponent: workflowComponent, - workflowName: workflowName, - input: input, - workflowOptions: workflowOptions, - cancellationToken: cts); - - startResponse.InstanceId.Should().Be("testInstanceId", $"Instance ID {startResponse.InstanceId} was not correct"); - - - // GET INFO TEST - var getResponse = await daprClient.GetWorkflowAsync(instanceId, workflowComponent, cts); - getResponse.InstanceId.Should().Be("testInstanceId"); - getResponse.RuntimeStatus.Should().Be("RUNNING", $"Instance ID {getResponse.RuntimeStatus} was not correct"); - - // PAUSE TEST: - await daprClient.PauseWorkflowAsync(instanceId, workflowComponent, cts); - getResponse = await daprClient.GetWorkflowAsync(instanceId, workflowComponent, cts); - getResponse.RuntimeStatus.Should().Be("SUSPENDED", $"Instance ID {getResponse.RuntimeStatus} was not correct"); - - // RESUME TEST: - await daprClient.ResumeWorkflowAsync(instanceId, workflowComponent, cts); - getResponse = await daprClient.GetWorkflowAsync(instanceId, workflowComponent, cts); - getResponse.RuntimeStatus.Should().Be("RUNNING", $"Instance ID {getResponse.RuntimeStatus} was not correct"); - - // RAISE EVENT TEST - await daprClient.RaiseWorkflowEventAsync(instanceId, workflowComponent, "ChangePurchaseItem", "computers", cts); - getResponse = await daprClient.GetWorkflowAsync(instanceId, workflowComponent, cts); - - // TERMINATE TEST: - await daprClient.TerminateWorkflowAsync(instanceId, workflowComponent, cts); - getResponse = await daprClient.GetWorkflowAsync(instanceId, workflowComponent, cts); - getResponse.RuntimeStatus.Should().Be("TERMINATED", $"Instance ID {getResponse.RuntimeStatus} was not correct"); - - // PURGE TEST - await daprClient.PurgeWorkflowAsync(instanceId, workflowComponent, cts); - - try - { - getResponse = await daprClient.GetWorkflowAsync(instanceId, workflowComponent, cts); - Assert.True(false); - } - catch (DaprException ex) - { - ex.InnerException.Message.Should().Contain("No such instance exists", $"Instance {instanceId} was not correctly purged"); - } - - // Start another workflow for event raising purposes - startResponse = await daprClient.StartWorkflowAsync(instanceId: instanceId2, - workflowComponent: workflowComponent, - workflowName: workflowName, - input: input, - workflowOptions: workflowOptions, - cancellationToken: cts); - - // RAISE EVENT TEST - await daprClient.RaiseWorkflowEventAsync(instanceId2, workflowComponent, "ChangePurchaseItem", "computers", cts); - await Task.Delay(TimeSpan.FromSeconds(30)); - getResponse = await daprClient.GetWorkflowAsync(instanceId2, workflowComponent, cts); - var outputString = getResponse.Properties["dapr.workflow.output"]; - outputString.Should().Be("\"computers\"", $"Purchased item {outputString} was not correct"); - - } - - } -} \ No newline at end of file diff --git a/test/Dapr.E2E.Test/configuration/featureconfig.yaml b/test/Dapr.E2E.Test/configuration/featureconfig.yaml index 81ef1ecb1..4806c630f 100644 --- a/test/Dapr.E2E.Test/configuration/featureconfig.yaml +++ b/test/Dapr.E2E.Test/configuration/featureconfig.yaml @@ -12,3 +12,5 @@ spec: enabled: true - name: "proxy.grpc" enabled: true + - name: "ActorStateTTL" + enabled: true diff --git a/test/Dapr.Extensions.Configuration.Test/Dapr.Extensions.Configuration.Test.csproj b/test/Dapr.Extensions.Configuration.Test/Dapr.Extensions.Configuration.Test.csproj index 79e980ccb..ef6cfbcee 100644 --- a/test/Dapr.Extensions.Configuration.Test/Dapr.Extensions.Configuration.Test.csproj +++ b/test/Dapr.Extensions.Configuration.Test/Dapr.Extensions.Configuration.Test.csproj @@ -1,17 +1,15 @@  - - netcoreapp3.1;net6;net7 - - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - - + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -23,11 +21,13 @@ + + - + \ 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 488c94983..74b66c3cb 100644 --- a/test/Dapr.Extensions.Configuration.Test/DaprSecretStoreConfigurationProviderTest.cs +++ b/test/Dapr.Extensions.Configuration.Test/DaprSecretStoreConfigurationProviderTest.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -16,9 +16,11 @@ using System.Net; using System.Threading.Tasks; using Dapr.Client; +using Dapr; using FluentAssertions; using Grpc.Net.Client; using Microsoft.Extensions.Configuration; +using Moq; using Xunit; using Autogenerated = Dapr.Client.Autogen.Grpc.v1; @@ -142,23 +144,23 @@ public void AddDaprSecretStore_UsingDescriptors_DuplicateSecret_ReportsError() [Fact] public void LoadSecrets_FromSecretStoreThatReturnsOneValue() { - // Configure Client - var httpClient = new TestHttpClient() + var storeName = "store"; + var secretKey = "secretName"; + var secretValue = "secret"; + + var secretDescriptors = new[] { - Handler = async (entry) => - { - var secrets = new Dictionary() { { "secretName", "secret" } }; - await SendResponseWithSecrets(secrets, entry); - } + new DaprSecretDescriptor(secretKey), }; - var daprClient = new DaprClientBuilder() - .UseHttpClientFactory(() => httpClient) - .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = httpClient }) - .Build(); + var daprClient = new Mock(); + + daprClient.Setup(c => c.GetSecretAsync(storeName, secretKey, + It.IsAny>(), default)) + .ReturnsAsync(new Dictionary { { secretKey, secretValue } }); var config = CreateBuilder() - .AddDaprSecretStore("store", new DaprSecretDescriptor[] { new DaprSecretDescriptor("secretName") }, daprClient) + .AddDaprSecretStore(storeName, secretDescriptors, daprClient.Object) .Build(); config["secretName"].Should().Be("secret"); @@ -167,32 +169,156 @@ public void LoadSecrets_FromSecretStoreThatReturnsOneValue() [Fact] public void LoadSecrets_FromSecretStoreThatCanReturnsMultipleValues() { - // Configure Client - var httpClient = new TestHttpClient() + var storeName = "store"; + var firstSecretKey = "first_secret"; + var secondSecretKey = "second_secret"; + var firstSecretValue = "secret1"; + var secondSecretValue = "secret2"; + + var secretDescriptors = new[] { - Handler = async (entry) => - { - var secrets = new Dictionary() { - { "first_secret", "secret1" }, - { "second_secret", "secret2" }}; - await SendResponseWithSecrets(secrets, entry); - } + 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) + .Build(); + + config[firstSecretKey].Should().Be(firstSecretValue); + config[secondSecretKey].Should().Be(secondSecretValue); + } + + [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[] + { + new DaprSecretDescriptor(parentSecretKey) + }; + + 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].Should().Be(firstSecretValue); + config[secondSecretKey].Should().Be(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].Should().Be(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 => + { + await SendEmptyResponse(entry); + } }; var daprClient = new DaprClientBuilder() .UseHttpClientFactory(() => httpClient) - .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = httpClient }) .Build(); var config = CreateBuilder() - .AddDaprSecretStore("store", new DaprSecretDescriptor[] { new DaprSecretDescriptor("secretName") }, daprClient) + .AddDaprSecretStore(storeName, secretDescriptors, daprClient) + .Build(); + + config[secretName].Should().BeNull(); + } + + [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 => + { + await SendEmptyResponse(entry); + } + }; + + var daprClient = new DaprClientBuilder() + .UseHttpClientFactory(() => httpClient) + .Build(); + + var ex = Assert.Throws(() => + { + var config = CreateBuilder() + .AddDaprSecretStore(storeName, secretDescriptors, daprClient) .Build(); + }); - config["first_secret"].Should().Be("secret1"); - config["second_secret"].Should().Be("secret2"); + Assert.Contains("Secret", ex.Message); } - //Here + [Fact] public void AddDaprSecretStore_WithoutStore_ReportsError() { @@ -565,7 +691,7 @@ await SendResponseWithSecrets(new Dictionary() ["otherSecretName≡value"] = "secret", }, entry); } - } + } } }; diff --git a/test/Directory.Build.props b/test/Directory.Build.props index e7d30ea2d..50b029a12 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -1,7 +1,9 @@ - + + net6;net7;net8 + $(RepoRoot)bin\$(Configuration)\test\$(MSBuildProjectName)\ @@ -10,6 +12,6 @@ - + \ No newline at end of file diff --git a/test/Shared/TestClient.cs b/test/Shared/TestClient.cs index 350c4c6e6..c84fe4c9e 100644 --- a/test/Shared/TestClient.cs +++ b/test/Shared/TestClient.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -394,7 +394,7 @@ public async Task> CaptureGrpcRequestAsync(Func