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/.github/holopin.yml b/.github/holopin.yml index 44a7f0c8a..7472cc284 100644 --- a/.github/holopin.yml +++ b/.github/holopin.yml @@ -1,6 +1,6 @@ organization: dapr -defaultSticker: clmjkxscc122740fl0mkmb7egi +defaultSticker: clrqfdv4x24910fl5n4iwu5oa stickers: - - id: clmjkxscc122740fl0mkmb7egi - alias: ghc2023 + id: clrqfdv4x24910fl5n4iwu5oa + alias: sdk-badge \ No newline at end of file diff --git a/.github/workflows/itests.yml b/.github/workflows/itests.yml index 8a299bd59..36741ce7c 100644 --- a/.github/workflows/itests.yml +++ b/.github/workflows/itests.yml @@ -42,9 +42,9 @@ jobs: GOOS: linux GOARCH: amd64 GOPROXY: https://proxy.golang.org - DAPR_CLI_VER: 1.12.0 - DAPR_RUNTIME_VER: 1.12.0 - DAPR_INSTALL_URL: https://raw.githubusercontent.com/dapr/cli/release-1.12/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: '' steps: - name: Set up Dapr CLI @@ -113,7 +113,7 @@ jobs: - 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: | @@ -128,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 4fde80610..fe935bfb8 100644 --- a/.github/workflows/sdk_build.yml +++ b/.github/workflows/sdk_build.yml @@ -32,7 +32,7 @@ jobs: - 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 }} @@ -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 d7232c54e..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%2Fgit%40github.com%3Adapr%2Fdotnet-sdk.git.svg?type=shield)](https://app.fossa.com/projects/custom%2B162%2Fgit%40github.com%3Adapr%2Fdotnet-sdk.git?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 e823ca29f..121dde310 100644 --- a/daprdocs/content/en/dotnet-sdk-docs/_index.md +++ b/daprdocs/content/en/dotnet-sdk-docs/_index.md @@ -18,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 ab41c3917..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 @@ -319,11 +319,8 @@ namespace MyActorService 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(); } } } @@ -394,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}"); } } } @@ -458,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 f68028dc5..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 %}} diff --git a/examples/Actor/ActorClient/Program.cs b/examples/Actor/ActorClient/Program.cs index 5d7f06fb2..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,7 +21,6 @@ namespace ActorClient using System.Threading.Tasks; using Dapr.Actors; using Dapr.Actors.Client; - using IDemoActorInterface; /// /// Actor Client class. @@ -43,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}."); 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 62c100f79..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() @@ -109,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 @@ -173,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 1ee37fdbe..24a42ee0e 100644 --- a/examples/Actor/DemoActor/DemoActor.csproj +++ b/examples/Actor/DemoActor/DemoActor.csproj @@ -1,13 +1,24 @@  - - net6 - - - - - - - + + 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 c2926a048..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,13 +11,12 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace IDemoActorInterface -{ - using System; - using System.Threading.Tasks; - using Dapr.Actors; - using Dapr.Actors.Runtime; +using System; +using System.Threading.Tasks; +using Dapr.Actors; +namespace IDemoActor +{ /// /// Interface for Actor method. /// @@ -27,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. 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/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/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 123763489..2319f6a56 100644 --- a/examples/AspNetCore/GrpcServiceSample/GrpcServiceSample.csproj +++ b/examples/AspNetCore/GrpcServiceSample/GrpcServiceSample.csproj @@ -10,15 +10,16 @@ - - - - - + + + + + + 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/Client/ConfigurationApi/ConfigurationApi.csproj b/examples/Client/ConfigurationApi/ConfigurationApi.csproj index dee6a9878..761ebb38f 100644 --- a/examples/Client/ConfigurationApi/ConfigurationApi.csproj +++ b/examples/Client/ConfigurationApi/ConfigurationApi.csproj @@ -8,6 +8,7 @@ + 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 9c3272e6e..4c04fb907 100644 --- a/examples/Client/DistributedLock/DistributedLock.csproj +++ b/examples/Client/DistributedLock/DistributedLock.csproj @@ -3,6 +3,7 @@ + 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 3f22acaf8..b1e7647c7 100644 --- a/examples/Client/PublishSubscribe/BulkPublishEventExample/BulkPublishEventExample.csproj +++ b/examples/Client/PublishSubscribe/BulkPublishEventExample/BulkPublishEventExample.csproj @@ -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 2df4ec967..52b77a3e5 100644 --- a/examples/Client/PublishSubscribe/PublishEventExample/PublishEventExample.csproj +++ b/examples/Client/PublishSubscribe/PublishEventExample/PublishEventExample.csproj @@ -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/ServiceInvocation.csproj b/examples/Client/ServiceInvocation/ServiceInvocation.csproj index e3df962a1..7b165835e 100644 --- a/examples/Client/ServiceInvocation/ServiceInvocation.csproj +++ b/examples/Client/ServiceInvocation/ServiceInvocation.csproj @@ -16,9 +16,9 @@ - - - + + + diff --git a/examples/Client/StateManagement/StateManagement.csproj b/examples/Client/StateManagement/StateManagement.csproj index e3df962a1..7b165835e 100644 --- a/examples/Client/StateManagement/StateManagement.csproj +++ b/examples/Client/StateManagement/StateManagement.csproj @@ -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/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 index af0b1fa13..d40078fc8 100644 --- a/examples/Workflow/WorkflowConsoleApp/Activities/RequestApprovalActivity.cs +++ b/examples/Workflow/WorkflowConsoleApp/Activities/RequestApprovalActivity.cs @@ -1,6 +1,5 @@ using Dapr.Workflow; using Microsoft.Extensions.Logging; -using WorkflowConsoleApp.Models; namespace WorkflowConsoleApp.Activities { diff --git a/examples/Workflow/WorkflowConsoleApp/Activities/ReserveInventoryActivity.cs b/examples/Workflow/WorkflowConsoleApp/Activities/ReserveInventoryActivity.cs index fc6c48921..cdae1c6ed 100644 --- a/examples/Workflow/WorkflowConsoleApp/Activities/ReserveInventoryActivity.cs +++ b/examples/Workflow/WorkflowConsoleApp/Activities/ReserveInventoryActivity.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/UpdateInventoryActivity.cs b/examples/Workflow/WorkflowConsoleApp/Activities/UpdateInventoryActivity.cs index 947dab6cb..c035aadde 100644 --- a/examples/Workflow/WorkflowConsoleApp/Activities/UpdateInventoryActivity.cs +++ b/examples/Workflow/WorkflowConsoleApp/Activities/UpdateInventoryActivity.cs @@ -1,6 +1,5 @@ using Dapr.Client; using Dapr.Workflow; -using WorkflowConsoleApp.Models; using Microsoft.Extensions.Logging; namespace WorkflowConsoleApp.Activities diff --git a/examples/Workflow/WorkflowConsoleApp/Models.cs b/examples/Workflow/WorkflowConsoleApp/Models.cs index 6c9583d84..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); diff --git a/examples/Workflow/WorkflowConsoleApp/Program.cs b/examples/Workflow/WorkflowConsoleApp/Program.cs index 055b1b4c1..26d34615d 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 DaprWorkflowComponent = "dapr"; // The workflow host is a background service that connects to the sidecar over gRPC var builder = Host.CreateDefaultBuilder(args).ConfigureServices(services => @@ -124,6 +124,8 @@ amount = 1; } + var daprWorkflowClient = host.Services.GetRequiredService(); + // Construct the order with a unique order ID string orderId = $"{itemName.ToLowerInvariant()}-{Guid.NewGuid().ToString()[..8]}"; double totalCost = amount * item.PerItemCost; @@ -131,18 +133,16 @@ // Start the workflow using the order ID as the workflow ID Console.WriteLine($"Starting order workflow '{orderId}' purchasing {amount} {itemName}"); - await daprClient.StartWorkflowAsync( - workflowComponent: DaprWorkflowComponent, - workflowName: nameof(OrderProcessingWorkflow), + await daprWorkflowClient.ScheduleNewWorkflowAsync( + name: nameof(OrderProcessingWorkflow), input: orderInfo, instanceId: orderId); // Wait for the workflow to start and confirm the input - GetWorkflowResponse state = await daprClient.WaitForWorkflowStartAsync( - instanceId: orderId, - workflowComponent: DaprWorkflowComponent); + WorkflowState state = await daprWorkflowClient.WaitForWorkflowStartAsync( + instanceId: orderId); - Console.WriteLine($"{state.WorkflowName} (ID = {orderId}) started successfully with {state.ReadInputAs()}"); + Console.WriteLine($"{nameof(OrderProcessingWorkflow)} (ID = {orderId}) started successfully with {state.ReadInputAs()}"); // Wait for the workflow to complete while (true) @@ -150,22 +150,20 @@ await daprClient.StartWorkflowAsync( using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); try { - state = await daprClient.WaitForWorkflowCompletionAsync( + state = await daprWorkflowClient.WaitForWorkflowCompletionAsync( instanceId: orderId, - workflowComponent: DaprWorkflowComponent, - cancellationToken: cts.Token); + cancellation: cts.Token); break; } catch (OperationCanceledException) { // Check to see if the workflow is blocked waiting for an approval - state = await daprClient.GetWorkflowAsync( - instanceId: orderId, - workflowComponent: DaprWorkflowComponent); - if (state.Properties.TryGetValue("dapr.workflow.custom_status", out string customStatus) && - customStatus.Contains("Waiting for approval")) + state = await daprWorkflowClient.GetWorkflowStateAsync( + instanceId: orderId); + + if(state.ReadCustomStatusAs()?.Contains("Waiting for approval") == true) { - Console.WriteLine($"{state.WorkflowName} (ID = {orderId}) requires approval. Approve? [Y/N]"); + 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)) @@ -182,11 +180,10 @@ await daprClient.StartWorkflowAsync( if (approvalResult != ApprovalResult.Unspecified) { // Raise the workflow event to the workflow - await daprClient.RaiseWorkflowEventAsync( + await daprWorkflowClient.RaiseEventAsync( instanceId: orderId, - workflowComponent: DaprWorkflowComponent, eventName: "ManagerApproval", - eventData: approvalResult); + eventPayload: approvalResult); } // otherwise, keep waiting diff --git a/examples/Workflow/WorkflowConsoleApp/Workflows/OrderProcessingWorkflow.cs b/examples/Workflow/WorkflowConsoleApp/Workflows/OrderProcessingWorkflow.cs index bd2a710b6..3b8af5951 100644 --- a/examples/Workflow/WorkflowConsoleApp/Workflows/OrderProcessingWorkflow.cs +++ b/examples/Workflow/WorkflowConsoleApp/Workflows/OrderProcessingWorkflow.cs @@ -1,6 +1,5 @@ using Dapr.Workflow; using WorkflowConsoleApp.Activities; -using WorkflowConsoleApp.Models; namespace WorkflowConsoleApp.Workflows { diff --git a/examples/Workflow/WorkflowUnitTest/OrderProcessingTests.cs b/examples/Workflow/WorkflowUnitTest/OrderProcessingTests.cs index ac53c4081..e38a0c940 100644 --- a/examples/Workflow/WorkflowUnitTest/OrderProcessingTests.cs +++ b/examples/Workflow/WorkflowUnitTest/OrderProcessingTests.cs @@ -1,8 +1,8 @@ using System.Threading.Tasks; using Dapr.Workflow; using Moq; +using WorkflowConsoleApp; using WorkflowConsoleApp.Activities; -using WorkflowConsoleApp.Models; using WorkflowConsoleApp.Workflows; using Xunit; diff --git a/examples/Workflow/WorkflowUnitTest/WorkflowUnitTest.csproj b/examples/Workflow/WorkflowUnitTest/WorkflowUnitTest.csproj index 4ce0c9801..dec14a713 100644 --- a/examples/Workflow/WorkflowUnitTest/WorkflowUnitTest.csproj +++ b/examples/Workflow/WorkflowUnitTest/WorkflowUnitTest.csproj @@ -7,14 +7,14 @@ - - - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/properties/dapr_managed_netcore.props b/properties/dapr_managed_netcore.props index 3bafcb50c..6e8c01bfe 100644 --- a/properties/dapr_managed_netcore.props +++ b/properties/dapr_managed_netcore.props @@ -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 55d161d9a..574a172a8 100644 --- a/src/Dapr.Actors.AspNetCore/ActorsEndpointRouteBuilderExtensions.cs +++ b/src/Dapr.Actors.AspNetCore/ActorsEndpointRouteBuilderExtensions.cs @@ -103,7 +103,7 @@ 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) @@ -112,14 +112,14 @@ private static IEndpointConventionBuilder MapActorMethodEndpoint(this IEndpointR 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[Constants.ErrorResponseHeaderName] = header; - await context.Response.Body.WriteAsync(body, 0, body.Length); + 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 1114b7828..82c5863db 100644 --- a/src/Dapr.Actors.AspNetCore/Dapr.Actors.AspNetCore.csproj +++ b/src/Dapr.Actors.AspNetCore/Dapr.Actors.AspNetCore.csproj @@ -1,12 +1,6 @@  - - 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/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/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 3fb63ea20..54d3487b8 100644 --- a/src/Dapr.Actors/Dapr.Actors.csproj +++ b/src/Dapr.Actors/Dapr.Actors.csproj @@ -6,17 +6,13 @@ $(PackageTags);Actors - - - - - + - + diff --git a/src/Dapr.Actors/DaprHttpInteractor.cs b/src/Dapr.Actors/DaprHttpInteractor.cs index 4695375fb..2565bab62 100644 --- a/src/Dapr.Actors/DaprHttpInteractor.cs +++ b/src/Dapr.Actors/DaprHttpInteractor.cs @@ -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); @@ -72,7 +72,18 @@ HttpRequestMessage RequestFunc() 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) diff --git a/src/Dapr.Actors/IDaprInteractor.cs b/src/Dapr.Actors/IDaprInteractor.cs index 8f30aa18f..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. diff --git a/src/Dapr.Actors/Runtime/ActorManager.cs b/src/Dapr.Actors/Runtime/ActorManager.cs index b7ee3bf3e..80049d65f 100644 --- a/src/Dapr.Actors/Runtime/ActorManager.cs +++ b/src/Dapr.Actors/Runtime/ActorManager.cs @@ -148,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 { @@ -186,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 + } } 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/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/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.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 c61fc5abc..12b512fbb 100644 --- a/src/Dapr.AspNetCore/Dapr.AspNetCore.csproj +++ b/src/Dapr.AspNetCore/Dapr.AspNetCore.csproj @@ -5,24 +5,13 @@ This package contains the reference assemblies for developing services using Dapr and AspNetCore. - - - - - - - - + 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 a3fd5b082..7d74a7bb3 100644 --- a/src/Dapr.Client/Dapr.Client.csproj +++ b/src/Dapr.Client/Dapr.Client.csproj @@ -1,30 +1,20 @@  - - - - - - - - - - - This package contains the reference assemblies for developing services using Dapr. + - - - - - + + + + - + - + + - + diff --git a/src/Dapr.Client/DaprClient.cs b/src/Dapr.Client/DaprClient.cs index 361ac54bc..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. /// @@ -303,6 +306,20 @@ public HttpRequestMessage CreateInvokeMethodRequest(string appId, string methodN return CreateInvokeMethodRequest(HttpMethod.Post, appId, methodName); } + /// + /// Creates an that can be used to perform service invocation for the + /// application identified by and invokes the method specified by + /// with the POST HTTP method. + /// + /// The Dapr application id to invoke the method on. + /// The name of the method to invoke. + /// A collection of key/value pairs to populate the query string from. + /// An for use with SendInvokeMethodRequestAsync. + public HttpRequestMessage CreateInvokeMethodRequest(string appId, string methodName, IReadOnlyCollection> queryStringParameters) + { + return CreateInvokeMethodRequest(HttpMethod.Post, appId, methodName, queryStringParameters); + } + /// /// Creates an that can be used to perform service invocation for the /// application identified by and invokes the method specified by @@ -314,6 +331,19 @@ public HttpRequestMessage CreateInvokeMethodRequest(string appId, string methodN /// An for use with SendInvokeMethodRequestAsync. public abstract HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMethod, string appId, string methodName); + /// + /// Creates an that can be used to perform service invocation for the + /// application identified by and invokes the method specified by + /// with the HTTP method specified by . + /// + /// The to use for the invocation request. + /// The Dapr application id to invoke the method on. + /// The name of the method to invoke. + /// A collection of key/value pairs to populate the query string from. + /// An for use with SendInvokeMethodRequestAsync. + public abstract HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMethod, string appId, + string methodName, IReadOnlyCollection> queryStringParameters); + /// /// Creates an that can be used to perform service invocation for the /// application identified by and invokes the method specified by @@ -326,9 +356,9 @@ 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 identified by and invokes the method 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. @@ -523,7 +578,7 @@ 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); } @@ -617,7 +672,7 @@ 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); } @@ -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. /// @@ -939,224 +1008,319 @@ public abstract Task UnsubscribeConfiguration( 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); - - - /// - /// Attempt to unlock the given resourceId with response indicating success. - /// - /// The name of the lock store to be queried. - /// Lock key that stands for which resource to protect. - /// Indicates the identifier of lock owner. - /// A that can be used to cancel the operation. - /// A containing a - [Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public abstract Task Unlock( - string storeName, - string resourceId, - string lockOwner, - CancellationToken cancellationToken = default); + #region Cryptography /// - /// Attempt to start the given workflow with response indicating success. + /// Encrypts an array of bytes using the Dapr Cryptography encryption functionality. /// - /// The component to interface with. - /// Name of the workflow to run. - /// Identifier of the specific run. - /// The JSON-serializeable input for the given workflow. - /// The list of options that are potentially needed to start a 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 workflowComponent, - string workflowName, - string instanceId = null, - object input = null, - IReadOnlyDictionary workflowOptions = default, + /// 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); /// - /// Waits for a workflow to start running and returns a object that contains metadata - /// about the started workflow. + /// Encrypts a stream using the Dapr Cryptography encryption functionality. /// - /// - /// - /// A "started" workflow instance is any instance not in the state. - /// - /// This method will return a completed task if the workflow has already started running or has already completed. - /// - /// - /// The unique ID of the workflow instance to wait for. - /// The component to interface with. - /// A that can be used to cancel the wait operation. - /// - /// Returns a record that describes the workflow instance and its execution status. - /// - /// - /// Thrown if is canceled before the workflow starts running. - /// - [Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public virtual async Task WaitForWorkflowStartAsync( - string instanceId, - string workflowComponent, - CancellationToken cancellationToken = default) - { - while (true) - { - var response = await this.GetWorkflowAsync(instanceId, workflowComponent, cancellationToken); - if (response.RuntimeStatus != WorkflowRuntimeStatus.Pending) - { - return response; - } - - await Task.Delay(TimeSpan.FromMilliseconds(500), cancellationToken); - } - } - - /// - /// Waits for a workflow to complete and returns a - /// object that contains metadata about the started instance. - /// - /// - /// - /// A "completed" workflow instance is any instance in one of the terminal states. For example, the - /// , , or - /// states. - /// - /// Workflows are long-running and could take hours, days, or months before completing. - /// Workflows can also be eternal, in which case they'll never complete unless terminated. - /// In such cases, this call may block indefinitely, so care must be taken to ensure appropriate timeouts are - /// enforced using the parameter. - /// - /// If a workflow instance is already complete when this method is called, the method will return immediately. - /// - /// - /// - /// Returns a record that describes the workflow instance and its execution status. - /// - /// - /// Thrown if is canceled before the workflow completes. - /// - [Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public virtual async Task WaitForWorkflowCompletionAsync( - string instanceId, - string workflowComponent, - CancellationToken cancellationToken = default) - { - while (true) - { - var response = await this.GetWorkflowAsync(instanceId, workflowComponent, cancellationToken); - if (response.RuntimeStatus == WorkflowRuntimeStatus.Completed || - response.RuntimeStatus == WorkflowRuntimeStatus.Failed || - response.RuntimeStatus == WorkflowRuntimeStatus.Terminated) - { - return response; - } - - await Task.Delay(TimeSpan.FromMilliseconds(500), cancellationToken); - } - } + /// 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 1580afb36..50a4979d1 100644 --- a/src/Dapr.Client/DaprClientBuilder.cs +++ b/src/Dapr.Client/DaprClientBuilder.cs @@ -57,6 +57,7 @@ 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. @@ -136,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. /// @@ -162,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 75df09323..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,7 +381,8 @@ 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)); + 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); @@ -365,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; } @@ -406,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)); @@ -597,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) @@ -605,7 +717,7 @@ public override async Task> GetBulkStateAsync(strin var envelope = new Autogenerated.GetBulkStateRequest() { - StoreName = storeName, + StoreName = storeName, Parallelism = parallelism ?? default }; @@ -628,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, @@ -1373,368 +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 workflowComponent, - string workflowName, - string instanceId = null, - object input = null, - 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. - byte[] jsonUtf8Bytes = null; - if (input is not null) + //Send the plaintext bytes in blocks in subsequent messages + await using (var bufferedStream = new BufferedStream(plaintextStream, streamingBlockSizeInBytes)) { - jsonUtf8Bytes = JsonSerializer.SerializeToUtf8Bytes(input); - } + var buffer = new byte[streamingBlockSizeInBytes]; + int bytesRead; + ulong sequenceNumber = 0; - var request = new Autogenerated.StartWorkflowRequest() - { - InstanceId = instanceId, - WorkflowComponent = workflowComponent, - WorkflowName = workflowName, - Input = jsonUtf8Bytes is not null ? ByteString.CopyFrom(jsonUtf8Bytes) : null, - }; - - 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) + //Send the completion message + await duplexStream.RequestStream.CompleteAsync(); + } + + /// + /// Retrieves the encrypted bytes from the encryption operation on the sidecar and returns as an enumerable stream. + /// + private async IAsyncEnumerable> RetrieveEncryptedStreamAsync(AsyncDuplexStreamingCall duplexStream, [EnumeratorCancellation] CancellationToken cancellationToken) + { + await foreach (var encryptResponse in duplexStream.ResponseStream.ReadAllAsync(cancellationToken) + .ConfigureAwait(false)) { - throw new DaprException("Start 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 GetWorkflowAsync( - 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.GetWorkflowRequest() + var decryptRequestOptions = new Autogenerated.DecryptRequestOptions { - InstanceId = instanceId, - WorkflowComponent = workflowComponent + ComponentName = vaultResourceName, + KeyName = keyName }; - try - { - var options = CreateCallOptions(headers: null, cancellationToken); - var response = await client.GetWorkflowAlpha1Async(request, options); - if (response == null) - { - throw new DaprException("Get workflow operation failed: the Dapr endpoint returned an empty result."); - } - - response.CreatedAt ??= new Timestamp(); - response.LastUpdatedAt ??= response.CreatedAt; + var options = CreateCallOptions(headers: null, cancellationToken); + var duplexStream = client.DecryptAlpha1(options); - return new GetWorkflowResponse - { - InstanceId = response.InstanceId, - WorkflowName = response.WorkflowName, - WorkflowComponentName = workflowComponent, - CreatedAt = response.CreatedAt.ToDateTime(), - LastUpdatedAt = response.LastUpdatedAt.ToDateTime(), - RuntimeStatus = GetWorkflowRuntimeStatus(response.RuntimeStatus), - Properties = response.Properties, - FailureDetails = GetWorkflowFailureDetails(response, workflowComponent), - }; - } - catch (RpcException ex) - { - throw new DaprException("Get workflow operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); - } + //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); } - private static WorkflowRuntimeStatus GetWorkflowRuntimeStatus(string runtimeStatus) + /// + [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) { - if (!System.Enum.TryParse(runtimeStatus, true /* ignoreCase */, out WorkflowRuntimeStatus status)) + //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)) { - status = WorkflowRuntimeStatus.Unknown; + 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++; + } } - - return status; + + //Send the completion message + await duplexStream.RequestStream.CompleteAsync(); } - private static WorkflowFailureDetails GetWorkflowFailureDetails(Autogenerated.GetWorkflowResponse response, string componentName) + /// + /// 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) { - // FUTURE: Make this part of the protobuf contract instead of getting it from properties - // NOTE: The use of | instead of || is intentional. We want to get all the values. - if (response.Properties.TryGetValue($"{componentName}.workflow.failure.error_type", out string errorType) | - response.Properties.TryGetValue($"{componentName}.workflow.failure.error_message", out string errorMessage) | - response.Properties.TryGetValue($"{componentName}.workflow.failure.stack_trace", out string stackTrace)) + await foreach (var decryptResponse in duplexStream.ResponseStream.ReadAllAsync(cancellationToken) + .ConfigureAwait(false)) { - return new WorkflowFailureDetails(errorMessage, errorType, stackTrace); + yield return decryptResponse.Payload.Data.Memory; } - - return null; } - - /// - [Obsolete] - public async override Task TerminateWorkflowAsync( - string instanceId, - string workflowComponent, + + /// + [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)); - - var request = new Autogenerated.TerminateWorkflowRequest() + if (MemoryMarshal.TryGetArray(ciphertextBytes, out var ciphertextSegment) && ciphertextSegment.Array != null) { - InstanceId = instanceId, - WorkflowComponent = workflowComponent - }; - - var options = CreateCallOptions(headers: null, cancellationToken); + 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); + } - try - { - await client.TerminateWorkflowAlpha1Async(request, options); - } - catch (RpcException ex) - { - throw new DaprException("Terminate workflow operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + return bufferedResult.WrittenMemory; } + throw new ArgumentException("The input instance doesn't have a valid underlying data store", nameof(ciphertextBytes)); } - /// - [Obsolete] - public async override Task RaiseWorkflowEventAsync( - string instanceId, - string workflowComponent, - string eventName, - Object eventData, - 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) - { - jsonUtf8Bytes = JsonSerializer.SerializeToUtf8Bytes(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, 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; + //} - var request = new Autogenerated.RaiseEventWorkflowRequest() - { - InstanceId = instanceId, - WorkflowComponent = workflowComponent, - EventName = eventName, - EventData = ByteString.CopyFrom(jsonUtf8Bytes), - }; + #endregion - var options = CreateCallOptions(headers: null, cancellationToken); - - 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 + #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 @@ -1799,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)); } /// @@ -1808,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 11fc253ac..000000000 --- a/src/Dapr.Client/GetWorkflowResponse.cs +++ /dev/null @@ -1,100 +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; -using System.Text.Json; - -namespace Dapr.Client -{ - /// - /// The response type for the API. - /// - public class GetWorkflowResponse - { - /// - /// Gets the instance ID of the workflow. - /// - public string InstanceId { get; init; } - - /// - /// Gets the name of the workflow. - /// - public string WorkflowName { get; init; } - - /// - /// Gets the name of the workflow component. - /// - public string WorkflowComponentName { get; init; } - - /// - /// Gets the time at which the workflow was created. - /// - public DateTime CreatedAt { get; init; } - - /// - /// Gets the time at which the workflow was last updated. - /// - public DateTime LastUpdatedAt { get; init; } - - /// - /// Gets the runtime status of the workflow. - /// - public WorkflowRuntimeStatus RuntimeStatus { get; init; } - - /// - /// Gets the component-specific workflow properties. - /// - public IReadOnlyDictionary Properties { get; init; } - - /// - /// Gets the details associated with the workflow failure, if any. - /// - public WorkflowFailureDetails FailureDetails { get; init; } - - /// - /// Deserializes the workflow input into using . - /// - /// The type to deserialize the workflow input into. - /// Options to control the behavior during parsing. - /// Returns the input as , or returns a default value if the workflow doesn't have an input. - public T ReadInputAs(JsonSerializerOptions options = null) - { - // FUTURE: Make this part of the protobuf contract instead of properties - string defaultInputKey = $"{this.WorkflowComponentName}.workflow.input"; - if (!this.Properties.TryGetValue(defaultInputKey, out string serializedInput)) - { - return default; - } - - return JsonSerializer.Deserialize(serializedInput, options); - } - - /// - /// Deserializes the workflow output into using . - /// - /// The type to deserialize the workflow output into. - /// Options to control the behavior during parsing. - /// Returns the output as , or returns a default value if the workflow doesn't have an output. - public T ReadOutputAs(JsonSerializerOptions options = null) - { - // FUTURE: Make this part of the protobuf contract instead of properties - string defaultOutputKey = $"{this.WorkflowComponentName}.workflow.output"; - if (!this.Properties.TryGetValue(defaultOutputKey, out string serializedOutput)) - { - return default; - } - - return JsonSerializer.Deserialize(serializedOutput, options); - } - } -} diff --git a/src/Dapr.Client/InvocationHandler.cs b/src/Dapr.Client/InvocationHandler.cs index 1e9000c4d..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 { @@ -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/WorkflowFailureDetails.cs b/src/Dapr.Client/WorkflowFailureDetails.cs deleted file mode 100644 index a61754ff1..000000000 --- a/src/Dapr.Client/WorkflowFailureDetails.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. -// ------------------------------------------------------------------------ - -namespace Dapr.Client -{ - /// - /// Represents workflow failure details. - /// - /// A summary description of the failure, which is typically an exception message. - /// The error type, which is defined by the workflow component implementation. - /// The stack trace of the failure. - public record WorkflowFailureDetails( - string ErrorMessage, - string ErrorType, - string StackTrace = null) - { - /// - /// Creates a user-friendly string representation of the failure information. - /// - public override string ToString() - { - return $"{this.ErrorType}: {this.ErrorMessage}"; - } - } -} 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 99% rename from src/Shared/DaprDefaults.cs rename to src/Dapr.Common/DaprDefaults.cs index b738de921..575a3c148 100644 --- a/src/Shared/DaprDefaults.cs +++ b/src/Dapr.Common/DaprDefaults.cs @@ -11,8 +11,6 @@ // limitations under the License. // ------------------------------------------------------------------------ -using System; - namespace Dapr { internal static class DaprDefaults diff --git a/src/Dapr.Client/DaprException.cs b/src/Dapr.Common/DaprException.cs similarity index 96% rename from src/Dapr.Client/DaprException.cs rename to src/Dapr.Common/DaprException.cs index e7b1efaba..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 diff --git a/src/Dapr.Extensions.Configuration/Dapr.Extensions.Configuration.csproj b/src/Dapr.Extensions.Configuration/Dapr.Extensions.Configuration.csproj index 71fd0153e..5cc1043d3 100644 --- a/src/Dapr.Extensions.Configuration/Dapr.Extensions.Configuration.csproj +++ b/src/Dapr.Extensions.Configuration/Dapr.Extensions.Configuration.csproj @@ -4,10 +4,6 @@ enable - - - - Dapr Secret Store configuration provider implementation for Microsoft.Extensions.Configuration. @@ -15,10 +11,11 @@ + - + \ No newline at end of file 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 d5820deb1..992baee73 100644 --- a/src/Dapr.Workflow/Dapr.Workflow.csproj +++ b/src/Dapr.Workflow/Dapr.Workflow.csproj @@ -13,17 +13,15 @@ - - - - - - + + + + \ No newline at end of file diff --git a/src/Dapr.Workflow/DaprWorkflowClient.cs b/src/Dapr.Workflow/DaprWorkflowClient.cs index 4c4902dbb..e4c88f0ef 100644 --- a/src/Dapr.Workflow/DaprWorkflowClient.cs +++ b/src/Dapr.Workflow/DaprWorkflowClient.cs @@ -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); } /// @@ -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/WorkflowContext.cs b/src/Dapr.Workflow/WorkflowContext.cs index f4e500134..98b8be96b 100644 --- a/src/Dapr.Workflow/WorkflowContext.cs +++ b/src/Dapr.Workflow/WorkflowContext.cs @@ -245,9 +245,9 @@ public abstract Task CallChildWorkflowAsync( /// 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 . + /// 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. diff --git a/src/Dapr.Client/WorkflowRuntimeStatus.cs b/src/Dapr.Workflow/WorkflowRuntimeStatus.cs similarity index 98% rename from src/Dapr.Client/WorkflowRuntimeStatus.cs rename to src/Dapr.Workflow/WorkflowRuntimeStatus.cs index dc652630e..24024cd63 100644 --- a/src/Dapr.Client/WorkflowRuntimeStatus.cs +++ b/src/Dapr.Workflow/WorkflowRuntimeStatus.cs @@ -11,7 +11,7 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Client +namespace Dapr.Workflow { /// /// Enum describing the runtime status of a workflow. diff --git a/src/Dapr.Workflow/WorkflowState.cs b/src/Dapr.Workflow/WorkflowState.cs index aefe4be6b..ea1ffae22 100644 --- a/src/Dapr.Workflow/WorkflowState.cs +++ b/src/Dapr.Workflow/WorkflowState.cs @@ -84,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/Directory.Build.props b/src/Directory.Build.props index 2794f1b1f..35f0fbf7c 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -15,7 +15,7 @@ true - + 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 deccfc1e6..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,14 +1,14 @@ - + 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 7e352d007..c448e915c 100644 --- a/test/Dapr.Actors.AspNetCore.Test/Dapr.Actors.AspNetCore.Test.csproj +++ b/test/Dapr.Actors.AspNetCore.Test/Dapr.Actors.AspNetCore.Test.csproj @@ -5,17 +5,16 @@ - + 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/Dapr.Actors.Test.csproj b/test/Dapr.Actors.Test/Dapr.Actors.Test.csproj index 8852dd465..9ef26cd13 100644 --- a/test/Dapr.Actors.Test/Dapr.Actors.Test.csproj +++ b/test/Dapr.Actors.Test/Dapr.Actors.Test.csproj @@ -5,16 +5,16 @@ - + 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/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 92cfa7096..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); } /// diff --git a/test/Dapr.AspNetCore.IntegrationTest/Dapr.AspNetCore.IntegrationTest.csproj b/test/Dapr.AspNetCore.IntegrationTest/Dapr.AspNetCore.IntegrationTest.csproj index 3cd79d908..d51dc70e8 100644 --- a/test/Dapr.AspNetCore.IntegrationTest/Dapr.AspNetCore.IntegrationTest.csproj +++ b/test/Dapr.AspNetCore.IntegrationTest/Dapr.AspNetCore.IntegrationTest.csproj @@ -1,22 +1,24 @@  - - - 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 aa463be98..9135e63d4 100644 --- a/test/Dapr.AspNetCore.Test/Dapr.AspNetCore.Test.csproj +++ b/test/Dapr.AspNetCore.Test/Dapr.AspNetCore.Test.csproj @@ -1,14 +1,14 @@  - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -21,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/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 aef5b4113..f0bea601f 100644 --- a/test/Dapr.Client.Test/Dapr.Client.Test.csproj +++ b/test/Dapr.Client.Test/Dapr.Client.Test.csproj @@ -1,21 +1,21 @@  - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - - - - - - - - - + + + + + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -33,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/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/Reminders/IReminderActor.cs b/test/Dapr.E2E.Test.Actors/Reminders/IReminderActor.cs index 0bf57f64c..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. 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 849870b98..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,6 +1,6 @@ - + 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/Actors/ReminderActor.cs b/test/Dapr.E2E.Test.App/Actors/ReminderActor.cs index b08e483c2..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. 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 e6ad11456..3454ac25d 100644 --- a/test/Dapr.E2E.Test.App/Dapr.E2E.Test.App.csproj +++ b/test/Dapr.E2E.Test.App/Dapr.E2E.Test.App.csproj @@ -7,10 +7,10 @@ - - - - - + + + + + diff --git a/test/Dapr.E2E.Test.App/Startup.cs b/test/Dapr.E2E.Test.App/Startup.cs index 8207c5883..bfca60f91 100644 --- a/test/Dapr.E2E.Test.App/Startup.cs +++ b/test/Dapr.E2E.Test.App/Startup.cs @@ -16,8 +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; @@ -104,6 +106,8 @@ public void ConfigureServices(IServiceCollection services) options.Actors.RegisterActor(); options.Actors.RegisterActor(); options.Actors.RegisterActor(); + options.Actors.RegisterActor(); + options.Actors.RegisterActor(); }); } 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 f899167c4..fc92396a6 100644 --- a/test/Dapr.E2E.Test/Dapr.E2E.Test.csproj +++ b/test/Dapr.E2E.Test/Dapr.E2E.Test.csproj @@ -1,12 +1,12 @@  - - - - - - - + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive 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 d95929ca3..000000000 --- a/test/Dapr.E2E.Test/Workflows/WorkflowTest.cs +++ /dev/null @@ -1,172 +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. -// ------------------------------------------------------------------------ -using System; -using System.IO; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Dapr.Client; -using FluentAssertions; -using Xunit; -using System.Linq; -using System.Diagnostics; - -namespace Dapr.E2E.Test -{ - [Obsolete] - public partial class E2ETests - { - [Fact] - public async Task TestWorkflowLogging() - { - // This test starts the daprclient and searches through the logfile to ensure the - // workflow logger is correctly logging the registered workflow(s) and activity(s) - - Dictionary logStrings = new Dictionary(); - logStrings["PlaceOrder"] = false; - logStrings["ShipProduct"] = false; - var logFilePath = "../../../../../test/Dapr.E2E.Test.App/log.txt"; - var allLogsFound = false; - var timeout = 30; // 30s - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeout)); - 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"); - - var searchTask = Task.Run(async() => - { - using (StreamReader reader = new StreamReader(logFilePath)) - { - string line; - while ((line = await reader.ReadLineAsync().WaitAsync(cts.Token)) != null) - { - foreach (var entry in logStrings) - { - if (line.Contains(entry.Key)) - { - logStrings[entry.Key] = true; - } - } - allLogsFound = logStrings.All(k => k.Value); - if (allLogsFound) - { - break; - } - } - } - }, cts.Token); - - try - { - await searchTask; - } - finally - { - File.Delete(logFilePath); - } - if (!allLogsFound) - { - Assert.True(false, "The logs were not able to found within the timeout"); - } - } - [Fact] - public async Task TestWorkflows() - { - var instanceId = "testInstanceId"; - var instanceId2 = "EventRaiseId"; - var workflowComponent = "dapr"; - var workflowName = "PlaceOrder"; - object input = "paperclips"; - Dictionary workflowOptions = new Dictionary(); - workflowOptions.Add("task_queue", "testQueue"); - - 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); - - startResponse.InstanceId.Should().Be("testInstanceId", $"Instance ID {startResponse.InstanceId} was not correct"); - - // GET INFO TEST - var getResponse = await daprClient.GetWorkflowAsync(instanceId, workflowComponent); - getResponse.InstanceId.Should().Be("testInstanceId"); - getResponse.RuntimeStatus.Should().Be(WorkflowRuntimeStatus.Running, $"Instance ID {getResponse.RuntimeStatus} was not correct"); - - // PAUSE TEST: - await daprClient.PauseWorkflowAsync(instanceId, workflowComponent); - getResponse = await daprClient.GetWorkflowAsync(instanceId, workflowComponent); - getResponse.RuntimeStatus.Should().Be(WorkflowRuntimeStatus.Suspended, $"Instance ID {getResponse.RuntimeStatus} was not correct"); - - // RESUME TEST: - await daprClient.ResumeWorkflowAsync(instanceId, workflowComponent); - getResponse = await daprClient.GetWorkflowAsync(instanceId, workflowComponent); - getResponse.RuntimeStatus.Should().Be(WorkflowRuntimeStatus.Running, $"Instance ID {getResponse.RuntimeStatus} was not correct"); - - // RAISE EVENT TEST - await daprClient.RaiseWorkflowEventAsync(instanceId, workflowComponent, "ChangePurchaseItem", "computers"); - getResponse = await daprClient.GetWorkflowAsync(instanceId, workflowComponent); - - // TERMINATE TEST: - await daprClient.TerminateWorkflowAsync(instanceId, workflowComponent); - getResponse = await daprClient.GetWorkflowAsync(instanceId, workflowComponent); - getResponse.RuntimeStatus.Should().Be(WorkflowRuntimeStatus.Terminated, $"Instance ID {getResponse.RuntimeStatus} was not correct"); - - // PURGE TEST - await daprClient.PurgeWorkflowAsync(instanceId, workflowComponent); - - try - { - getResponse = await daprClient.GetWorkflowAsync(instanceId, workflowComponent); - Assert.True(false, "The GetWorkflowAsync call should have failed since the instance was purged"); - } - 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); - - // PARALLEL RAISE EVENT TEST - var event1 = daprClient.RaiseWorkflowEventAsync(instanceId2, workflowComponent, "ChangePurchaseItem", "computers"); - var event2 = daprClient.RaiseWorkflowEventAsync(instanceId2, workflowComponent, "ChangePurchaseItem", "computers"); - var event3 = daprClient.RaiseWorkflowEventAsync(instanceId2, workflowComponent, "ChangePurchaseItem", "computers"); - var event4 = daprClient.RaiseWorkflowEventAsync(instanceId2, workflowComponent, "ChangePurchaseItem", "computers"); - var event5 = daprClient.RaiseWorkflowEventAsync(instanceId2, workflowComponent, "ChangePurchaseItem", "computers"); - - var externalEvents = Task.WhenAll(event1, event2, event3, event4, event5); - var winner = await Task.WhenAny(externalEvents, Task.Delay(TimeSpan.FromSeconds(30))); - externalEvents.IsCompletedSuccessfully.Should().BeTrue($"Unsuccessful at raising events. Status of events: {externalEvents.IsCompletedSuccessfully}"); - - // Wait up to 30 seconds for the workflow to complete and check the output - using var cts = new CancellationTokenSource(delay: TimeSpan.FromSeconds(30)); - getResponse = await daprClient.WaitForWorkflowCompletionAsync(instanceId2, workflowComponent, cts.Token); - var outputString = getResponse.Properties["dapr.workflow.output"]; - outputString.Should().Be("\"computers\"", $"Purchased item {outputString} was not correct"); - var deserializedOutput = getResponse.ReadOutputAs(); - deserializedOutput.Should().Be("computers", $"Deserialized output '{deserializedOutput}' was not expected"); - } - } -} 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 2e4523582..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,14 +1,15 @@  - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - - + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -20,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 0ce23c19e..50b029a12 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -1,9 +1,9 @@ - + net6;net7;net8 - + $(RepoRoot)bin\$(Configuration)\test\$(MSBuildProjectName)\ @@ -12,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